The Model Matrix
Move, turn and stretch an object by dragging the numbers that do it, and watch the matrix fill in as you go.
What you should come away with: Why a matrix chain reads right to left, and why the columns are the object’s own axes.
A model is drawn once, at the origin, facing whichever way the person who made it happened to be facing. The world needs it somewhere else — over there, turned forty degrees, half the size. Nobody edits the vertices to do that. Instead the object keeps the coordinates it was born with, and a matrix says where those coordinates should be read.
That matrix is the subject of this lab. Sixteen numbers, and every one of them is visible below while you move it.
Translation lives in the last column#
Start with the simplest instruction there is: put it over there. Drag the slider and watch the readout rather than the cube.
| 1.00 | 0.00 | 0.00 | 1.20 |
| 0.00 | 1.00 | 0.00 | 0.00 |
| 0.00 | 0.00 | 1.00 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
Three of the sixteen numbers do anything at all, and they sit in the rightmost column. This is the reason 3D graphics uses a four-by-four matrix for a three-dimensional world: a 3×3 matrix can rotate, scale, shear and reflect, but it has nowhere to put a displacement. Every 3×3 transform leaves the origin exactly where it found it, because multiplying a column of zeros can only ever give zeros back.
The fourth column is bought with a fourth coordinate. Positions are carried as (x, y, z, 1), and it is that trailing 1 that lets the last column contribute — it multiplies by one and is added in. Directions are carried as (x, y, z, 0) instead, which is not a technicality but the whole trick: a direction has no position, so the zero deletes the translation and a or a light vector is rotated without being dragged across the scene with the object.
The other nine numbers are the object’s axes#
Turn the cube and the last column stays exactly where it was. Rotation happens entirely inside the upper-left 3×3.
| 0.82 | 0.00 | 0.57 | 0.00 |
| 0.00 | 1.00 | 0.00 | 0.00 |
| -0.57 | 0.00 | 0.82 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
The three coloured arms are not a decoration drawn to look like axes. They are the first three columns of the matrix, plotted as arrows: the object’s . The first column is where the object’s own x axis has ended up in the world; the second is its y; the third is its z. Read the readout and the picture together for a moment — the numbers in column one are the coordinates of the red arm.
Once you have seen that, a stops being a grid of numbers and becomes a sentence with four clauses: here is where your x points, here is your y, here is your z, and here is where you are. Everything else in this lab follows from that reading.
In memory the matrix is those four columns, end to end#
A Mat4 here is a Float32Array of sixteen, stored column-major: an entry’s index is column * 4 + row. Four columns of four, so they land as blocks: m[0]–m[3] is the first column, m[4]–m[7] the second, m[8]–m[11] the third, m[12]–m[15] the last. Set that beside the reading the arrows gave you and the layout is no longer an arbitrary convention — the array is the object’s x axis, then its y axis, then its z axis, then where it stands.
M, as sixteen floats in order
x axis
m[0]–m[3]
y axis
m[4]–m[7]
z axis
m[8]–m[11]
position
m[12]–m[15]
| 0.81 | 0.00 | 0.58 | 1.43 |
| 0.00 | 1.00 | 0.00 | 0.00 |
| -0.58 | 0.00 | 0.81 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
m[0], m[2], m[8] and m[10] — the x and z blocks, because a turn about y happens in the xz plane — and leaves the y block sitting at 0, 1, 0, 0. The move lands entirely in m[12]. Underneath is the same array printed as a matrix, where the numbers read across instead of down.#Open this in the full instrument The builders in lib/math/mat4.ts are typed out in that order too. translation(x, y, z) is the identity with x, y, z, 1 on its last line, which is why the arguments land at indices 12, 13 and 14. rotationY puts its cosine at m[0] and m[10], the sine at m[8] and the negated sine at m[2], and never touches m[4]–m[7]: the y column is the axis it turns about, so the y column is the one thing it leaves alone.
Nothing rearranges those floats on the way to the GPU. The upload is gl.uniformMatrix4fv(location, false, m), and that false is a transpose flag: the array already sits in the order OpenGL wants, so it goes across as it is. In WebGL 1, which every canvas on this page runs on, the flag is not even a choice: passing true is an error.
The one place the order does get rearranged is the readout you have been watching all along. toRows() walks the array with a stride of four, so the top row it prints is m[0], m[4], m[8], m[12] — one entry taken from each column. That is whiteboard notation, and it is the transpose of the buffer. Read the sixteen floats four at a time as though they were rows instead, and the 2.20 you drove into m[12] comes out at the start of the bottom row rather than the top of the last column. Both pictures describe the same buffer. Only one of them is the buffer.
The blocking buys something practical as well: a column is contiguous, so asking where an object is means reading three adjacent floats — m[12], m[13], m[14] — not gathering three that sit four apart.
Scale stretches the axes, and can invert them#
If the columns are the axes, scaling has an obvious meaning: make one of them longer.
| 0.91 | 0.00 | 0.42 | 0.00 |
| 0.00 | 1.50 | 0.00 | 0.00 |
| -0.42 | 0.00 | 0.91 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
A uniform scale multiplies all three columns equally and is harmless. A non-uniform one is where trouble starts, and lab 4 is largely about the consequence: stretch an object along one axis and its surface normals, if you transform them with this same matrix, stop being perpendicular to the surface. They need the instead. That bug is waiting in Light & Normals with a preset that turns it on.
A negative scale is worth a second of your attention because it is the one transform here that changes the winding of the triangles — the order their corners appear in on screen. decides what to throw away using exactly that, so a mirrored object rendered without thinking about it comes out with its faces inside out.
Order is the whole difficulty#
Matrix multiplication is not commutative, which is a dry way of saying that turning something and then moving it does not put it where moving it and then turning it would. This is the single most common source of confusion in a transform chain, and it is much easier to see than to argue about.
T · R — turn, then move
R · T — move, then turn
Both sides are made of the same two matrices. What differs is which one the vertex meets first — and the vertex meets them right to left. In T · R · v the rotation is adjacent to the vector, so it happens first, in the object’s own frame, and the translation is applied afterwards to the already-turned result. Reverse them and the translation has moved the object away from the origin before the rotation arrives, so the rotation sweeps it through an arc instead of spinning it in place.
The convention people are taught — scale, then rotate, then translate — is this observation with the usual answer already chosen. Written as a product it reads T · R · S, backwards from the order it happens in. That is not a quirk of notation to memorise around; it is what right-to-left evaluation means, and once the chain is read that way it stops being something to get wrong.
You want the cube at half size, turned 90° about y, and standing 5 units along x. Written as a product, which chain does that?
Now move all of it at once#
Everything above is one control at a time. Below is the whole matrix with nothing held back: nine numbers of rotation and scale, three of position, the , and the factors it multiplies out from. The presets are worth starting with — each one sets the controls to something that makes a point, and says what to look at.
- X
- Y
- Z
The matrix
| 1.00 | 0.00 | 0.00 | 1.40 |
| 0.00 | 1.00 | 0.00 | 0.50 |
| 0.00 | 0.00 | 1.00 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
| 0.91 | 0.00 | 0.42 | 0.00 |
| 0.00 | 1.00 | 0.00 | 0.00 |
| -0.42 | 0.00 | 0.91 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
| 1.00 | 0.00 | 0.00 | 0.00 |
| 0.00 | 1.00 | 0.00 | 0.00 |
| 0.00 | 0.00 | 1.00 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
| 0.91 | 0.00 | 0.42 | 1.40 |
| 0.00 | 1.00 | 0.00 | 0.50 |
| -0.42 | 0.00 | 0.91 | 0.00 |
| 0.00 | 0.00 | 0.00 | 1.00 |
The first three columns of M are the cube’s own axes after the transform — that is exactly what the red, green and blue arms on the canvas are drawing. The fourth column, in amber, is where its origin ended up. Drag a translate slider and watch only that column move.
Where that leaves you
You can now read a model matrix without multiplying it out — which column is the position, which three are the object’s own axes — and say why turning a cube and then moving it leaves it somewhere else entirely from moving it and then turning it.
This did not teach you what happens when one object hangs off another. Every matrix here starts at the world origin, so the compounding chain a scene graph keeps — each object’s matrix multiplied by its parent’s before it ever reaches a shader — never appears; that is among the things a library does on your behalf, and the Three.js entry under /tech is where this site says so.