3D Signed Distance Functions

Shapes in three dimensions

With the raymarching algorithm in hand, we need shapes to render. In 2D, we built circles and boxes from simple distance calculations. The same principles extend to three dimensions, and the formulas remain surprisingly compact.

This chapter builds your library of 3D primitives. Each one is a function that takes a point in space and returns the signed distance to the surface. Negative inside, positive outside, zero on the surface.

The Sphere

The sphere is the simplest 3D SDF, a direct extension of the 2D circle:

float sphereSDF(vec3 p, float r) {
    return length(p) - r;
}
glsl

The distance from any point p to a sphere centered at the origin is the distance to the origin minus the radius. Points inside the sphere return negative values. Points on the surface return zero. Points outside return positive values.

Interactive: Sphere SDF

The simplest 3D SDF: distance to origin minus radius. Points inside return negative, outside return positive.

To place a sphere elsewhere, subtract the center position before computing:

float sphereSDF(vec3 p, vec3 center, float r) {
    return length(p - center) - r;
}
glsl

The sphere is your go-to primitive. Smooth, symmetric, cheap to compute. Many complex shapes start as spheres that get warped, combined, or repeated.

The Box

Boxes are more interesting. A naive approach might compute distance to each face, but there is an elegant formula that handles all cases:

float boxSDF(vec3 p, vec3 b) {
    vec3 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
}
glsl

The parameter b is a vec3 containing the half-extents in each dimension. A box with b = vec3(1, 2, 0.5) extends 1 unit along X, 2 units along Y, and 0.5 units along Z from its center.

Let us break down the formula:

  1. abs(p) - b computes how far p is from the box surface along each axis
  2. For points outside: length(max(d, 0.0)) gives the Euclidean distance to the nearest corner or edge
  3. For points inside: min(max(d.x, max(d.y, d.z)), 0.0) gives the negative distance to the nearest face

Interactive: Box SDF

The box SDF handles corners and edges correctly, giving proper Euclidean distances even at the sharp corners.

The box SDF handles corners and edges correctly, giving smooth distances even at the sharp corners of the geometry. This is why the formula uses length() for the exterior: it computes the proper distance to the nearest point on the surface, not just the nearest face.

The Plane

An infinite plane is defined by a normal direction and an offset from the origin:

float planeSDF(vec3 p, vec3 n, float h) {
    return dot(p, n) + h;
}
glsl

The normal n must be normalized. The dot product projects p onto the normal, giving the perpendicular distance from the plane. The offset h shifts the plane along its normal.

Interactive: Plane SDF

An infinite plane defined by its normal direction and offset. Useful for floors, walls, and slicing other shapes.

For a ground plane at y = 0:

float groundSDF(vec3 p) {
    return p.y;
}
glsl

This is just planeSDF(p, vec3(0, 1, 0), 0.0) simplified. The plane at y = 0 with an upward normal returns the y-coordinate of the point directly.

Planes are useful for floors, walls, and slicing operations. Combined with other primitives, they create half-spaces that can cut or bound shapes.

Combining 3D SDFs

The same combining operations from 2D work in 3D:

float opUnion(float d1, float d2) {
    return min(d1, d2);
}
 
float opIntersection(float d1, float d2) {
    return max(d1, d2);
}
 
float opSubtraction(float d1, float d2) {
    return max(d1, -d2);
}
glsl

Union takes the minimum distance, giving you both shapes. Intersection takes the maximum, giving only the overlap. Subtraction carves one shape out of another.

Interactive: Combining SDFs

Union: min(d1, d2) - both shapes are visible

With just these three operations and a few primitives, you can model surprisingly complex geometry. A rounded box is a box with a sphere subtracted from its corners. A cylinder is an infinite line intersected with two planes. A torus is... well, that one has its own formula:

float torusSDF(vec3 p, vec2 t) {
    vec2 q = vec2(length(p.xz) - t.x, p.y);
    return length(q) - t.y;
}
glsl

The vector t contains the major radius (distance from center to tube center) and minor radius (tube thickness).

Smooth Blending

Hard boolean operations create sharp edges where shapes meet. For organic forms, we want smooth blending:

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);
}
glsl

