Three.js From Zero · Article s5-05

S5-05 SDFs & Raymarching

Season 5 · Article 05

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.