GPU Computing · Graphics Pipeline

Perspective-Correct Interpolation and the Reciprocal-W Trick

Why screen-space linear interpolation of a clip-space attribute is wrong, how dividing by w before the rasterizer and dividing back after the vector unit restores correctness, and what it costs.

A triangle projected onto the screen is a flat shape, but the surface it represents is not. Under a perspective projection, a point halfway across a triangle in screen space is not halfway across the triangle in the world: it is pulled toward the far end, because the projection compresses distance with depth. Interpolating a colour or a texture coordinate linearly across screen space therefore produces, by default, visibly wrong results — texture pattern that swims on the surface as the camera moves, and shading that does not vary the way the geometry says it should.

The fix is a small algebraic adjustment applied inside the hit/miss evaluation of every fragment. It is cheap enough to be unconditional on modern hardware, but it only became cheap after the arithmetic was restructured to avoid a per-fragment division.

The failure of planar interpolation

Consider a floor quad with two vertices near the camera and two far away, textured with a regular checkerboard. In world space the checks are equal size. In screen space, the far checks occupy fewer pixels than the near ones, and the ratio is not constant across the quad. A rasterizer that interpolates the texture coordinate linearly in screen space will sample the texture at a uniform rate across the quad, which is the same as drawing equal-size checks in screen space: the texture appears to compress toward the horizon instead of receding with the floor. The distortion is exactly the perspective foreshortening, and the error is largest where the depth gradient is largest.

More precisely: the correct value of an attribute at a screen position is found by interpolating in the space where the triangle exists, then projecting. Planar interpolation in screen space computes a different function. Since the projection is rational in w, the two functions differ by a factor that depends on position.

The reciprocal-w construction

The relation is derived in one line. A point on the triangle can be written as a weighted sum of the vertices using barycentric coordinates that sum to one: p = αa + βb + γc. In homogeneous clip space this is linear, so a clip-space attribute interpolates the same way: attr = α·attr_a + β·attr_b + γ·attr_c, and so does w. After the divide, the screen position is p’ = p/w, and the barycentric coordinates that describe the screen position are not α, β, γ but

α' = (α / w_a) / (α / w_a + β / w_b + γ / w_c)

and likewise for the others. The denominator is the reciprocal of the interpolated 1/w, which is a quantity the rasterizer already needs for depth. So the correct attribute at a fragment is

attr = (α' · attr_a / w_a + β' · attr_b / w_b + γ' · attr_c / w_c) / (α' / w_a + β' / w_b + γ' / w_c)

The structure to notice is that every term is divided by w, including the denominator. If each vertex’s attributes are pre-divided by w before the triangle reaches the rasterizer, and 1/w is interpolated alongside them, then the per-fragment computation reduces to a single division of the interpolated numerator by the interpolated 1/w.

/* Perspective-correct interpolation given per-vertex values already
   divided by w, and per-vertex 1/w. Weights are the screen-space
   barycentrics from the rasterizer's edge functions. */
struct pd { float inv_w; float attr[4]; };   /* attr stored / w */

static void interpolate(const struct pd v[3], const float lam[3],
                        float out[4])
{
    float num[4] = {0};
    float den = 0.0f;

    for (int k = 0; k < 3; k++) {
        den += lam[k] * v[k].inv_w;
        for (int i = 0; i < 4; i++)
            num[i] += lam[k] * v[k].attr[i];
    }
    /* den is the interpolated 1/w; one divide yields every attribute. */
    for (int i = 0; i < 4; i++)
        out[i] = num[i] / den;
}

The division is per fragment, not per attribute, because the denominator is shared. Four attributes cost four multiplies and four adds each, then one reciprocal and four multiplies — a small fraction of the vector unit’s capability, which is why implementations apply the correction to every interpolated value rather than offering a cheap approximation.

Where it is done in the pipeline

The pre-division by w happens at attribute setup, sometimes called primitive setup, at the boundary between geometry and fragment work. The setup unit receives the triangle in screen space, computes the plane equations for each attribute and for 1/w, and writes those coefficients into per-triangle state that the rasterizer and the fragment unit read. A triangle is therefore represented by a small set of planes rather than by three vertex records: one plane per attribute component, each of the form a·x + b·y + c.

