Depth and the Z-Buffer

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:

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.