University of Wales Trinity Saint David
ACCA6021 - Graphics and Games Programming

Lab 08 - Projections

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:

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.

  1. Open res/shaders/transform.shader. Rename the uniform from u_Transform to u_MVP and update the main() call to match:
    uniform mat4 u_MVP;
    
    void main() {
        gl_Position = u_MVP * vec4(position, 1.0);
        v_TexCoords = texCoords;
    }
  2. 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));
  3. Build and run. The quad should spin identically to Lab 07. The behaviour has not changed; only the structure has been made explicit.
  4. Now enable depth testing. After glewInit() and before the vertex data, call glEnable(GL_DEPTH_TEST). Inside the loop, add GL_DEPTH_BUFFER_BIT to the glClear call.
  5. 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));
  6. Replace the identity view matrix with a glm::lookAt call. Place the camera 3 units back along Z, aimed at the origin, with Y as the up direction.
  7. 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.
src/main.cpp
// 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.

  1. Inside the render loop, replace glm::mat4 proj = glm::mat4(1.0f); with a glm::perspective call. Use a 45-degree vertical field of view, the window aspect ratio (800.0f / 600.0f), and clipping planes at 0.1 and 100.
  2. 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.
  3. Experiment: change the field of view to 90 degrees and observe the wider, more distorted view. Change it back to 45 before continuing.
src/main.cpp
// --- 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.

  1. Before the render loop, declare two variables:
    bool useOrtho  = false;
    bool pWasDown  = false;
  2. 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;
  3. Replace the single proj line with an if/else block. 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.
  4. 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.
src/main.cpp
// 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.