2024-11-30
Matrices
Similar to vectors, matrices are used a lot in game development. Most commonly you find them in graphics, physics, and animations. In this article we’ll take a look at how to construct them and how to transform vectors using matrices.
First let’s check some common transformations stored in a matrix:
- Scaling: Changes the size of an object.
- Rotation: Rotates an object around an axis.
- Translation: Moves an object from one position to another.
- Projection: Projects 3D objects onto a 2D screen.
The matrices we will use are two dimensional. They can have any shape, but we will mostly focus on square matrices, that means row and column size are equal. Here are two examples of matrics, a 2x2 and a 4x4 matrix:
$$ \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ \end{bmatrix} $$$$ \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$These matrices are called identity matrices, because they are empty (filled with zeroes) except for the diagonal. Later in this article you will find out why this is a good starting point for many matrix operations.
Matrix multiplications
Vectors from the previous article are essentially one dimensional matrices:
$$ \begin{bmatrix} 1 & 0 \end{bmatrix} $$We’ve seen that we can define a dot product for a vector as follows:
$$a \cdot b = a_x b_x + a_y b_y$$We can use this exact same operation to create matrix multiplications. You can see rows and colums of the matrix as vectors, then using a dot product we can also define a matrix multiplication. Here’s an example:
When you visualize it like this, you can see we use the vectors $(A_{11} A_{12})$ and $(B_{11} B_{21})$ to create the result for the first entry in the result matrix. It’s important to realize here: if we would swap the matrices around, you would not get the same result! Since we use a row from matrix A to multiply with a column of matrix B. The matrix multiplication is not commutative. Enough explanation, let’s get to some code.
You may have noticed when printing out the matrix it’s slightly different to what you expect. We’re setting matrix b to [1, 2, 3, 4]
in memory layout, yet if we print out the matrix it seems to look like [1, 3, 2, 4]
. This is because we are defining the matrix to have a column major loadout. That means, when reading the memory in order, we’ll read through the matrix in columns. When we reach the end of the first column, we continue with the second and so on.
I’ve explicitly chosen this example to feature this to make you aware. There isn’t really a reason to choose either column major or row major over the other. It’s a design choice. When making games, you will probably use a math library or be provided one. In this case it’s important to know how these matrices are laid out in memory.
One specific library that’s popular to use in games is called GLM. When using this library you could make the case that it wouldn’t be necessary to figure out how they are stored in memory. GLM provides common operations to set specific data in the matrix:
1mat4x4 identity = mat4x4(1);
2auto S = glm::scale(identity, Scale);
3auto R = glm::rotate(identiy, Rotation);
4auto T = glm::translate(identity, Translation);
5auto result = T * R * S * position;
These functions help to keep the implementation hidden behind the scenes. However when you want to use the matrix to move objects around, for example in a vertex shader, it would still be crucial to make sure the resulting multiplication is what you expect it to be! So always make sure you know how a matrix is laid out in memory.
If you want to change the matrix, but not the underlying library, there is another common operation you can use: transpose. Transposing the matrix will flip the rows and columns. In the code example, matrix[x][y]
will become matrix[y][x]
. You can visualize it as flipping the numbers over the diagonal of the matrix:
One last thing to notice about the last example is the order of multiplication: T * R * S * position
. You can read this as: translate the rotated, scaled, transform. This comes down to applying the matrix operations in reverse: we first apply the scale to the given transform, then rotate it, and finally translate it. This is exactly what we want.
Imagine doing these operations in the wrong order: first translating a point and then rotating. When rotating a vector, we rotate it around the origin. So the translated point will then be rotated around the origin, usually resulting in something completely unexpected:
The red dots show the origin of each cube before applying the operations on the vertices. You can see the rotation moves the vertices of the cubes around the origin, no matter where they are.
Coming back to the operation T * R * S * position
: the result in this case will be a vector, because the position is a vector. However if we only took T * R * S
the result would be a matrix containing all the operations. If you would inspect the matrix you could retrieve the translation, rotation, and scale from it:
Where $\color{blue}{blue}$ numbers apply a translation. The $\color{red}{red}$ diagonal represents a scaling. Within the $\color{green}{green}$ square rotations are stored. Of course $\color{yellow}{yellow}$ is where the diagonal and square overlap, so these can both have part of a rotation and scale. The translation is either stored on the right column, or the bottom row. This depends once again on the design being row major or column major.
These are the most common things you should know about matrices for game development. There are more specific applications, for example calculating a perspective projection for rendering. We will cross that bridge when we get to it!