Rasterization and the depth buffer

03-rasterization-and-depth.md · updated 2026-10-15

Rasterization is where a triangle stops being three points and becomes pixels; the depth buffer stops those pixels stacking up in the wrong order. Both are fixed hardware you never write, but their rules leak into your results as shimmer, missing faces and edges one pixel off.

Coverage: three inside tests

For each pixel centre, ask which side of each edge it falls on. Inside means all three agree.

Each edge gives an edge function: a cheap cross product whose sign says left or right. It's linear, so the hardware evaluates it once and then steps it by addition — which is why rasterizers walk pixels in tiles, often 8×8. Whole tiles get accepted or rejected at once, and neighbouring pixels share cache lines and shading groups.

A pixel centre landing exactly on a shared edge would be claimed by both triangles or neither, so the top-left rule gives it to whichever triangle has that edge as a top or left edge. No seams, no double shading.

Barycentrics

Every covered pixel gets three weights summing to 1 — its recipe of the three vertices.

They are how everything the vertex shader produced reaches the fragment shader: colour, UV, normal, depth, all just w0·a + w1·b + w2·c.

Interpolating straight across the screen is wrong in 3D, though, because equal steps on screen aren't equal steps along a receding surface. Perspective-correct interpolation divides each attribute by w, interpolates that and 1/w, then divides back out per pixel. Skip it and floor textures visibly bend — the PlayStation-1 wobble.

The depth buffer

One depth per pixel; a fragment gets in only if it beats what is there.

Fail the comparison and the fragment is discarded; pass, and (if depth writes are on) it replaces the stored value. That makes opaque geometry order-independent. Early-z runs the test before the fragment shader, so hidden fragments cost nothing — free, unless the shader uses discard or writes its own depth, which forces the test late and makes you pay for the shading.

Precision and z-fighting

Stored depth is roughly 1/z, so the first few metres get nearly all the precision and distant surfaces share a handful of values.

Two surfaces on the same stored value are separated only by rounding noise, which flips pixel to pixel: z-fighting. Fixes, in order: push the near plane out, don't model coplanar surfaces, bias decals — and use reversed-Z, mapping near to 1 and far to 0 so float's dense region near zero lines up with 1/z's coarse one. On a float depth buffer it is free and near-total.

Backface culling

The sign of a triangle's screen-space area says which way it faces.

Winding after projection tells the hardware a face points away, so half of a closed mesh is dropped before rasterization. Free performance — and free confusion when a negative scale flips the winding and surfaces vanish.

Demo

Drag the triangles, watch the coverage — big pixels, barycentric colour, a near-coplanar second triangle, and sliders for depth precision and reversed-Z so z-fighting appears and disappears on demand.

Gotchas

Symptom Cause
Shimmering speckle where two surfaces meetZ-fighting: depths too close to separate at this precision
Textures swim or bend on a receding floorInterpolation that isn't perspective-correct
Half the model missing, interior visibleBackface culling with reversed winding — negative scale or a mirrored transform
Hairline gaps along shared edgesNo fill rule (hand-rolled rasterizer), or vertices not exactly shared
Adding discard tanks performanceEarly-z disabled for the draw; the shader now runs on hidden fragments
Transparent objects vanish behind each otherDepth writes left on for blended geometry
Decal flickers against the wall it sits onCoplanar depth; needs a depth bias or its own pass

Open the demo →

← all notes & demos