Chapter 17

Homogeneous Coordinates

Translation isn't a linear transformation -- it breaks the rule that the origin stays fixed. So how do game engines handle translation with matrices? A clever dimensional trick.

We've spent many chapters building up the theory of linear transformations: matrices that rotate, scale, shear, and reflect. But we've been quietly avoiding a transformation that every game engine, every graphics pipeline, and every robotics system needs constantly: moving things around. Translation -- shifting every point by some offset -- is the most basic geometric operation, and it's the one thing a 2x2 matrix cannot do.

The reason is fundamental. Linear transformations must map the origin to the origin. That's baked into the definition: A0=0A\vec{0} = \vec{0} for any matrix AA. But translation moves the origin somewhere else. If you want to shift everything right by 3 and up by 2, the origin goes to (3,2)(3, 2). No 2x2 matrix can do that.

The solution is one of the most elegant tricks in all of computer graphics: embed 2D space into 3D, perform the translation as a linear transformation in the higher dimension, and project back down. This is the idea behind homogeneous coordinates, and it's the reason every transform in a modern graphics engine is a matrix multiply.

Translation can't be a 2x2 matrix

Let's see the problem concretely. Take a small triangle at the origin and try to translate it to position (3,2)(3, 2). Every vertex needs to shift right by 3 and up by 2.

A 2x2 matrix maps v\vec{v} to AvA\vec{v}. When v=0\vec{v} = \vec{0}, the result is always 0\vec{0}. The origin is pinned. But we need the origin to move to (3,2)(3, 2). No 2x2 matrix can accomplish this.

original translated +(3, 2) 2x2 matrix? a b c d (0,0) = (0,0) origin stays at (0,0)!

The blue triangle sits at the origin. We want to move it to where the orange triangle is -- shifted by (3,2)(3, 2). But any 2x2 matrix maps (0,0)(0, 0) to (0,0)(0, 0). The origin is immovable. Translation is not a linear transformation in 2D.

You could handle this outside the matrix framework -- just add a translation vector separately:

v=Av+t\vec{v}' = A\vec{v} + \vec{t}

And many systems do exactly that. But this breaks the elegant composability of matrices. You can't combine a rotation and a translation into a single matrix multiply. You lose the ability to represent your entire transform pipeline as one matrix. Game engines need a better solution.

The trick: add a dimension

The trick is deceptively simple. Instead of representing a 2D point as (x,y)(x, y), represent it as (x,y,1)(x, y, 1). This extra coordinate -- always set to 1 for points -- lifts 2D space into a slice of 3D space. Now a 3x3 matrix can encode translation in its third column, because that column gets multiplied by the 1.

[10tx01ty001][xy1]=[x+txy+ty1]\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ 1 \end{bmatrix}

The third column (tx,ty,1)(t_x, t_y, 1) is what makes translation work. When multiplied by the 1 in the coordinate vector, it adds the translation directly to xx and yy. The bottom row [0  0  1][0\;0\;1] preserves the 1, keeping us in the homogeneous "slice."

(2, 1) 2D point lift to 3D 2 1 1 [ ] (x, y, 1) 3x3 Matrix Structure a b c d tx ty 0 0 1 scale / rotate translate always [0 0 1] [ ] The extra 1 unlocks translation Top-left 2x2 handles rotation/scale. Third column handles translation.

A 2D point (x,y)(x, y) gets lifted to (x,y,1)(x, y, 1) in homogeneous coordinates. The 3x3 matrix has a clear structure: the top-left 2x2 block handles rotation and scaling (the linear part), the third column handles translation, and the bottom row is always [0  0  1][0\;0\;1] to preserve the homogeneous coordinate.

The name "homogeneous coordinates" comes from projective geometry, but the practical idea is straightforward. By embedding 2D space as the w=1w = 1 plane in 3D, we turn translation (which is affine, not linear) into a linear operation in the higher-dimensional space. The 3x3 matrix is a genuine linear transformation in 3D -- it's just that we only care about the w=1w = 1 slice.

Translation as a 3x3 matrix

Now let's see this in action. The homogeneous translation matrix that moves every point by (tx,ty)(t_x, t_y) is:

T=[10tx01ty001]T = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}

The top-left block is the identity (no rotation or scaling), and the translation sits in the third column. Let's translate a triangle by (3,2)(3, 2):

[103012001][001]=[321]\begin{bmatrix} 1 & 0 & 3 \\ 0 & 1 & 2 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 3 \\ 2 \\ 1 \end{bmatrix}

The origin moved to (3,2)(3, 2). Translation is now a matrix multiply.

