Fields that return their own gradient

Built on 2D SDFs with gradients by Inigo Quilez. Sources for this reference

Everything downstream of a distance field wants the gradient: normals, bevels, outlines, contact shadows, pen-and-ink stroke direction. The standard way to get it is central differences, which costs four more evaluations of the whole scene. Most of the time you can just return it.

Zoomed on the box corner. Left: closed form. Middle: central differences, with the epsilon sweeping from tiny to large. Right: the angle between them, black where they agree and hot where they disagree by 25 degrees or more.

Return a vec3 instead of a float

Every primitive with a closed-form distance also has a closed-form derivative, and it usually falls out of the same intermediate values you already computed:

vec3 sdgCircle(vec2 p, float r){
    float l = length(p);
    return vec3(l - r, p/l);   // distance, then the gradient
}

The circle is the clean case: you needed length(p) anyway, and the gradient is p normalized, which is the same division. It is genuinely free.

It composes through the operators

This is the part that makes it useful rather than a curiosity. A union takes whichever field won, gradient and all. And smin's blend weight is exactly the right interpolation factor for the gradients too:

vec3 sming(vec3 a, vec3 b, float k){
    float h = clamp(0.5 + 0.5*(b.x - a.x)/k, 0.0, 1.0);
    return vec3(mix(b.x, a.x, h) - k*h*(1.0-h),
                normalize(mix(b.yz, a.yz, h)));
}

So an entire construction tree can carry its gradient to the top without ever sampling.

The epsilon problem you no longer have

Central differences need a step size, and there is no good one. Too small and you are subtracting nearly equal floats and reading noise. Too large and sharp corners get rounded off, because you are measuring across the corner rather than at it. The right value depends on scale, so it drifts the moment anything zooms.

The third panel draws the angle between the two gradients, and it puts the error exactly where it lives. Most of the shape is black, because out on a flat face central differences are fine. What lights up is the medial axis: the X running corner to corner inside the box, plus the corners themselves.

That is not a coincidence. The medial axis is the set of points equidistant from two different parts of the boundary, so it is exactly where the true gradient is discontinuous, flipping from pointing at one edge to pointing at another. Central differences straddle that discontinuity and return the average of two unrelated directions. The closed form has no such problem, because it never had to measure anything. And the error grows as the epsilon sweeps up, because a wider offset straddles more.

Watch the small epsilon end too. It does not go to zero error, it goes to noise, because you are subtracting two nearly equal floats and reading what is left of the mantissa.

When to keep sampling

Honestly: when the field has a domain warp in it. Once you have bent or tapered the space, the gradient has to be pushed back through the Jacobian of that warp, and for anything but simple warps writing that out is more error-prone than four extra evaluations. Mixed trees are fine too: carry gradients where you have them and fall back to differences on the subtrees you do not.

Rules of thumb

  1. Return vec3(distance, gradient). The gradient usually falls out of what you already computed.
  2. Union picks a side; smin interpolates by its own h. Both compose cleanly.
  3. Renormalize after blending. The interpolated vector is not unit length.
  4. Closed-form gradients are exact at corners, where central differences are worst.
  5. Domain warps need the Jacobian. That is the case where sampling is still the sane choice.

All 61 notes How to use them Credits