Signed distance functions explained: drawing shapes with math
A signed distance function (an SDF) answers one question for any point you give it: how far is this point from the shape? The answer is negative inside the shape, zero exactly on the edge, and positive outside. That one number is enough to fill the shape, outline it, give it a soft edge or a glow, and combine it with other shapes. It is also the idea that makes 3D ray marching work.
The simplest SDF: a circle
The distance from a point p to the middle of a circle is length(p). Subtract the radius and you have the distance to the edge:
float sdCircle(float2 p, float r)
{
return length(p) - r;
}
Inside the circle length(p) is smaller than r, so the result is negative. On the rim it is zero. Outside it grows. To draw the circle you only need to decide what to do with that number:
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0);
float d = sdCircle(p, 0.25);
float fill = 1.0 - smoothstep(0.0, 0.01, d); // 1 inside, softly 0 outside
return float4(fill, fill, fill, 1.0);
}
If the circle looks stretched, read the guide on aspect ratio first.
A box
For a box centered at the origin with half-sizes b (half the width and half the height):
float sdBox(float2 p, float2 b)
{
float2 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
}
Here is how it works. abs(p) folds all four quadrants onto one, so you only have to think about the top-right corner. q is how far the point is beyond the box in each direction. If both parts of q are negative the point is inside; max(q.x, q.y) then gives the (negative) distance to the nearest side. If the point is outside, length(max(q, 0.0)) gives the straight distance, which correctly rounds at the corners.
Moving, scaling and repeating
You never move the shape. You move the point before asking about it. To put a circle at (0.3, 0.1) evaluate sdCircle(p - float2(0.3, 0.1), r). To repeat it in a grid, fold the point first:
float2 cell = frac(p * 4.0) - 0.5; // 4 x 4 cells, each centered on (0, 0) float d = sdCircle(cell, 0.3);
That one extra line gives you sixteen circles for the price of one.
Combining shapes
Because every shape is just a number, combining shapes is arithmetic on numbers:
- Union (either shape):
min(a, b) - Intersection (only where both overlap):
max(a, b) - Subtraction (cut shape b out of a):
max(a, -b)
float scene(float2 p)
{
float a = sdCircle(p, 0.25);
float b = sdBox(p - float2(0.15, 0.0), float2(0.12, 0.3));
return max(a, -b); // a circle with a box-shaped bite taken out
}
Smooth blending of shapes
A plain min leaves a sharp crease where two shapes meet. A smooth minimum rounds that crease off so they melt into each other, like two drops of liquid touching:
float smin(float a, float b, float k)
{
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
The value k sets how wide the blend is. With k = 0 it is the ordinary min; with a larger k the shapes merge over a wider zone. This one function is the whole trick behind "metaball" blobs.
Outlines, glows and borders
Because d is a real distance, you can do things that are hard with a plain on/off mask:
float outline = 1.0 - smoothstep(0.0, 0.01, abs(d) - 0.01); // a ring 0.01 thick around the edge float glow = exp(-8.0 * max(d, 0.0)); // fades out smoothly with distance
abs(d) is the distance to the edge from either side, so thresholding it draws a line along the edge. exp(-k * d) falls off smoothly the further you get from the shape, which is the standard soft glow.
Antialiasing with the right width
The softness of an edge should match the size of a pixel. Too sharp and the edge looks like a staircase; too soft and it looks blurry. The derivative function fwidth tells you how much a value changes across one pixel, so it gives the right width automatically:
float w = fwidth(d); float fill = 1.0 - smoothstep(-w, w, d); // an edge about one pixel wide at any size
This stays crisp when you zoom in and does not blur when you zoom out. It is the same fix that cleans up shapes whose distance values change very slowly in places.
The same idea in 3D
A 3D signed distance function has exactly the same job: given a point in space, say how far it is from the nearest surface. That is the key to ray marching. To find where a ray hits an object you step along it, and the SDF tells you how far you can safely step without passing through anything. Far from the object you take big steps; close to it you take tiny ones. When the distance drops below a small value, you have hit the surface. The 3D lessons build this up one step at a time, starting with smart steps (sphere tracing).
Where to practise
- Distance fields and antialiasing turns a circle's distance into a ring with a smooth edge.
- Raymarching: put it all together combines SDFs, lighting and normals into a 3D scene.
- Several of the challenges, such as the metaballs, are built from exactly the functions above.
Keep reading
- What is a fragment shader? A plain explanation with code
- HLSL vs GLSL vs WGSL vs Metal: shader languages compared
- How to learn shader programming: a step-by-step roadmap
- What is ray marching? Distance fields explained simply
- Noise in shaders: value noise, fBm and what they are for
- How to make an ocean shader: waves, Fresnel and foam
- Why is my circle an oval? UV coordinates and aspect ratio in shaders
- step, lerp and smoothstep: the three functions behind most shader effects
- Porting a Shadertoy shader to Unity (GLSL to HLSL): a conversion guide
- How to debug a shader when you cannot print anything
- Why is my shader slow? A practical guide to shader performance