(0,0) (1,0) (0.5,1) (3,2) (4,2) (3.5,3) +(3, 2) Translation Matrix T [ 1 0 3 0 1 2 0 0 1 ] T(x, y, 1) = (x+3, y+2, 1)

The translation matrix shifts every vertex by (3,2)(3, 2). The dashed blue triangle is the original, and the solid orange triangle is the translated result. Each vertex moves by exactly the same offset -- that's what translation does. And now it's a matrix multiply.

Notice what happens if you apply this matrix to the origin in homogeneous coordinates:

T[001]=[321]T \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 3 \\ 2 \\ 1 \end{bmatrix}

The origin moved to (3,2)(3, 2). In 3D, this is perfectly valid -- the origin of 3D space (0,0,0)(0, 0, 0) isn't being mapped to itself. We're operating on the point (0,0,1)(0, 0, 1), which represents 2D point (0,0)(0, 0), and it maps to (3,2,1)(3, 2, 1), representing 2D point (3,2)(3, 2). The linearity rules are satisfied in 3D. We've sidestepped the constraint by working in a higher dimension.

Full transform: scale, rotate, translate

The real power of homogeneous coordinates isn't just translation -- it's that you can combine scale, rotation, and translation into a single 3x3 matrix. One matrix multiply does everything.

Each transformation type embeds into 3x3 form:

Scale by (sx,sy)(s_x, s_y): put the scale in the top-left 2x2 block.

S=[sx000sy0001]S = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}

Rotate by angle θ\theta: the rotation goes in the top-left 2x2 block.

R=[cosθsinθ0sinθcosθ0001]R = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}

Translate by (tx,ty)(t_x, t_y): translation sits in the third column.

T=[10tx01ty001]T = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}

To apply all three, you multiply the matrices together. The standard order is TRS (translate ×\times rotate ×\times scale), applied right to left. This means: first scale the object in its local space, then rotate it, then translate it to its final position.

original scaled 1.5x rotated 30 final position Transform Pipeline (right to left): S: Scale 1.5x applied first R: Rotate 30 applied second T: Translate (2, 1) applied last M = T * R * S (one matrix does everything)

The pipeline: start with the unit square (blue dashed), scale it by 1.5 (green dashed), rotate 30 degrees (purple dashed), then translate to (2,1)(2, 1) (solid orange). The composition M=TRSM = TRS is a single 3x3 matrix that does all three steps in one multiply.

This is exactly how every 2D game engine works. Each game object stores a position, rotation angle, and scale. At render time, those values get packed into a single 3x3 matrix M=TRSM = TRS. Then every vertex of the object gets multiplied by MM. One matrix-vector multiply per vertex, no matter how complex the transformation.

The formal bit

Homogeneous coordinates represent a 2D point (x,y)(x, y) as the 3D vector (x,y,1)(x, y, 1). More generally, any scalar multiple (wx,wy,w)(wx, wy, w) with w0w \neq 0 represents the same 2D point (x,y)(x, y) -- you recover the 2D point by dividing by ww. The case w=1w = 1 is the standard representation.

The translation matrix in homogeneous coordinates:

T(tx,ty)=[10tx01ty001]T(t_x, t_y) = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}

Any 2D linear transformation AA embeds into 3x3 form by placing AA in the top-left 2x2 block:

[A00T1]=[ab0cd0001]\begin{bmatrix} A & \vec{0} \\ \vec{0}^T & 1 \end{bmatrix} = \begin{bmatrix} a & b & 0 \\ c & d & 0 \\ 0 & 0 & 1 \end{bmatrix}

The composition order for a typical game transform is:

M=TRSM = T \cdot R \cdot S

Applied right to left: first scale (in local space), then rotate (around the origin), then translate (to the final position). The combined matrix is:

M=[sxcosθsysinθtxsxsinθsycosθty001]M = \begin{bmatrix} s_x\cos\theta & -s_y\sin\theta & t_x \\ s_x\sin\theta & s_y\cos\theta & t_y \\ 0 & 0 & 1 \end{bmatrix}

This single matrix encodes scale, rotation, and translation. To transform a point, you do one matrix-vector multiply. To compose two transforms (parent and child in a scene graph), you do one matrix-matrix multiply. The entire transform hierarchy reduces to matrix arithmetic.

Key properties of homogeneous transform matrices:

That last point is subtle and powerful. If you set the third coordinate to 0 instead of 1:

[10tx01ty001][xy0]=[xy0]\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 0 \end{bmatrix} = \begin{bmatrix} x \\ y \\ 0 \end{bmatrix}

The translation has no effect. This is exactly right for direction vectors -- a velocity or a surface normal shouldn't change when you move an object. Homogeneous coordinates automatically handle the distinction between points (which translate) and directions (which don't).

Worked example: a 2D game sprite transform

