InstancedMesh

InstancedMesh for Repeated Geometry

Every mesh is at least one draw call, and each call costs CPU time in the driver (Batching and Atlases). InstancedMesh(geometry, material, count) draws count copies in one call, the Three.js 30,815 face of Instanced Drawing's drawArraysInstanced, with a matrix and an optional color per instance:

A wall of 240 books in one draw call with InstancedMeshHTML
<script src="three-map.js"></script>
<script type="module">
import * as THREE from 'three';
import * as BN from './bookshelf.js';
const { renderer, scene, camera } = BN.createStage({ width: 640, height: 200 });
camera.position.set(0, 0.2, 3.1);
camera.lookAt(0, 0.1, 0);
const count = 240, perRow = 80;
const wall = new THREE.InstancedMesh(new THREE.BoxGeometry(0.055, 0.34, 0.2),
  new THREE.MeshStandardMaterial({ roughness: 0.6 }), count);
const matrix = new THREE.Matrix4(), color = new THREE.Color();
for (let i = 0; i < count; i++) {
  const row = Math.floor(i / perRow), height = 0.8 + 0.2 * Math.abs(Math.sin(i * 12.9898));
  matrix.makeScale(1, height, 1)                       // every instance has its own matrix
    .setPosition(-2.45 + (i % perRow) * 0.062, 0.35 - row * 0.42 + 0.17 * height, 0);
  wall.setMatrixAt(i, matrix);
  wall.setColorAt(i, color.set(BN.BOOKS[(i * 7) % 6].color));   // and its own color
}
scene.add(wall);
renderer.render(scene, camera);
const { calls, triangles } = renderer.info.render;
console.log(calls, 'draw call,', triangles, 'triangles');
BN.renderFrames(renderer, scene, camera);
</script>
Browser output of Listing 5.49
Browser output of 49
Output of 49
1 draw call, 2880 triangles

Built from 240 separate meshes, the same wall took 240 calls. To move instances, call setMatrixAt() and set instanceMatrix.needsUpdate = true; raycasts report hit.instanceId. BatchedMesh batches different geometries that share one material.

A wall of 240 books drawn by one InstancedMesh in a single draw callHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  p { margin: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<p id="out"></p>
<script type="module">
import * as THREE from 'three';

const colors = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 260);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 260, 0.1, 50);
camera.position.set(0, 0.2, 4.7);
camera.lookAt(0, 0.1, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);

const count = 240, perRow = 80;
const wall = new THREE.InstancedMesh(new THREE.BoxGeometry(0.055, 0.34, 0.2),
  new THREE.MeshStandardMaterial({ roughness: 0.6 }), count);
const matrix = new THREE.Matrix4(), color = new THREE.Color();
const heights = [];
for (let i = 0; i < count; i++) {
  const row = Math.floor(i / perRow), height = 0.8 + 0.2 * Math.abs(Math.sin(i * 12.9898));
  heights.push(height);
  matrix.makeScale(1, height, 1)                                   // every instance has its own matrix
    .setPosition(-2.45 + (i % perRow) * 0.062, 0.35 - row * 0.42 + 0.17 * height, 0);
  wall.setMatrixAt(i, matrix);
  wall.setColorAt(i, color.set(colors[(i * 7) % 6]));              // and its own color
}
scene.add(wall);
// Shelf planks under each row, merged into one more mesh
for (let row = 0; row < 3; row++) {
  const plank = new THREE.Mesh(new THREE.BoxGeometry(5.1, 0.03, 0.26), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
  plank.position.set(0, 0.335 - row * 0.42, 0);
  scene.add(plank);
}

renderer.render(scene, camera);
const { calls, triangles } = renderer.info.render;
document.getElementById('out').textContent =
  `${count} books: ${calls - 3} draw call for the InstancedMesh (+3 planks), ${triangles} triangles. As 240 meshes: 240 calls.`;

// Move instances: update a matrix, then flag instanceMatrix for re-upload
const lift = new THREE.Matrix4();
renderer.setAnimationLoop((t) => {
  for (let i = 0; i < count; i += 1) {
    const row = Math.floor(i / perRow), h = heights[i];
    const bob = Math.max(0, Math.sin(t / 300 - (i % perRow) / 6)) * 0.03;
    lift.makeScale(1, h, 1).setPosition(-2.45 + (i % perRow) * 0.062, 0.35 - row * 0.42 + 0.17 * h + bob, 0);
    wall.setMatrixAt(i, lift);
  }
  wall.instanceMatrix.needsUpdate = true;
  renderer.render(scene, camera);
});
</script>