In chapter 09, we chose two axes and reconstructed a world point from local coordinates. Keep that geometry in mind. A matrix will give us a compact way to store the same choices and apply the same calculation.
Pack the axes into columns
Start with the previous chapter's axes. A GLSL mat2 stores two columns, each containing two numbers:
vec2 axisX = vec2(0.7, 0.1);
vec2 axisY = vec2(0.2, 0.6);
mat2 M = mat2(axisX, axisY);
The first vector becomes the first column. The second becomes the second column. If we print the matrix in the usual rectangular arrangement, it looks like this:
first column second column
0.7 0.2
0.1 0.6
Notice the difference between reading this picture across a row and supplying the constructor arguments. The constructor consumes a whole column before moving to the next. These two forms create the same matrix:
mat2 M = mat2(vec2(0.7, 0.1), vec2(0.2, 0.6));
// Equivalently, when supplying four scalars:
mat2 M = mat2(0.7, 0.1, 0.2, 0.6);
Choose one declaration in actual code. The two lines above show alternatives. Thinking in columns is useful because each column still has a geometric job: it tells us where one local axis step goes.
Open up the multiplication
We use column vectors in this book, with the matrix on the left:
vec2 local = vec2(0.6, 0.5);
vec2 world = M * local; // (0.52, 0.36)
This operation scales the first column by local.x, scales the second by local.y, and adds the two. GLSL indexes matrix columns starting at zero:
vec2 firstColumn = M[0]; // (0.7, 0.1)
vec2 secondColumn = M[1]; // (0.2, 0.6)
vec2 expanded = local.x * M[0] + local.y * M[1];
Nothing new happened to the point. We changed the notation for the calculation we already understand. Open it one step further to see the individual output components:
world.x = M[0].x * local.x + M[1].x * local.y;
world.y = M[0].y * local.x + M[1].y * local.y;
// world.x = 0.7 * 0.6 + 0.2 * 0.5 = 0.52
// world.y = 0.1 * 0.6 + 0.6 * 0.5 = 0.36
Each output uses one row of the printed matrix. That does not change how the matrix is constructed or indexed. In M[column][row], the first index selects the column.
Writing local * M is also legal GLSL, but it treats the vector on the left as a row vector. It is a different operation and does not follow our chosen convention. Keep M * local for the forward mapping throughout these chapters.
Read a matrix from two arrows
Apply the matrix to the three test inputs from the previous chapter:
M * vec2(1.0, 0.0) // M[0]
M * vec2(0.0, 1.0) // M[1]
M * vec2(1.0, 1.0) // M[0] + M[1]
This gives a direct way to design a transformation. Decide where one X step should land and put that destination in the first column. Decide where one Y step should land and put it in the second.
For example, send X to (1,0.5) and Y to (-0.2,1):
mat2 M = mat2(vec2(1.0, 0.5), vec2(-0.2, 1.0));
// Four-scalar form: mat2(1.0, 0.5, -0.2, 1.0)
The sum input lands at (0.8,1.5). For local=(0.6,0.5), the result is (0.5,0.8). We can obtain both predictions by adding scaled arrows, without memorizing a special formula for this tilted grid.
Identity and the origin
To leave every vector unchanged, choose the ordinary X and Y steps. This is the identity matrix:
mat2 identity = mat2(1.0, 0.0, 0.0, 1.0);
// GLSL also provides a single-scalar diagonal constructor:
mat2 sameIdentity = mat2(1.0);
A single scalar fills the diagonal and sets the remaining entries to zero. It does not fill every entry with that scalar. Identity returns local unchanged; mat2(0.0) sends every input to zero.
Every mat2, not just identity, sends the origin to itself. Multiplying both columns by zero leaves nothing to add. This also explains why a mat2 alone cannot translate all points by a nonzero offset: a translation would move the origin.
We can add an offset separately, and later we will learn how larger matrices package it. For now, keep the origin fixed. Rotation, scaling, shearing, reflection, and collapse can all fit this rule.
What linear means here
A matrix multiplication preserves addition and scalar multiplication:
M * (a + b) = M * a + M * b
M * (s * a) = s * (M * a)
These are mathematical equalities, not GLSL assignments. To see the first one, expand the column sum. The coefficient of M[0] is a.x + b.x; distributing that multiplication gives the two X contributions. The same happens for the Y column. Group the parts for a and b, and we have the right-hand side.
Scaling a vector scales both of its coordinates, so it scales both column contributions by the same amount. Together these properties make the mapping linear. The parallelogram formed by a and b maps to the parallelogram formed by their images, though it can flatten to a line or point.
Using the exercise matrix, let a=(0.2,0.1) and b=(0.1,0.3). Their images are (0.18,0.2) and (0.04,0.35). Adding gives (0.22,0.55), the same result as transforming a+b=(0.3,0.4). Doubling a gives (0.36,0.4) after transformation.
These identities hold in exact arithmetic. Shader calculations may round their last digits differently, especially after cancellation. Compare with a small tolerance rather than expecting every alternative expression to produce identical bits.
Dependent columns are allowed in a matrix. They no longer provide a full two-dimensional basis, but the forward product remains defined and linear. No inverse is needed to draw such a collapsed result.
Catch a row-column mistake
The exercise matrix prints as two rows: (1,-0.2) and (0.5,1). Pasting those rows directly into a four-scalar constructor creates the wrong columns:
mat2 wrong = mat2(1.0, -0.2, 0.5, 1.0);
// Wrong column 0: (1.0, -0.2)
// Wrong column 1: (0.5, 1.0)
This exchanges rows and columns, called a transpose. The code compiles, so the compiler cannot diagnose our geometric intention. The unit test inputs can: X now goes to (1,-0.2), and Y goes to (0.5,1). Our local (0.6,0.5) lands at (0.85,0.38) instead of (0.5,0.8).
Writing the constructor as two explicit vec2 columns makes this mistake easier to spot. In the lab, each column has the same color as its corresponding axis arrow.