Overview
This worksheet explains why a 4x4 matrix is required to represent translation alongside rotation and scale. You will construct a translation matrix and a Y-axis rotation matrix in column-major layout, then compose them and trace a vertex through the combined transform by hand. The values used here connect directly to the Transformations lab (Lab 07).
Learning Objectives
By the end of this lab you will be able to:
- By the end of this worksheet you will be able to explain why a 3x3 matrix cannot represent translation and why a 4x4 matrix is needed
- By the end of this worksheet you will be able to construct a 4x4 translation matrix T(tx, ty, tz) and apply it to a homogeneous point and direction vector
- By the end of this worksheet you will be able to write out the Y-axis rotation matrix R_y(theta) and verify it on concrete vectors
- By the end of this worksheet you will be able to compose two transforms by matrix multiplication and trace a vertex through the result
Concept
A 3x3 matrix can only encode linear transformations: operations that map the origin to itself. Rotation, scale, and shear all satisfy this constraint. Translation does not. Moving every point by a fixed offset means the origin maps to a non-zero position, which no matrix whose columns span from the origin can achieve. Without a unified representation that includes translation, combining rotate-then-translate requires two separate operations, making composition and inversion awkward.
The solution is to add a fourth component w to every vector, producing homogeneous coordinates. A point uses w = 1 and a direction vector uses w = 0. The 4x4 translation matrix T(tx, ty, tz) stores the offset in its fourth column. When it multiplies a point (w = 1) the offset is added to x, y, z. When it multiplies a direction (w = 0) the offset is ignored, which is geometrically correct because directions have no position. Rotation matrices are extended to 4x4 by placing the 3x3 block in the top-left corner and filling the rest with identity values.
Transforms are combined by multiplying their matrices together. The resulting composite matrix applies both transforms in a single multiplication. GLM functions such as glm::translate and glm::rotate construct these 4x4 matrices internally. Understanding the matrices they produce is what makes the order of function calls in code predictable rather than guesswork.
Tasks
Task 1 - Why 3x3 Cannot Translate
This task shows concretely that no 3x3 matrix can add a fixed offset to every input vector, then demonstrates the 4x4 solution.
Suppose we want to translate every point by (tx, ty, tz) = (4, 5, 6). A 3x3 matrix M must satisfy M * v = v + (4, 5, 6) for every possible v. Consider the y component of the result:
y_out = m10 * vx + m11 * vy + m12 * vz
For this to equal vy + 5 for every input, we would need m11 = 1 and m10 * vx + m12 * vz = 5 for all vx and vz simultaneously. There are no constants m10 and m12 that satisfy this for every possible vx and vz. Translation is not a linear operation in 3D.
The 4x4 solution: add a fourth component w = 1 to every point. The translation matrix is:
T(4, 5, 6): | 1 0 0 4 | | 0 1 0 5 | | 0 0 1 6 | | 0 0 0 1 |
Apply T(4, 5, 6) to the point p = (1, 2, 3, 1):
| 1 0 0 4 | | 1 | | 1*1 + 0*2 + 0*3 + 4*1 | | 5 | | 0 1 0 5 | * | 2 | = | 0*1 + 1*2 + 0*3 + 5*1 | = | 7 | | 0 0 1 6 | | 3 | | 0*1 + 0*2 + 1*3 + 6*1 | | 9 | | 0 0 0 1 | | 1 | | 0*1 + 0*2 + 0*3 + 1*1 | | 1 |
Result: p_translated = (5, 7, 9, 1). The offset (4, 5, 6) was added correctly.
Now apply the same T(4, 5, 6) to the direction vector d = (0, 1, 0, 0):
| 1 0 0 4 | | 0 | | 1*0 + 0*1 + 0*0 + 4*0 | | 0 | | 0 1 0 5 | * | 1 | = | 0*0 + 1*1 + 0*0 + 5*0 | = | 1 | | 0 0 1 6 | | 0 | | 0*0 + 0*1 + 1*0 + 6*0 | | 0 | | 0 0 0 1 | | 0 | | 0*0 + 0*1 + 0*0 + 1*0 | | 0 |
Result: d_translated = (0, 1, 0, 0). The direction is unchanged. The w = 0 zeroed out the entire fourth column of the matrix, so translation has no effect on direction vectors. This is geometrically correct: directions represent orientation, not position.
Task 2 - The Y-Axis Rotation Matrix
The Y-axis rotation matrix R_y(t) rotates points around the world Y axis by angle t (in radians). Note the sign swap on the sin terms compared to the X and Z rotation matrices: the -sin(t) appears at row 2, column 0 (bottom-left of the 3x3 block) rather than at row 0, column 2.
R_y(t): | cos(t) 0 sin(t) 0 | | 0 1 0 0 | | -sin(t) 0 cos(t) 0 | | 0 0 0 1 |
At theta = 90 degrees: cos(90) = 0, sin(90) = 1. R_y(90) becomes:
R_y(90): | 0 0 1 0 | | 0 1 0 0 | | -1 0 0 0 | | 0 0 0 1 |
Verification 1: apply R_y(90) to the point (1, 0, 0, 1) (tip of the X axis).
| 0 0 1 0 | | 1 | | 0 | | 0 1 0 0 | * | 0 | = | 0 | | -1 0 0 0 | | 0 | |-1 | | 0 0 0 1 | | 1 | | 1 |
Result: (0, 0, -1, 1). Rotating the X-axis tip 90 degrees around Y swings it to -Z. This follows the right-hand rule: curl the fingers of the right hand from X toward Z; the thumb points up (+Y); a positive rotation swings X toward -Z.
Verification 2: apply R_y(90) to the point (0, 0, 1, 1) (tip of the Z axis).
| 0 0 1 0 | | 0 | | 1 | | 0 1 0 0 | * | 0 | = | 0 | | -1 0 0 0 | | 1 | | 0 | | 0 0 0 1 | | 1 | | 1 |
Result: (1, 0, 0, 1). The Z-axis tip swings to +X. Both results are consistent: a 90-degree Y rotation takes X to -Z and Z to +X, cycling the horizontal axes as expected.
The sign of the -sin(t) entry at row 2, column 0 encodes this direction. When rotating from X toward -Z (a positive angle around Y), the X component contributes negatively to the new Z component. This is exactly what -sin(t) at position [2][0] produces.
Task 3 - Compose and Apply by Hand
Goal: start at p = (0, 0, 0, 1) (the world origin). First translate by (2, 0, 0), then rotate 90 degrees around Y. The final position should be (0, 0, -2, 1).
In column-major convention, the transform applied first goes on the right in the multiplication. To translate first and then rotate: C = R_y(90) * T(2, 0, 0).
The two matrices:
T(2, 0, 0): R_y(90): | 1 0 0 2 | | 0 0 1 0 | | 0 1 0 0 | | 0 1 0 0 | | 0 0 1 0 | | -1 0 0 0 | | 0 0 0 1 | | 0 0 0 1 |
Compute C = R_y(90) * T(2, 0, 0) by multiplying row by column. Each entry C[row][col] is the dot product of row row of R_y(90) with column col of T(2, 0, 0). Fill in the 16 entries below.
C[0][0]: row 0 of R_y dot col 0 of T = (0)(1) + (0)(0) + (1)(-1) + (0)(0) ... wait, use row of R_y = (0,0,1,0) and col of T = (1,0,0,0): (0)(1)+(0)(0)+(1)(0)+(0)(0) =
C[0][1]: row 0 of R_y = (0,0,1,0), col 1 of T = (0,1,0,0): (0)(0)+(0)(1)+(1)(0)+(0)(0) =
C[0][2]: row 0 of R_y = (0,0,1,0), col 2 of T = (0,0,1,0): (0)(0)+(0)(0)+(1)(1)+(0)(0) =
C[0][3]: row 0 of R_y = (0,0,1,0), col 3 of T = (2,0,0,1): (0)(2)+(0)(0)+(1)(0)+(0)(1) =
C[1][0]: row 1 of R_y = (0,1,0,0), col 0 of T = (1,0,0,0): =
C[1][1]: row 1 of R_y = (0,1,0,0), col 1 of T = (0,1,0,0): =
C[1][2]: row 1 of R_y = (0,1,0,0), col 2 of T = (0,0,1,0): =
C[1][3]: row 1 of R_y = (0,1,0,0), col 3 of T = (2,0,0,1): =
C[2][0]: row 2 of R_y = (-1,0,0,0), col 0 of T = (1,0,0,0): (-1)(1)+(0)(0)+(0)(0)+(0)(0) =
C[2][1]: row 2 of R_y = (-1,0,0,0), col 1 of T = (0,1,0,0): =
C[2][2]: row 2 of R_y = (-1,0,0,0), col 2 of T = (0,0,1,0): =
C[2][3]: row 2 of R_y = (-1,0,0,0), col 3 of T = (2,0,0,1): (-1)(2)+(0)(0)+(0)(0)+(0)(1) =
C[3][0]: row 3 of R_y = (0,0,0,1), col 0 of T = (1,0,0,0): =
C[3][1] =
C[3][2] =
C[3][3]: row 3 of R_y = (0,0,0,1), col 3 of T = (2,0,0,1): (0)(2)+(0)(0)+(0)(0)+(1)(1) =
The composite matrix C = R_y(90) * T(2, 0, 0) should be:
C: | 0 0 1 0 | | 0 1 0 0 | | -1 0 0 -2 | | 0 0 0 1 |
Apply C to the starting point p = (0, 0, 0, 1):
| 0 0 1 0 | | 0 | | 0 | | 0 1 0 0 | * | 0 | = | 0 | | -1 0 0 -2 | | 0 | |-2 | | 0 0 0 1 | | 1 | | 1 |
Result: (0, 0, -2, 1). Interpretation: the origin first moved to (2, 0, 0) by the translation, then a 90-degree Y rotation swung that point to (0, 0, -2). This matches the expected result.
The GLM equivalent (note: GLM calls are written in reverse order because each call left-multiplies the accumulated matrix):
// GLM applies right to left: rotate is called FIRST in code, translate SECOND
// This gives model = T(2,0,0) * I then model = R_y(90) * model = R_y(90) * T(2,0,0)
glm::mat4 model(1.0f);
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
Solution
Try the tasks above before revealing the solution.