Skip to main content
GameDev.net gamedev.net

Smooth SDF Blending

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

A plain union leaves a crease where two shapes meet, because a minimum switches abruptly from one field to the other. A smooth minimum interpolates between them over a chosen radius instead, producing the fillet you see where a real object joins another. It is the single operator that makes distance field modelling look organic rather than assembled.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image6
1 uniform float uBlend; // @param 0.0..0.30 = 0.12 "Blend radius"
2 uniform float uSeparation; // @param 0.0..0.45 = 0.13 "Separation"
3 uniform float uSize; // @param 0.08..0.28 = 0.16 "Shape size"
4
5 float sdCircle(vec2 p, float radius) {
6 return length(p) - radius;
7 }
8
9 float sdBox(vec2 p, vec2 halfSize) {
10 vec2 d = abs(p) - halfSize;
11 return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
12 }
13
14 float sminPoly(float a, float b, float k) {
15 k = max(k, 0.0001);
16 float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
17 return mix(b, a, h) - k * h * (1.0 - h);
18 }
19
20 vec3 shadeField(float d) {
21 vec3 color = (d < 0.0) ? vec3(0.18, 0.42, 0.72) : vec3(0.96, 0.74, 0.34);
22 color *= 1.0 - exp(-7.0 * abs(d));
23 color *= 0.88 + 0.12 * cos(140.0 * d);
24 return mix(color, vec3(1.0), 1.0 - smoothstep(0.0, 0.006, abs(d)));
25 }
26
27 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
28 vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
29 float left = sdCircle(p + vec2(uSeparation, -0.06), uSize);
30 float right = sdCircle(p - vec2(uSeparation, 0.06), uSize);
31 float bar = sdBox(p + vec2(0.0, 0.20), vec2(0.42, 0.05));
32 float blob = sminPoly(left, right, uBlend);
33 float d = sminPoly(blob, bar, uBlend);
34 fragColor = vec4(shadeField(d), 1.0);
35 }
36

Learn from this shader

How it works

The polynomial smooth minimum builds a blend factor from the signed difference between the two distances, scaled by the blend radius and clamped. Mixing the fields with that factor removes the hard switch, but alone it leaves the surface slightly outside where it belongs. The final term subtracts a quadratic correction, largest halfway between the shapes and zero at both ends, which pulls the seam back into a circular fillet. The radius is clamped away from zero because the factor divides by it. Two circles blend into a blob, and the blob blends into a bar, so the operator composes with its own output.

Try changing

Set Blend radius to zero for the sharp union this replaces, then raise it slowly and watch the fillets grow at both seams. Push Separation until the circles no longer touch: the blend still bulges them toward each other, which is the operator's tell and the reason large radii look like melting. Shrink Shape size and the same radius becomes visually huge.

Using it in a game

Smooth minimum is how metaballs, slime, blobby creatures, welded terrain and liquid surfaces are built without geometry. The result is not a true distance field any more, since the fillet region reports slightly less than the real distance, so raymarching it needs a conservative step size.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...