Overview
This worksheet demystifies the projection matrix. You will learn what each frustum parameter controls geometrically, carry a view-space point through the projection matrix by hand, perform the perspective divide, and explain why a large near/far ratio degrades depth precision in a 24-bit depth buffer.
Learning Objectives
By the end of this lab you will be able to:
- State what each parameter of glm::perspective controls geometrically
- Carry a view-space point through the projection matrix and perspective divide by hand to obtain NDC coordinates
- Explain why w_clip = -z_view enables perspective and produces the size-with-distance effect
- Explain why a small near/far ratio gives better depth precision than a large one
Concept
How does a 3D point become a 2D pixel coordinate with objects appearing smaller in the distance? Scaling alone cannot explain the effect. Something must divide by depth. The projection matrix sets up that division but does not perform it. The hardware divides as a separate step after the vertex shader finishes. Understanding the two-step process - matrix then divide - is the key to understanding the w component and depth buffer precision.
The projection matrix encodes the frustum shape defined by field of view, aspect ratio, near plane, and far plane. Multiplying a view-space point by it produces a clip-space vector where w_clip = -z_view. The hardware then divides x, y, z by w to get NDC. Because w carries depth, dividing by it causes far objects to have a larger divisor and thus appear smaller - that is perspective. NDC depth is not linear between near and far. Precision concentrates near the near plane, compressing rapidly toward the far plane.
In GLM, the projection matrix is created with glm::perspective(glm::radians(fov), aspect, near, far). A small near/far ratio spreads depth precision evenly. A very large ratio (such as near = 0.01, far = 10000) compresses nearly all precision into a thin band close to the camera, causing z-fighting on distant geometry even with a 24-bit depth buffer.
Tasks
Task 1 - The Projection Matrix and Its Parameters
Using FoV = 90 degrees, aspect = 1.0, near = 1.0, far = 10.0. At 90 degrees, FoV/2 = 45 degrees and tan(45) = 1, so the focal length f = 1/tan(FoV/2) = 1. These values produce clean numbers.
The key entries of the projection matrix and what they mean:
f = 1 / tan(FoV/2) = 1 / tan(45 deg) = 1
M[0][0] = f / aspect = 1 / 1.0 = 1 scales x by focal length, corrects for aspect ratio
M[1][1] = f = 1 scales y by focal length
M[2][2] = -(far+near)/(far-near) maps z_view into a depth value
= -(10+1)/(10-1) = -11/9 approx -1.222
M[3][2] = -(2*far*near)/(far-near) scale of the depth mapping
= -(2*10*1)/(10-1) = -20/9 approx -2.222
M[2][3] = -1 copies -z_view into w_clip (enables perspective divide)
Full matrix (column-major display):
| 1 0 0 0 | | 0 1 0 0 | | 0 0 -11/9 -20/9 | | 0 0 -1 0 |
The entry M[2][3] = -1 is the most important. It causes w_clip = -z_view. For visible geometry in OpenGL, z_view is negative (the camera looks down -Z), so w_clip = -z_view is positive. After dividing by w_clip, NDC values land in [-1, 1] for geometry within the frustum.
For FoV = 90 degrees, tan(FoV/2) = tan(45 deg) = , so f = 1.
The matrix entry M[2][3] = causes w_clip = -z_view.
For visible geometry, z_view is (sign), so w_clip is positive.
With near=1 and far=10: M[2][2] = -(far+near)/(far-near) = -(10+1)/(10-1) = /9
Task 2 - Carry a Point to NDC by Hand
Use the projection matrix from Task 1 (FoV=90, aspect=1, near=1, far=10). Starting view-space point: v = (2, 1, -3, 1). z_view = -3 is between near=1 and far=10, so this point is visible.
Clip-space components are computed as matrix rows dotted with v = (2, 1, -3, 1):
x_clip = (1)(2) + (0)(1) + (0)(-3) + (0)(1) y_clip = (0)(2) + (1)(1) + (0)(-3) + (0)(1) z_clip = (0)(2) + (0)(1) + (-11/9)(-3) + (-20/9)(1) w_clip = (0)(2) + (0)(1) + (-1)(-3) + (0)(1)
Fill in the results, then divide by w_clip to get NDC.
x_clip = (1)(2) =
y_clip = (1)(1) =
z_clip = (-11/9)(-3) + (-20/9)(1) = 33/9 - 20/9 = 13/9 approx (to 3 decimal places)
w_clip = (-1)(-3) =
NDC_x = x_clip / w_clip = 2 / 3 approx (to 3 decimal places)
NDC_y = y_clip / w_clip = 1 / 3 approx (to 3 decimal places)
NDC_z = z_clip / w_clip = (13/9) / 3 = 13/27 approx (to 3 decimal places)
Are all three NDC values in the range [-1, 1]? Is the point ?
Task 3 - Depth Precision
Two scenes use the same FoV and geometry but different near/far planes: Scene A has near=1, far=10. Scene B has near=0.01, far=10000. A wall at z=-9 and another at z=-9.01 must be distinguishable in a 24-bit depth buffer. In which scene is this more likely to fail, and why?
Without changing FoV or the scene geometry, what single parameter change most effectively reduces z-fighting on distant objects?
Solution
Try the tasks above before revealing the solution.