Using Three.js InstancedMesh to Render Thousands of Identical Objects Efficiently
Learn how Three.js InstancedMesh cuts draw calls for thousands of identical objects, with a working code example, limits, and verification tips.
18 Aug 2025, 13:23 UTC

Why InstancedMesh matters
When you need to draw many copies of the same shape—for example, a forest of trees, a particle field, or a grid of blocks—creating a separate Mesh for each instance forces the renderer to issue one draw call per object. With Three.js InstancedMesh you share a single geometry and material and let the GPU apply a unique 4×4 transform to each copy, collapsing thousands of draw calls into just one (or a few) per frame.
Worked configuration
The following snippet shows how to create an InstancedMesh that renders 10 000 cubes with random positions. It assumes you are already using a WebGLRenderer with a WebGL2 context (or the EXT_draw_instanced extension).
// 1. Geometry and material shared by all instances
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 });
// 2. Create the InstancedMesh, specifying how many copies you need
const instanceCount = 10000;
const cubes = new THREE.InstancedMesh(geometry, material, instanceCount);
scene.add(cubes);
// 3. Fill the per‑instance matrix buffer
const dummy = new THREE.Object3D(); // used to compute a matrix
for (let i = 0; i < instanceCount; i++) {
dummy.position.set(
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 200
);
dummy.rotation.set(Math.random() * Math.PI, 0, 0);
dummy.updateMatrix();
cubes.setMatrixAt(i, dummy.matrix);
}
// Mark the matrix buffer as needing an update
cubes.instanceMatrix.needsUpdate = true;
// 4. In your render loop (no changes needed)
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
After the first frame, open the browser console and inspect renderer.info.render.calls. You should see a value of 1 (or a small constant if you have other objects) despite rendering ten thousand cubes.
Limits and common mistakes
- WebGL2 or EXT_draw_instanced required. If the renderer falls back to WebGL1 without the extension,
InstancedMeshwill still be created but will render using the same code path as regularMeshobjects, resulting in one draw call per instance. Verify withrenderer.extensions.get('EXT_draw_instanced')or checkrenderer.getContext().version. - Forgetting to set
needsUpdate. After writing matrices viasetMatrixAt(or directly toinstanceMatrix), you must setinstanceMatrix.needsUpdate = true; otherwise the GPU retains the old data and instances appear stuck. - Reusing a single matrix object. If you create one
THREE.Matrix4and callsetMatrixAt(i, sameMatrix)in a loop, every instance receives the *same* transform because the method stores a reference to the matrix, not a copy. Either clone the matrix or compute a fresh one each iteration (as shown with the dummyObject3D). - Modifying the shared geometry after creation. Changing vertices, UVs, or indices on the geometry forces Three.js to rebuild the underlying buffers, which breaks the instance mapping unless you also recreate the
InstancedMeshor manually update the GPU data. - Per‑instance material variations are not supported. The default
InstancedMeshcan only vary the transform. If you need per‑instance color, texture, or other material properties, you must write a custom shader that reads per‑instance attributes (e.g., usingInstancedBufferAttribute) or fall back to separate meshes. - Vertex animation per instance. Animating vertex positions (like a waving flag) per instance requires a custom vertex shader that reads instance‑based animation data; the built‑in material only handles static geometry.
- GPU memory usage. Each instance consumes a 4×4 float matrix (64 bytes). For 10 000 instances that is ~640 KB, modest compared to the memory saved by reducing draw calls. Still, monitor usage via the browser’s performance panel if you push into the hundreds of thousands.
Practical verification steps
- Create a
WebGLRendererwith{ antialias: true, powerPreference: 'high-performance' }and logrenderer.getContext().versionto confirm WebGL2. - Build an
InstancedMeshwith a known instance count (e.g., 5 000). - Populate matrices with distinct values and set
instanceMatrix.needsUpdate = true. - Render a frame and check
renderer.info.render.calls; it should be close to 1 (plus any overhead from sky, UI, etc.). - Replace the
InstancedMeshwith the same number of individualMeshobjects, re‑render, and observe the call count rise to match the instance count and a noticeable FPS drop.
These steps confirm that instancing is active and that you are gaining the expected draw‑call reduction.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.