Projection & the Frustum
See the camera’s frustum as an object in the world, and the picture it produces, side by side.
What you should come away with: What the perspective divide actually does, and why near and far are a hard clip rather than a fade.
Assumes: The Model Matrix. It will still make sense without it, but that one comes first.
A camera in a renderer is not a lens and not a position. It is a volume. The has already moved the world so that the camera stands at the origin looking down its own −z; what the projection matrix adds is a shape — a bounded region of the space in front of that origin — and a rule for squashing whatever is inside it into the same cube every time, [-1, 1] on all three axes.
Everything inside the volume is kept and everything outside it is thrown away, by comparisons rather than by fading. The lab below draws that volume as an object in the world it is clipping, and underneath it the picture the same camera produces. Two panels, because the interesting thing about a is that it is a shape, and a shape is the one thing you cannot see from inside.
The frustum is an object, not a setting#
The wireframe in the outside view is not a drawing of the field of view. It is built the other way round: take the eight corners of the clip cube, the points (±1, ±1, ±1), and push them backwards through the inverse of the projection and view matrices. Wherever those eight corners land is the region that survives, so the wireframe cannot disagree with the clipping — it is the clipping, drawn. Move the slider and watch the shape rather than the boxes.
The four faint lines converging outside the near rectangle are the eye rays, and where they meet is the camera. A frustum is a pyramid with its tip cut off, and the tip is cut off at exactly the ; the rays show you the apex the volume would have had. In they lie along the frustum’s own side edges, because those edges pass through the eye.
Field of view sets the angle of that pyramid and nothing else. Widening it fits more of the world into the same rectangle of pixels, which is the same statement as everything in the picture getting smaller. Narrowing it crops, and it crops the near boxes first: an object close to the camera covers a much wider angle than the same object further off, so the near ones are the first to fall outside a narrow cone.
The angle the slider sets is the vertical one. The horizontal opening is that same angle stretched by the aspect ratio, held at 16:10 throughout this lab because the shape of the picture is not the thing under study. In the projection matrix printed further down the page it is the difference between the first entry, 1.340, and the second, 2.145 — one number, divided by 1.6 in x.
Near and far are a test, not a fade#
The clip test has no falloff in it and no distance term. A point in is kept when each of x, y and z lies between −w and +w: six comparisons and a boolean. The outside view runs those same six comparisons on the CPU, once per box, which is why a box the camera is about to lose goes dim in the world panel before it vanishes from the picture.
World · the volume
Camera · the picture
Near is the plane people set to 0.01 without thinking, and it is the most expensive number in the projection. Depth is not spread evenly across the volume: at the default near of 1.5 and far of 11, half of the depth buffer’s range is used up by 2.6 units out, and dropping near to 0.1 pulls that halfway mark in to 0.2. What the rest of the scene is left to share, and what happens to two surfaces sharing too little of it, is Depth & Transparency — where the fix is this plane and not anything in the model.
The far plane is the cheap one by comparison; it is already doing work you can see, since two of the six boxes are missing from the picture before you touch anything. They stand 12.8 and 15.8 units out, and far is 11. The lab will not let near reach far — the handler pushes whichever plane you are not dragging half a unit out of the way — because at near === far the projection divides by zero and the scene goes with it. Near cannot be zero either, for a quieter reason: at near = 0 every depth in the scene maps to the same value, and the depth buffer stops being able to tell anything from anything.
w carries the distance, and the hardware divides by it#
Open the vertex shader in the source panel at the foot of the lab. It ends with gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0) and there is no division in it anywhere. What comes out is clip space — four numbers, w among them, and the perspective not yet applied.
The bottom row of the perspective matrix reads 0 0 −1 0. Dot that row with (x, y, z, 1) and it computes −z: w comes out as the distance the point stands in front of the camera, measured along the camera’s forward axis. Then, between the vertex shader and the , the hardware divides x, y and z by w. Dividing by the distance is the whole of perspective. The rest of the matrix is framing.
| 1.340 | 0.000 | 0.000 | 0.000 |
| 0.000 | 2.145 | 0.000 | 0.000 |
| 0.000 | 0.000 | -1.316 | -3.474 |
| 0.000 | 0.000 | -1.000 | 0.000 |
0 0 −1 0 to 0 0 0 1.#Open this in the full instrument Nothing in the upper three rows could have done that, and no matrix anywhere in the chain performs the division. The matrix’s whole contribution is to have the right number waiting in w when the hardware arrives. Where that step sits between the others — clip space, the divide, , then the viewport transform that turns ±1 into pixels — is walked a vertex at a time in Coordinate Spaces.
Orthographic deletes the distance#
The matrix keeps the identity’s bottom row, 0 0 0 1, so w comes out as 1 for every vertex and the divide divides by one. Nothing shrinks with distance because nothing consults the distance. The volume changes shape to match: with nothing converging, the near rectangle and the far rectangle are the same size, and what is left is not a frustum at all but a box.
Losing the divide costs the picture its depth cue and buys back a guarantee: parallel edges stay parallel, and a measurement taken on screen means the same thing wherever on screen it is taken. That is worth more than realism to a CAD drawing, to an isometric game that wants a tile at the back of the board to match a tile at the front, and to a shadow map for a directional light, which has no position for anything to converge on. For a camera it looks wrong, and the reason it looks wrong sits in the bottom row.
Now move all of it at once#
Each figure above moved one control. Below, the camera has all of them live — both modes, the angle or the height, both planes, and the frustum drawn or hidden — with the projection matrix printed beside it to three decimals. Drag the top canvas to orbit: the picture underneath does not change while you do, because orbiting moves the viewpoint you are watching from and not the camera being studied. Each preset lands on a state worth looking at, and says what to look at once it does.
- frustum
- kept
- clipped
The projection matrix
| 1.340 | 0.000 | 0.000 | 0.000 |
| 0.000 | 2.145 | 0.000 | 0.000 |
| 0.000 | 0.000 | -1.316 | -3.474 |
| 0.000 | 0.000 | -1.000 | 0.000 |
The −1 in the bottom row is the whole trick. It copies −z into w, and the divide the GPU performs after your vertex shader — x/w, y/w — is what makes distant things small. Switch to orthographic and that entry becomes 0: w stays 1, nothing shrinks.
Where that leaves you
You can now say what a projection matrix contributes and what it leaves to the hardware — it parks the distance in w, and the divide happens later — and predict which boxes leave the picture first when the field of view narrows.
This did not teach you the view matrix. The camera here is already standing at the origin looking down its own −z before the projection is reached, so aiming a camera, and the fact that doing so means moving the world the other way instead, is one handover earlier: the world-to-view step in Coordinate Spaces.