30 min

Rotations in 3D: three axes, one rule

The three basic 3D rotations are the same 2D matrix you already derived, each acting in its own pair of axes, and stacking them never leaves the set called SO(3).

Where you are. You can build a 2D rotation matrix from trigonometry and read one by its columns. This lesson takes that machinery into three dimensions, where the columns rule survives completely intact and one genuinely new thing appears.

Three knobs on a tripod

Borrow a photographer’s geared tripod head, the kind with three separate knobs rather than one ball joint. Put a camera on it and work each knob on its own, watching through the viewfinder.

The bottom knob sweeps the camera across the horizon. The view pans left and right. The horizon stays perfectly level and the camera keeps looking at the same height.

Lock that one and try the middle knob. Now the camera nods, tipping up toward the ceiling and down toward the floor. The horizon is still level. The camera still faces the same compass direction it did before.

Lock that and try the third. Nothing about where the camera is aimed changes at all; it stares at the same spot on the wall. What happens is that the horizon in the viewfinder tips over.

Three knobs, and each one leaves something completely alone. What each one leaves alone is a line running through the camera. Every other direction in the scene swings; that one line does not budge. And whatever orientation you eventually want, you get there by working the three knobs in some order, one at a time.

The idea in one paragraph

A rotation in 3D does exactly what a rotation in 2D did: it sends the unit axes somewhere, and column i of the matrix is where axis i landed. The new fact is that a 3D rotation has an axis, a direction it leaves untouched, and the three simplest rotations are the ones whose axis is one of the coordinate axes. Each of those three is the identical 2×2 block you derived last lesson, sitting inside a 3×3 with a 1 parked on the untouched axis. You combine them by multiplying, and the rightmost matrix is the one that acts first. The product of two rotations is always another rotation, which means the honest rotations form a closed set; that set is called SO(3), and unpacking the name tells you the whole story: nine numbers, six constraints, three real degrees of freedom.

The rotation with a 1 in the corner

Turn about the z-axis. Whatever is happening, z itself does not move, and x and y turn within their own plane exactly as they did in 2D. So the matrix writes itself. Take last lesson’s 2×2 block, put it in the x and y rows and columns, and park a 1 where z maps to itself:

Rz(γ)=[cosγsinγ0sinγcosγ0001]R_z(\gamma) = \begin{bmatrix} \cos\gamma & -\sin\gamma & 0 \\ \sin\gamma & \cos\gamma & 0 \\ 0 & 0 & 1 \end{bmatrix}

In words: x and y turn into each other by γ\gamma; z is left alone. Read it by columns, the same as before. At γ=90\gamma = 90^{\circ} the entries collapse to integers, and the columns say x^(0,1,0)\hat{\mathbf{x}} \rightarrow (0, 1, 0), y^(1,0,0)\hat{\mathbf{y}} \rightarrow (-1, 0, 0), z^(0,0,1)\hat{\mathbf{z}} \rightarrow (0, 0, 1). Right went to up, up went to left, and z sat still.

The other two are the same matrix wearing different labels

Now do the same for x. Turning about x leaves x alone, and y and z turn into each other. And for y: y is left alone, and z and x turn into each other.

each disc is seen face on · the axis in the middle points at you and never moves x y z out of page Rz(γ) turns x toward y y z x out of page Rx(α) turns y toward z z x y out of page Ry(β) turns z toward x x → y → z → x · one cycle, three readings of the same 2D rotation
The three axis rotations drawn as discs seen face on. R about z turns x toward y with z pointing out of the page; R about x turns y toward z with x out of the page; R about y turns z toward x with y out of the page.

Wider than the screen; scroll it sideways.

Notice the pattern in the diagram, because it is doing real work. The pairs are not arbitrary. They follow the cycle x → y → z → x. RzR_z turns the pair (x, y). RxR_x turns the next pair along, (y, z). RyR_y turns the one after that, (z, x).

RotationLeaves aloneTurns the pairFirst axis goes to
Rx(α)R_x(\alpha)x(y, z)y toward z
Ry(β)R_y(\beta)y(z, x)z toward x
Rz(γ)R_z(\gamma)z(x, y)x toward y

Written out in the usual x, y, z row-and-column order, the three matrices are:

Rx(α)=[1000cosαsinα0sinαcosα]Ry(β)=[cosβ0sinβ010sinβ0cosβ]R_x(\alpha) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos\alpha & -\sin\alpha \\ 0 & \sin\alpha & \cos\alpha \end{bmatrix} \qquad R_y(\beta) = \begin{bmatrix} \cos\beta & 0 & \sin\beta \\ 0 & 1 & 0 \\ -\sin\beta & 0 & \cos\beta \end{bmatrix}

Which one happens first

Combine rotations by multiplying them. The convention is that R=R2R1R = R_2\,R_1 means “do R1R_1, then R2R_2,” so the rightmost matrix acts first. That looks backwards until you see where it comes from, and the columns rule gives it to you in one line.

Column i of the product R2R1R_2 R_1 equals R2R_2 applied to column i of R1R_1. That is simply how matrix multiplication is defined. Read it as a sentence: take where R1 sent axis i, then apply R2 to that. Axis i meets R1R_1 first. So the rightmost matrix acts first, not by decree but because that is what the arithmetic does.

