Skip to content
Fundamental · Distance fieldsIntermediate

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 field is just a number per pixel, shapes combine with ordinary math: min, 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 min meets at a sharp crease where the two distances are equal. A smooth minimum rounds that crease, so close shapes reach toward each other and merge like drops of liquid:

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);
}

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 mix blends the two distances by it, and the last term pulls the result down in the blend zone so the joint bulges out into a fillet. That dip is largest where the distances are equal, k / 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.

Further reading