Overview
In the Transformations lab (Lab 07), a single rotation matrix was uploaded as u_Transform and the GPU applied it directly to every vertex. That worked, but it left no room for a camera or any sense of depth. This lab introduces the Model-View-Projection pipeline: three distinct matrices that together move geometry from local object coordinates through world space and camera space into the clip coordinates that OpenGL rasterises. You will rename the shader uniform to u_MVP, compose the three matrices in C++, position a real camera with glm::lookAt, and switch between a perspective frustum and an orthographic box at runtime by pressing P.
Learning Objectives
By the end of this lab you will be able to:
- Explain the role of each matrix in the Model-View-Projection pipeline and why they multiply as
proj * view * model - Rename a shader uniform and update the corresponding C++ upload without changing any rendering logic
- Construct a view matrix using
glm::lookAtand describe what its three parameters represent - Build a perspective projection matrix with
glm::perspectiveand an orthographic matrix withglm::ortho - Enable depth testing and poll a GLFW key each frame to toggle the active projection at runtime
Concept
The Lab 07 solution uploaded a single rotation matrix as u_Transform and applied it directly to each vertex position. The renderer happened to work because OpenGL's default camera sits at the world origin looking down the negative Z axis and the rotating quad was already in front of it. As soon as you need to move the camera, describe a scene with multiple objects at different distances, or choose between a 2D and 3D look, a single raw transform is not enough.
The standard solution is to decompose the transform into three matrices multiplied together: MVP = Projection * View * Model. The Model matrix moves vertices from local object space into world space - it is the rotation matrix from Lab 07. The View matrix then repositions everything relative to a virtual camera. glm::lookAt(eye, centre, up) builds this matrix from three vectors: eye is the camera position in world space, centre is the point the camera aims at, and up is the world up direction (usually glm::vec3(0, 1, 0)). The Projection matrix maps camera space into clip space, the coordinate system the GPU uses before rasterisation. The vertex shader still performs one matrix multiplication: gl_Position = u_MVP * vec4(position, 1.0). Only the name and the value passed from C++ change.
glm::perspective(fov, aspect, near, far) builds a frustum where the vertical field of view angle and aspect ratio define the visible region. Perspective division during rasterisation makes distant vertices project to smaller screen positions, producing the familiar depth cue. glm::ortho(left, right, bottom, top, near, far) maps a rectangular box directly to the clip cube, skipping perspective division so object size stays constant at all depths. Orthographic mode is standard for 2D games, UI layers, and technical diagrams. Both require glEnable(GL_DEPTH_TEST) at startup and GL_DEPTH_BUFFER_BIT added to every glClear so stale depth values do not accumulate across frames.
Tasks
Task 1 - Introduce the MVP Pipeline
This task introduces the Model-View-Projection decomposition by renaming the shader uniform and refactoring the C++ upload to compose three named matrices.
- Open
res/shaders/transform.shader. Rename the uniform fromu_Transformtou_MVPand update themain()call to match:uniform mat4 u_MVP; void main() { gl_Position = u_MVP * vec4(position, 1.0); v_TexCoords = texCoords; } - In
src/main.cpp, inside the render loop, replace the single-matrix upload with three named matrices. Start with identity view and projection so the result is identical to Lab 07:glm::mat4 view = glm::mat4(1.0f); glm::mat4 proj = glm::mat4(1.0f); glm::mat4 mvp = proj * view * model; glUseProgram(shader); glUniformMatrix4fv(glGetUniformLocation(shader, "u_MVP"), 1, GL_FALSE, glm::value_ptr(mvp)); - Build and run. The quad should spin identically to Lab 07. The behaviour has not changed; only the structure has been made explicit.
- Now enable depth testing. After
glewInit()and before the vertex data, callglEnable(GL_DEPTH_TEST). Inside the loop, addGL_DEPTH_BUFFER_BITto theglClearcall. - Change the rotation axis from Z to Y so depth effects will be visible once the camera is real:
model = glm::rotate(model, angle, glm::vec3(0.0f, 1.0f, 0.0f)); - Replace the identity view matrix with a
glm::lookAtcall. Place the camera 3 units back along Z, aimed at the origin, with Y as the up direction. - Build and run. The quad should now appear smaller (it is 3 units away) and its Y-axis spin should look convincingly 3D even though projection is still identity.
// After glewInit()
// TODO: glEnable(GL_DEPTH_TEST);
// --- Inside the render loop ---
glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
// TODO: add GL_DEPTH_BUFFER_BIT
glClear(GL_COLOR_BUFFER_BIT);
float angle = (float)glfwGetTime();
glm::mat4 model = glm::mat4(1.0f);
// TODO: change rotation axis from Z to Y: glm::vec3(0.0f, 1.0f, 0.0f)
model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f));
// TODO: replace identity view with glm::lookAt
// eye = glm::vec3(0.0f, 0.0f, 3.0f)
// centre = glm::vec3(0.0f, 0.0f, 0.0f)
// up = glm::vec3(0.0f, 1.0f, 0.0f)
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 proj = glm::mat4(1.0f);
// TODO: compose and upload
glm::mat4 mvp = proj * view * model;
glUseProgram(shader);
glUniformMatrix4fv(glGetUniformLocation(shader, "u_MVP"), 1, GL_FALSE, glm::value_ptr(mvp));
Task 2 - Add Perspective Projection
With the camera positioned, replace the identity projection with a perspective frustum. glm::perspective takes a vertical field of view in radians, an aspect ratio, and near and far clipping distances. Use glm::radians to convert a degree value to radians.
- Inside the render loop, replace
glm::mat4 proj = glm::mat4(1.0f);with aglm::perspectivecall. Use a 45-degree vertical field of view, the window aspect ratio (800.0f / 600.0f), and clipping planes at 0.1 and 100. - Build and run. The quad should recede convincingly when its far edge swings away from the camera. Notice how the near edge appears larger than the far edge.
- Experiment: change the field of view to 90 degrees and observe the wider, more distorted view. Change it back to 45 before continuing.
// --- Inside the render loop, after the view matrix ---
// TODO: replace the identity projection with glm::perspective
// fov = glm::radians(45.0f)
// aspect = 800.0f / 600.0f
// near = 0.1f
// far = 100.0f
glm::mat4 proj = glm::mat4(1.0f);
glm::mat4 mvp = proj * view * model;
glUseProgram(shader);
glUniformMatrix4fv(glGetUniformLocation(shader, "u_MVP"), 1, GL_FALSE, glm::value_ptr(mvp));
Task 3 - Add an Orthographic Toggle
Orthographic projection removes depth distortion entirely. Objects at any depth appear the same size, which is the standard mode for 2D games, UI layers, and technical diagrams. Add a boolean flag and a key-edge detector so pressing P switches projection modes at runtime.
- Before the render loop, declare two variables:
bool useOrtho = false; bool pWasDown = false; - At the top of the loop body, detect a fresh press of the P key. A fresh press means the key is down this frame but was up the previous frame:
bool pDown = glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS; if (pDown && !pWasDown) { useOrtho = !useOrtho; std::cout << (useOrtho ? "Orthographic\n" : "Perspective\n"); } pWasDown = pDown; - Replace the single
projline with anif/elseblock. For orthographic mode, match the aspect ratio by setting horizontal extents to -1.33 and 1.33 (800 / 600 times the vertical half-extent of 1.0). Keep the perspective call from Task 2 in the else branch. - Build and run. Press P while the quad spins and compare the two modes. In perspective mode the near edge appears noticeably larger. In orthographic mode both edges stay the same width throughout the rotation.
// Before the render loop
bool useOrtho = false;
bool pWasDown = false;
// --- Inside the render loop ---
bool pDown = glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS;
if (pDown && !pWasDown) {
useOrtho = !useOrtho;
std::cout << (useOrtho ? "Orthographic\n" : "Perspective\n");
}
pWasDown = pDown;
// TODO: replace the single proj line with an if/else block
// if (useOrtho)
// proj = glm::ortho(-1.33f, 1.33f, -1.0f, 1.0f, 0.1f, 100.0f)
// else
// proj = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f)
glm::mat4 proj = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
Solution
Try the tasks above before revealing the solution.