Clip Space

Clip space is the vertex shader's output — a 4D homogeneous space where the GPU tests −w ≤ x,y,z ≤ w and discards what fails, before any expensive per-pixel work. The ÷w that follows turns the frustum into the NDC cube.

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:

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.