Let's build a complete 2D game object transform from scratch. We have a sprite that needs to be:

  1. Scaled by 2 (doubled in size)
  2. Rotated 30 degrees counterclockwise
  3. Translated to position (100,50)(100, 50) in world coordinates

We'll work in world units (not pixels) and build each 3x3 matrix.

Step 1: Scale matrix.

S=[200020001]S = \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix}

Step 2: Rotation matrix (30 degrees, cos30°0.866\cos 30° \approx 0.866, sin30°=0.5\sin 30° = 0.5).

R=[0.8660.500.50.8660001]R = \begin{bmatrix} 0.866 & -0.5 & 0 \\ 0.5 & 0.866 & 0 \\ 0 & 0 & 1 \end{bmatrix}

Step 3: Translation matrix.

T=[101000150001]T = \begin{bmatrix} 1 & 0 & 100 \\ 0 & 1 & 50 \\ 0 & 0 & 1 \end{bmatrix}

Step 4: Compose M=TRSM = T \cdot R \cdot S (right to left).

First, compute RSRS:

RS=[0.8660.500.50.8660001][200020001]=[1.7321.001.01.7320001]RS = \begin{bmatrix} 0.866 & -0.5 & 0 \\ 0.5 & 0.866 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 1.732 & -1.0 & 0 \\ 1.0 & 1.732 & 0 \\ 0 & 0 & 1 \end{bmatrix}

Then multiply by TT:

M=TRS=[101000150001][1.7321.001.01.7320001]=[1.7321.01001.01.73250001]M = T \cdot RS = \begin{bmatrix} 1 & 0 & 100 \\ 0 & 1 & 50 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1.732 & -1.0 & 0 \\ 1.0 & 1.732 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 1.732 & -1.0 & 100 \\ 1.0 & 1.732 & 50 \\ 0 & 0 & 1 \end{bmatrix}

The final matrix MM encodes the entire transform. Let's verify by applying it to a vertex, say the corner at (1,0)(1, 0):

M[101]=[1.7321+(1.0)0+10011.01+1.7320+5010+0+1]=[101.73251.01]M \begin{bmatrix} 1 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 1.732 \cdot 1 + (-1.0) \cdot 0 + 100 \cdot 1 \\ 1.0 \cdot 1 + 1.732 \cdot 0 + 50 \cdot 1 \\ 0 + 0 + 1 \end{bmatrix} = \begin{bmatrix} 101.732 \\ 51.0 \\ 1 \end{bmatrix}

The vertex (1,0)(1, 0) ended up at approximately (101.7,51.0)(101.7, 51.0) in world coordinates. That's exactly what we'd expect: the point was scaled to (2,0)(2, 0), rotated 30 degrees to (1.732,1.0)(1.732, 1.0), then translated by (100,50)(100, 50) to land at (101.732,51.0)(101.732, 51.0).

In code, this looks like:

function makeTransform(tx, ty, angleDeg, sx, sy) {
  const rad = angleDeg * Math.PI / 180;
  const cos = Math.cos(rad);
  const sin = Math.sin(rad);

  // M = T * R * S, combined into one matrix
  return [
    [sx * cos, -sy * sin, tx],
    [sx * sin,  sy * cos, ty],
    [0,         0,         1]
  ];
}

function transformPoint(matrix, x, y) {
  return [
    matrix[0][0] * x + matrix[0][1] * y + matrix[0][2],
    matrix[1][0] * x + matrix[1][1] * y + matrix[1][2]
  ];
}

const M = makeTransform(100, 50, 30, 2, 2);
transformPoint(M, 1, 0);  // [101.732, 51.0]
transformPoint(M, 0, 0);  // [100, 50] (the origin moved to the position)

Every game engine and UI framework has some version of this. Unity calls it Transform, CSS calls it matrix(), SVG calls it the transform attribute. Under the hood, it's always the same 3x3 (or 4x4 in 3D) homogeneous matrix.

Key Takeaway: Homogeneous coordinates let you represent translation as a matrix multiply by working in one dimension higher. A 2D point (x,y)(x, y) becomes (x,y,1)(x, y, 1), and a 3x3 matrix encodes scale, rotation, and translation all at once. One matrix per game object, one multiply per vertex. The TRS composition order -- translate ×\times rotate ×\times scale, applied right to left -- is the standard pipeline in every 2D (and 3D) graphics system.

What's next

Everything we've done in 2D extends to 3D. The 3D graphics pipeline adds one more transformation: perspective projection -- turning a 3D scene into a flat image. That requires 4x4 homogeneous matrices, and the same principles apply: embed 3D space into 4D with a ww coordinate, and suddenly projection is just another matrix multiply.