The idea: from triangles to pixels

A 3D scene reaches the screen as triangles. Perspective projection has already moved each vertex to a screen position (x, y) and kept its depth z. Rasterization answers two questions for every pixel: which triangles cover it, and which of those is in front. The first is answered with three edge functions, the second with a z-buffer.

A triangle v0, v1, v2 on a pixel grid. Each edge splits the plane into a side where its edge function is positive and a side where it is negative. A pixel centre inside the triangle has all three values positive; a pixel centre outside has one negative value
A pixel is covered when its centre is on the inner side of all three edges: E12, E20, E01 ≥ 0.

The page uses a 16 × 12 pixel screen, x to the right and y down. Pixel (x, y) is tested at its centre (x + 0.5, y + 0.5). Triangle A is near (z = 2) on the left and far (z = 8) on the right; triangle B is the other way round. So the two triangles cross in depth.

Edge functions

For an edge from a to b and a point p,

E(p) = (bx − ax)·(py − ay) − (by − ay)·(px − ax)

This is the 2D cross product of the edge with the vector from a to p. It is 0 on the line through the edge, positive on one side and negative on the other. With the vertices listed in the right order, the inside of the triangle is the side where all three edge functions E12 (edge v1→v2), E20 and E01 are ≥ 0. (Demo 1: edge functions, inside and outside)

pixelcentre pE12E20E01covered by A?
(4, 3)(4.5, 3.5)813021yes ✓
(12, 9)(12.5, 9.5)−399873no ✗ (outside edge v1→v2)

The triangle's doubled area is E01 evaluated at v2: 132 for A, 136 for B. The three values at any point always add up to it.

The bounding box and the row walk

A pixel can only be covered if it lies in the bounding box of the three vertices. For A that is x 1…14, y 1…10: 140 pixels to test, of which 67 are inside. The rasterizer walks the box row by row and evaluates the three edge functions at each pixel centre. (Demo 2: rasterize A row by row)

E is linear in p, so one step to the right adds the same constant −(by − ay) and one step down adds (bx − ax). Hardware never multiplies per pixel: it adds. And because every pixel is tested on its own, a GPU tests blocks of pixels (for example 2 × 2 quads or 8 × 8 tiles) in parallel.

Barycentric weights and depth

Divide the three edge values by the doubled area and you get the barycentric weights w0 = E12 / area, w1 = E20 / area, w2 = E01 / area. They are all between 0 and 1 inside the triangle and add up to 1. E12 is the weight of v0 because it measures how far p is from the opposite edge. The weights mix any vertex value; the page mixes the depths:

z = w0·z0 + w1·z1 + w2·z2

At pixel (4, 3) of A: z = (81·2 + 30·8 + 21·2) / 132 = 444 / 132 ≈ 3.4. The page interpolates z linearly in screen space. That is right for the depth a GPU stores (z/w after the perspective divide is linear on the screen); colours and texture coordinates need perspective-correct interpolation instead.

The z-buffer

The z-buffer (depth buffer) holds one depth per pixel, cleared to ∞ (the far plane). Every covered pixel, called a fragment, runs the depth test:

  • z < stored z: the fragment is nearer. Write its colour to the frame buffer and its z to the z-buffer.
  • otherwise: something nearer is already there. Drop the fragment.

The comparison is the default GL_LESS. At pixel (6, 6), A gives z = 4.0 and B gives 5.6, so A wins; at pixel (11, 5), A gives 6.4 and B gives 3.3, so B wins. Drawing B after A, the first fragment is dropped and the second is written. (Demo 6: the depth test at one pixel)

Two crossing triangles, A orange and B blue. Left, painter's algorithm drawing B last: blue covers the whole overlap, wrong where A is nearer. Right, z-buffer: the overlap is split along the line where the two depths are equal, A in front on the left, B on the right
Crossing triangles can't be sorted as wholes; the z-buffer decides per pixel, so the overlap splits where the depths are equal.

Why the order doesn't matter, and when it does

With the depth test on, every pixel ends up with the smallest z of all the fragments that reached it, and a minimum doesn't depend on the order. Drawing A then B or B then A gives the same frame buffer and the same z-buffer. (Demo 3: z-buffer, A then B and Demo 4: B then A, the same picture)

The older painter's algorithm sorts whole triangles far to near and paints them in that order, with no z-buffer. It fails here: A and B cross, so each is in front of the other somewhere and no order is right. With the depth test off the page behaves like a painter with a fixed order: the last triangle drawn covers the whole overlap, and in 19 of the 37 overlap pixels it shows the farther triangle. (Demo 5: depth test off, the last one wins)

painter's algorithmz-buffer
decidesper triangle (sort once)per pixel (compare z)
crossing triangleswrong somewhere ✗right ✓
drawing ordermust be far to nearany (for opaque triangles)
memorynone extraone depth value per pixel
transparent trianglesworks if sortedstill needs sorting

Shared edges and the tie rule

Meshes are made of triangles that share edges. A pixel centre that lies exactly on a shared edge has E = 0 for both triangles. If both drew it, it would be drawn twice (wrong with blending); if neither did, there would be a crack. So a tie counts for one side only. The page writes E = A·px + B·py + C and accepts E = 0 when A > 0, or A = 0 and B > 0. The same edge seen from the other triangle has −A and −B, so exactly one of the two gets the pixel. Direct3D and OpenGL use the same idea under the name top-left rule.

What the page leaves out

Clipping triangles against the screen and the near plane, sub-pixel vertex positions in fixed point, anti-aliasing (several samples per pixel, MSAA), perspective-correct interpolation of colours and texture coordinates, the depth precision of a 24-bit buffer and z-fighting, early-z and hierarchical z that reject whole tiles at once, back-face culling, blending of transparent triangles, and the shading that decides each fragment's colour.