Matrices & MVP
4×4 matrices, homogeneous coordinates and the model, view and projection chain that puts a vertex on the screen.
How it works
A mesh stores its vertices relative to its own origin, the way the artist modeled it. To draw it, every vertex has to
end up at a pixel: placed in the world, seen from the camera, projected onto the screen. Each of those steps is a
matrixA grid of numbers (mat2, mat3, mat4) that maps one vector to another in a single multiplication. Rotation, scale and, with homogeneous coordinates, translation and projection are all matrices. Matrix A grid of numbers (mat2, mat3, mat4) that maps one vector to another in a single multiplication. Rotation, scale and, with homogeneous coordinates, translation and projection are all matrices. Coordinate space The frame a position or direction is measured in, such as object, world, view, clip or screen space. Only values in the same space can be combined.rotate(a) = [cos a −sin a; sin a cos a]
Why 4×4
A Homogeneous coordinates A 3D point written with a fourth component w. With w = 1 a 4×4 matrix can translate it; with w = 0 it is a direction that ignores translation. Dividing by w undoes it, which is how perspective projection works.mat3 can rotate and scale a 3D vector, but it can’t translate it: multiplying the zero vector by any matrix gives
zero, so the origin can never move. The fix is homogeneous coordinatesA 3D point written with a fourth component w. With w = 1 a 4×4 matrix can translate it; with w = 0 it is a direction that ignores translation. Dividing by w undoes it, which is how perspective projection works.(x, y, z, w) → (x/w, y/w, z/w)vec4(x, y, z, 1.0) and put the translation in the matrix’s fourth column. Multiplying adds the
translation times that 1:
mat4 translate(vec3 t) {
return mat4(1.0, 0.0, 0.0, 0.0, // column 0
0.0, 1.0, 0.0, 0.0, // column 1
0.0, 0.0, 1.0, 0.0, // column 2
t.x, t.y, t.z, 1.0); // column 3: the translation
}
As with mat2, GLSL fills matrices column by column, so the translation sits in the last group of four numbers, and
m[3] reads that column back.
The fourth component also tells points from directions. A point has w = 1 and is moved by translation. A direction
(a normal, a light direction, a velocity) has w = 0, so the translation column is multiplied by zero and it only
rotates and scales. That is the right behavior: moving an object shouldn’t change which way its surface faces.
The chain
- Model takes the vertex from object space to world space: the object’s scale, rotation and position in the scene. Each object has its own.
- View takes world space to view space, where the camera sits at the origin looking down an axis (−z in OpenGL and WebGL). It is the inverse of the camera’s own model matrix: moving the camera right is the same as moving the whole world left, the same reversal as moving space instead of the shape in 2D.
- Projection takes view space to clip space. A perspective projection copies the distance in front of the camera
into
w; an orthographic one leaveswat 1.
Matrices apply right to left, so the one nearest the vector acts first:
// vertex shader
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
in vec3 aPosition;
void main() {
vec4 world = uModel * vec4(aPosition, 1.0);
// model first, then view, then projection
gl_Position = uProjection * uView * world;
}
Matrix products are associative but not commutative: P * V * M can be grouped as (P * V) * M and computed once,
but the order of the three can’t change. Programs usually multiply them on the CPU, once per object, and send one
matrix rather than doing two extra 4×4 multiplications in every vertex.
Clip space and the divide
gl_Position is in clip space, still four-dimensional. The GPU clips triangles to the visible volume, where x and y lie
between −w and w, and z between −w and w in OpenGL and WebGL (0 and w in Direct3D, Vulkan and Metal), then divides by
w. The result is normalized device coordinates (NDC): −1 to 1 on x and y across the viewport. Because a perspective
matrix put the distance in w, that divide shrinks far things toward the center, which is what perspective is. The
viewport transform then maps NDC to pixels, the gl_FragCoord a fragment shader reads.
Depth conventions differ. OpenGL and WebGL map NDC z to −1..1; Direct3D, Vulkan and Metal use 0..1, and many engines reverse it (1 near, 0 far) for better precision. Code that reads depth back has to know which one it is getting.
Inverses
Going backward is multiplying by the inverse. Unprojecting a pixel to a world-space point, for a ray from the camera
or to rebuild a position from the depth buffer, uses the inverse of P * V and then divides by w:
vec4 h = inverse(uProjection * uView) * vec4(ndc, 1.0); // ndc: x, y and depth in −1..1
vec3 worldPos = h.xyz / h.w;
GLSL ES 3.00 has inverse(), but it is expensive; compute inverses on the CPU and pass them in when you can.
Normals need care. With a non-uniform scale (say 2 in x and 1 in y), transforming a normal by the model matrix tilts it so it is no longer perpendicular to the surface. The correct matrix for normals is the inverse transpose of the model matrix’s upper 3×3, often called the normal matrix:
mat3 normalMatrix = transpose(inverse(mat3(uModel))); // better computed once on the CPU
vec3 n = normalize(normalMatrix * aNormal);
With only rotation and uniform scale, mat3(uModel) works too, followed by normalize.
In engines and on this site
Engines build these matrices for you and expose them by name: Godot’s spatial shaders have MODEL_MATRIX,
VIEW_MATRIX and PROJECTION_MATRIX, Unity has UNITY_MATRIX_MVP and friends, and three.js passes modelMatrix,
viewMatrix and projectionMatrix. The cards on this site hand effect.glsl positions and normals already in world
space, so a card’s own code rarely touches the chain. It matters when you move vertices, read depth, or port a shader
between engines that differ in handedness and depth range.