Raymarching and signed distance fields

15-raymarching.md · updated 2026-10-15

Classic raytracing (14-raytracing.md) solves "where does this ray hit?" with algebra, so it needs an intersection equation per shape type. Raymarching declines to solve it: it walks along the ray until it is close enough to a surface. All it ever asks is "from here, how far is the nearest surface?"

The field

A signed distance field maps a point to the distance to the nearest surface — negative inside, zero exactly on it.

A sphere is length(p) - r. A box is length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) with q = abs(p) - b. A ground plane at height h is p.y - h, essentially free. The sign is not decoration — it tells the march which side it is on, makes "inside?" a comparison rather than a query, and is what lets you subtract one shape from another at all.

Sphere tracing

The distance value is a safe step size — nothing can be nearer than that, so stepping exactly that far can never overshoot.

That sentence is the whole algorithm. Start at t = 0; evaluate d = map(o + t*dir); if d < epsilon you have hit; otherwise t += d and repeat until t passes a maximum distance (miss) or the iteration cap runs out (give up). Each step is the radius of a sphere known to be empty — hence sphere tracing. A ray driving head-on at a surface converges in a handful of steps; one that skims a surface keeps finding a small distance and crawls, which is why silhouettes are the expensive part of the picture.

Normals for free

The gradient of the field is the normal.

No stored vertex normals, no mesh: sample the field a hair either side on each axis and difference the results — six evaluations, or four with the tetrahedron trick. The same field gives soft shadows (march toward the light, track the minimum distance met; a near miss means a partial occluder) and ambient occlusion (step along the normal — if the field grows more slowly than you walked, something is crowding the point).

The operators are three characters

Union is min, intersection is max, subtraction is max(a, -b).

Real constructive solid geometry, one line each, no mesh booleans and no topology to repair. A smooth minimum blends two shapes into a fillet instead of a crease. And the input point is yours to abuse: p = mod(p, c) - 0.5*c tiles one object through infinite space, and rotating, twisting or displacing p before evaluating bends the geometry (domain warping). A world in thirty lines of fragment shader is why raymarching owns demoscene and shader art.

What it is bad at

Cost scales with step count, not triangle count.

Good at dense procedural scenes, bad at large sparse ones where every ray marches a long way through nothing. It does not share a rasteriser's depth buffer without extra work. And a field that overstates distance — non-Lipschitz, usually from scaling or deforming without correcting — lets a step jump clean through thin geometry; the patch is taking a fraction (0.5–0.9) of each step. So games use it for clouds, fog, volumetrics and effects rather than general geometry. See also ../math/trig-and-vector-heuristics.md.

Demo

The march drawn on a 2D slice — the field is visualised as distance bands, and each step draws a circle whose radius is the distance there, with the next step landing on its edge. Drag the ray until it grazes a surface and watch the circles crawl; switch scenes to see a boolean change the bands, not just the outline; push step × above 1 to punch through the thin walls.

Gotchas

Symptom Cause
Surfaces look eaten away or pocked with holesStep multiplier at 1.0 on a non-exact field; distance is overstated, steps overshoot
A scaled object marches through itselfScaling the input scales the answer: sd(p*k) must become sd(p*k)/k
Edges of shapes are noisy and crawl as the camera movesGrazing rays hitting the iteration cap, not a real miss; raise max steps or fade cap-outs to background
Frame rate collapses when the camera backs offRays now cross a lot of empty space; add a bounding-volume skip or raise the step floor far from geometry
Surfaces look inflated or details are missingEpsilon too large; scale it with distance (eps * t) instead of using one constant
Twisting or bending the domain produces artefactsDomain warping breaks the distance guarantee; take fractional steps in the warped region
Normals come out speckled, or faceted and softGradient offset badly sized: too small and float precision dominates, too large and it smooths real detail away
Raymarched geometry draws over rasterised meshesNo depth written from the march; output gl_FragDepth from the hit t

Open the demo →

← all notes & demos