The
MVP pipeline
delivers triangles to pixels, but it never decided which triangle wins when two overlap. Draw
a wall, then draw a chair behind it — without a referee, whichever you drew last paints over the other,
and the chair shows through solid stone. The referee is the z-buffer: a hidden image,
the same size as the screen, that remembers depth.
How the z-buffer decides, line by line
Step 1 — keep a depth per pixel. Alongside the colour buffer, the GPU holds a depth
buffer storing, for each pixel, the depth of the nearest surface drawn there so far. Clear it
each frame to "infinitely far", 1.0.
Step 2 — test every incoming fragment. When a triangle produces a fragment at pixel
(i, j) with depth d, compare against the stored
value d_{\text{buf}}:
\text{draw the fragment} \iff d < d_{\text{buf}}(i, j).
Step 3 — write only if closer. If the fragment is nearer, paint its colour and
overwrite d_{\text{buf}}(i, j) \leftarrow d; otherwise discard it. Order of
drawing no longer matters — the nearest surface always wins. That is correct
hidden-surface removal, automatically.
Step 4 — recall where the stored depth came from. The depth handed to the buffer is
the post-divide value. After the
perspective divide,
the stored depth is not the true distance z — it is essentially a
function of 1/z. With near and far planes
n and f, the buffer holds
d(z) = \frac{f}{f - n}\left(1 - \frac{n}{z}\right) \;\in\; [0, 1].
Step 5 — see why precision crowds near the camera. Because
d depends on 1/z, equal steps in stored depth
correspond to unequal steps in real distance. Half of all the buffer's precision is spent in
the slab between n and 2n; the entire far half of
the world shares only a sliver of depth values. Depth resolution is lavish up close and miserly far
away.
Step 6 — meet z-fighting. When two far-apart-in-the-world surfaces fall into the
same depth bucket, the test can't separate them. From frame to frame the winner flickers
between them — the shimmering, stitched seam known as z-fighting:
d(z_1) = d(z_2), \quad z_1 \ne z_2 \;\Longrightarrow\; \text{flicker}.
Step 7 — fix it by moving the planes. The crowding is driven by the ratio
f/n. Push the near plane out (raise n)
and pull the far plane in (lower f), and the
1/z curve flattens — precision spreads more evenly and the fighting stops.
The near plane does almost all the work: doubling n buys far more than
halving f.
Hidden surfaces are resolved per pixel by a stored depth:
-
The z-buffer stores the nearest depth so far at each pixel, cleared to far each
frame.
-
A fragment is drawn only if it is closer
(d < d_{\text{buf}}), giving correct hidden-surface removal regardless of
draw order.
-
Stored depth is non-linear, roughly 1/z — precision is
concentrated near the camera and sparse far away.
-
The near/far ratio f/n controls precision; too small a
near plane wastes resolution and causes z-fighting.
It feels harmless — even generous — to set the near plane to n = 0.001 so
nothing ever clips against your nose. It is the opposite of harmless. The usable depth precision
scales with the ratio f/n, and a 24-bit depth buffer has only about
16 million distinct values to spend across the whole view.
Drop n from 1 to
0.001 and you have multiplied f/n by a
thousand: the 1/z curve becomes a near-vertical cliff at the camera, almost
every depth value is hoarded in the first few centimetres, and everything beyond a metre is jammed
into a handful of buckets. Distant walls, terrain, and shadows start fighting. The cure is unglamorous
and free: choose the largest near plane you can bear and the smallest far plane that
still contains the scene. Near plane discipline is the cheapest precision you will ever buy.