Command Palette

Search for a command to run...

Hectal
Course outline
Topic 1.4Beginner ~23 min · step 4 of 6

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.

0/6 · 0%

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. 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. 2

    A triangle behind the camera

    In "The depth test", change tri(1, "#ff0000") to tri(5, "#ff0000"). The red triangle is now behind the camera (the camera is at z = 4), so it is clipped away. All three lines say blue (far), even the first two with the depth test on.

  3. 3

    Switch culling off

    In "What reaches the GPU", the second and third lines show that frustumCulled = false makes 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.

One triangle, and what the renderer reports New tab

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.

idle

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: 45000
The depth test, with a pixel read back New tab

A 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.

idle

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)
What reaches the GPU, and what is skipped New tab

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.

idle

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 0

Break 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.

terminal
$ Run
── what you'll see ──
draw calls: 1
triangles: 1
geometries on the GPU: 1
shader programs: 1
corners: 3 | pixels in the canvas: 45000
The numbers are identical, but the canvas is empty: nothing is drawn, and there is no error.

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 scale 0 is still a draw call that draws nothing (the fourth line of the example). To really skip an object, set visible = 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 a near value that is too small and a far that is too large make distant surfaces flicker, called z-fighting (Topic 7.2 shows it in a depth viewer).

Remember this

  1. 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. 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. 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. 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 antialias exists.

  5. 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. 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. 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 depthTest and side. Everything between the stations is fixed by the hardware. Phase 12 writes the two shaders yourself.

Explain it without notes

01

Walk one vertex through the pipeline in your own words, from a position in your code to a pixel on the screen.

02

A hand-built triangle doesn't show up, and renderer.info.render.triangles says 1. Give three possible causes.

Practice

01

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.

02

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.info and still not be visible