Rasterization and the depth buffer
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 meet | Z-fighting: depths too close to separate at this precision |
| Textures swim or bend on a receding floor | Interpolation that isn't perspective-correct |
| Half the model missing, interior visible | Backface culling with reversed winding — negative scale or a mirrored transform |
| Hairline gaps along shared edges | No fill rule (hand-rolled rasterizer), or vertices not exactly shared |
Adding discard tanks performance | Early-z disabled for the draw; the shader now runs on hidden fragments |
| Transparent objects vanish behind each other | Depth writes left on for blended geometry |
| Decal flickers against the wall it sits on | Coplanar depth; needs a depth bias or its own pass |