3D Matrix Cheat Sheet — Model, View, Projection
All the 4×4 homogeneous matrices used in 3D graphics pipelines. Includes translation, rotation around each axis, scale, LookAt view, perspective, and orthographic projection — in WebGL/OpenGL column-major layout with Three.js code snippets.
1. Conventions: Row vs Column Major
OpenGL and WebGL use column-major storage. This means
the 16 floats in a Float32Array fill the matrix column by
column. A translation matrix looks like this in memory:
// Float32Array index: 0 4 8 12 // 1 5 9 13 // 2 6 10 14 // 3 7 11 15 // Translation T=(tx,ty,tz) in column-major: const m = new Float32Array([ 1, 0, 0, 0, // column 0 0, 1, 0, 0, // column 1 0, 0, 1, 0, // column 2 tx,ty,tz,1 // column 3 (translation) ]);
Three.js uses column-major internally and automatically transposes when needed for GLSL. Direct3D / HLSL uses row-major — all matrices below would be transposed.
gl.uniformMatrix4fv() set
transpose=false because the data is already column-major.
2. Translation, Scale, Identity
Identity
Translation T(tx, ty, tz)
Scale S(sx, sy, sz)
3. Rotation Matrices
Positive angle = counter-clockwise rotation when looking from positive axis toward origin. All angles in radians.
Rx(θ) — Rotation around X axis
Ry(θ) — Rotation around Y axis
Rz(θ) — Rotation around Z axis
where cθ = cos(θ), sθ = sin(θ).
Quaternion.setFromEuler() avoids gimbal lock.
4. View / LookAt Matrix
The view matrix transforms world-space coordinates into camera space. LookAt takes eye position E, target point T, and up vector U:
// Three orthonormal basis vectors of camera space: f = normalize(T − E) // forward (into screen) r = normalize(f × U) // right (U = world up, e.g. (0,1,0)) u = r × f // recalculated up (orthogonal) // LookAt matrix (column-major): // [ r.x u.x -f.x 0 ] // [ r.y u.y -f.y 0 ] // [ r.z u.z -f.z 0 ] // [−r·E −u·E f·E 1 ]
The translation row encodes −dot(axis, eye) to shift
everything so the camera sits at the origin of camera space.
5. Perspective Projection
Projects from camera space (frustum) to clip space. Parameters:
vertical field-of-view fovy, aspect ratio
a = width/height, near plane n, far plane
f.
// t = tan(fovy/2) (half-height at unit distance) t = tan(fovy / 2) // Perspective matrix (column-major, OpenGL/WebGL depth range −1..+1): // [ 1/(a·t) 0 0 0 ] // [ 0 1/t 0 0 ] // [ 0 0 −(f+n)/(f−n) −1 ] // [ 0 0 −2·f·n/(f−n) 0 ] // (−1 in position [2][3] means w_clip = −z_eye, causing perspective divide)
6. Orthographic Projection
No perspective divide — parallel projection. Defined by a box [l,r]×[b,t]×[n,f]:
// Orthographic matrix (column-major, OpenGL/WebGL depth −1..+1): // [ 2/(r−l) 0 0 0 ] // [ 0 2/(t−b) 0 0 ] // [ 0 0 −2/(f−n) 0 ] // [ −(r+l)/(r−l) −(t+b)/(t−b) −(f+n)/(f−n) 1 ]
Used for 2D HUDs, UI elements, shadow map passes, and top-down CAD
views. In Three.js:
new THREE.OrthographicCamera(left, right, top, bottom, near,
far).
7. MVP Pipeline Summary
// Transform pipeline in vertex shader: gl_Position = uProjection * uView * uModel * aPosition; // Equivalently: P * V * M * v_local // Object space → World space (Model matrix) // World space → Camera space (View matrix) // Camera space → Clip space (Projection matrix) // Clip space → NDC (perspective divide: xyz/w) // NDC → Screen pixels (viewport transform)
Normal vectors require a different transform — the transposed inverse of the upper-left 3×3 of the model matrix — to remain perpendicular to surfaces after non-uniform scaling:
// In GLSL — normal transform (pass as uniform) // mat3 normalMatrix = transpose(inverse(mat3(uModel))); vec3 worldNormal = normalize(uNormalMatrix * aNormal);
8. Three.js Matrix API
import * as THREE from 'three'; // Build model matrix from TRS (recommended over manual matrix composition) const object = new THREE.Object3D(); object.position.set(tx, ty, tz); object.rotation.set(rx, ry, rz, 'XYZ'); // or use .quaternion object.scale.set(sx, sy, sz); object.updateMatrix(); // updates .matrix (model matrix) object.updateMatrixWorld(true); // propagates to children // Access matrices directly const model = object.matrixWorld; // THREE.Matrix4 const view = camera.matrixWorldInverse; // THREE.Matrix4 const proj = camera.projectionMatrix; // THREE.Matrix4 // Manual matrix operations const m = new THREE.Matrix4(); m.makeTranslation(1, 2, 3); m.makeRotationY(Math.PI / 4); m.makeScale(2, 2, 2); // Compose from TRS m.compose(position, quaternion, scale); // Extract to Float32Array for WebGL uniforms const arr = new Float32Array(16); model.toArray(arr); gl.uniformMatrix4fv(loc, false, arr);
inverse(mat4) in GLSL is expensive (20+ operations).
Pre-compute inverses on the CPU (where float64 is available) and pass
them as uniforms: uModelInvTranspose,
uViewInv, etc.