SDF Raymarching — An Interactive Exploration
March 2026
This page contains a working WebGL shader. The smooth organic shapes, infinite pillars, and soft shadows — they're all computed in real time. I'll walk through how each technique works, grounding explanations in the actual GLSL code.
Live Demo
Click and drag to rotate. The shapes animate continuously.
What Is Raymarching?
Traditional 3D rendering builds geometry from triangles. Raymarching takes a different path: instead of building shapes, you define a function that tells you how far any point in space is from the nearest surface. This is the signed distance function (SDF).
For a sphere at the origin with radius 1:
float sdSphere(vec3 p, float s) {
return length(p) - s;
}
If the result is negative, you're inside the sphere. Zero, you're on the surface. Positive, outside. This function lets us "sphere trace" — step along a ray by exactly the distance to the nearest surface, never overshooting.
The Raymarching Loop
The core algorithm is surprisingly simple:
float dO = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * dO; // Position along ray
scene = getDist(p); // Distance to nearest surface
dO += scene.d; // Step forward by that distance
if (dO > MAX_DIST || abs(scene.d) < SURF_DIST) break;
}
We march until we hit something (distance < 0.001) or run out of distance (> 80 units). The magic is in what getDist() returns — not just distance, but material properties too.
Smooth Boolean Operations
Hard-edged boolean operations (union, intersection, subtraction) are easy — min(), max(). But organic shapes need blending. The shader uses smooth minimum:
float opSmoothUnion(float d1, float d2, float k) {
float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
return mix(d2, d1, h) - k * h * (1.0 - h);
}
The k parameter controls blend radius. Larger k = more blending. The center shape in the demo uses this — a rounded box intersected with a sphere, then holes smoothly subtracted.
Domain Repetition
The pillars repeat infinitely in all directions. But we're not storing infinite geometry. The trick is transforming space itself:
vec3 opRep3(vec3 p, vec3 c) {
return mod(p + 0.5 * c, c) - 0.5 * c;
}
Before passing a point to the SDF, we fold space. A single pillar definition now repeats across infinite coordinates. The raymarching algorithm doesn't know it's hitting copies — it just sees "distance to nearest surface."
Deformations
The twisted column rotates space based on height before calculating distance:
vec3 opTwist(vec3 p, float k) {
float c = cos(k * p.y);
float s = sin(k * p.y);
mat2 m = mat2(c, -s, s, c);
return vec3(m * p.xz, p.y).xzy;
}
This isn't a transformation of the object — it's a transformation of the space the raymarcher queries. The object doesn't twist; space twists.
Soft Shadows
Hard shadows are binary: blocked or not. Soft shadows account for how close a ray passes to occluding surfaces:
float getShadow(vec3 ro, vec3 rd, float mint, float maxt, float k) {
float res = 1.0;
float t = mint;
for (int i = 0; i < 40; i++) {
float h = getDist(ro + rd * t).d;
if (h < 0.001) return 0.0; // Fully occluded
res = min(res, k * h / t); // Soften by proximity
t += h;
if (t > maxt) break;
}
return res;
}
The key insight: k * h / t accumulates the closest approach ratio. Rays that graze surfaces cast soft shadows. Direct hits cast hard shadows.
Ambient Occlusion
Ambient occlusion samples along the surface normal to detect nearby geometry:
float getAO(vec3 p, vec3 n) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = getDist(p + h * n).d;
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
Points with nearby surfaces get darker. Corners and crevices accumulate occlusion.
Volumetric Glow
The neon pillars don't just emit light at their surface. They accumulate glow along the ray:
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * dO;
scene = getDist(p);
totalGlow += scene.glow * (0.02 / (0.01 + abs(scene.d)));
dO += scene.d;
if (dO > MAX_DIST || abs(scene.d) < SURF_DIST) break;
}
Each step contributes to the glow based on proximity and an emissive property. Pillars have glow = 1.0; other shapes have glow = 0.0. The glow bleeds into nearby space.
What This Costs
This shader runs at fullscreen on modern hardware. But the cost accumulates:
- 160 raymarching steps — each step evaluates the full scene SDF
- Soft shadow loop — up to 40 additional SDF evaluations per lit pixel
- 5 AO samples — 5 more evaluations
- Scene complexity — smooth unions, intersections, deformations all add SDF calls
The optimizations are in the SDF design. Sphere and box primitives are cheap. Deformations are cheap because they transform coordinates, not geometry. The expensive part is repetition of the march.
Why This Matters
SDF raymarching is a different relationship to geometry. You don't build shapes — you describe distance. You don't place lights — you march shadows. The entire scene is defined by functions, edited by combining them, and rendered by asking "how far?"
The techniques here — smooth blending, domain manipulation, volumetric effects — they're composible. You can twist repeated objects, subtract with soft edges, add glow to intersection shapes. The grammar of operations is small, but the combinations are vast.
References
- Inigo Quilez — smooth minimum variants, SSAO, and distance functions
- Jamie Wong — Ray Marching and Signed Distance Functions
- Andreas Terrius — SDF soft shadows