Skip to content
Fundamental · Distance fieldsIntermediate

2D signed distance fields

Describe a shape by the distance to its edge, negative inside, and get fills, outlines, glows and clean edges from one number.

How it works

The quick way to draw a circle is a test: is this pixel inside or not? That gives one bit per pixel, which is enough for a hard-edged fill and nothing else. A signed distance field asks a better question: how far is this pixel from the shape’s edge? The answer is negative inside, zero on the edge and positive outside. From that one number you can fill the shape, outline it, give it a soft glow, round its corners, or blend it with another shape.

The circle

The distance from a point to a circle’s edge is its distance to the center minus the radius:

float sdCircle(vec2 p, float r) {
    return length(p) - r;
}

p is the pixel’s position relative to the shape’s center, so a circle elsewhere is sdCircle(p - center, r), the translation from the 2D transforms. Use centered, aspect-corrected coordinates (see the UV fundamental), or the distances are stretched and the circle becomes an ellipse whose “distance” is wrong.

The box

For an axis-aligned box with half-size b, fold the plane into one quadrant with abs, since a box is symmetric, then measure how far past the corner the point is:

float sdBox(vec2 p, vec2 b) {
    // per axis: how far outside each pair of edges
    vec2 d = abs(p) - b;
    // outside: the distance to the nearest edge or corner;
    // inside: the (negative) distance to the nearest edge
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

Outside the box, max(d, 0.0) keeps only the axes on which the point is past an edge. If it is past one edge, the length is the distance to that edge; if past both, it is the distance to the corner. Inside, both components of d are negative, and the larger one is the distance to the nearest edge. The two terms never contribute at the same time.

The segment

A line segment from a to b projects the point onto the line, clamps the projection to the segment, and measures the distance to that closest point:

float sdSegment(vec2 p, vec2 a, vec2 b) {
    vec2 pa = p - a;
    vec2 ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); // 0 at a, 1 at b
    return length(pa - ba * h);
}

A segment has no inside, so its distance is never negative. Subtracting a thickness gives a capsule, a line with round ends: sdSegment(p, a, b) - width.

From distance to pixels

To draw a shape, turn the distance into coverage. step(0.0, -d) gives a hard edge that stair-steps. A smoothstep across about one pixel gives a clean, anti-aliased edge:

vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; // y runs -1..1
float px = 2.0 / uResolution.y; // one pixel in these units

float d = sdBox(p, vec2(0.5, 0.3));
float fill = 1.0 - smoothstep(-px, px, d); // 1 inside, 0 outside, soft for a pixel

The edge stays one pixel soft at any size and on any screen, because the distance is measured in the same units as px. When the coordinates are scaled or warped, the pixel size changes across the screen; the screen-space derivatives fundamental shows fwidth(d), which measures it per pixel.

Other looks come from the same d:

// a 0.04-wide stroke centered on the edge
float outline = 1.0 - smoothstep(0.0, px, abs(d) - 0.02);
// fades outward from the edge
float glow    = exp(-8.0 * max(d, 0.0));
// the same box with 0.1 rounded corners
float rounded = sdBox(p, vec2(0.5, 0.3) - 0.1) - 0.1;

abs(d) is the unsigned distance to the edge from either side, so subtracting a width gives a stroke. Subtracting a constant from any distance grows the shape outward by that amount and rounds every convex corner, which is why the rounded box shrinks the box first.

Exact, bound and scale

The functions above are exact: they return the true Euclidean distance everywhere. Many useful shapes only have a bound, a value that is correct on the edge and never larger than the true distance, but may be smaller elsewhere. Bounds are fine for fills. They make outlines and glows uneven, and in 3D raymarching they cost extra steps.

Scaling the coordinates scales the distance with them. Draw a shape at scale s as sdShape(p / s) * s; without the final * s the distance is in the wrong units and the edge softness changes with the size. Non-uniform scaling (different in x and y) breaks the distance, so stretch the shape’s parameters (like the box’s b) instead of the space. Rotation and translation keep distances exact, so the transforms from the 2D transforms fundamental work directly.

Further reading