The MVP Pipeline

You have now met every stage on its own. Time to put a single vertex on the conveyor belt and watch it travel all the way from a modelling tool to a glowing pixel. The journey has a name every graphics programmer mutters in their sleep: the MVP pipelineModel, View, Projection — followed by the two fixed-function steps that finish the job.

One vertex, end to end, line by line

Step 1 — list the spaces in order. A vertex is reborn in a new coordinate system at each stage:

\text{object} \xrightarrow{\;M\;} \text{world} \xrightarrow{\;V\;} \text{camera} \xrightarrow{\;P\;} \text{clip} \xrightarrow{\;\div w\;} \text{NDC} \xrightarrow{\;\text{viewport}\;} \text{screen}.

Step 2 — place it in the world with the model matrix. The model matrix M carries the vertex from the mesh's private object space into the shared world:

\vec{x}_{\text{world}} = M\,\vec{x}_{\text{object}}.

Step 3 — look at it through the camera. The view matrix V re-expresses the world from the camera's point of view:

\vec{x}_{\text{camera}} = V\,\vec{x}_{\text{world}} = V M\,\vec{x}_{\text{object}}.

Step 4 — project to clip space. The projection matrix P sets up the perspective by writing the depth into w:

\vec{x}_{\text{clip}} = P\,\vec{x}_{\text{camera}} = P V M\,\vec{x}_{\text{object}}.

Step 5 — collapse the three into one matrix. Matrix multiplication is associative, so bake the trio together once and apply a single matrix per vertex:

\mathrm{MVP} = P \cdot V \cdot M, \qquad \vec{x}_{\text{clip}} = \mathrm{MVP}\,\vec{x}_{\text{object}}.

Step 6 — read the order off the product. With column vectors the rightmost matrix acts first. Reading \mathrm{MVP}\,\vec{x} = P(V(M\vec{x})) right-to-left, the vertex is modelled, then viewed, then projected — exactly the order of Steps 2–4, even though P is written on the left.

Step 7 — finish with the fixed-function steps. The programmable matrices stop at clip space. The hardware then does the perspective divide and the viewport transform:

\vec{x}_{\text{NDC}} = \frac{\vec{x}_{\text{clip}}}{w}, \qquad \vec{x}_{\text{screen}} = \text{viewport}\big(\vec{x}_{\text{NDC}}\big).

Step 8 — trace a concrete vertex. A teapot's spout vertex lives at (0, 1, 0) in object space; M sets it down in the scene; V swings it into the camera's frame; P loads its depth into w; the divide shrinks it for distance; the viewport plants it on, say, pixel (812, 339). One vertex, six coordinate systems, a couple of microseconds.

Every vertex follows the same route from mesh to pixel:

Get the order wrong and the screen doesn't just look slightly off — it goes blank or smears into nonsense. With column vectors the rule is fixed: \mathrm{MVP} = P \cdot V \cdot M, applied right-to-left. Swap two matrices — say upload M \cdot V \cdot P instead — and the product is a different transform entirely, not a small perturbation of the right one. There's no "almost right" here: the vertex is projected before it's even placed in the world, and whatever survives the divide is garbage.

The confusion usually starts with row-vector engines. Some APIs and shader languages (notably HLSL's default, used by DirectX-style pipelines) store vectors as rows and multiply on the left: \vec{x}_{\text{row}}\,M\,V\,P. Read left-to-right, that line looks like M acts first and P last — the opposite order on the page from the column-vector form above, even though it is the same physical pipeline (model, then view, then project). Mixing the two conventions — copying a column-vector matrix into a row-vector engine without transposing it, or writing P \cdot V \cdot M where the library expects M \cdot V \cdot P — is exactly how "I definitely wrote the matrices in the right order" still produces a black screen.

The fix is to fix a convention and stick to it. Before writing a single matrix, know whether your engine treats vectors as columns (post-multiply, rightmost acts first) or rows (pre-multiply, leftmost acts first), check every library function against that choice, and never copy a formula from one convention into code written for the other without transposing.

The three matrices update on three different clocks, which is exactly why engines keep them separate until the last moment:

Engines therefore upload V and P once and loop over objects, multiplying in each object's M to form \mathrm{MVP} = P V M. Getting that multiplication order wrong — writing M V P — is the single most common bug in a fresh renderer, and the symptom (a scene that vanishes or smears) is gloriously unhelpful.