The GLM Library
Install and start using GLM, the popular mathematics library for C++ graphics programming.
In the previous lesson, we explored the theory behind transformation matrices and homogeneous coordinates. We saw how a simple grid of numbers can represent complex movements, rotations, scaling operations, and more.
While we could write our own C++ classes to handle 3x3 or 4x4 matrices, implementing matrix multiplication and linear algebra from scratch is error-prone and tedious.
In this lesson, we will introduce GLM (OpenGL Mathematics), the most popular C++ library for graphics software. We will cover how to install it using CMake, explore its fundamental types like vec3 and mat3, and understand how it handles memory layout through column-major ordering.
What is GLM?
GLM stands for OpenGL Mathematics. It is a C++ library written to provide vector and matrix classes that function almost exactly like GLSL (OpenGL Shading Language). GLSL is the language used to write code that runs directly on the Graphics Processing Unit (GPU).
Because GLM mirrors this language, it has become the standard for C++ graphics programming. Even though we are using SDL3 and software rendering (CPU) for now, the math concepts and syntax we learn here will apply directly if you later move to hardware-accelerated APIs like DirectX, Vulkan, or Metal.
GLM is a header-only library. This means there is no library file to compile or link. The entire library consists of header files with .hpp extensions. To use it, we simply need to tell our compiler where these header files are located.
Installing GLM
You can download the library manually from the GLM GitHub page, which also includes installation instructions.
If you're using CMake, you can use FetchContent to automatically download GLM from its official repository and make it available to the project:
CMakeLists.txt
# ...
include(FetchContent)
FetchContent_Declare(
glm
GIT_REPOSITORY https://github.com/g-truc/glm.git
GIT_TAG 1.0.2
)
FetchContent_MakeAvailable(glm)
# ...Then, we need to link our executable to the glm library. For simple projects, linking to the "header-only" library tends to be easier:
CMakeLists.txt
# ...
add_executable(Game
src/main.cpp
# ... other source files
)
target_link_libraries(Game PRIVATE
SDL3::SDL3
SDL3_image::SDL3_image
SDL3_ttf::SDL3_ttf
glm::glm-header-only
)Basic Vector Types
To use the core features of GLM, we include the main header:
#include <glm/glm.hpp>GLM provides vector types that are very similar to the custom Vec2 struct we built , and have been using heavily.
GLM's versions of the core vector types are:
glm::vec2: A vector with 2 components (x, y)glm::vec3: A vector with 3 components (x, y, z)glm::vec4: A vector with 4 components (x, y, z, w)
#include <glm/glm.hpp>
int main(int argc, char** argv) {
// Creating vectors
glm::vec2 A{0.0f, 1.0f};
glm::vec3 B{0.0f, 1.0f, 2.0f};
glm::vec4 C{0.0f, 1.0f, 2.0f, 3.0f};
}We can access their data using .x, .y, .z, and .w.
GLM is primarily used in 3D applications, but we can use it for 2D transformations too. In that context, we just need to be aware that, when using a homogenous coordinate system, glm::vec3's third component is called z instead of w.
GLM also overloads all standard arithmetic operators for these vector types, allowing us to add, subtract, and multiply vectors easily, just like we had in our custom Vec2 struct.
#include <glm/glm.hpp>
int main(int argc, char** argv) {
glm::vec2 Direction{0.f, 1.f};
float Speed{5.f};
glm::vec2 Velocity{Direction * Speed}; // (0, 5)
glm::vec2 Position{10.0f, 20.0f};
glm::vec2 NewPosition{Position + Velocity}; // (10, 25)
}It also provides the glm::length() and glm::normalize() functions, which replaces the .Normalize() and .GetLength() methods of our Vec2 type.
#include <glm/glm.hpp>
int main(int argc, char** argv) {
glm::vec2 Velocity{0.0f, 5.0f};
float Speed{glm::length(Velocity)}; // 5
glm::vec2 Direction{glm::normalize(Velocity)}; // (0, 1)
}Logging GLM Types
GLM vectors and matrices (which we cover later) are complex types, so we can't directly print them.
However, GLM provides a utility header glm/gtx/string_cast.hpp which gives us the glm::to_string() function. This formats vectors and matrices into strings that we can print.
To use it, we need to #define GLM_ENABLE_EXPERIMENTAL. We can do that through our build tools, or just before our #include directive:
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>Here is an example where we print some of our previous vectors:
src/main.cpp
#include <iostream>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
int main(int argc, char** argv) {
// Creating vectors
glm::vec2 Position{10.0f, 20.0f};
glm::vec2 Velocity{0.0f, 5.0f};
// Basic Arithmetic
glm::vec2 NewPosition{Position + Velocity};
// Scalar Multiplication
glm::vec2 ScaledVelocity{Velocity * 2.0f};
// Vector Length / Magnitude
float Speed{glm::length(Velocity)};
// Normalizing a Vector
glm::vec2 Direction{glm::normalize(Velocity)};
// Logging using glm::to_string()
std::cout << "Original Pos: "
<< glm::to_string(Position) << "\n";
std::cout << "New Pos: "
<< glm::to_string(NewPosition) << "\n";
std::cout << "Scaled Vel: "
<< glm::to_string(ScaledVelocity) << "\n";
std::cout << "Speed: "
<< Speed << "\n";
std::cout << "Direction: "
<< glm::to_string(Direction);
return 0;
}Original Pos: vec2(10.0, 20.0)
New Pos: vec2(10.0, 25.0)
Scaled Vel: vec2(0.0, 10.0)
Speed: 5
Direction: vec2(0.0, 1.0)Note that we're rounding the numbers shown in our outputs for presentation reasons. If you're following along, your output should have the same values, but will be shown with many more decimal places.
Matrix Types
In the , we learned that to perform transformations like translation, rotation, and scaling, we need matrices. GLM provides these types:
glm::mat3: A 3x3 matrix (Used for 2D transformations with homogeneous coordinates)glm::mat4: A 4x4 matrix (Used for 3D transformations with homogeneous coordinates)
The Identity Matrix
When we construct a matrix in GLM with a single scalar value (usually 1.0f), it creates an Identity Matrix.
glm::mat3 Identity{1.0f};An identity matrix has 1s along the main diagonal and 0s everywhere else.
They key property of an identity matrix is that, when we multiply a vector by it, the vector doesn't change.
Conceptually, this means that, when used as a transformation, the identity matrix performs no transformation at all. This means it is effectively a blank canvas that serves as a starting point for our transformation pipeline. We start with an identity matrix, and we then apply our required transformations to it.
Below, we create an identity matrix, and we'll learn how to add transformations later:
src/main.cpp
#include <iostream>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
int main(int argc, char** argv) {
// Create a 3x3 Identity Matrix
glm::mat3 Identity{1.0f};
std::cout << "Identity Matrix:\n"
<< glm::to_string(Identity) << "\n";
return 0;
}Identity Matrix:
mat3x3((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))Column-Major Order
One of the most confusing aspects of working with matrices can be the matrix memory layout.
In mathematics, we typically write matrices in row-major order. We read them left-to-right, top-to-bottom.
However, many systems represent and store matrices in column-major order. This is particularly common in graphics APIs, including GLM.
This is something we need to be mindful of when creating or manipulating matrices manually, and also when we use them to apply transformations which we'll cover soon.
When we access elements using the subscript operator [], the first index is the column, and the second index is the row.
#include <iostream>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
int main(int argc, char** argv) {
glm::mat3 MyMatrix(1.0f);
MyMatrix[0][0] = 1; // Column 0, Row 0
MyMatrix[1][0] = 2; // Column 1, Row 0
MyMatrix[2][0] = 3; // Column 2, Row 0
MyMatrix[0][1] = 4; // Column 0, Row 1
MyMatrix[1][1] = 5; // Column 1, Row 1
MyMatrix[2][1] = 6; // Column 2, Row 1
MyMatrix[0][2] = 7; // Column 0, Row 2
MyMatrix[1][2] = 8; // Column 1, Row 2
MyMatrix[2][2] = 9; // Column 2, Row 2
std::cout << "Col 1: "
<< glm::to_string(MyMatrix[0]);
return 0;
}Col 1:vec3(1.0, 4.0, 7.0)If we construct a mat3 manually, the arguments fill the first column, then the second, and so on. If we stringify the full matrix using glm::to_string(), our string is also formatted in column-major order. That is, as a list of column vectors:
src/main.cpp
#include <iostream>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
int main(int argc, char** argv) {
// Constructing a matrix manually.
// We provide arguments COLUMN by COLUMN.
glm::mat3 MyMatrix{
1.f, 4.f, 7.f, // Column 0
2.f, 5.f, 8.f, // Column 1
3.f, 6.f, 9.f, // Column 2
};
std::cout << glm::to_string(MyMatrix);
return 0;
}mat3x3((1.0, 4.0, 7.0), (2.0, 5.0, 8.0), (3.0, 6.0, 9.0))Transposing Matrices
We can convert row-major matrices to column-major matricies (and vice versa) via transposation.
Transposing flips the matrix over its diagonal, converting rows into columns and vice-versa.
GLM provides the glm::transpose() function for this purpose.
src/main.cpp
#include <iostream>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
int main(int argc, char** argv) {
// If we load this into GLM, it interprets it as columns
glm::mat3 RowMajor{
1.f, 2.f, 3.f,
4.f, 5.f, 6.f,
7.f, 8.f, 9.f
};
// Transpose it to fix the layout for GLM
glm::mat3 ColMajor{glm::transpose(RowMajor)};
std::cout << "Original:\n"
<< glm::to_string(RowMajor) << "\n";
std::cout << "Transpose:\n"
<< glm::to_string(ColMajor);
return 0;
}Original:
mat3x3((1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0))
Transpose:
mat3x3((1.0, 4.0, 7.0), (2.0, 5.0, 8.0), (3.0, 6.0, 9.0))In mathematical writing, the transpose operation is represented by a superscript :
Transposing Vectors
Transposing a row vector converts it to a column vector, and vice versa:
Vector transposition is rarely required in code, but it is often used in papers and textbooks to present column vectors horizontally for stylistic reasons. This is because a horizontal style like flows organically within a line of text.
The equivalent vertical presentation requires unnatural spacing.
Matrix Multiplication
As we covered previously, the reason we define translations using matrices is that it allows us matrix multiplication to apply those transformations. That is, we can transform a vector by multiplying it with a matrix.
Unlike regular multiplication, matrix multiplication is not commutitive. That is, and are not equivalent if and are matrices.
When we want to transform a column vector by a column-major matrix, the matrix goes on the left, and the vector goes on the right.
In a row-major system, we'd do the exact opposite. Our vector would be represented as a row, and would be the left operand, with the transformation matrix on the right.
Practical Example
Let's look at a practical example, within GLM's column-major system. We will manually construct a matrix that translates objects by (50, 100) and use it to transform a position vector and direction vector.
Given we're using a 3x3 matrix to represent our transformations, we now need to add the homogenous coordinate to our vectors. This third component is typically called in a 2D system, although if we ever need to directly access it within the glm::vec3 type, it is stored in the member variable called z.
#include <glm/glm.hpp>
int main(int argc, char** argv) {
glm::vec3 Position{8.f, 5.f, 1.f};
float x{Position.x}; // 8.0
float y{Position.y}; // 5.0
float w{Position.z}; // 1.0
return 0;
}For the position, we set this third component to 1.0f, which indicates that this vector should be affected by translation.
For velocity, or any other vector that represents a relative effect, we set w to 0.0f.
We create this transformation matrix manually in the following example, but it's not necessary to understand why this specific matrix represents a translation. We'll cover transformations and how to define them in a more intuitive way in the next lesson.
src/main.cpp
#include <iostream>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/string_cast.hpp>
int main(int argc, char** argv) {
// A position at (10, 10)
// w = 1 means it will be affected by translations
glm::vec3 Position{10.f, 10.f, 1.f};
// A velocity of (5, 2)
// w = 0 means this vector represents a
// relative value, and should not be translated
glm::vec3 Velocity{5.f, 2.f, 0.f};
// A matrix that translates by (50, 100)
// Constructed manually using column-major order
glm::mat3 Translation{
1.0f, 0.0f, 0.0f, // Col 0
0.0f, 1.0f, 0.0f, // Col 1
50.0f, 100.0f, 1.0f // Col 2
};
// Apply the transformation
// Important: The matrix is the LEFT operand
// and the vector is the RIGHT
glm::vec3 NewPosition{Translation * Position};
std::cout << "Old Position: "
<< glm::to_string(Position) << "\n";
std::cout << "New Position: "
<< glm::to_string(NewPosition) << "\n";
// Transform the velocity
glm::vec3 NewVelocity{Translation * Velocity};
std::cout << "Old Velocity: "
<< glm::to_string(Velocity) << "\n";
std::cout << "New Velocity: "
<< glm::to_string(NewVelocity) << "\n";
return 0;
}Old Position: vec3(10.0, 10.0, 1.0)
New Position: vec3(60.0, 110.0, 1.0)
Old Velocity: vec3(5.0, 2.0, 0.0)
New Velocity: vec3(5.0, 2.0, 0.0)The position has successfully moved by (50, 100), whilst the velocity remained the same at (5, 2).
Summary
In this lesson, we integrated the GLM library into our project and learned its fundamental syntax.
- Installation: We used CMake's
FetchContentto download and link the header-only GLM library. - Vectors:
glm::vec2,vec3, andvec4behave similarly to our custom structs but come with built-in arithmetic and utility functions. - Matrices:
glm::mat4is the standard type for 3D transformations, andglm::mat3for 2D transformations. We generally start by constructing an identity matrix. - Column-Major Order: GLM stores matrices by column. We must access them as
matrix[column][row], and we pass constructor arguments column-by-column.glm::transpose()can swap rows and columns if needed. - Transformation: We transform vectors by multiplying them with a matrix:
Matrix * Vector.
In the next lesson, we will stop constructing matrices manually. We will use GLM's powerful helper functions to generate Translation, Rotation, and Scale matrices automatically, and use them to implement the Scene/Camera transformation pipeline we designed previously.