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:
- Upgrade vertex data from 2D to 3D coordinates and update the corresponding vertex attribute pointers
- Declare and apply a
mat4transformation uniform in a GLSL vertex shader - Build a rotation matrix using
glm::rotateand understand howglm::translateandglm::scalefollow the same pattern - Upload a matrix to the GPU with
glUniformMatrix4fvandglm::value_ptr - Drive continuous animation by reading elapsed time with
glfwGetTime()
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.
- Open
src/main.cppand find theverticesarray. Insert0.0fas a third coordinate after each x, y pair so every vertex has the layout: x, y, z, u, v. - Locate the
glVertexAttribPointercall for attribute 0 (position). Change the component count from2to3. Update the stride from4 * sizeof(float)to5 * sizeof(float). - For attribute 1 (UV), the component count stays at
2. Update the stride to5 * sizeof(float)and the offset to(void*)(3 * sizeof(float)). The UV data now starts three floats into each vertex instead of two. - Build and run. The textured quad should display exactly as it did in the Textures lab (Lab 06) before any rotation is added.
// 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.
- Create a new file
res/shaders/transform.shader. - In the
#shader vertexsection, change the position attribute type fromvec2tovec3at location 0. - Before
main(), declareuniform mat4 u_Transform;. - In
main(), replacevec4(position, 0.0, 1.0)withu_Transform * vec4(position, 1.0). The fourth component1.0marks the vertex as a point so translation is applied correctly. - Copy the
#shader fragmentsection fromres/shaders/texture.shaderunchanged. The fragment stage does not need to know about the transformation. - In
src/main.cpp, update theparseShadercall to load"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.
- 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> - 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)); - Upload the matrix to the shader. Pass
GL_FALSEfor 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)); - Build and run. The textured quad should spin continuously around the Z axis.
- Experiment: try changing the rotation axis to
glm::vec3(1.0f, 0.0f, 0.0f)to spin around X, or add aglm::translatecall beforeglm::rotateto orbit the quad around the centre of the screen.
// --- 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.