Signed distance functions explained: drawing shapes with math

Updated 2026-10-12

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

Keep reading

Start the interactive lessons