The plane form has an important property: evaluating it at a fragment is two multiplies and one add, independent of the fragment’s position in the triangle. This is why GPU rasterizers evaluate attributes incrementally, adding a constant per step in x and another per step in y, rather than recomputing barycentrics from scratch. The steady-state cost per fragment per attribute is one add; the correction and the division are separate.

The w component used in the pre-division is the clip-space w after interpolation of the clipped vertices, not the depth value. Under a standard perspective projection, w is the view-space distance along the camera axis, so it is proportional to depth but scaled and offset. The correction divides the weighted sums of attribute-over-w and of 1/w by each other, so a constant factor cancels: any quantity proportional to the clip-space w serves equally well, and implementations use whichever their setup stage already holds. Under an orthographic projection, w is constant across the triangle and the correction degenerates to the identity, which is why orthographic rendering never shows the swimming-texture artifact.

Flat and noperspective qualifications

Not every attribute should be corrected. The specification allows a shader to declare an interpolant as flat, meaning one value is taken from a designated vertex and interpolated not at all, or as noperspective, meaning screen-space linear interpolation is the required behaviour. Flat is used for flags and indices where interpolation would produce meaningless intermediate values — a bitfield interpolated linearly between 0 and 1 becomes an invalid state rather than a blend.

Noperspective exists for attributes that are genuinely linear in screen space by definition: a screen-space overlay’s coordinates, or a value that is already a screen-space quantity and would be distorted by the correction. The distinction between the three modes is a per-attribute qualifier, so a single triangle can carry flat attributes, perspective-correct attributes and noperspective attributes simultaneously, each with its own plane equation.

The provoking vertex designates which vertex supplies a flat value, and the default differs between APIs. Where flat values vary between primitives — a per-triangle identifier, a face normal, a material index — the choice of provoking vertex decides which of the three values a primitive carries, so the same mesh can produce different flat data under the two conventions. It is a portability matter, not a rounding detail. Clipping compounds it — a clipped triangle that gains interpolated vertices must still designate the original provoking vertex, as homogeneous clipping and the guard band notes.

Degenerate cases and precision

The correction divides by interpolated 1/w, which is positive for fragments in front of the eye, but the value can be small for fragments at large depth and the reciprocal large. Interpolating attributes as 32-bit floats and dividing by a small denominator loses precision at the far end of a deep scene; this is why depth precision and attribute precision degrade together and why a reverse-Z style depth range does not help attributes, which are a separate matter.

A second degenerate case is a triangle with a vertex at w near zero from clipping. The pre-division by a small w produces a large attribute value and a large 1/w, and the subsequent interpolation subtracts nearly equal large numbers. The result can be a visible spike in interpolated values at the triangle’s tip. Implementations guard this by the near-plane epsilon described in the clipping article rather than by special-casing the interpolation.

A third is the case of a triangle whose screen-space area is below the granularity of the barycentric weights. The weights quantise to the subpixel grid, so attributes are evaluated at quantised positions; for a small triangle the quantisation is a large fraction of the triangle’s extent, and shading appears not to vary across it at all. For thin, subpixel geometry this is the dominant artifact, and it is a consequence of coverage being decided at sample points rather than of the interpolation arithmetic.

The correction itself is exact for linear interpolation in clip space, which means it is exact for a projective map: a texture coordinate interpolated this way reproduces the correct sampling positions on a flat quad. It is not exact for attributes that are nonlinear in clip space, such as values computed from a curved surface approximated by the triangle. No interpolation scheme can recover a function the triangle does not carry, and the visible faceting of a low-polygon curved surface is a property of the geometry, not of the rasterizer.

References

  1. The OpenGL Graphics System: A Specification, Version 4.6 (Core Profile) — Khronos Group
  2. A Parallel Algorithm for Polygon Rasterization — Juan Pineda, SIGGRAPH 1988
  3. Real-Time Rendering, Fourth Edition — Akenine-Moller, Haines, Hoffman, CRC Press 2018