the rightmost matrix acts first · follow one axis per row, left to right start apply Rx(90°) first then Rz(90°) where it lands x (1, 0, 0) (1, 0, 0) (0, 1, 0) = column 1 of R_z R_x y (0, 1, 0) (0, 0, 1) (0, 0, 1) = column 2 z (0, 0, 1) (0, −1, 0) (1, 0, 0) = column 3 stack the three landings side by side and you have written down R_z(90°) R_x(90°)
Three rows track each unit axis through a 90 degree rotation about x followed by a 90 degree rotation about z. The three final triples are exactly the three columns of the product matrix.

Wider than the screen; scroll it sideways.

The diagram works a concrete case: Rz(90)Rx(90)R_z(90^{\circ})\,R_x(90^{\circ}), one row per axis. The z-axis is the interesting one. Rx(90)R_x(90^{\circ}) sends it to (0,1,0)(0, -1, 0), and then Rz(90)R_z(90^{\circ}) sends that to (1,0,0)(1, 0, 0). Stack the three results side by side and you have written down the product matrix without ever multiplying two matrices together.

Order is now load-bearing

In 2D, R(a)R(b)=R(a+b)R(a)\,R(b) = R(a + b), and addition does not care about order. In 3D that shortcut is gone, because two rotations about different axes are not a single rotation about a shared axis, so there is no single number left to add.

The smallest demonstration: take the x-axis and apply Rz(90)Rx(90)R_z(90^{\circ})\,R_x(90^{\circ}). It ends up at (0,1,0)(0, 1, 0). Swap the two and apply Rx(90)Rz(90)R_x(90^{\circ})\,R_z(90^{\circ}) to the same starting axis, and it ends up at (0,0,1)(0, 0, 1). Same two turns, same starting vector, two different answers, a full quarter turn apart.

This deserves its own lesson and gets one; why rotation order matters makes you feel it rather than read it. Hold onto the consequence: the order of a sequence of rotations is part of the data, and a convention that leaves it unstated is a bug waiting to be filed.

A rotation of a rotation is a rotation

Here is the property that makes any of this safe to build on. Multiply two rotations and you always get a rotation. Both checks from last lesson survive the product, and the proofs are one line each.

Orthonormality: (AB)(AB)=B(AA)B=BB=I(A B)^{\top} (A B) = B^{\top} (A^{\top} A) B = B^{\top} B = I, using nothing but AA=IA^{\top} A = I.

Determinant: determinants multiply, so det(AB)=det(A)det(B)=1×1=1\det(A B) = \det(A)\,\det(B) = 1 \times 1 = 1.

So you can chain as many turns as you like and never fall out of the set. Add that the identity matrix is a rotation of zero, and that every rotation has an inverse inside the set (its transpose), and you have a closed system: compose, invert, compose again, and you are always holding something valid.

Nine numbers, three freedoms

A rotation matrix holds nine numbers, but you cannot choose them freely. Count the constraints: three columns must each have length 1, and three pairs of columns must each be perpendicular. Six equations. Nine numbers minus six constraints leaves three.

nine numbers go in · six of them are not free 9 numbers 3 columns × 3 entries must obey 6 constraints 3 · every column has length 1 3 · every pair is perpendicular det = +1 rules out the mirrors leaving 3 free numbers however you store it
Nine numbers feed into six constraints - three columns of length one, three perpendicular pairs, plus determinant plus one to rule out mirrors - which leaves three genuinely free numbers.

Wider than the screen; scroll it sideways.

The det=+1\det = +1 requirement is a further condition, but a different kind of one. Orthonormality already pins the determinant to +1+1 or 1-1, so requiring +1+1 is not a seventh equation shaving off another dimension; it is a choice between two separated halves, rotations or mirrors. That is why the count comes out at three and not two.

Three is the honest size of an orientation, and it explains two things you will meet repeatedly.

First, it is why schemes with three angles are so tempting. Three numbers is exactly the right budget, so surely three angles will do. They very nearly do, and euler angles and gimbal lock is the lesson about the bill.

Second, it is why you cannot simply predict nine numbers and call them a rotation. Nine floats land inside SO(3) essentially never. Drawing 200,000 random 3×3 matrices from a normal distribution produces exactly zero that come within 0.05 of being orthonormal; the set of rotations is a three-dimensional sliver inside a nine-dimensional space, and nothing lands on it by accident.

The three knobs were not special

One last fact, which closes the loop back to the tripod. Multiply any sequence of axis rotations together and the result is always a single rotation about a single axis by a single angle.

Take a concrete stack: Rz(30)Ry(70)Rx(15)R_z(30^{\circ})\,R_y(-70^{\circ})\,R_x(15^{\circ}). Three separate turns about three different axes. The product is one turn of 80.1693° about the axis (0.389,0.8101,0.4387)(0.389, -0.8101, 0.4387), and rebuilding the matrix from that axis and that angle reproduces the original to 2.2e-16.

