MathematicsTransformscore3 min read
Transforms and Matrices
pack move, turn and resize into one multiply
A 4×4 matrix stores translation, rotation and scale in one object. Multiply to combine transforms, invert to undo them, and mind the order.
The solid white ship uses the order you pick; the grey wireframe ghost uses the other order with the same T, R and S. The short axis lines at the origin are world X (red) and Z (white). Rotate with a non-zero translation and watch R·T·S swing the ship around the origin while T·R·S only turns it in place.
What it is
Every object in a 3D scene has a transform: where it is (translation T), how it is turned (rotation R) and how big it is (scale S). A 4×4 matrix can hold all three, so moving a vertex from model space into the world is one matrix–vector multiply, and a chain of transforms collapses into one matrix.
The 4th row and column exist because translation is not linear in 3D. Writing a point as (x, y, z, 1) (homogeneous coordinates) lets the last column add the offset. Directions use w = 0, so they rotate and scale but never move. That is why normals and velocities must not pick up an object's position.
Order matters. With column vectors (Three.js, OpenGL, most maths texts) the rightmost matrix acts first. T·R·S scales the mesh, spins it in place, then moves it: the usual object transform. R·T·S moves it first and then spins the whole offset around the origin, so the object orbits instead. Matrix multiplication is not commutative.
In Three.js each Object3D keeps position, quaternion and scale, and builds matrix = T·R·S from them. matrixWorld = parent.matrixWorld · matrix is refreshed by updateMatrixWorld(), which the renderer calls once per frame. See local vs world space for the hierarchy.
- matrix
- matrix4
- TRS
- transform
- homogeneous coordinates
Key formulas
- Object transform (rightmost first)
White is the object built with T·R·S; the grey outline is the same pieces multiplied as R·T·S.
- Homogeneous point and direction
- Inverse of a TRS
Reverse the order and invert each piece. A pure rotation inverts by transposing.
- Normal matrix
Needed for normals when the scale is non-uniform.
Rules of thumb
- Read a product right to left:
A·B·papplies B, then A. - Three.js
a.multiply(b)isa = a·b;a.premultiply(b)isa = b·a. - If you set
object.matrixyourself, setmatrixAutoUpdate = falseor Three.js overwrites it from position/quaternion/scale. - Call
updateMatrixWorld()before readingmatrixWorldin the same frame you moved something. - Keep scales uniform where you can; non-uniform scale under a rotated child creates shear that cannot be decomposed back into TRS.
Implementation
THREE.Matrix4:const T = new THREE.Matrix4().makeTranslation(3, 0, 0)const R = new THREE.Matrix4().makeRotationY(Math.PI / 2)const S = new THREE.Matrix4().makeScale(2, 2, 2)
// T·R·S: scale, then rotate in place, then move. (Rightmost first.)const trs = new THREE.Matrix4().multiplyMatrices(T, R).multiply(S)// R·T·S: move first, then rotate the offset → the object orbits.const rts = new THREE.Matrix4().multiplyMatrices(R, T).multiply(S)
const p = new THREE.Vector3(0, 0, 0)p.clone().applyMatrix4(trs) // (3, 0, 0)p.clone().applyMatrix4(rts) // (0, 0, -3)
// Same as T·R·S, from parts:const m = new THREE.Matrix4().compose(position, quaternion, scale)
// Undo: world point back into the object's space.const inv = trs.clone().invert()const local = worldPoint.clone().applyMatrix4(inv)
// Directions ignore translation (w = 0).const dir = new THREE.Vector3(1, 0, 0).transformDirection(trs)Common mistakes
- Multiplying in the wrong order and getting orbiting instead of spinning (or vice versa).
- Transforming a direction or normal with
applyMatrix4, which adds the translation. UsetransformDirectionor the normal matrix. - Reading
matrixWorldright after moving a parent, beforeupdateMatrixWorld()has run. - Writing to
object.matrixwhilematrixAutoUpdateis still true; your values vanish next frame. - Inverting a matrix every frame when you only need the transpose of a pure rotation.
Prompt
Write `src/lib/mat4.ts` in TypeScript with column-major 4×4 matrices stored as number[16] (same layout as THREE.Matrix4.elements): identity, translation(x,y,z), rotationX/Y/Z(rad), scaling(x,y,z), multiply(a,b) where b is applied first, invert(m) returning null when |det| < 1e-12, transformPoint (w = 1) and transformDirection (w = 0). Add composeTRS(position, quaternion, scale) that matches THREE.Matrix4.compose. Write Vitest tests: T·R·S and R·T·S give different results for a 90° Y rotation; invert(m)·m equals identity within 1e-6 for a random TRS; transformDirection ignores translation; composeTRS matches Three.js element by element. No Three.js imports in the library itself.In learning paths
Related
- Local vs World Spaceevery position is relative to someone
- Euler Angles and Gimbal Lockthree turns in a row, and the order is part of the answer
- Quaternionsfour numbers that rotate without gimbal lock
- Vector Basicsan arrow with a length and a direction, nothing more
- Dot and Cross Productone number for alignment, one vector for perpendicular
Further reading
Updated 2026-11-01
