Introduction
Hello! In our last lesson, we put all the pieces of the graphics pipeline together to render a 2D rectangle using indexed drawing. This gave us an efficient way to draw shapes by reusing vertex data.
Now, we're ready to make the leap from 2D to 3D. Your goal is to build a 3D game engine, and the most fundamental aspect of 3D graphics is creating the illusion of depth. This lesson will focus on exactly that, as we work to achieve the following outcome: Construct a perspective projection matrix to create 3D depth illusion.
We'll start by understanding the concept of a "viewing frustum"—the region of 3D space your virtual camera can see. Then, we'll derive the mathematics needed to project the 3D world onto your 2D screen, making distant objects appear smaller. Finally, you'll write a C function to construct this powerful matrix, which we will soon pass to our shaders using the uniforms you learned about previously. This lesson directly builds on the matrix math from Module 2 and the rendering pipeline from Module 4.
The Goal of Perspective Projection
In the real world, your eyes see a 3D scene, but the image formed on your retina is 2D. Your brain then interprets this 2D image to perceive depth. In computer graphics, we simulate this process. The "camera" in our game world exists in a 3D space, but our monitor is a 2D surface. Projection is the process of transforming 3D coordinates into 2D screen coordinates.
The specific type we're interested in is perspective projection, which mimics how human vision works. The key principle is that objects farther away from the camera should appear smaller.
This projection is defined by a volume in space shaped like a pyramid with its top cut off. This shape is called the viewing frustum. Anything inside this frustum will be visible on the screen; anything outside will be clipped (cut away).

The job of the projection matrix is to take every 3D point inside this frustum and transform it into a perfectly regular cube, with coordinates ranging from -1 to 1 on all axes. This standardized cube is called Normalized Device Coordinates (NDC). OpenGL then takes this NDC cube and maps it to your 2D screen to create the final image.
The perspective projection matrix accomplishes three main tasks to achieve this. Let's watch a video that introduces them.
Perspective Projection Matrix (Math for Game Developers)
This video from the pikuma channel provides an excellent overview of the three main goals of a perspective projection matrix.
Watch from 00:28 to 05:45. As you watch, take note of the three distinct responsibilities of the matrix: Accounting for the screen's aspect ratio. Accounting for the camera's field of view (FOV). Normalizing the depth (Z) values.
Deriving the Projection Matrix
Now that you have a conceptual overview, let's dive into the mathematics. A complete projection matrix looks intimidating, but it's built from smaller, understandable parts that correspond to the three tasks you just learned about. We'll use the principles of similar triangles and some algebra to derive the components.
The core idea relies on similar triangles, as shown in this diagram.