So the three knobs on the tripod head were only ever a convenient way of reaching an orientation. The orientation itself was always one turn about one line. That is why three numbers is the right budget, and it is the fact that quaternions will eventually cash in.

Check yourself

1. Read Rz(90°) = [[0, −1, 0], [1, 0, 0], [0, 0, 1]] by its columns. What does each column tell you?

Column one is (0,1,0)(0, 1, 0): the x-axis landed on +y. Column two is (1,0,0)(-1, 0, 0): the y-axis landed on −x. Column three is (0,0,1)(0, 0, 1): the z-axis did not move, which is the definition of z being the axis of this rotation. Right to up, up to left, z untouched.

2. Why does Ry appear to have its minus sign in the wrong corner?

It does not have one. RyR_y turns the pair (z,x)(z, x), in that order, because the cycle is x → y → z → x. Written with its rows and columns in the order (z,x)(z, x) it is the plain 2D block [cossinsincos]\left[\begin{smallmatrix}\cos & -\sin \\ \sin & \cos\end{smallmatrix}\right], identical to the other two. The apparent relocation of the minus sign is entirely an artefact of insisting on sorting rows and columns as x, y, z. Nothing about the rotation is different.

3. R = R2 @ R1. Which rotation happens first, and can you justify it from the columns rule rather than by convention?

R1R_1 happens first. Justification: column i of R2R1R_2 R_1 is R2R_2 applied to column i of R1R_1. Column i of R1R_1 is where R1R_1 sent axis i, so the axis meets R1R_1 before it meets R2R_2. The rightmost matrix touches the axes first. This is arithmetic, not a naming choice.

4. Why is the product of two rotations always a rotation? Two facts, one line each.

Orthonormality survives: (AB)(AB)=B(AA)B=BB=I(AB)^{\top}(AB) = B^{\top}(A^{\top}A)B = B^{\top}B = I. And the determinant survives, because determinants multiply: det(AB)=det(A)det(B)=11=1\det(AB) = \det(A) \cdot \det(B) = 1 \cdot 1 = 1. Together they mean you can compose as long as you like and never leave SO(3), which is what makes chaining frames safe.

5. A model predicts orientation as nine raw floats. What goes wrong, and what do you do about it?

Nine unconstrained floats are almost never a rotation. SO(3) is a three-dimensional set living inside a nine-dimensional space, so random or slightly-noisy outputs land off it essentially always: 200,000 random Gaussian 3×3 matrices produced zero within 0.05 of orthonormal. The repair is to project back with an SVD, using UVU\,V^{\top} and flipping the last column of UU if the determinant comes out negative. The deeper fix is to have the model predict something with fewer redundant numbers in the first place.

6. What does the word “special” buy you in “special orthogonal”?

It rules out mirrors. Orthogonal alone means RR=IR^{\top}R = I, which allows a determinant of either +1+1 or 1-1; the 1-1 cases reverse handedness and cannot be reached by physically turning anything. “Special” pins the determinant to +1+1. It removes no degree of freedom, since it selects between two separated halves rather than constraining a continuous quantity, which is why an orientation still has three degrees of freedom and not two.

Do this

Open code/rotations_lab.py and do the first two parts. Twenty minutes.

1. Write the three matrices. Fill in the Rx, Ry and Rz stubs using nothing but numpy. Derive each one rather than looking it up: name the axis it leaves alone, put the 1 there, and drop the 2×2 block into the other pair. Then run part1_properties(). It composes a hundred random triple products and asserts that each is a genuine rotation, using the is_rotation helper already in the file, and it cross-checks your Rz against scipy. If you got a sign wrong, one of those two assertions catches it immediately; a sign error is the only realistic mistake here, and both checks are designed to find it.

2. Break commutativity yourself. Run part2_order_matters(), which applies Rz(90)Rx(90)R_z(90^{\circ})\,R_x(90^{\circ}) and Rx(90)Rz(90)R_x(90^{\circ})\,R_z(90^{\circ}) to the x-axis and prints both. You should get (0,1,0)(0, 1, 0) and (0,0,1)(0, 0, 1). Before you look at the output, predict both using only the columns rule and the diagram above; getting the prediction right is the actual exercise, and the print statement is just the marker.

Then, on paper: pull the sub-block of your Ry on indices [2, 0], in that order, and confirm by eye that it is the plain 2D rotation matrix. That is the minus-sign gotcha, retired for good.

part3_double_cover() and part4_slerp_vs_euler() in the same file belong to quaternions; leave them alone for now. The finished matrices, with the reasoning as comments, are in solutions/rotations_lab.py once yours runs.

What you can now do

You can write RxR_x, RyR_y and RzR_z from first principles instead of memory, explain why RyR_y‘s minus sign only looks displaced, compose rotations in the correct order and justify that order from the columns rule, and prove that composing rotations never produces anything but a rotation. You can say what each word in “special orthogonal group” is doing, count why an orientation has three degrees of freedom rather than nine, and repair a matrix that is nearly-but-not-quite a rotation.

What you can now do

You can write down Rx, Ry and Rz from first principles, compose them in the right order, and say exactly what SO(3) means and why an orientation only has three degrees of freedom.