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 ismax(a, b), and cutting one shape out of another ismax(a, -b). - Edges are sharp at any size. Turn the distance into colour with
smoothstepand 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 isexp(-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
xby the aspect ratio first. - Moving a shape by changing its code instead of subtracting an offset from
p. To move a shape right, subtract fromp, assdCircle(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
- Distance fields & antialiasingTurn the solid disc into a glowing ring: radius 0.3, soft edge width 0.02.
- Center, distance & aspect ratioMake the circle truly round by correcting for the aspect ratio.
- 3D, step 4: smart steps (sphere tracing)Use smart steps and show the step count as grey: saturate(steps / 12.0).
- Raymarching: put it all togetherBuild the whole raymarcher: map() with a sphere, the ray loop, calcNormal from the Toolbox, and diffuse light.
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.