Signed Distance Functions
The powerful abstraction behind shape rendering
Beyond Circles
In the previous chapter, we used length(uv) - radius to draw circles. That expression is actually something more general: a signed distance function, or SDF.
A signed distance function takes a point in space and returns the shortest distance from that point to the surface of a shape. The magic word is signed: the distance is negative when you are inside the shape, positive when outside, and exactly zero on the boundary.
Interactive: The SDF Contract
Blue is negative (inside), orange is positive (outside), white is the boundary
This simple contract unlocks extraordinary power. With SDFs, we can render any shape with perfect edges, combine shapes with boolean operations, and create effects that would be impossible with traditional rasterization.
The Circle SDF
Let's make explicit what we did implicitly before. The signed distance function for a circle centered at the origin is:
float circleSDF(vec2 p, float r) {
return length(p) - r;
}At the center (p = vec2(0.0)), the distance is -r, the most negative point. On the boundary (where length(p) == r), the result is zero. Outside, it grows positive without bound.
The Box SDF
Rectangles require a bit more thought. For a box centered at the origin with half-extents size, we need to consider which face of the box is closest:
float boxSDF(vec2 p, vec2 size) {
vec2 d = abs(p) - size;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}This formula handles all cases elegantly. The abs(p) - size gives us the signed distance in each axis independently. The max(d, 0.0) term handles the exterior corners. The min(max(d.x, d.y), 0.0) term handles the interior.
Interactive: Box SDF
Move your mouse over the canvas to see distance at that point
Move your mouse to see how the distance changes. Inside the box, values are negative. On the boundary, values are zero. Outside, values are positive. The gradient always points away from the nearest surface.
Visualizing Distance Fields
One of the most useful techniques is to visualize the SDF itself, not just the final shape. This helps build intuition about how distance fields work.
A common approach is to draw contour lines at regular distance intervals, and to color the interior differently from the exterior:
float d = boxSDF(uv, vec2(0.3, 0.2));
// Contour lines
float lines = abs(fract(d * 10.0) - 0.5);
// Inside vs outside
vec3 color = d < 0.0 ? vec3(0.2, 0.4, 0.8) : vec3(0.8, 0.4, 0.2);
color *= 0.8 + 0.2 * smoothstep(0.4, 0.5, lines);Interactive: SDF Visualizer
Contour lines show equal distances from the surface
The contour lines reveal the structure of the distance field. They are spaced equally, showing how distance increases uniformly as you move away from the surface. This property makes SDFs ideal for rendering effects like outlines, glows, and shadows.
From SDF to Shape
Given an SDF, rendering the shape is straightforward. We just need to decide how to interpret the distance:
float shape = smoothstep(0.01, -0.01, sdf);This returns 1.0 inside the shape (where sdf < -0.01) and 0.0 outside (where sdf > 0.01), with a smooth transition at the boundary. The small range creates anti-aliased edges.
Interactive: SDF to Shape
Smaller edge width gives sharper edges, larger gives softer glow
Adjust the edge width to see how smoothstep creates different levels of anti-aliasing. A tiny width creates sharp edges. A larger width creates soft, glowing boundaries.
Why SDFs Are Powerful
Traditional shape rendering asks "is this pixel inside the shape?" and answers with yes or no. SDFs answer a richer question: "how far is this pixel from the shape?"
This extra information enables:
- Perfect anti-aliasing: The distance tells us exactly how much of a pixel is covered by the shape.
- Efficient outlines: Adding an outline is just shifting the threshold, no extra geometry needed.
- Soft shadows: Distance naturally encodes how far we are from a shadow caster.
- Shape morphing: Blend two SDFs and the shapes smoothly interpolate.
- Ray marching: In 3D, SDFs tell us how far we can safely step without hitting anything.
Interactive: Compare Shapes
Morphing between shapes: Circle
The same SDF-to-shape rendering works for any signed distance function. Once you have the distance, the visualization is identical regardless of the underlying geometry.
Building Your SDF Library
Every shape has its own SDF formula. Here are a few common ones:
Line segment (from a to b):
float lineSDF(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
float t = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * t);
}Rounded box (just subtract a radius from the box SDF):
float roundedBoxSDF(vec2 p, vec2 size, float r) {
return boxSDF(p, size - r) - r;
}Equilateral triangle:
float triangleSDF(vec2 p, float r) {
const float k = sqrt(3.0);
p.x = abs(p.x) - r;
p.y = p.y + r / k;
if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
p.x -= clamp(p.x, -2.0 * r, 0.0);
return -length(p) * sign(p.y);
}The art of shader graphics is building a library of these primitives and learning to combine them.
Key Takeaways
- Signed distance functions return negative values inside shapes, positive outside, zero on the boundary
- The circle SDF is
length(p) - radius - The box SDF handles corners and interiors with a careful formula
smoothstep(edge, -edge, sdf)converts any SDF to a rendered shape- Visualizing the distance field with contours reveals the structure
- SDFs enable anti-aliasing, outlines, shadows, and morphing for free
- Build a library of SDF primitives to combine into complex shapes