Published on

Homogeneous Coordinates: The Fourth Number in Every Vertex

Authors
  • avatar
    Name
    Mamun Rashid

Every vertex a GPU draws carries a number nobody models. A point that should be three numbers, (x, y, z), shows up in a shader as four. For a long time I read the 1.0 at the end of vec4(pos, 1.0) as boilerplate. It turns out to be the reason one kind of operation can move, rotate, scale and project everything on screen.

This post builds the idea up with small diagrams you can drag. They stay in 2D, where the same trick uses 3×3 matrices instead of 4×4 and is much easier to see.

One extra number

A homogeneous vector is an ordinary point with one more coordinate, w, on the end:

3D point (x, y, z)   →   homogeneous (x, y, z, w)

To get the ordinary point back, divide by w:

(x, y, z, w)   →   (x/w, y/w, z/w)

So (2, 3, 4, 1), (4, 6, 8, 2) and (20, 30, 40, 10) all name the same point, (2, 3, 4). Scaling the whole vector doesn't change the point it stands for.

The picture that made this click for me is a slide projector. Every spot on the screen is hit by one beam of light, and anything along that beam lands on the same spot. A homogeneous vector is a position somewhere along a beam, and dividing by w asks where that beam hits the screen, which sits at w = 1.

Here it is one dimension down. A 1D point x becomes (x, w), the lamp sits at the origin and the screen is the line w = 1. Slide the point along its beam and the spot on the screen stays put. Drag w to 0 and the beam turns parallel to the screen.

Dividing by w: where the beam hits the screen
  • lamp at the origin
  • screen, the line w = 1
  • your vector (x, w)
  • where its beam lands

x runs left to right and w runs up.

3
1.5

(3, 1.5) → x / w = 3 / 1.5 = 2

The pale dots, (1.5, 0.75), sit on the same beam, so they land on the same spot.

What w means

w Meaning Example
1 A position (a point) A vertex of a mesh
0 A direction, or a point at infinity A surface normal, the direction of sunlight
anything else A point that hasn't been divided yet Coordinates partway through perspective projection

Moving things becomes a matrix multiply

With plain 3×3 matrices you can rotate and scale a 3D point, but you can't shift it sideways. A 3×3 matrix always leaves the origin where it is, and moving the origin is the whole point of a translation. Adding w makes room for a fourth column that holds the shift:

| 1 0 0 tx |   | x |   | x + tx |
| 0 1 0 ty | × | y | = | y + ty |
| 0 0 1 tz |   | z |   | z + tz |
| 0 0 0 1  |   | 1 |   |   1    |

Think of that trailing 1 as a switch on every point. With w = 1 the switch is on, and the last column, the shift, gets added in. With w = 0 the switch is off and the shift is ignored, which is what a direction needs. Moving a teapot shouldn't change which way its normals point.

The playground below is the 2D version, so the matrices are 3×3 and the shift lives in the third column. It scales the F, then rotates it, then moves it, and multiplies one sample vector by the same matrix. Flip the sample between a point and a direction and watch the shaded column switch on and off.

Translation is just the last column
2
1
30°
1×

Sample vector (2, 1, w)

0.87-0.520.50.871001
211
3.232.871

x′ = (0.87)(2) + (-0.5)(1) + (2)(1) = 3.23

y′ = (0.5)(2) + (0.87)(1) + (1)(1) = 2.87

w = 1 switches the last column on, so the point moves by (2, 1) on top of turning and scaling.

The scale slider scales both axes evenly on purpose. Stretch one axis only and multiplying a normal by the model matrix tilts it the wrong way. Normals need the inverse transpose of the matrix instead, which is a post of its own.

Chaining matrices, and why it's cheap

Once rotation, scaling and translation are all 4×4 matrices, any chain of them multiplies down to one matrix. Engines usually keep three. The model matrix places an object in the world, the view matrix moves the world in front of the camera, and the projection matrix adds perspective. Multiplied together they make the MVP matrix.

