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;
}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;
}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);
}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:
abs(p) - bcomputes how farpis from the box surface along each axis- For points outside:
length(max(d, 0.0))gives the Euclidean distance to the nearest corner or edge - 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;
}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;
}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);
}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;
}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);
}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);
}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;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) - rEach 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));
}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;
}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);
}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);
}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;
}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));
}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) - ris the foundation of 3D SDFsboxSDFuses a clever formula that handles all cases: interior, faces, edges, cornersplaneSDF(p, n, h) = dot(p, n) + hdefines 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()