The goal is to derive a matrix that:
- Projects the
xandycoordinates using the principle of similar triangles, resulting in division by the point's depth (). - Remaps the projected
xandycoordinates from the frustum's dimensions ([left, right],[bottom, top]) to the NDC range[-1, 1]. - Remaps the original
zcoordinate from the frustum's depth range[near, far]to the NDC range[-1, 1].
The following reading from Scratchapixel provides a detailed, step-by-step derivation of the standard OpenGL projection matrix. Your background in engineering means you're used to applying mathematical models, and this is a prime example.
The OpenGL Perspective Projection Matrix
This article provides a rigorous mathematical derivation of the OpenGL perspective projection matrix. We'll walk through it section by section.
Read the sections titled 'Principle' and 'Derivation'. Don't get bogged down in memorizing every algebraic step. Instead, focus on understanding the process: For X and Y: First, they use similar triangles to get the basic projection formula (e.g., P_s_y = rac{n \cdot P_y}{-P_z}). Then, they use a series of algebraic steps (scaling and offsetting) to remap a value from a range like [l, r] to [-1, 1]. For Z: They set up a system of two equations with two unknowns (A and B) to find the coefficients that will map the original z-coordinate from the [n, f] range to [-1, 1]. This derivation directly leads to the matrix we will implement.
From Parameters to a Matrix
You saw in the derivation that the matrix is defined by six parameters: left, right, bottom, top, near, and far. However, it's often more intuitive to control the camera using a vertical field of view (FOV) and the window's aspect ratio.
The Scratchapixel article you just read has a section explaining how to calculate l, r, b, t from these more intuitive parameters. The formulas are:
Where aspect_ratio = screen_width / screen_height.
With these values calculated, we can finally populate our projection matrix. The video from earlier does a great job of showing how all these derived components come together and, most importantly, explains how the "magic" of perspective division actually happens.
Perspective Projection Matrix (Math for Game Developers)
Let's return to the pikuma video to see the final matrix assembled and to understand the crucial role of the 'w' coordinate.
Watch from 17:45 to 28:48. Focus on these key points: How the formulas for aspect ratio, FOV, and Z-normalization map directly to specific cells in the 4x4 matrix. The purpose of the -1 (or 1 in the video's convention) in the third row, fourth column: it copies the vertex's z value into the w component of the output vector. The perspective divide is not done by the matrix itself. It's a separate, automatic hardware step where OpenGL divides the x, y, and z components of the output vector by its w component. This is how the division by z is achieved.
Implementing the Perspective Matrix in C
Now it's time to turn theory into practice. We'll create a function that constructs a perspective projection matrix. This function will take the intuitive parameters (FOV, aspect ratio, near/far planes) and produce a mat4 ready to be sent to our vertex shader.
The final OpenGL-style projection matrix we derived is:
Note that this matrix is in column-major order, which is what OpenGL expects. When you store this in a C array, the element at matrix[column][row] will correspond to the mathematical notation.
Here is a C function you can add to your math library that combines the calculation of the frustum planes with the construction of this matrix.
// Add to your matrix/math header file
#include <math.h> // For tan()
void mat4_perspective(mat4 out, float fovy, float aspect, float near, float far) {
float f = 1.0f / tanf(fovy / 2.0f);
out[0][0] = f / aspect;
out[0][1] = 0.0f;
out[0][2] = 0.0f;
out[0][3] = 0.0f;
out[1][0] = 0.0f;
out[1][1] = f;
out[1][2] = 0.0f;
out[1][3] = 0.0f;
out[2][0] = 0.0f;
out[2][1] = 0.0f;
out[2][2] = (far + near) / (near - far);
out[2][3] = -1.0f;
out[3][0] = 0.0f;
out[3][1] = 0.0f;
out[3][2] = (2.0f * far * near) / (near - far);
out[3][3] = 0.0f;
}
Note: This implementation is slightly different from the raw Scratchapixel matrix but mathematically equivalent and common in many modern graphics libraries. It combines the tan() calculation directly into the matrix elements.
You can now generate the projection matrix in your main application code like this:
mat4 projection_matrix;
float fov = 45.0f; // degrees
float aspect_ratio = (float)window_width / (float)window_height;
float near_plane = 0.1f;
float far_plane = 100.0f;
// Convert fov to radians for tanf()
mat4_perspective(projection_matrix, fov * 3.14159f / 180.0f, aspect_ratio, near_plane, far_plane);
Conclusion
In this lesson, you took a major step toward rendering a full 3D scene. You have bridged the gap between 3D mathematics and the practicalities of the GPU rendering pipeline.
Key Takeaways:
- The perspective projection matrix transforms 3D points from a viewing frustum into a standardized cube called Normalized Device Coordinates (NDC).
- This matrix is constructed from intuitive parameters: Field of View (FOV), aspect ratio, and the near and far clipping planes.
- The matrix encodes the scaling and remapping operations for the x, y, and z axes.
- The actual perspective division (dividing by depth to make far objects smaller) is performed automatically by the GPU, which divides the
x, y, zof each vertex by itswcoordinate after the vertex shader runs.
Preview of the Next Lesson:
We now have the 'P' (Projection) matrix, a critical component of 3D rendering. But it's only one part of the transformation. In the next lesson, we will learn how to combine the model, view, and projection matrices into a single MVP matrix. This will allow us to take a model from its own local space, place it in the world, position the camera to look at it, and finally project it onto the screen, all in one efficient operation within the vertex shader.