Coordinate Spaces
Follow one vertex from model space to the pixel it lands on, one stage at a time.
What you should come away with: Where each matrix in the chain hands over to the next, and what the GPU does between them.
Assumes: Projection & the Frustum. It will still make sense without it, but that one comes first.
A vertex arrives at the GPU as three numbers in a buffer and leaves as a position measured in pixels. In between it passes through six coordinate systems, and each of the six is somebody else’s decision about where the origin sits and what one unit means.
This lab follows a single vertex the whole way — the corner of a cube at (0.5, 0.5, 0.5) — through , world, view, clip, and screen space. The cube never moves and the camera being studied never moves. What changes is which of the six spaces you are standing in while you look at them.
Each matrix hands the vertex to the next#
Three matrices do the work, and each exists to take coordinates in the space the previous one produced and hand back coordinates in the next. Only the first is usually yours to write.
| x | y | z | w | |
|---|---|---|---|---|
| Model | 0.50 | 0.50 | 0.50 | 1.00 |
| World | 1.23 | 0.80 | -0.79 | 1.00 |
(0.50, 0.50, 0.50) to (1.23, 0.80, −0.79), and the world it was carried into appears around it. The cube looks smaller at the end only because the observer pulls back to fit the grid in.#Open this in the full instrument The model matrix here is a 28° turn about y followed by a move to (0.55, 0.30, −1.00), and it is the only step in the chain most programs author by hand; that matrix on its own is The Model Matrix. What follows it is the camera, and the camera is not in the scene.
| x | y | z | w | |
|---|---|---|---|---|
| World | 1.23 | 0.80 | -0.79 | 1.00 |
| View | 1.23 | 0.78 | -3.71 | 1.00 |
A view matrix is the inverse of where the camera is standing. The hardware has no notion of a camera at all, only of geometry, so “put the eye at (0, 0.9, 3) looking at (0, 0, −0.8)” is implemented by moving everything else the other way. Watch the x column across those two rows: 1.23 in world space, 1.23 in . This camera stands on the plane x = 0 and its right-hand axis is the world’s x axis, so that coordinate passes through untouched.
Through all three rows so far, w is still 1. Both matrices are a rotation and a translation; distances and angles survive them, and the cube is still a cube of side 1 that happens to be somewhere else.
w stops being 1 at clip space#
The projection matrix is the first one that is not a rigid move, and the entry responsible is m[11], which is −1. It is the only entry anywhere in the chain that makes w depend on the vertex at all, and what it puts there is the negated view-space z: the vertex’s distance in front of the eye.
| x | y | z | w | |
|---|---|---|---|---|
| View | 1.23 | 0.78 | -3.71 | 1.00 |
| Clip | 1.85 | 1.88 | 3.27 | 3.71 |
w yet, so the far face is still 4.5 times the near one — 9 units across against 2 — and the shape is still a pyramid rather than a cube.#Open this in the full instrument The vertex leaves view space at (1.23, 0.78, −3.71, 1.00) and arrives in at (1.85, 1.88, 3.27, 3.71). Read the last number of that row against the third number of the row above it. w is 3.71; view z was −3.71. Nothing has divided anything. The projection has only arranged for a later step to be possible, and parked the number that step will need where it cannot be lost.
This is also, as the name says, where clipping happens — against each vertex’s own w rather than against the ±1 cube, which does not exist yet. Two of this cube’s eight corners fail that test, coming out a little past the far plane at 4.5. The lab transforms geometry and does not clip it, so it draws them anyway — a real rasteriser would have cut those triangles here, before the divide, not after.
No matrix performs the divide#
Between clip space and NDC there is a step that no matrix in the chain performs. The hardware does it, once per vertex, after your vertex shader has returned.
| x | y | z | w | |
|---|---|---|---|---|
| Clip | 1.85 | 1.88 | 3.27 | 3.71 |
| NDC | 0.50 | 0.50 | 0.88 | — |
w — the near four by 1, which moves them not at all, the far four by 4.5, which pulls them in by that factor — and both ends land on ±1. The cube’s own corners divide by numbers between 3.26 and 4.80, which is exactly why its nearer face stays the larger one.#Open this in the full instrument A matrix applies the same linear map to every vertex it touches. Division by a number that differs from vertex to vertex is not that, and the difference is the entire reason distant things come out small. The fourth coordinate exists so that the projection can compute the divisor without performing the division: the matrix parks it in w, and the division happens later, at one fixed point in the pipeline, for everything at once.
That deferral is why gl_Position is a vec4: what a vertex shader writes is in clip space, and nothing it can write ever sees the result of the divide. In the readout, the w column goes blank at the NDC row, because once the division has happened there is nothing left to carry. Under an orthographic projection m[11] is zero, so the step runs with nothing left to do — the difference between a volume that converges and one that does not, which is Projection & the Frustum.
The viewport transform is the least mysterious step#
The last step is two lines of arithmetic, and the lab performs precisely these:
x = ((ndc.x + 1) / 2) * widthy = ((1 − ndc.y) / 2) * height
| x | y | z | w | |
|---|---|---|---|---|
| NDC | 0.50 | 0.50 | 0.88 | — |
| Screen | 719 | 149 | — | — |
The + 1 and the halving map −1…1 onto 0…1; the multiplication scales that to the . The only part worth committing to memory is the subtraction in the second line. NDC counts upwards from the bottom and a window counts downwards from the top, so y is flipped, and when a coordinate you computed lands mirrored vertically on screen, this is the line that did it.
Our vertex sits at NDC (0.50, 0.50) and lands on pixel (719, 149): three-quarters of the way across, and a quarter of the way down rather than a quarter of the way up. The z and w columns of that row are blank. Depth has not been discarded — it goes to the depth buffer, and what happens to it there is Depth & Transparency.
Now step through all six#
Everything above is one handover at a time. Below is the whole chain: six stages to walk with Back and Next, the vertex’s coordinates in all six spaces at once so you can read any row against any other, a ground grid you can switch off once it stops meaning anything, and a scene you can orbit — worth doing in clip space, where one viewpoint is not enough to see that the shape is still a pyramid. The four presets each jump to a stage and say what to look at once you are there.
- frustum
- tracked vertex
One vertex, all the way down
| Space | x | y | z | w | |
|---|---|---|---|---|---|
| Model | 0.50 | 0.50 | 0.50 | 1.00 | |
| World | 1.23 | 0.80 | -0.79 | 1.00 | |
| View | 1.23 | 0.78 | -3.71 | 1.00 | |
| Clip | 1.85 | 1.88 | 3.27 | 3.71 | w now carries depth |
| NDC | 0.50 | 0.50 | 0.88 | — | ÷ w |
| Screen | 719 | 149 | — | — | pixels |
One corner of the cube, followed the whole way. Watch w pick up the depth at the clip stage — every row after it is that same point divided by that number. The frustum you see in the canvas is not drawn separately: it is the camera’s own frustum pushed through the identical pipeline, which is why it lands exactly on the −1…1 cube at the NDC stage.
Where that leaves you
You can now name which of the six spaces a coordinate is in from the shape of the numbers alone — w still 1, w carrying the distance, ±1 on every axis, pixels counting down from the top — and say what put it there.
This did not teach you rasterisation. The lab follows one vertex to the pixel it lands on and stops; filling the triangle between three such pixels, and interpolating every varying across it with the perspective accounted for, is fixed-function work with no matrix in it and nothing a reader could move.