---
title: "Choosing the blend radius"
description: "k is the most load-bearing number in any shape built from primitives, and a constant k is wrong the moment anything tapers."
source: https://andrewdetwiler.com/sdf/notes/blend-radius
section: "Building shapes"
author: Andrew Detwiler
---

# Choosing the blend radius

> Built on the polynomial smooth minimum by [Inigo Quilez](https://iquilezles.org/).

`smin` is what makes a pile of primitives read as one object. The blend radius `k` is the single number that decides whether it looks like a creature or like a bag of balls, and it is almost always picked by feel.

```glsl
float smin(float a, float b, float k){
    float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0);
    return mix(b, a, h) - k*h*(1.0 - h);
}
```

*This note has a runnable demo. It needs a browser, so it lives on the page:*
[https://andrewdetwiler.com/sdf/notes/blend-radius](https://andrewdetwiler.com/sdf/notes/blend-radius)

The first panel is a hard `min`: two circles with a visible crease where they meet, and the distance bands kink at the seam. By the last panel the joint has swallowed the shapes themselves, which is the failure people notice late, because it looks smooth and expensive right up until the forms stop reading.

## What k actually costs you

Three things move together as `k` grows, and only the first is the one anybody is aiming at:

1. The fillet radius at the joint, which is the point.
2. The field stops being a true distance. `smin` undershoots, so a sphere trace takes smaller steps than it needs to. Safe, but slower.
3. The surface moves *everywhere near the joint*, not only in the gap. Cyan in the demo is that region, and it grows faster than people expect.

## A constant k is wrong the moment anything tapers

This is the part that matters for characters. A limb is a chain of circles whose radius falls from shoulder to wrist. Blend every joint with the same `k` and the thin end drowns: at the wrist, `k` is a large fraction of the local radius, so the joint bulges and the taper disappears.

*This note has a runnable demo. It needs a browser, so it lives on the page:*
[https://andrewdetwiler.com/sdf/notes/blend-radius](https://andrewdetwiler.com/sdf/notes/blend-radius)

On the left the thin end swells into a sausage and the silhouette stops tapering. On the right the same chain keeps its shape all the way down, because `k` is proportional to the radius it is joining. Scale-relative is the fix, and it is one multiply.

## Rules of thumb

1. Make `k` a fraction of the smaller radius at the joint, not a constant. Around 0.4 to 0.6 of it is a good starting band.
2. If the fillet is eating the forms, you are past the point where more smoothing helps.
3. `smin` undershoots the true distance, so it is safe for sphere tracing but costs steps.
4. Chained `smin` is order-dependent. Blending A with B then C is not the same shape as A with C then B.
5. Look at the distance bands, not just the silhouette. A crease in the bands is a crease in the field.

## The shader

Self-contained. It runs on Shadertoy as is, and in any WebGL2 canvas that supplies
`iResolution`, `iTime` and `iMouse`.

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

// Polynomial smooth minimum, by Inigo Quilez.
// https://iquilezles.org/articles/smin/
// h is a clamped ramp of how close the two fields
// are relative to k; the trailing term is what rounds the joint instead of
// creasing it.
float smin(float a, float b, float k){
    float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0);
    return mix(b, a, h) - k*h*(1.0 - h);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord){
    vec2 uv = fragCoord/iResolution.xy;
    // Four panels, each the SAME two circles at a different k.
    int panel = int(floor(uv.x*4.0));
    float ux = fract(uv.x*4.0);

    // ASPECT MATTERS. A quarter-width panel is tall and narrow, so mapping x
    // and y to the same range squashes every circle into an ellipse, which is
    // a bad look on a page about shapes. Scale x by the PANEL's aspect, not
    // the canvas's, and stack the pair vertically so it fits that shape.
    float panelAspect = (iResolution.x/4.0)/iResolution.y;
    vec2 p = vec2((ux-0.5)*2.2*panelAspect, (uv.y-0.5)*2.2);

    float r = 0.30;
    float sep = 0.40;                       // fixed: only k changes
    float a = sdCircle(p - vec2(0.0, -sep), r);
    float b = sdCircle(p - vec2(0.0,  sep), r);

    // The rungs have to be spaced against the GAP, not chosen as round
    // numbers. With r = 0.30 at +-0.40 the circles are 0.20 apart, so k = 0.10
    // barely reaches and the second panel looked identical to the first. The
    // demo audit caught that; by eye it just looked like a subtle ladder.
    float k = panel == 0 ? 0.001
            : panel == 1 ? 0.22
            : panel == 2 ? 0.45
            : 0.90;

    float d = smin(a, b, k);

    // Distance bands, so the FIELD is visible and not just the silhouette.
    float band = abs(fract(d*6.0) - 0.5)*2.0;
    vec3 col = mix(vec3(0.07,0.08,0.11), vec3(0.15,0.17,0.23), band);

    float w = fwidth(d);
    col = mix(col, vec3(0.98,0.62,0.36), 1.0 - smoothstep(-w, w, d));

    // The fillet: where the blend actually changed the surface.
    float hard = min(a,b);
    col = mix(col, vec3(0.30,0.85,0.95), clamp((hard - d)*5.0, 0.0, 0.65));

    // panel dividers
    float e = abs(fract(uv.x*4.0) - 0.0);
    col = mix(col, vec3(0.30), 1.0 - smoothstep(0.0, 2.0/iResolution.x*4.0, min(e, 1.0-e)));

    float dh = fract(52.9829189*fract(dot(fragCoord, vec2(0.06711056,0.00583715))));
    col += (dh-0.5)/255.0;
    fragColor = vec4(col,1.0);
}
```
