Computer Graphics · Rasterization

Why Depth Interpolates as 1/w and Screen-Space Attributes Do Not

Perspective projection destroys linearity in screen space, so barycentric interpolation of depth requires the reciprocal of w, and the same reciprocal distributes every varying correctly.

After the divide by $w$, screen-space coordinates are linear in the projected vertices: the edge functions of the coverage test vary linearly, and barycentric weights computed from them vary linearly too. Every attribute that was linear in world space, however, is no longer linear in screen space. Interpolating a varying with plain screen-space barycentrics gives the wrong answer everywhere except where all vertices share a $w$, and the error is not subtle: floors bend, textures swim, and depth ordering breaks. The correction is a single division per fragment, and the fact that depth needs a different correction from colour is the source of a great deal of confusion.

The projection does not preserve midpoints

Consider a line segment in eye space running from $A$ to $B$, and a point $M$ halfway along it in three dimensions. Under a perspective projection, with $A$ at depth $w_A$ and $B$ at depth $w_B$, the projected midpoint is not halfway between the projected endpoints on screen. The screen-space fraction $t$ where $M$ lands satisfies

$$t = \frac{w_A}{w_A + w_B}$$

rather than $1/2$, because the divide shortens the far half of the segment more than the near half. If $A$ is near and $B$ is far, the world-space midpoint projects closer to $A$ on screen. A rasterizer that interpolates an attribute by screen-space distance therefore assigns the wrong value at every interior sample, and the size of the error is governed by the ratio $w_B/w_A$.

The correct screen-space weight is not the projected one. For an attribute $P$ with eye-space values $P_A, P_B$, the value at screen fraction $t$ is

$$P(t) = \frac{(1-t)\frac{P_A}{w_A} + t\frac{P_B}{w_B}}{(1-t)\frac{1}{w_A} + t\frac{1}{w_B}}$$

Every term carries a factor of $1/w$. The numerator interpolates $P/w$ linearly, the denominator interpolates $1/w$ linearly, and the ratio restores $P$. So the rasterizer’s job reduces to interpolating two quantities linearly in screen space: the reciprocal $1/w$, and each attribute divided by $w$. Depth happens to be one of the quantities where this matters in a special way.

Depth is already a reciprocal

The depth value that emerges from the perspective divide is $z/w$ in normalized device coordinates, remapped to the $[0,1]$ or $[-1,1]$ range by the viewport transform. Both $z$ and $w$ are linear functions of eye-space position, so their ratio is not. Linearly interpolating $z_{ndc}$ across the screen gives a value that differs from the true depth at interior samples. The difference is largest for triangles that span a wide depth range and is zero along the triangle’s edges, which is why the artifact appears as a subtle bulge in the interior rather than as an edge crack.

Depth-correct interpolation requires interpolating $1/w$ and taking its reciprocal, or equivalently interpolating $z_{ndc}/w$ together with $1/w$ and dividing. The latter is what a general perspective-correct interpolation unit already computes for every varying, so depth is not special in hardware: it is carried as one more interpolant. Where depth is special is that its precision is often more important than its accuracy in the last few bits, because it feeds a comparison rather than a blend, and the comparison can flip on a rounding error.

The near-plane choice amplifies this. Depth in a standard projection is a hyperbolic function of eye-space distance: for a near plane $n$ and far plane $f$, the normalized depth is roughly

$$\frac{z_{ndc}+1}{2} \approx \frac{f}{f-n}\left(1 - \frac{n}{z}\right)$$

so most of the representable depth range covers the region near the near plane, and the far field compresses into a small interval. A large ratio $f/n$ means most of the range is used up in the first few metres of view. This is a property of the projection, not of the interpolation, but it determines whether an interpolation error of one unit in the last place is visible as z-fighting. Reversed-Z depth attacks the same problem by changing the mapping rather than the interpolation.

Doing it with a few multiplies

The standard implementation interpolates $1/w$ (often called $w_{inv}$) and all varyings premultiplied by $w_{inv}$, then divides at the end. Setup computes, per triangle, a plane equation for $w_{inv}$ and for each $(attribute \cdot w_{inv})$. Per fragment, each plane is evaluated by addition, and a single reciprocal converts the pair into the attribute:

