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

Lab 07 - Transformations

Overview

Building on the Textures lab (Lab 06), this lab introduces transformation matrices using the GLM library. You will upgrade the textured quad from 2D to 3D vertex positions, write a new shader that accepts a mat4 uniform, and use GLM's transform functions to make the quad rotate continuously. By the end of this lab the GPU is applying a matrix to every vertex each frame, which is the foundation for everything that follows.

Learning Objectives

By the end of this lab you will be able to:

Concept

Without transformation matrices, every vertex must be placed manually in Normalised Device Coordinates (NDC). Moving or rotating an object means recalculating and re-uploading every vertex on the CPU each frame. As a scene grows this becomes unworkable.

The solution is to encode the transformation as a 4x4 matrix and let the GPU apply it to every vertex inside the shader. The vertex shader receives the matrix as a uniform mat4 and multiplies it by each incoming position: gl_Position = u_Transform * vec4(position, 1.0). The fourth component 1.0 marks the vertex as a point so that translation takes effect correctly. A value of 0.0 would mark a direction vector, which is unaffected by translation.

GLM provides the C++ types and functions for this on the application side. glm::mat4 stores a 4x4 floating-point matrix in column-major order, which is the layout OpenGL expects. glm::mat4(1.0f) constructs an identity matrix that leaves vertices unchanged and is the safe starting value for any transform. glm::rotate(mat, angle, axis) returns a new matrix with a rotation appended around the given axis vector. glm::translate and glm::scale follow exactly the same pattern. To send the finished matrix to the shader, call glGetUniformLocation(shader, "u_Transform") to find the uniform slot, then glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(model)) to transfer the 16 floats. Pass GL_FALSE for the transpose argument because GLM already stores data in column-major order.

Tasks

Task 1 - Upgrade to 3D Vertex Positions

The textured quad from the Textures lab (Lab 06) stores each vertex as two position floats followed by two UV floats. Adding a z coordinate upgrades the format to five floats per vertex and makes the positions compatible with a 4x4 transformation matrix. The vertex attribute pointers must be updated to reflect the new stride and offset.

  1. Open src/main.cpp and find the vertices array. Insert 0.0f as a third coordinate after each x, y pair so every vertex has the layout: x, y, z, u, v.
  2. Locate the glVertexAttribPointer call for attribute 0 (position). Change the component count from 2 to 3. Update the stride from 4 * sizeof(float) to 5 * sizeof(float).
  3. For attribute 1 (UV), the component count stays at 2. Update the stride to 5 * sizeof(float) and the offset to (void*)(3 * sizeof(float)). The UV data now starts three floats into each vertex instead of two.
  4. Build and run. The textured quad should display exactly as it did in the Textures lab (Lab 06) before any rotation is added.
src/main.cpp
// Each vertex now has 3D position (x, y, z) followed by UV coordinates (u, v)
float vertices[] = {
    -0.5f, -0.5f,  0.0f,  0.0f, 0.0f,  // bottom-left
     0.5f, -0.5f,  0.0f,  1.0f, 0.0f,  // bottom-right
     0.5f,  0.5f,  0.0f,  1.0f, 1.0f,  // top-right
    -0.5f,  0.5f,  0.0f,  0.0f, 1.0f   // top-left
};

// TODO: update glVertexAttribPointer for attribute 0
//       change component count from 2 to 3
//       update stride from 4 * sizeof(float) to 5 * sizeof(float)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);

// TODO: update glVertexAttribPointer for attribute 1
//       update stride to 5 * sizeof(float)
//       update byte offset to (void*)(3 * sizeof(float))
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));

Task 2 - Write the Transformation Shader

The vertex shader needs a uniform mat4 u_Transform input. Multiplying this matrix by the homogeneous vertex position replaces the fixed NDC output with a transformed clip-space coordinate, allowing the GPU to move and rotate the geometry each frame without touching the vertex buffer.

  1. Create a new file res/shaders/transform.shader.
  2. In the #shader vertex section, change the position attribute type from vec2 to vec3 at location 0.
  3. Before main(), declare uniform mat4 u_Transform;.
  4. In main(), replace vec4(position, 0.0, 1.0) with u_Transform * vec4(position, 1.0). The fourth component 1.0 marks the vertex as a point so translation is applied correctly.
  5. Copy the #shader fragment section from res/shaders/texture.shader unchanged. The fragment stage does not need to know about the transformation.
  6. In src/main.cpp, update the parseShader call to load "res/shaders/transform.shader".
res/shaders/transform.shader
#shader vertex
#version 430 core
// TODO: change vec2 to vec3 for the position attribute
layout(location = 0) in vec2 position;
layout(location = 1) in vec2 texCoords;

out vec2 v_TexCoords;

// TODO: declare uniform mat4 u_Transform

void main() {
    // TODO: replace vec4(position, 0.0, 1.0) with u_Transform * vec4(position, 1.0)
    gl_Position = vec4(position, 0.0, 1.0);
    v_TexCoords = texCoords;
}

#shader fragment
#version 430 core
out vec4 FragColour;

in vec2 v_TexCoords;
uniform sampler2D u_Texture;

void main() {
    FragColour = texture(u_Texture, v_TexCoords);
}

Task 3 - Animate with the Rotation Matrix

With the shader ready, the C++ side must build a rotation matrix each frame and upload it as the u_Transform uniform. GLM provides the matrix types and transform functions. The animation is driven by glfwGetTime(), which returns elapsed seconds as a double.

  1. Add three GLM includes near the top of src/main.cpp, after the existing headers:
    #include <glm/glm.hpp>
    #include <glm/gtc/matrix_transform.hpp>
    #include <glm/gtc/type_ptr.hpp>
  2. Inside the render loop, before the draw call, read the elapsed time and build the rotation matrix. The angle increases by one radian per second:
    float angle = (float)glfwGetTime();
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f));
  3. Upload the matrix to the shader. Pass GL_FALSE for the transpose argument. GLM stores data in column-major order, which is exactly the layout OpenGL expects:
    glUseProgram(shader);
    glUniformMatrix4fv(glGetUniformLocation(shader, "u_Transform"), 1, GL_FALSE, glm::value_ptr(model));
  4. Build and run. The textured quad should spin continuously around the Z axis.
  5. Experiment: try changing the rotation axis to glm::vec3(1.0f, 0.0f, 0.0f) to spin around X, or add a glm::translate call before glm::rotate to orbit the quad around the centre of the screen.
src/main.cpp
// --- Inside the render loop, before the draw call ---

float angle = (float)glfwGetTime();

// TODO: create an identity matrix as the starting point
glm::mat4 model = glm::mat4(1.0f);

// TODO: apply a rotation around the Z axis
//       model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f));

// TODO: upload model to the u_Transform uniform
//       glUseProgram(shader);
//       glUniformMatrix4fv(glGetUniformLocation(shader, "u_Transform"), 1, GL_FALSE, glm::value_ptr(model));
glUseProgram(shader);
int loc = glGetUniformLocation(shader, "u_Transform");
// glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(model));

Solution

Try the tasks above before revealing the solution.