Say a model has a million vertices. Applying model, then view, then projection to every vertex takes three million matrix-times-vector multiplies. Multiplying the three matrices together first takes two matrix-times-matrix multiplies, and then each vertex needs one. A 4×4 matrix times a vector is 16 multiplications and a 4×4 matrix times another is 64, so the saving is close to the length of the chain:

Multiplications per frame
1,000,000
3
One matrix at a time, per vertex48,000,000

1,000,000 × 3 matrices × 16

Matrices multiplied together first16,000,128

2 × 64 + 1,000,000 × 16

3.0× fewer multiplications. A 4×4 matrix times a vector is 16 of them and a matrix times a matrix is 64, so combining first pays off as soon as there are more than a handful of vertices.

To be fair to the alternative, you could get the same saving without w. Carry a 3×3 matrix and a separate offset, and combine those by hand: A₂(A₁x + t₁) + t₂ is (A₂A₁)x + (A₂t₁ + t₂). That covers moving, rotating and scaling. It stops working at the next step, because perspective isn't that kind of transform at all. Homogeneous coordinates turn all of it, perspective included, into the same single operation, and that operation is what GPUs are built to do fast.

One catch in shaders. GLSL evaluates P * V * M * vec4(pos, 1.0) from left to right, so written that way it multiplies matrices together again for every vertex, unless the compiler happens to lift that work out. Engines usually multiply MVP once per object on the CPU and upload it as a single uniform:

uniform mat4 uMVP;
in vec3 pos;

void main() {
    gl_Position = uMVP * vec4(pos, 1.0);
}

The 1.0 is the w that marks the vertex as a point.

Perspective is a divide the GPU does for you

Far things look smaller, and on screen that means dividing x and y by distance. A matrix multiply only adds and multiplies, so it can't divide. The fix is a projection matrix that copies each point's distance in front of the camera into w. (In OpenGL's convention the camera looks down the negative z axis, so the matrix sets w to -z.) After the vertex shader runs, the GPU divides by w as a fixed step called the perspective divide:

after projection:   (x, y, z, w = distance)
after the divide:   (x / distance, y / distance, ...)

Slide the post down the track. Its height on screen is its real height divided by its distance, and every point on the rails gets the same divide, which is why they meet at a vanishing point.

Perspective: divide by distance
  • eye
  • screen, 1 unit from the eye
  • the post

Left: the scene from the side. Right: what lands on the screen.

3

clip space, top of the post: (y, w) = (1, 3)

perspective divide: y / w = 1 / 3 = 0.33

The post is 2 units tall wherever it stands. On screen it is 2 / 3 = 0.67, and every rail point gets the same divide, so the rails meet at the dot in the middle.

Clipping happens before the divide

GPUs work out which triangles are off screen while the coordinates are still homogeneous, in what's called clip space, before dividing by w. A point is in view when its x and y each sit between -w and w, and z gets a similar test. Checking it that way never divides by zero for points level with the camera, and geometry behind the camera, where w goes negative, gets cut off without any special case.

Points at infinity

With w = 0 you can describe something infinitely far away, like the sun, with no special code. Old fixed-function OpenGL used exactly this: a light position with w = 0 meant a directional light, every ray parallel, which is what sunlight looks like from down here. A vanishing point is the same idea on the screen side.

Where it all sits

Everything down to clip space is matrix multiplication you control. The divide and the step to pixels are fixed, and the GPU does them for you.

In short

Adding one number lets a single operation, 4×4 matrix multiplication, handle moving, rotating, scaling and perspective. That's why 3D engines and GPUs are built around it. The math underneath is called projective geometry, if you want to dig further.

Enjoyed this post?

Subscribe to get notified about new posts and updates. No spam, unsubscribe anytime.

By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.

Discussion

Loading comments...

This website is still under development. If you encounter any issues, please contact me.