The parameter k controls the blend radius. Larger values create smoother, more gradual transitions. When k approaches zero, this becomes the standard min() operation.

Smooth subtraction and intersection have similar forms. These operations enable organic shapes that flow into each other without harsh boundaries.

Infinite Repetition

Here is where SDFs reveal their true power. To repeat a shape infinitely in space, we simply wrap the coordinates:

float infiniteSphereSDF(vec3 p, float spacing, float radius) {
    vec3 q = mod(p + spacing * 0.5, spacing) - spacing * 0.5;
    return sphereSDF(q, radius);
}
glsl

The mod function wraps p into a repeating cell. Subtracting spacing * 0.5 centers the cell around the origin. The SDF then sees only a single sphere, but the wrapping creates infinite copies.

Interactive: Infinite Repetition

Infinite repetition using mod(). A single sphere SDF becomes an endless grid, with zero additional cost per copy.

This works because the raymarching algorithm only cares about distance to the nearest surface. In a repeating grid, the nearest surface is always the one in the current cell (or an adjacent cell for points near cell boundaries).

You can repeat in one, two, or three dimensions by applying mod selectively:

vec3 q = p;
q.x = mod(q.x + spacing * 0.5, spacing) - spacing * 0.5;
glsl

This repeats only along X, creating an infinite row of shapes.

A Library of Primitives

Here is a gallery of common 3D SDFs. Each one follows the same pattern: take a point, return a distance.

3D SDF Gallery

length(p) - r

Each primitive follows the same pattern: take a point, return the signed distance to the surface.

Cylinder (aligned to Y axis):

float cylinderSDF(vec3 p, float r, float h) {
    vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
    return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
glsl

Capsule (between two points):

float capsuleSDF(vec3 p, vec3 a, vec3 b, float r) {
    vec3 ab = b - a;
    vec3 ap = p - a;
    float t = clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0);
    return length(p - (a + t * ab)) - r;
}
glsl

Rounded Box:

float roundBoxSDF(vec3 p, vec3 b, float r) {
    vec3 d = abs(p) - b;
    return length(max(d, 0.0)) - r + min(max(d.x, max(d.y, d.z)), 0.0);
}
glsl

These primitives combine with the operations we learned to create any shape you can imagine. A character becomes spheres for the head and body, capsules for limbs, rounded boxes for hands. Architecture becomes boxes, planes, and cylinders. Abstract forms emerge from smooth unions of warped spheres.

Transformations

To rotate, scale, or otherwise transform an SDF, transform the input point before evaluation:

float rotatedBox(vec3 p, mat3 rotation, vec3 size) {
    vec3 q = rotation * p;
    return boxSDF(q, size);
}
glsl

For non-uniform scaling, you must also scale the output distance:

float scaledSphere(vec3 p, vec3 scale, float r) {
    float s = min(scale.x, min(scale.y, scale.z));
    vec3 q = p / scale;
    return sphereSDF(q, r) * s;
}
glsl

The key insight: transforming the space before the SDF is equivalent to transforming the shape itself. This mental model makes complex compositions intuitive.

Building Scenes

A complete scene SDF samples all objects and returns the minimum distance:

float sceneSDF(vec3 p) {
    float ground = p.y + 1.0;
    float sphere = sphereSDF(p - vec3(0, 0.5, 0), 0.5);
    float box = boxSDF(p - vec3(1.5, 0.3, 0), vec3(0.3, 0.3, 0.3));
    
    return min(ground, min(sphere, box));
}
glsl

This scene has a ground plane, a sphere, and a box. The raymarcher does not care how complex the scene is. It just needs this function to return the distance to the nearest surface.

Key Takeaways

  • sphereSDF(p, r) = length(p) - r is the foundation of 3D SDFs
  • boxSDF uses a clever formula that handles all cases: interior, faces, edges, corners
  • planeSDF(p, n, h) = dot(p, n) + h defines infinite planes
  • Union, intersection, and subtraction work identically to 2D
  • Smooth blending creates organic transitions between shapes
  • mod() wrapping enables infinite repetition with zero cost
  • Transform the input point to transform the shape
  • Complex scenes combine multiple primitives with min()