Core Idea
Clip space is what the vertex shader hands back — gl_Position, a 4D homogeneous vector (x, y, z, w). Here the GPU decides what is visible and throws the rest away before any per-pixel work. A vertex survives if and only if:
−w ≤ x ≤ w −w ≤ y ≤ w −w ≤ z ≤ w
Drag the triangle above across the boundary: each vertex carries its own w, the test flips per axis, and the triangle is actually cut at the frustum wall — the GPU manufactures new vertices on the boundary, exactly as the demo does.
Why Test Before Dividing?
w holds the vertex’s camera-space depth — the projection matrix puts it there, so farther vertices get a bigger w. The GPU could divide first and test against ±1, but:
- comparisons are cheaper than divisions, and clipping discards geometry before paying for it,
- a vertex near the camera has
w ≈ 0— dividing first would be numerically unstable.
So: test in clip space, divide only the survivors.
÷w — The Squeeze Into NDC
After clipping, the GPU divides automatically:
NDC = (x/w, y/w, z/w) → everything visible fits [−1, 1]³
This one division is why distant things are small: a far vertex has a large w, so its x and y shrink toward the centre. It is also why the frustum — an awkward truncated pyramid — becomes a trivially testable cube (the right panel above). Testing “inside a skewed pyramid” is complex; testing three coordinates against ±1 is nothing. The cube is called the canonical view volume; clip space is the 4D space before the divide, NDC the 3D space after.
From NDC, gl.viewport(x, y, width, height) maps linearly to pixels:
pixel.x = (ndc.x + 1) / 2 · width
pixel.y = (ndc.y + 1) / 2 · height
All the hard work happened in resolution-independent space; the viewport mapping is the only place pixels exist.
The Depth Buffer
ndc.z isn’t discarded — it’s written to the depth buffer. Each pixel remembers the depth of what it shows; a later fragment only wins if it’s closer. Correct occlusion, no sorting.
But the buffer’s precision is nonlinear — dense near the camera, starved at the far end — and two surfaces that land in the same quantized bucket cannot be ordered: they flicker. That’s z-fighting, and the biggest lever against it is the near plane:
Pull the near plane outward and watch the far half of the range regain buckets — which is why the standing advice is to keep the near/far ratio as tight as the scene allows (near = 0.001 is how z-fighting is made).
Connections
Clip space is reached by the projection matrix — see projection for what perspective vs orthographic encode, and coordinate spaces for the full journey around it. The w machinery is homogeneous coordinates doing its second job (the first is making translation a matrix multiply). The viewport call and the raster steps that follow belong to WebGL.