💡 Reference · 3D Graphics
📅 March 2026⏱ ~10 min read🟡 Intermediate

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.

Convention shorthand: In displayed matrices on this page, rows go left-to-right and columns top-to-bottom. When passing to gl.uniformMatrix4fv() set transpose=false because the data is already column-major.

2. Translation, Scale, Identity

Identity

I
1
0
0
0
0
1
0
0
0
0
1
0
0
0
0
1

Translation T(tx, ty, tz)

1
0
0
0
0
1
0
0
0
0
1
0
tx
ty
tz
1

Scale S(sx, sy, sz)

sx
0
0
0
0
sy
0
0
0
0
sz
0
0
0
0
1

3. Rotation Matrices

Positive angle = counter-clockwise rotation when looking from positive axis toward origin. All angles in radians.

Rx(θ) — Rotation around X axis

1
0
0
0
0
0
0
−sθ
0
0
0
0
1

Ry(θ) — Rotation around Y axis

0
−sθ
0
0
1
0
0
0
0
0
0
0
1

Rz(θ) — Rotation around Z axis

0
0
−sθ
0
0
0
0
1
0
0
0
0
1

where cθ = cos(θ), sθ = sin(θ).

Gimbal lock: Composing Euler angles (Rx·Ry·Rz) loses a degree of freedom when one axis aligns with another. For smooth 3D rotation — particularly in simulations — use quaternions. Three.js 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)
Depth precision: The depth buffer maps [n,f] non-linearly to [−1,+1]. Precision is concentrated near the near plane and sparse at the far plane. For large worlds, use a reverse depth buffer (WebGPU) or logarithmic depth buffer (Three.js extension) to avoid z-fighting far from the camera.

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);
Avoid manual inverse in shaders: Computing 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.