Course outline
The Rendering Pipeline
In one line
The rendering pipeline is the fixed chain of steps that turns corner positions into coloured pixels: vertex shader, clip space, rasterisation, fragment shader, depth test and the picture. You program two of the steps; the rest are built into the GPU.
Think of it like this
A factory line makes a toy. One station cuts the shapes, the next folds them, the next paints, the last checks and boxes. Every toy goes through the same stations in the same order. The rendering pipeline is the factory line for pictures, and every triangle goes through every station.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- clip space
- The coordinates the vertex shader returns: four numbers (x, y, z, w). Points outside the camera's view are cut away here.
- NDC
- Normalised device coordinates: x, y and z divided by w, so the visible part runs from −1 to +1 on every axis.
- rasterisation
- Working out which pixels a triangle covers. Each covered pixel becomes a fragment.
- fragment
- One pixel's worth of a triangle, waiting for a colour. The fragment shader runs once per fragment.
- depth buffer
- A hidden image that stores the nearest depth drawn at each pixel, so nearer things hide farther things.
- culling
- Skipping work that can't be seen: three.js skips whole meshes outside the camera's view, and the GPU skips triangles seen from behind.
Step by step
01Send three corners
Your code describes one triangle with three positions and three colours. three.js uploads them to a GPU buffer the first time you render, and issues one draw call.
02The vertex shader places them
Each corner is multiplied by the model, view and projection matrices. The result is in clip space, with a w that grows with distance. Use the stepper to see the real numbers for each corner.
03Clip, divide, map to pixels
Anything outside the view is cut. The rest is divided by w (perspective) and stretched onto the canvas grid. In the stepper's 16 × 10 screen the triangle becomes a shape on a grid you can count.
04Rasterise and colour
The GPU lists the pixels whose centres are inside the triangle. The fragment shader gives each its colour, blended from the three corners.
05Depth test and write
Each fragment's depth is compared with the depth buffer; a farther fragment is dropped. The winners are written to the canvas. A frame is the whole scene's triangles going through this line, one draw call at a time.
Try it yourself
- 1
Walk the stepper
In the stepper, go to stage 3 and drag "away" to 5. The triangle is now 8 m from the camera, its w is larger, and after the divide it shrinks towards the centre of the screen: that is perspective. Now go to stage 5 and turn the triangle to 80°: it is nearly edge-on, so it becomes a thin sliver and covers far fewer pixel centres than before.
- 2
A triangle behind the camera
In "The depth test", change
tri(1, "#ff0000")totri(5, "#ff0000"). The red triangle is now behind the camera (the camera is at z = 4), so it is clipped away. All three lines sayblue (far), even the first two with the depth test on. - 3
Switch culling off
In "What reaches the GPU", the second and third lines show that
frustumCulled = falsemakes three.js send a mesh that is behind the camera: 1 draw call, 12 triangles. The GPU then clips it and draws no pixel. Sending what can't be seen is wasted work, which is why the setting is on by default.
Code & diagrams
Try it: interactive
Follow one triangle through every stage
Press Next to step through the seven stages and read the real numbers. Then turn the triangle with the sliders and move it away, and watch what changes at stages 3 to 7.
The corners are coloured light red, green and blue; every pixel in between is a blend. Open the scene tree to see the single mesh.
Console
Run the scene to see its console.Expected console output
draw calls: 1
triangles: 1
geometries on the GPU: 1
shader programs: 1
corners: 3 | pixels in the canvas: 45000A near red triangle and a far blue one overlap. The middle pixel is red however you order them, until the depth test is turned off. The blue triangle is hidden behind the red one.
Console
Run the scene to see its console.Expected console output
far drawn first, depth test on : red (near)
near drawn first, depth test on : red (near)
near drawn first, depth test off: blue (far)The same box in five situations. Look for the cases where the draw call count is 0 and the case where it is 1 but nothing is drawn.
Console
Run the scene to see its console.Expected console output
in front of the camera draw calls 1, triangles 12
behind the camera (z = 10) draw calls 0, triangles 0
behind it, frustumCulled = false draw calls 1, triangles 12
scale 0, frustumCulled = false draw calls 1, triangles 12
near = 5 (box is 3 away) draw calls 0, triangles 0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Wind the corners the other way
In "One triangle, and what the renderer reports", swap the second and third corner in both arrays: positions -0.8, -0.6, 0, 0, 0.75, 0, 0.9, -0.5, 0 and colours 1, 0.27, 0.27, 0.27, 0.47, 1, 0.24, 0.78, 0.43.
Myth vs fact
Myth
renderer.info.render.triangles is the number of triangles you can see.
Fact
It is the number sent to the GPU. Triangles facing away, hidden behind others, or covered by a closer mesh are still counted. Only the meshes three.js itself skips (outside the view) are left out.
Myth
The GPU draws the scene back to front, like a painter.
Fact
It draws triangles in the order given and uses the depth buffer to decide, per pixel, which one wins. The order only matters for transparency, where blending needs the far things drawn first.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Two kinds of removal happen at different places. Frustum culling is done by three.js on the CPU per object, using a bounding sphere, before any WebGL call (it is why the draw call count drops to 0). Clipping is done by the GPU per triangle after the vertex shader. With culling off, even a mesh of
scale0 is still a draw call that draws nothing (the fourth line of the example). To really skip an object, setvisible = false. - ▸
Most GPUs run the depth test before the fragment shader when the shader doesn't change depth (early-Z), so a hidden pixel costs almost nothing. That is why drawing opaque objects near to far is a cheap win, and why three.js sorts opaque objects front to back. Transparent objects can't use it, and are sorted back to front instead.
- ▸
The vertex shader's output is called
gl_Position; the division by w and the viewport transform are fixed-function steps you cannot change. Depth is stored non-linearly (more precision near the camera), which is why anearvalue that is too small and afarthat is too large make distant surfaces flicker, called z-fighting (Topic 7.2 shows it in a depth viewer).
Remember this
- 1
The problem this solves: when a scene looks wrong, something went wrong at one particular station. A triangle behind the camera, a missing face, a pixel that should be hidden and isn't: each one is a different stage. Knowing the stations tells you where to look.
- 2
1. Vertex shader. (Recall from Topic 1.3: the small program that runs once for every vertex.) It turns the vertex's position into clip space, by multiplying it by three matrices (grids of numbers that move, turn and view points): the model matrix (where the object is), the view matrix (where the camera is) and the projection matrix (the camera's lens). The result has four numbers, x, y, z and w; w is how far in front of the camera the point is.
- 3
2. Clip space, clipping and NDC. The GPU throws away (clips) anything outside the camera's view. Then it divides x, y and z by w. Far points have a large w, so they move towards the centre: this is perspective. The result is normalised device coordinates (NDC), where the visible part of every axis runs from −1 to +1, whatever the screen size.
- 4
3. Viewport and rasterisation. The GPU stretches NDC to the canvas: −1…+1 becomes 0…width pixels. Then rasterisation asks one question for each pixel: is its centre inside the triangle? Each pixel that says yes becomes a fragment. You can't program this step. It is why edges are made of square pixels and why
antialiasexists. - 5
4. Fragment shader. It runs once per fragment and returns a colour. The three vertices' values (colour, texture coordinates, directions) are blended across the triangle, and corrected for perspective, so each fragment gets its own mix. All lighting and texturing happens here.
- 6
5. Depth test and blending. Each fragment has a depth. The depth buffer remembers the nearest depth drawn so far at every pixel; a fragment behind it is thrown away, so near things hide far things whatever order they are drawn in. Transparent materials instead blend with what is already there, which depends on the order (Phase 5). What survives is written to the canvas.
- 7
What you control. With three.js you choose the geometry, the material (which picks the shaders), the matrices (through position, rotation, scale and the camera) and a few switches such as
depthTestandside. Everything between the stations is fixed by the hardware. Phase 12 writes the two shaders yourself.
Explain it without notes
Walk one vertex through the pipeline in your own words, from a position in your code to a pixel on the screen.
A hand-built triangle doesn't show up, and renderer.info.render.triangles says 1. Give three possible causes.
Practice
Using the pixel-read code in the depth-test example, write a check that reads the middle pixel of a scene and logs whether it is black.
A transparent window and a car behind it are drawn in the wrong order and the car shows through wrongly. Which stage of the pipeline is responsible, and what would you check first?
Done when you can
Name the stages in order and say which two you can program
Say what clip space, NDC and a fragment are
Explain what the depth test does and what turning it off changes
Say why a triangle can be counted by
renderer.infoand still not be visible