Shader concept

Signed distance fields (SDF) explained for shaders

A signed distance field is a function that tells you, for any point, how far it is from the nearest edge of a shape: negative inside, positive outside, zero exactly on the edge. Shaders use them to draw crisp shapes and to combine them with simple math.

Running live in your browser. The code below is exactly what draws it.

The idea

Instead of storing a picture of a shape, you store a function of position that returns a distance. For a circle of radius r centred on the origin it is length(p) - r: at the centre it is -r, on the edge 0, and one unit outside it is 1 more than r away. The picture above shows that value as repeating bands, so you can see the distance rippling out from both shapes.

Why shader artists like them

  • Combining is trivial. Union is min(a, b), intersection is max(a, b), and cutting one shape out of another is max(a, -b).
  • Edges are sharp at any size. Turn the distance into colour with smoothstep and the outline stays clean however far you zoom.
  • Outlines, glows and shadows are one line. An outline is the band where abs(d) is small; a glow is exp(-d * k).
  • It works in 3D. The same idea is the engine of ray marching: the distance to the scene tells a ray how far it can safely step.

Common mistakes

  • Forgetting to correct for the screen's aspect ratio, so circles come out as ovals. Multiply x by the aspect ratio first.
  • Moving a shape by changing its code instead of subtracting an offset from p. To move a shape right, subtract from p, as sdCircle(p - offset, r) does above.
  • Scaling a shape by multiplying p: that also scales the distance, which breaks outlines. Divide the result by the same factor.

The code, in every shader language

The same example for each language family. Open one to see it.

HLSL Unity, Unreal
float sdCircle(float2 p, float r)
{
    return length(p) - r;
}

float sdBox(float2 p, float2 b)
{
    float2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0);
    float a = sdCircle(p - float2(-0.18, 0.0), 0.2);
    float b = sdBox(p - float2(0.18, 0.0), float2(0.17, 0.12));
    float d = min(a, b);                              // union of the two shapes
    float fill = 1.0 - smoothstep(0.0, 0.006, d);     // inside the shape?
    float rings = 0.5 + 0.5 * cos(d * 90.0);          // the distance itself, drawn as bands
    float3 bg = float3(0.07, 0.09, 0.2) * (0.6 + 0.4 * rings);
    return float4(lerp(bg, float3(0.25, 0.85, 0.8), fill), 1.0);
}
GLSL Shadertoy, three.js, OpenGL
float sdCircle(vec2 p, float r)
{
    return length(p) - r;
}

float sdBox(vec2 p, vec2 b)
{
    vec2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
    float a = sdCircle(p - vec2(-0.18, 0.0), 0.2);
    float b = sdBox(p - vec2(0.18, 0.0), vec2(0.17, 0.12));
    float d = min(a, b);                              // union of the two shapes
    float fill = 1.0 - smoothstep(0.0, 0.006, d);     // inside the shape?
    float rings = 0.5 + 0.5 * cos(d * 90.0);          // the distance itself, drawn as bands
    vec3 bg = vec3(0.07, 0.09, 0.2) * (0.6 + 0.4 * rings);
    return vec4(mix(bg, vec3(0.25, 0.85, 0.8), fill), 1.0);
}
WGSL WebGPU, Bevy
fn sdCircle(p: vec2f, r: f32) -> f32
{
    return length(p) - r;
}

fn sdBox(p: vec2f, b: vec2f) -> f32
{
    let d = abs(p) - b;
    return length(max(d, vec2f(0.0))) + min(max(d.x, d.y), 0.0);
}

fn frag(uv: vec2f) -> vec4f
{
    let p = (uv - 0.5) * vec2f(aspect, 1.0);
    let a = sdCircle(p - vec2f(-0.18, 0.0), 0.2);
    let b = sdBox(p - vec2f(0.18, 0.0), vec2f(0.17, 0.12));
    let d = min(a, b);                              // union of the two shapes
    let fill = 1.0 - smoothstep(0.0, 0.006, d);     // inside the shape?
    let rings = 0.5 + 0.5 * cos(d * 90.0);          // the distance itself, drawn as bands
    let bg = vec3f(0.07, 0.09, 0.2) * (0.6 + 0.4 * rings);
    return vec4f(mix(bg, vec3f(0.25, 0.85, 0.8), fill), 1.0);
}
Godot Godot 4 shading language
shader_type canvas_item;

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

float sdBox(vec2 p, vec2 b)
{
    vec2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
    float a = sdCircle(p - vec2(-0.18, 0.0), 0.2);
    float b = sdBox(p - vec2(0.18, 0.0), vec2(0.17, 0.12));
    float d = min(a, b);                              // union of the two shapes
    float fill = 1.0 - smoothstep(0.0, 0.006, d);     // inside the shape?
    float rings = 0.5 + 0.5 * cos(d * 90.0);          // the distance itself, drawn as bands
    vec3 bg = vec3(0.07, 0.09, 0.2) * (0.6 + 0.4 * rings);
    return vec4(mix(bg, vec3(0.25, 0.85, 0.8), fill), 1.0);
}

void fragment()
{
    // Godot's UV starts at the top-left, the lessons use bottom-left
    COLOR = frag(vec2(UV.x, 1.0 - UV.y));
}
Metal iOS, macOS
float sdCircle(float2 p, float r)
{
    return length(p) - r;
}

float sdBox(float2 p, float2 b)
{
    float2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0);
    float a = sdCircle(p - float2(-0.18, 0.0), 0.2);
    float b = sdBox(p - float2(0.18, 0.0), float2(0.17, 0.12));
    float d = min(a, b);                              // union of the two shapes
    float fill = 1.0 - smoothstep(0.0, 0.006, d);     // inside the shape?
    float rings = 0.5 + 0.5 * cos(d * 90.0);          // the distance itself, drawn as bands
    float3 bg = float3(0.07, 0.09, 0.2) * (0.6 + 0.4 * rings);
    return float4(mix(bg, float3(0.25, 0.85, 0.8), fill), 1.0);
}

Practise it in the lessons

Start with "Distance fields & antialiasing"

Questions

What does signed mean in signed distance field?

The sign tells you which side you are on: negative inside the shape, positive outside. The size is the distance to the nearest edge.

Are signed distance fields only for 3D?

No. They are just as useful in 2D for crisp icons, text and UI shapes, and in 3D for ray marching.

Is it exact or approximate?

Simple shapes (circle, box, line) give exact distances. After some operations, such as bending or stretching space, the value is only a safe underestimate, which is fine for drawing and for ray marching.

Related concepts