Collision detection is the most natural game-dev application of signed distance fields. The field that describes a shape for rendering also describes it for contact queries: evaluate the SDF at any point and you get two answers at once, how far the point is from the surface and which side it is on. This article covers the three query types that physics engines build on top of that contract (point containment, penetration depth with separation direction, and sphere casting for continuous collision detection), with the GLSL code, worked examples, edge cases, and the performance comparison against mesh-based approaches.
For the high-level picture of SDFs across the engine, see SDFs in game development . If the sign convention or the gradient concept needs a refresher, the Signed Distance Fields overview builds both up from first principles.
The Query Contract
Three facts about an SDF make every collision query below a pure arithmetic expression:
- Sign: means inside, means outside, means on the surface.
- Magnitude: is the distance to the nearest surface, exact for analytical SDFs, approximate for sampled ones.
- Gradient: points away from the nearest surface, so at the boundary it is the outward normal.
None of these require traversing a tree or testing triangle pairs. The cost of a query is the cost of evaluating the field expression.
Point-in-Volume Queries
The simplest collision question is whether a point lies inside a volume. With an SDF, the answer is a single sign check:
bool isInside(vec3 point, SDF volume) {
return evaluateSDF(point, volume) < 0.0;
}
That one-liner replaces what would otherwise be a point-in-mesh test requiring ray casting or winding-number computation. For a sphere SDF, the distance evaluation costs one vector subtraction and one length computation. For a complex CSG tree built from dozens of primitives, the cost scales with the tree depth, but remains a pure arithmetic expression with no branching search structure.
A worked example makes the arithmetic concrete. Consider a sphere SDF centered at the origin with radius 2:
Test the point . Its distance from the center is . The SDF value is , so the point is inside, and it sits 0.882 units below the surface. The same query against a triangle mesh would need to find the nearest triangle and compute the closest point on it, a far more expensive operation for the same answer.
Penetration Depth and Separation Direction
A binary inside/outside answer is rarely enough for a physics engine. When two objects overlap, the solver needs to know how deeply they interpenetrate and in what direction to push them apart. The SDF provides both.
For a point inside a volume, the penetration depth is and the separation direction is the normalized gradient pointing toward the surface. Together they form the minimum translation vector : move the point by that vector and it lands exactly on the surface.
Continuing the sphere example, the point has depth 0.882 and gradient direction . Pushing the point 0.882 units along that direction gives , whose distance from the origin is , exactly the radius. The arithmetic checks out: one query produced both the overlap amount and the fix.
In practice, you sample the SDF at each vertex of the penetrating object and take the deepest penetration:
struct PenetrationResult {
float depth;
vec3 direction;
vec3 contactPoint;
};
PenetrationResult computePenetration(MeshVolume mesh, SDF volume) {
PenetrationResult result;
result.depth = 0.0;
for (int i = 0; i < mesh.vertexCount; i++) {
vec3 p = mesh.vertices[i];
float d = evaluateSDF(p, volume);
if (d < result.depth) {
result.depth = d;
result.direction = normalize(gradientSDF(p, volume));
result.contactPoint = p;
}
}
return result;
}
Because the SDF gradient points outward from the surface, the separation direction naturally pushes the penetrating point toward the nearest boundary.
The edge cases are where the approach needs care. For convex volumes defined by exact SDFs, the deepest penetrating vertex always points toward the correct separation direction. For non-convex or sampled SDFs, vertex-only sampling may miss thin penetrations: a box can interpenetrate a non-convex surface through an edge or face without any of its vertices crossing the boundary. A production solver therefore adds edge-edge and face-vertex sampling, and for sampled fields it treats the interpolated distance as approximate, relying on a small safety margin so the solver does not jitter on noisy values. Sampling extra points multiplies the query cost, which is why the per-evaluation constant-time property matters so much: you can afford several samples per object when each sample is a grid lookup.
Sphere Casting for Continuous Collision Detection
Fast-moving objects can tunnel through thin geometry between frames when using discrete collision checks. A bullet moving 50 units per frame against a wall 0.1 units thick may be on one side at the start of the frame and on the other at the end, with no discrete test ever seeing the overlap. The standard solution is continuous collision detection (CCD), which sweeps a volume along the motion path and finds the earliest time of impact.
With an SDF, you can implement CCD using sphere casting. The idea is to march a point along the motion vector using the distance field as the step size, exactly like sphere tracing but along a displacement vector instead of a view ray:
float sphereCast(vec3 origin, vec3 direction, float maxDist, float radius,
SDF volume) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = origin + direction * t;
float d = evaluateSDF(p, volume) - radius;
if (d < EPSILON) return t; // Hit
t += d;
if (t > maxDist) break; // No hit within the motion segment
}
return -1.0; // No hit
}
The caller passes the frame’s displacement length as maxDist and a unit motion direction as direction. The returned t is the hit distance along that direction, and t / maxDist is the time of first contact as a fraction of the frame’s motion, which the physics engine uses to stop the object exactly at the collision surface.
Two details matter. Subtracting the moving object’s radius from the field value shrinks the shape being swept into: the cast treats the world surface as if it had been inflated by the projectile’s radius, which is exactly the surface the projectile’s center point collides with. And the step guarantee holds because the SDF never overestimates distance, so no step can jump past a surface.
A worked example: a projectile of radius 0.25 starts at and moves in direction toward a sphere of radius 1 centered at the origin, with maxDist = 4. At , the field value is , so the cast steps to . At that position, , the value is , and the cast reports a hit at . The analytic answer is , so the cast found first contact exactly, in a single step.
The edge cases of sphere casting mirror those of sphere tracing. If the field is a sampled or smoothed SDF that only approximates the distance guarantee, the march can overshoot thin features, so you either keep a conservative epsilon, cap the step count, or fall back to a discrete test when the cast reports no hit. Objects starting the frame already interpenetrating (the field value is negative at the origin) return an immediate hit at , which the engine should treat as an overlap to resolve, not a fresh collision event.
Ray and Capsule Casts
Sphere casting generalizes to other swept shapes. A ray cast is a sphere cast with radius zero, useful for line-of-sight checks and weapon hitscan. A capsule cast replaces the point-origin march with a segment that stays a fixed distance from a line, which requires sampling the SDF at the segment endpoints and taking the minimum, or using a capsule SDF directly for the swept volume. The same safe-step loop works in every case; only the distance query inside the loop changes. This uniformity is a practical win: one cast loop, many query types, no new collision structures.
Performance Profile
SDF collision queries scale differently from traditional approaches. A mesh-versus-mesh collision test requires traversing bounding volume hierarchies for both objects and testing triangle pairs. An SDF-versus-mesh test replaces one hierarchy with a constant-time distance evaluation per vertex of the other shape.
This makes SDFs especially strong for scenarios where one shape is simple and the other is complex, or where many objects need collision queries against the same static environment. Many games bake distance fields for static level geometry and query them at runtime against hundreds of dynamic objects. The baking cost is paid once at build time; the runtime cost is a handful of grid lookups per query. When the environment is static, the field never needs updating. When it changes, rebuilding or locally updating the baked field is a separate pipeline concern, covered in baked signed distance fields .
SDFs are not the right tool for every collision pair. Two dense dynamic meshes colliding with each other stay cheaper as mesh-versus-mesh, because evaluating a sampled field for both shapes still costs grid lookups and interpolation while the mesh path uses fixed-function hardware. The sweet spot is one fixed, query-heavy shape (the level) against many cheap dynamic shapes (projectiles, characters, vehicles).
Summary
The SDF contract turns the three workhorse collision queries into arithmetic:
- Containment is a sign check.
- Penetration depth and separation direction are the magnitude and gradient at the deepest sampled point, combined into a minimum translation vector.
- Continuous collision detection is a sphere cast with the same safe-step guarantee as sphere tracing, using the field value minus the moving radius as the step size.
The same gradient and penetration math feeds the next engine subsystem: particle collision response and force fields resolve exactly these overlaps at per-particle scale.