Three.js From Zero · Article s5-05
S5-05 SDFs & Raymarching
SDFs & Raymarching — render without triangles
Signed Distance Fields define shapes as "return the distance to the surface." A whole render pipeline — raymarching — builds worlds from math functions alone. No meshes. No vertices.
1. A signed distance function
Given a point in 3D, return how far you are from the surface. Negative = inside. Zero = on the surface. Positive = outside.
// Sphere centered at origin, radius r
float sphereSDF(vec3 p, float r) {
return length(p) - r;
}
// Box half-extents b
float boxSDF(vec3 p, vec3 b) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
2. Raymarching — using SDFs to render
float march(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < 80; i++) {
vec3 p = ro + rd * t;
float d = sceneSDF(p);
if (d < 0.001) return t; // hit
if (t > 100.0) return -1.0; // escape
t += d; // safe to step the SDF distance
}
return -1.0;
}
Key insight: the SDF tells you how FAR you can step without hitting anything. Step that distance. Evaluate again. Repeat until distance is tiny → you're on the surface.
3. Smooth union (the magic)
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);
}
Blend two shapes smoothly. Drop a ball of clay into another. Molten metals. Metaballs. CSG operations on analytic primitives — impossible (well, extremely expensive) with triangle meshes.
4. Live demo — sculpt with SDFs
5. Normal via gradient
vec3 normal(vec3 p) {
float h = 0.001;
vec2 k = vec2(1.0, -1.0);
return normalize(k.xyy*sceneSDF(p + k.xyy*h) + k.yyx*sceneSDF(p + k.yyx*h)
+k.yxy*sceneSDF(p + k.yxy*h) + k.xxx*sceneSDF(p + k.xxx*h));
}
Central-differences the distance field. Cheap, good enough for lighting.
6. Soft shadows (SDF's superpower)
float softShadow(vec3 ro, vec3 rd, float k) {
float res = 1.0, t = 0.1;
for (int i = 0; i < 32; i++) {
float d = sceneSDF(ro + rd * t);
res = min(res, k * d / t); // smooth attenuation
if (d < 0.001 || t > 20.0) break;
t += d;
}
return clamp(res, 0.0, 1.0);
}
One raymarch per pixel gives perfect penumbra. Triangle shadow maps can't beat it for quality at this cost.
7. Ambient occlusion via SDFs
float ao(vec3 p, vec3 n) {
float o = 0.0, s = 1.0;
for (int i = 0; i < 5; i++) {
float h = 0.02 + 0.12 * float(i) / 4.0;
float d = sceneSDF(p + h * n);
o += (h - d) * s;
s *= 0.5;
}
return clamp(1.0 - 3.0 * o, 0.0, 1.0);
}
Sample along the normal, check how quickly the field closes in. Cheap AO, baked into the geometry.
8. Use cases
- Shadertoy: 90% of Shadertoy entries are raymarched SDFs.
- Demoscene: Mercury's SDF lib, Dreams (PS4) uses SDFs everywhere.
- Volume rendering: clouds, smoke, Unreal Niagara.
- Soft bodies: raymarch + CPU physics = melting creatures.
- Godot SDFGI: lit scenes using SDF proxy for GI rays.
9. vs triangles
SDFs shine when: CSG, smooth blends, infinite detail (fractals), dense occlusion (AO/shadows come cheap).
Triangles shine when: animation, texture detail, established asset pipelines.
Hybrids exist — raymarch in parts of the frame that benefit from it.
10. Takeaways
- SDF = signed distance function. Describes shapes as math.
- Raymarch = step along the view ray by the SDF's safe distance.
- Smooth unions enable blends impossible with triangles.
- Free soft shadows + AO via the same function.
- Shadertoy, Dreams, Godot SDFGI run on this.