We already have rotation and scale. This chapter adds no new kind of transform. It connects two familiar ones. Both act around the origin, and all vectors remain column vectors.
Follow one point
Keep the card from chapter 12: width 1.2, height 0.8, centered at the origin. Keep its circle of radius 0.5 and the marked point P=(0.4,0.3). Let S double X and leave Y alone. Let R rotate counterclockwise by 45°.
vec2 intermediate = S * local;
vec2 world = R * intermediate;
First, P becomes (0.8,0.3). Then the rotation mixes those two coordinates. At 45°, sine and cosine are both approximately 0.707107, giving world≈(0.35355,0.77782).
Now reverse the actions. Rotation first gives approximately (0.07071,0.49497). Doubling this result's X coordinate gives (0.14142,0.49497). The two paths reach different points because scale receives different inputs.
vec2 intermediate = R * local;
vec2 world = S * intermediate;
Notice what the second scale means. It stretches along the fixed world X and Y directions. After a turn, those directions no longer match the card's edges. Stretching them can change the angle between those edges.
Put the steps together
Substitute the first assignment into the second. The parentheses show the action closest to local happening first:
vec2 scaleThenRotate = R * (S * local);
vec2 rotateThenScale = S * (R * local);
We can store the combined action in one matrix:
mat2 combined = R * S;
vec2 world = combined * local;
Matrix multiplication is associative: mathematically, (R*S)*local equals R*(S*local). Regrouping keeps the order. Floating-point evaluation can produce small rounding differences, so compare computed values with a tolerance.
It is generally not commutative: exchanging R and S can change the result. Read R * S * local from the vector outward: scale, then rotate. The written matrix order describes a composition, not a left-to-right list of commands.
The same reasoning extends to three matrices: A * B * C * local applies C, then B, then A. Parentheses may group the work differently, but they do not permit exchanging the matrices.
Follow the columns
What does multiplying two matrices actually build? Each column of the right-hand matrix is a vector. Apply the left-hand matrix to each one:
mat2 combined = mat2(R * S[0], R * S[1]);
// The same column construction as R * S.
Write c=cos(angle) and s=sin(angle). Scale's columns are (scaleX,0) and (0,scaleY). Turning them produces the columns (scaleX*c,scaleX*s) and (-scaleY*s,scaleY*c). This is R*S.
At our settings, they are approximately (1.41421,1.41421) and (-0.70711,0.70711). Their lengths are 2 and 1, and their dot product is zero. Scale first stretches the card along its original edges; rotation then turns that rectangle without changing its right angles.
For S*R, start with rotation's columns (c,s) and (-s,c), then scale their components. The results are (scaleX*c,scaleY*s) and (-scaleX*s,scaleY*c). At 45° and (2,1), both have length about 1.58114, and their dot product is -1.5. They are not perpendicular.
The lab draws these full columns as orange X and green Y arrows. At step 1, they show only the first action. At step 2, they show the complete product. The intermediate overlay lets you keep the first step visible while inspecting the final one.
Both final cards have area 1.92: the original 0.96 multiplied by 2. Rotation preserves area, so placing it before or after this scale does not change that multiplier. Equal area does not imply equal shape.
The circle makes another difference visible. Scale first creates an ellipse with semiaxes 1 and 0.5, then rotation tilts it. Rotation first leaves the original circle unchanged as a shape; the following scale creates an ellipse aligned with the world axes. Its marked point still records the rotation.
When do they agree?
Try uniform scale=(1.5,1.5). Multiplying both coordinates by the same factor before or after rotation gives the same final matrix. The intermediate pictures still differ, but the endpoints agree for every point.
A zero angle also makes the final orders agree. So does a half-turn: R=-I sends every vector to its negative, which commutes with this diagonal scale. In general, these particular R and S commute when sin(angle)×(scaleX-scaleY)=0. Compare their columns above to see which off-diagonal terms must match.
This is a statement about the complete maps. A single coincident point proves less: the origin stays fixed for both orders at every setting. A symmetric unmarked shape can also hide changes. Use the two columns and a marked point instead of relying on one outline.
Undo the displayed steps
Our arrows and marker move forward. The fragment shader still starts at a fixed world pixel p and recovers the original drawing coordinate. For scale then rotate, undo the last action first: rotate back, then divide by scale.
// Forward: R * S * local
vec2 localRS = (rotation(-angle) * p) / scale;
// Forward: S * R * local
vec2 localSR = rotation(-angle) * (p / scale);
Check the first expression by reading its parentheses: the inverse rotation acts on p before division. For the second, division happens first. Both scale factors stay in [0.2,2], so division is defined. We will generalize this reversal into inverse matrices in the next chapter.
The masks remain local fields after deformation. As in chapter 12, their zero contours define the shapes, while fwidth supplies approximate screen-space smoothing. These values are not exact world-space distances.