Skip to content
Fundamental · CoordinatesIntermediate

Screen-space derivatives

dFdx, dFdy and fwidth measure how fast a value changes from one pixel to the next, for edges that stay one pixel soft at any size.

How it works

A fragment shader seems to see one pixel at a time, but the GPU doesn’t run it that way. It shades pixels in 2×2 blocks called quads, all four running the same instructions at once. That lets it answer a question no single pixel could: how much does this value change between me and my neighbor? The answer is a screen-space derivative.

dFdx, dFdy and fwidth

  • dFdx(v) is roughly the difference in v between this pixel and its horizontal neighbor in the quad.
  • dFdy(v) is the same, vertically.
  • fwidth(v) is abs(dFdx(v)) + abs(dFdy(v)): how much v changes over about one pixel, in any direction.

They work on any float or vector expression the shader computes, not only on inputs. If uv runs from 0 to 1 across a 1000-pixel canvas, fwidth(uv.x) is about 0.001. Zoom in on the surface and it shrinks; tilt the surface away so its texture is squeezed into fewer pixels and it grows. GLSL ES 3.00 has all three built in (WebGL 1 needed an extension), and they exist only in the fragment shader.

Edges one pixel wide

Smoothstep with a fixed width, such as smoothstep(0.49, 0.51, d), looks sharp at one size and blurry or jagged at another, because 0.02 units covers a different number of pixels each time. fwidth measures one pixel in the units of d, so the edge can be exactly that wide wherever it is drawn:

float d = length(p) - 0.5;             // signed distance to a circle of radius 0.5
float w = fwidth(d);                   // one pixel, in the units of d
float disc = clamp(0.5 - d / w, 0.0, 1.0);

The last line is a linear ramp from 1 to 0 across one pixel centered on the edge, close to how much of the pixel the shape covers. This is analytic anti-aliasing: no extra samples, just one derivative. It works for any field that changes smoothly, which is why signed distance fields and derivatives go together.

Filtering a pattern

A repeating pattern drawn with fract aliases at a distance: once one period gets smaller than a pixel, the pixel picks an essentially random point in it, and the pattern breaks into shimmer and moiré. The derivative tells you how many pixels a period covers, so the pattern can fade out before that happens. Here, grid lines that fade out over one pixel on each side at any distance:

vec2 g = uv * 10.0;                              // ten cells per unit
vec2 fw = fwidth(g);                             // cells per pixel, per axis
vec2 dist = abs(fract(g - 0.5) - 0.5) / fw;      // distance to the nearest line, in pixels
float line = 1.0 - clamp(min(dist.x, dist.y), 0.0, 1.0);

Once fw passes 0.5, the lines fill the whole cell. A full solution fades line toward the pattern’s average value as fw grows. Texture filtering does the same thing in hardware: the mip level a texture lookup picks comes from the derivatives of its coordinates.

A normal for free

Derivatives also give a surface’s orientation. The differences in world position to the right and upward are two vectors along the surface; their cross product is perpendicular to it:

vec3 n = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));

The result is constant over each triangle, a flat-shaded look, and needs no normals in the mesh. Flip it if your faces come out dark: the sign depends on the convention for the y axis.

Pitfalls

  • Branches. Derivatives compare values across the quad, so all four pixels must compute the value. Inside an if whose condition differs between neighbors, the result is undefined. Compute derivatives, and texture lookups that rely on them, before the branch.
  • Jumps. A value with a discontinuity, like fract(x) at its wrap or an atan angle at ±π, has a huge derivative there, and a filter based on it draws a seam. Take the derivative of the smooth value before the jump, as the grid above does with g rather than fract(g). For a polar angle t, which has no smooth version, take the derivatives of both t and fract(t + 0.5), keep the pair with the smaller fwidth, and pass it to textureGrad.
  • Precision. The derivatives are approximate: often one value is shared by the whole quad. They are good for filtering and edge widths, not for exact math.

Further reading