/* Per-fragment perspective-correct interpolation from plane equations.
   p_inv_w and p_attr_over_w are (a,b,c) for value = a*x + b*y + c. */
static inline float interp(float a_iw, float b_iw, float c_iw,
                           float a_ao, float b_ao, float c_ao,
                           int x, int y)
{
    float iw   = a_iw * x + b_iw * y + c_iw;   /* 1/w at this sample */
    float ao_w = a_ao * x + b_ao * y + c_ao;   /* attr/w at this sample */
    return ao_w / iw;                          /* divide once */
}

The division is the expensive part, and it is the reason hardware rasterizers carry a reciprocal unit rather than a general divider. Because every varying on a fragment shares the same $iw$, one reciprocal serves all of them: the cost is one reciprocal per fragment, not one per attribute. An implementation with sixteen varyings therefore pays for one division and sixteen divides, which is a ratio worth preserving, and it is why the parameter space of varying counts grows faster than the interpolator’s cost.

Flags, macros, and the flat case

Not every interpolant needs the correction. Values that are constant across the primitive, such as a primitive identifier or a material index, are marked to be passed through unaltered. Values that represent data stored per-vertex and intentionally interpolated without perspective correction, such as screen-space effects that want to behave as if painted on the surface, can request linear interpolation explicitly. The Vulkan specification exposes both: a flat decoration for no interpolation and a noperspective decoration for linear-in-screen-space interpolation, alongside the default perspective-correct behaviour.

The flat case has an interesting hardware cost: because the value comes from a single provoking vertex, the rasterizer must know which vertex that is, and all implementations of the same primitive must agree. Choosing vertex 0 versus the last vertex changes which of three values appears across the whole triangle, which matters when the value encodes something like a triangle index used in a later lookup.

The noperspective case is easy to get wrong in the other direction. It is sometimes used to interpolate values that were computed per-vertex in screen space, where the values genuinely are linear in screen space and the perspective correction would introduce error. Debug overlays, screen-space gradients, and certain shadow-map lookups fall into this class.

Derivatives do not interpolate at all

Screen-space derivatives, the quantities a fragment shader obtains by differencing an expression across neighbouring samples, are computed from the final, already-divided attribute values. They are therefore correct, but they are not the perspective-corrected derivative of the world-space attribute. For a plane tilted away from the viewer, the world-space gradient of an attribute and its screen-space gradient differ by the local compression factor, and the difference grows as the surface turns away. Applications that need the world-space gradient, such as normal mapping or anisotropic filtering, reconstruct it from the screen-space derivative plus the surface parameterization rather than assuming the two are equal.

The subtlety is that the reciprocal $1/w$ used for the divide is itself an interpolant with a nonzero screen-space gradient. An implementation that computes derivatives of the premultiplied attribute and divides afterwards gets a different, incorrect answer from one that computes derivatives of the divided attribute. Since the shading language contract specifies derivatives of the post-divide value, the rasterizer must present the divide result before the derivative quad is formed, which places the interpolation and the derivative computation in a fixed order relative to each other.

Where interpolation meets the sample grid

The plane equations are evaluated at sample positions, which for the default pixel-center convention are $(x+0.5, y+0.5)$. If the sample position used by the interpolator differs from the one used by the coverage test, the resulting image shows a one-sample shift, visible as a systematic skew between geometry and the attributes painted on it. Both must use the same definition of a sample, and in multisampling the distinction between the coverage sample and the shading sample becomes important for exactly this reason: the interpolator evaluates at the shading position while coverage is tested at several positions.

A related detail is that the plane equations must be evaluated with enough precision that the ratio does not lose the attribute’s low bits. If $w$ is large, $attr \cdot w_{inv}$ is small and near the precision floor of a float; dividing by a correspondingly small $iw$ recovers the magnitude but not the precision that was lost. This is why implementations keep the interpolants in a wide enough format and why very distant geometry shows banding in its attributes even when depth ordering is correct. The failure is graceful, degrading with distance, which is why it is often accepted rather than solved.

References

  1. Hyperbolic Interpolation — Blinn, IEEE Computer Graphics and Applications, 1992
  2. Vulkan Specification — Interpolation of Vertex Attributes — Khronos Group
  3. Computer Graphics: Principles and Practice — Foley, van Dam, Feiner & Hughes, Addison-Wesley