Command Palette

Search for a command to run...

Hectal
Course outline
Topic 17.3Advanced ~21 min · step 3 of 7

Level of Detail and Culling

In one line

The cheapest triangle is the one you never send. Frustum culling skips what the camera cannot see; LOD swaps in simpler meshes as objects get far away. Both depend on numbers you must keep true.

0/7 · 0%

Think of it like this

A film set builds only the front of a street. Behind the camera, nothing is built at all. And a house far in the background is a painted board, not a full house, because nobody can tell.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

frustum
The pyramid-shaped volume the camera can see: everything outside it is not drawn.
bounding sphere
A sphere that contains an object, used for quick visibility and picking tests.
LOD
Level of detail: several versions of an object, from detailed to simple, chosen by distance.
occlusion culling
Skipping objects hidden behind others. three.js does not do this for you.
hysteresis
A margin that stops a choice from flipping back and forth at a border.

Step by step

01Move a ball around

Run the first example. At the view's edge the ball still draws. Far outside, behind the camera, or with culling off, the draw call counts change.

02Meet the vanishing wall

The stretched wall vanishes with its centre off screen (although most of it should show), and returns with a true bounding sphere or frustumCulled = false.

03Build a three-level LOD

The second example puts one sphere at 64, 16 and 6 segments in one LOD: watch the triangles at each distance.

04Calm the flicker

The end of that example runs one camera path with and without hysteresis.

Try it yourself

  1. 1

    Move the borders

    In the LOD example, change the second level's distance from 10 to 5 (in the addLevel line, not the hysteresis example). At 9 m the line now prints level 1 | triangles drawn: 224, and 3 m still prints 3968.

  2. 2

    Fix the wall two ways

    In the first example, delete the two lines wall.frustumCulled = true; and wall.geometry.boundingSphere = ..., so the wall stays at frustumCulled = false. The last line then prints draw calls: 1 and orange again. The wall is now drawn even when it is far from the camera: acceptable here only because it is big and always near.

Code & diagrams

Frustum culling, and a bounding sphere that lies New tab

A fixed 300 x 150 picture. "Middle of the picture" reads the real pixel after render().

idle

Console

Run the scene to see its console.

Expected console output

ball cut by the edge of the view           draw calls: 1 | middle of the picture: background
ball outside the view                      draw calls: 0 | middle of the picture: background
same ball, frustumCulled = false           draw calls: 1 | middle of the picture: background
camera turned away from the ball           draw calls: 0 | middle of the picture: background
stretched wall, centred                    draw calls: 1 | middle of the picture: orange
stretched wall, its centre off screen      draw calls: 0 | middle of the picture: background
  ... with frustumCulled = false           draw calls: 1 | middle of the picture: orange
  ... with a bounding sphere of radius 10  draw calls: 1 | middle of the picture: orange
LOD: three levels, and hysteresis New tab

The triangle counts are for these exact spheres. The bike's own LOD choices come in Topic 19.3.

idle

Console

Run the scene to see its console.

Expected console output

camera at  3 m -> level 0 | triangles drawn: 3968
camera at  9 m -> level 0 | triangles drawn: 3968
camera at 11 m -> level 1 | triangles drawn:  224
camera at 29 m -> level 1 | triangles drawn:  224
camera at 31 m -> level 2 | triangles drawn:   36
camera at 80 m -> level 2 | triangles drawn:   36
hovering round 10 m with 10% hysteresis: 0 1 1 1 1
the same path with no hysteresis:       0 1 0 1 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

A bounding sphere that is too small

In the first example, change the radius 10 in new THREE.Sphere(new THREE.Vector3(), 10) to 0.1.

terminal
$ Run
── what you'll see ──
... with a bounding sphere of radius 10 draw calls: 0 | middle of the picture: background
The printed label still says radius 10, because it is only text. The wall is at x = 7, so the tiny sphere is off screen: no draw call, and the wall is gone.

Myth vs fact

Myth

three.js skips everything hidden behind other objects.

Fact

Only what is outside the camera's view volume is skipped. Things behind a wall inside the view are still sent, and the depth test discards their pixels.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Object3D.frustumCulled is checked per object, including groups' children: culling a parent does not cull children, each child is tested on its own. A skinned mesh's bounding sphere is worked out once, from the pose it has at that moment, so a strongly animated character can vanish at the edge unless you call computeBoundingSphere() again or set frustumCulled = false.

Remember this

  1. 1

    The problem this solves: a detailed model is wasted when it covers 20 pixels, and an object outside the picture is wasted completely. Both cost draw calls and triangles for nothing.

  2. 2

    Frustum culling is on by default. Before drawing a mesh, three.js tests its bounding sphere (a sphere that contains it, worked out once from the geometry) against the camera's view volume. Outside the volume: no draw call. The first example moves a ball out of view (0 draw calls), behind the camera (0), and turns culling off (1 call again, for nothing).

  3. 3

    The classic bug. Culling trusts the bounding sphere, not what the shader does. If a vertex shader (or a skeleton, or an instance matrix) moves vertices far from the geometry, the sphere is wrong. When the sphere's centre leaves the screen, the object vanishes while still partly on screen. The last lines of the first example show a 1 m wall stretched 20 times by a shader: it disappears when its centre goes off-screen. The fix is a true sphere (geometry.boundingSphere = new THREE.Sphere(center, radius)) or, as a last resort, frustumCulled = false.

  4. 4

    What culling does not do. three.js does not do occlusion culling: an object hidden behind a wall is still drawn (the depth test then throws the pixels away). Nothing at the draw-call level knows it is hidden. If that matters you cull yourself (rooms, portals, distance), or use BatchedMesh and instances to make the calls cheap.

  5. 5

    Level of detail. A THREE.LOD holds several versions of one object, each with a distance. lod.addLevel(object, distance, hysteresis): from that distance on, that level shows. The second example uses spheres with 3,968, 224 and 36 triangles at 0, 10 and 30 m, and prints the level and triangle count at six distances. render() updates the LOD for you.

  6. 6

    Hysteresis. If the camera hovers around 10 m, a plain LOD swaps level every frame: a flicker. With a hysteresis of 0.1, the far level still switches in at 10 m, but once it shows, the camera must come back 10% closer (to 9 m) before the near level returns. The second example shows the levels: with hysteresis 0 1 1 1 1, without 0 1 0 1 0. The distance is measured from the object's origin to the camera (divided by camera.zoom), not to its surface, so a very large object needs larger distances.

Explain it without notes

01

A mesh with a custom vertex shader disappears when you pan the camera so that its original position leaves the screen. Why, and what are the fixes?

02

Does three.js avoid drawing an object hidden behind another? Explain.

Practice

01

Add a fourth level to the LOD example: a billboard PlaneGeometry shown beyond 60 m. What do the triangles read at 80 m?

02

A tree sits at 8 m behind a house, inside the camera's view. Is it drawn?

Done when you can

  • Explain frustum culling and the bounding sphere it uses

  • Fix an object that vanishes because its sphere is wrong

  • Build a LOD with hysteresis and read its level

  • Say what three.js does not cull