Command Palette

Search for a command to run...

Hectal
Course outline
Topic 17.2Advanced ~22 min · step 2 of 7

Draw Calls and Instancing

In one line

Each mesh costs one draw call. When many objects share a shape and a material, InstancedMesh, BatchedMesh or merged geometry send them in one call. The counts prove it.

0/7 · 0%

Think of it like this

A postman with 1000 letters can make 1000 trips to the same street, or one trip with a full bag. The letters are the same. The trips are the cost.

Words you'll meet

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

instancing
Drawing many copies of one mesh in a single draw call, each with its own matrix (and maybe colour).
InstancedMesh
A three.js mesh that draws N copies of one geometry and one material in one call.
BatchedMesh
A mesh that draws many copies of different geometries with one material, in one call.
merged geometry
Several geometries copied into one buffer, so they draw as one static mesh.
attribute
One per-vertex array in a geometry, such as position, normal or uv.

Step by step

01Make 1000 shapes four ways

Run the first example and read the four lines: same shape, material and places, only the packaging differs.

02Know what each gives up

Instances share geometry and material. Merged geometry cannot be moved one by one. BatchedMesh needs more CPU. Plain meshes are simplest but cost one call each.

03Break the rules on purpose

The second example merges mismatched geometries (an error and null), merges with two materials (2 draw calls), sets an instance count of 0 (nothing drawn), and moves instances far away (the mesh vanishes until its bounding sphere is recomputed).

04Look at the real bike

The third example loads the course's bike twice: as exported (23 meshes plus the two instanced wheels' spokes) and after glTF Transform's join step.

Try it yourself

  1. 1

    Count one more way

    In the first example, change COUNT to 5000. The first line prints 5000 draw calls and 400000 triangles, and the other three still print 1 draw call. BatchedMesh needs no bigger buffers here: its vertex and index space is for the one geometry, not for each copy.

  2. 2

    Spend fewer instances

    In the second example, change [1000, 500, 0] to [1000, 250, 0]. The middle line prints 1 draw calls, 3000 triangles: count is how many you draw, and the number given to the constructor is how many you may draw.

Code & diagrams

1000 shapes: meshes, instances, merged, batched New tab

Seeded placement, so every run is the same. All four versions draw the same 80,000 triangles.

idle

Console

Run the scene to see its console.

Expected console output

1000 meshes      1000 draw calls 80000 triangles | geometries so far: 1
InstancedMesh       1 draw calls 80000 triangles | geometries so far: 1
merged geometry     1 draw calls 80000 triangles | geometries so far: 2
BatchedMesh         1 draw calls 80000 triangles | geometries so far: 3
Edge cases of merging and instancing New tab

The first line is a real error from three.js, followed by what mergeGeometries returns.

idle

Console

Run the scene to see its console.

Expected console output

THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index 1. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.
merge box + bare: null
merged, two materials                  2 draw calls, 24 triangles
InstancedMesh, count 1000              1 draw calls, 12000 triangles
InstancedMesh, count 500               1 draw calls, 6000 triangles
InstancedMesh, count 0                 0 draw calls, 0 triangles
moved instances, old bounding sphere   0 draw calls, 0 triangles
after computeBoundingSphere()          1 draw calls, 12000 triangles
The Lumen bike: 25 draw calls, or 7 after join New tab

Both files are in /data/3d/models/. Phase 10 made them with glTF Transform.

idle

Console

Run the scene to see its console.

Expected console output

lumen-bike-draco.glb         meshes 23 | InstancedMesh 2 | draw calls 25 | triangles 8168
lumen-bike-draco-joined.glb  meshes  5 | InstancedMesh 2 | draw calls  7 | triangles 8136

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

Instances that vanish

In the second example, delete the line instanced.computeBoundingSphere();.

terminal
$ Run
── what you'll see ──
moved instances, old bounding sphere 0 draw calls, 0 triangles
after computeBoundingSphere() 0 draw calls, 0 triangles
Both lines say 0: the stale sphere is never fixed.

Myth vs fact

Myth

Instancing makes the GPU draw faster.

Fact

The GPU still shades the same triangles and pixels. Instancing removes CPU work per object. If you are GPU-bound, it will not help.

Pro corner

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

  • ▸

    InstancedMesh also supports per-instance colour (setColorAt) and, for picking, raycaster.intersectObject(mesh) returns an instanceId. Phase 8's picking works on parts by name, so the bike's spokes are not separately pickable, which is what we want.

Remember this

  1. 1

    The problem this solves: every draw call makes three.js and the browser set up state and talk to the GPU driver. That setup costs CPU time per call, and on a phone it adds up fast. A scene of 1000 small, identical objects (bolts, trees, bollards, spokes) can be CPU-bound with only 80,000 triangles.

  2. 2

    **InstancedMesh.** One geometry, one material, N copies. You give each copy a 4 x 4 matrix with setMatrixAt(i, matrix) (and optionally a colour with setColorAt). One draw call, whatever N is. Each matrix is 16 floats, 64 bytes, so 100,000 copies need 6.4 MB of matrices. Use it when many objects share both shape and material. The Lumen bike's 18 spokes per wheel have been instanced since Phase 7.

  3. 3

    Merged geometry. mergeGeometries([...]) from BufferGeometryUtils copies many geometries into one big buffer: one mesh, one call. It suits static things that never move separately. You lose individual transforms and picking. All geometries must have the same attributes, or it prints an error and returns null. With useGroups = true it keeps one group per source, so two materials still mean two draw calls.

  4. 4

    **BatchedMesh. Like an instanced mesh, but the copies may use different geometries** (still one material). You add each geometry with addGeometry, make copies with addInstance, and move them with setMatrixAt. It can also hide copies (setVisibleAt), cull each copy against the camera, and sort for transparency. It costs more CPU than InstancedMesh, so use it only when shapes differ.

  5. 5

    The numbers. The first example makes 1000 copies four ways: 1000 meshes give 1000 draw calls, and InstancedMesh, merged geometry and BatchedMesh each give 1. The triangle count is identical: the GPU does the same shading work. Draw calls are a CPU saving.

  6. 6

    The "100 draw calls" rule. You will read that a phone should stay under about 100 draw calls. It is a community rule of thumb from three.js forums and talks, not a standard, and it moves with the phone and the shaders. Treat it as a reason to *measure*: find your own limit on your weakest device and write it into the budget (Topic 17.7).

Explain it without notes

01

When would you choose BatchedMesh over InstancedMesh, and what does it cost?

02

Why does a merged mesh draw the same number of triangles as 1000 meshes, but cost less?

Practice

01

A scene has 300 identical lamp posts and 300 trees from 3 different models, all with one material each. Name the draw-call count if you use plain meshes, and the best arrangement.

02

mergeGeometries returns null. Give two likely causes.

Done when you can

  • Make 1000 copies as plain meshes, InstancedMesh, merged and BatchedMesh, and read the draw calls

  • Say what each one gives up

  • Fix an instanced mesh that vanishes after you moved its instances

  • Say honestly where the "100 draw calls" rule comes from