SDF booleans & smooth min
Combine distance fields with min and max for union, intersection and subtraction, and melt them together with a smooth minimum.
How it works
Single shapes are only the start. Because a distance fieldA function that returns how far a point is from the edge of a shape, negative inside and positive outside. Zero is the outline itself. Signed distance field (SDF) A function that returns how far a point is from the edge of a shape, negative inside and positive outside. Zero is the outline itself.circle(p) = |p| − rmin, max and a minus sign give the three boolean operations of constructive solid
geometry, and a smooth version of min blends shapes as if they were made of clay. Everything here works the same in
2D and 3D.
Union, intersection, subtraction
Given two distances a and b:
float unite = min(a, b); // inside either shape
float intersect = max(a, b); // inside both shapes
float subtract = max(a, -b); // inside a, but not inside b
The union is min because a point is inside the union when it is inside either shape, that is, when either distance
is negative, and the nearest edge is the nearer of the two. The intersection is max: inside both means both are
negative. Negating a distance turns a shape inside out (inside becomes outside), so subtracting b is intersecting
with “not b”. Subtraction is the only one of the three whose order matters.
float d = min(sdCircle(p - vec2(-0.25, 0.0), 0.3),
sdBox(p - vec2(0.25, 0.0), vec2(0.2)));
float shape = 1.0 - smoothstep(-px, px, d); // a circle and a square as one shape
Where the distance stays exact
These operations put the edge exactly where it belongs, but away from the edge the value is not always the true
distance. The union is exact outside both shapes; inside, where they overlap, it underestimates how deep a point is.
Intersection and subtraction are exact inside but only a bound outside: near the corners they create, the true
distance is larger than the value they return. A fill or a thin outline doesn’t care. Anything that reads the value
far from the edge does. Growing a shape with d - r rounds the corners of a union properly, but leaves the corners an
intersection or subtraction created sharp, because their outside distance is wrong there; glows come out uneven around
those corners too, and in 3D raymarching a bound costs extra steps.
Smooth minimum
A plain Smooth minimum A version of min that rounds the corner where two values cross. Applied to two distance fields it melts the shapes into one, with the blend width set by k.min meets at a sharp crease where the two distances are equal. A smooth minimumA version of min that rounds the corner where two values cross. Applied to two distance fields it melts the shapes into one, with the blend width set by k.h = clamp(0.5 + 0.5 (b − a) / k, 0, 1); smin = mix(b, a, h) − k h (1 − h)
float smin(float a, float b, float k) {
// 1 where a wins, 0 where b wins
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
Linear interpolation (mix) Blends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.h is a blend weight: 1 when a is clearly smaller, 0 when b is, and in between only where the two distances are
within k of each other. The mixBlends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.mix(a, b, t) = a (1 − t) + b tk / 4, and fades to zero at the edges of the zone, so the function equals min wherever the two distances
differ by more than k.
k is in distance units: the size of the fillet. Near zero it behaves like min; larger values give a thicker, gooier
join. Animating the shapes’ positions with a fixed k gives the classic metaball look.
The same h blends anything else between the two shapes. Mixing their colors by it gives a smooth color transition
across the joint, which a hard min can’t:
vec3 color = mix(colorB, colorA, h); // compute h as in smin, from the same a, b and k
Smooth intersection and subtraction
Smooth maximum follows from the smooth minimum by negating, the same way max(a, b) equals -min(-a, -b):
float smax(float a, float b, float k) { return -smin(-a, -b, k); }
float carved = smax(a, -b, k); // a, with b scooped out and the edge rounded
A smooth subtraction rounds the rim of the hole instead of leaving a sharp edge, the shape of a bevel or of a bite.
Other blends
There are many smooth minimums: exponential, power-based, cubic and circular ones, which differ in the joint’s profile and in how far their influence reaches. The quadratic one above is the cheapest and the most common; its only flaw is that the fillet’s curvature jumps at the edges of the blend zone, which is invisible in shading at most sizes. None of them returns an exact distance inside the blend zone.
Morphing between two shapes is a different operation: mix(a, b, t) interpolates the two distance fields themselves,
and the zero edge moves smoothly from one outline to the other as t goes from 0 to 1. It works best between convex
shapes of similar size; between very different ones, the midway shape can break apart.