Overview
Up to this point the view in every test scene has been fixed. This lab introduces
the Camera class from src/utils/Camera.h, which provides first-person
WASD movement and mouse-look using Euler angles (yaw and pitch). You will construct a camera in a
new test scene, drive it from the main loop using delta time, and connect its view matrix to the
shader uniforms. By the end you will be able to fly freely around a 3D scene at the same speed
regardless of frame rate. This camera is also used in the Maze Game series that follows (Labs 20-23).
Learning Objectives
By the end of this lab you will be able to:
- Construct a Camera object with position, world-up, yaw, pitch, and FOV parameters
- Call processInput and Update each frame to handle WASD keyboard and mouse-look input
- Retrieve the view matrix from the Camera using getViewMatrix and pass it to the shader as a uniform
- Explain why movement velocity is scaled by delta time to achieve frame-rate independence
- Describe what yaw and pitch represent and why pitch is clamped to within 89 degrees of the horizon
Concept
Without a camera system, your scene is viewed from a hardcoded position. Adding one requires solving three problems: how to represent orientation, how to derive a view matrix from it, and how to make movement consistent across different frame rates.
The Camera class stores orientation as two Euler angles: yaw
(horizontal rotation around the world Y-axis) and pitch (vertical tilt around the
camera-local X-axis). Each frame, mouse movement increments these angles. The private
UpdateCameraVectors method converts yaw and pitch into three orthonormal
basis vectors using a spherical-to-Cartesian formula: front is the
direction the camera faces, right is its local X-axis computed via a cross product
with world-up, and up is the corrected local Y-axis. Pitch is clamped to 89 degrees
in either direction to prevent the front vector from becoming parallel to world-up, which would make
the cross product undefined (a gimbal singularity).
getViewMatrix() calls glm::lookAt(position, position + front, up),
which builds the view matrix that moves the entire world so the camera sits at the origin looking
down -Z. WASD movement is handled in ProcessKeyboard by adding or subtracting
front and right scaled by movementSpeed * deltaTime.
Multiplying by delta time (the elapsed seconds since the last frame) makes
movement frame-rate independent: at 30 fps each frame lasts longer, so each step is larger,
covering the same distance per second as at 120 fps.
Integration requires two calls per frame: processInput(deltaTime) reads GLFW
keyboard and mouse state and updates position and angles, and Update(deltaTime)
handles smooth interpolation when the camera is detached for GUI use. Press Escape to release
the cursor and interact with ImGui. Press Escape again to reattach and resume flying.
Tasks
Task 1 - Construct the Camera
The diagram below shows how yaw and pitch map to the camera basis vectors
front, right, and up. Study it before writing
the constructor call.
Create a new test called TestCamera. In the constructor, declare a
std::unique_ptr<Camera> member and initialise it with
std::make_unique<Camera>. The Camera constructor signature is:
Camera(GLFWwindow* window,
glm::vec3 startPosition,
glm::vec3 startUp,
float startYaw,
float startPitch,
float startFOV,
bool invertPitch = true);
Use these starting values: position (0, 0, 3) (behind the origin),
world-up (0, 1, 0) (Y-up), yaw -180.0f (facing -Z toward the
origin), pitch 0.0f (level), and FOV 45.0f degrees.
Also enable depth testing with glEnable(GL_DEPTH_TEST) so geometry is
sorted correctly in 3D.
The skeleton below shows the header and constructor body. Fill in the
make_unique call and the depth test enable.
#pragma once
#include "Tests.h"
#include "../Shader.h"
#include "../Mesh/GeometryFactory.h"
#include "../utils/Camera.h"
#include <memory>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
namespace test {
class TestCamera : public Tests {
public:
TestCamera(GLFWwindow* window);
void Update(float deltaTime) override;
void Render() override;
void RenderGUI() override;
private:
GLFWwindow* m_window;
// TODO: declare m_Camera as a unique_ptr<Camera>
std::unique_ptr<Mesh> m_Cube;
std::unique_ptr<Shader> m_Shader;
};
}
TestCamera::TestCamera(GLFWwindow* window)
: m_window(window)
{
m_Cube = GeometryFactory::CreateCube();
m_Cube->setPosition(glm::vec3(0.5f, 0.0f, 0.0f));
// TODO: construct m_Camera using make_unique<Camera>
// Use: window, position (0,0,3), world-up (0,1,0),
// yaw -180.0f, pitch 0.0f, FOV 45.0f
m_Camera = /* TODO */;
m_Shader = std::make_unique<Shader>(R"(res/Shaders/MVPVertexColour.shader)");
// TODO: enable depth testing
}
The parameter controls left-right rotation of the camera (horizontal look).
Pitch is clamped to within degrees of the horizon to prevent a gimbal singularity.
Task 2 - Drive the Camera Each Frame
Two calls are needed every frame to keep the camera responsive. Open
testCamera.cpp and complete the Update method.
- Call
m_Camera->processInput(deltaTime)to read GLFW keyboard and mouse state and update the camera position and angles. - Call
m_Camera->Update(deltaTime)to apply smooth interpolation when the camera is detached for GUI use.
Both calls receive deltaTime (the time in seconds since the last frame).
Movement speed is multiplied by this value so the camera travels the same world-space
distance per second regardless of frame rate.
void TestCamera::Update(float deltaTime)
{
// TODO: call processInput to read WASD and mouse input
// TODO: call Update for smooth interpolation
}
A student removes deltaTime from the velocity calculation: `position += front * movementSpeed`. What is the effect at high frame rates?
Task 3 - Connect the View Matrix to the Shader
In the Render method, three matrices must be passed to the shader
as uniforms: model, view, and projection.
modelmoves the cube from local space to world space. Useglm::mat4(1.0f)for a stationary cube, or apply a rotation withglm::rotate.viewcomes fromm_Camera->getViewMatrix(). It moves the world so the camera sits at the origin.projectionis built withglm::perspectiveusing the camera FOV fromm_Camera->getFOV().
Complete the skeleton below. The shader is already bound before these calls.
void TestCamera::Render()
{
Renderer renderer;
renderer.ClearColour_White();
renderer.Clear();
m_Shader->Bind();
// Model matrix: rotate the cube slowly over time
glm::mat4 model = glm::rotate(glm::mat4(1.0f),
(float)glfwGetTime(),
glm::vec3(0.5f, 1.0f, 0.0f));
// TODO: build the projection matrix
// Use glm::perspective with the camera FOV, aspect ratio 800/600,
// near plane 0.1f and far plane 100.0f
glm::mat4 projection = /* TODO */;
// TODO: set the model uniform on m_Shader
// TODO: set the view uniform using m_Camera->getViewMatrix()
// TODO: set the projection uniform
m_Cube->Draw();
}
The view matrix transforms vertices from space into space.
The method that returns the ready-to-use view matrix from the Camera class is .
Task 4 - Camera Maths and Design
Answer the questions below to consolidate your understanding of the Camera
class internals. Use the concept section and the source files in
src/utils/Camera.h and src/utils/Camera.cpp as references.
Explain why pitch is clamped to 89 degrees rather than allowing the full 90 degrees. What mathematical operation would fail at exactly 90 degrees?
The Camera class stores yaw and pitch as Euler angles rather than a quaternion. State one practical limitation of the Euler-angle approach and describe how the code works around it.
The front vector is derived from yaw and pitch using the formulas: x = cos(pitch)*cos(yaw), y = sin(pitch), z = cos(pitch)*sin(yaw). What does the cos(pitch) factor in the x and z components do when the camera looks straight ahead (pitch = 0)?
Solution
Try the tasks above before revealing the solution.