How to Render Thousands of Geometries in Three.js
Rendering thousands of individual 3D objects in Three.js can quickly
bottleneck your application’s performance due to excessive draw calls.
This article explains how to use THREE.InstancedMesh, the
most efficient tool in Three.js for rendering thousands of identical
geometries with different transformations, and provides a clear guide on
how to implement it to keep your frame rates high.
The Problem with Standard Meshes
In Three.js, creating a new THREE.Mesh for every object
results in a separate draw call for each one. If you attempt to render
10,000 individual cubes using standard meshes, the CPU must send 10,000
separate instructions to the GPU every frame. This communication
overhead quickly overwhelms the CPU, causing severe frame rate
drops.
The Solution: InstancedMesh
To solve this performance bottleneck, Three.js provides
THREE.InstancedMesh. Instancing allows you to render
thousands of objects that share the exact same geometry and material in
a single draw call. The GPU processes all instances simultaneously,
drastically reducing CPU overhead and keeping your application running
at 60 FPS or higher.
How to Implement InstancedMesh
Implementing InstancedMesh requires defining the shared
geometry, the shared material, and the total count of instances. You
then define the transformation matrix (position, rotation, and scale)
for each individual instance.
Here is a step-by-step implementation:
import * as THREE from 'three';
// 1. Define geometry, material, and the number of instances
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const count = 5000;
// 2. Create the InstancedMesh
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
// 3. Position each instance using a dummy Object3D
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
// Set unique positions
dummy.position.set(
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 50
);
// Update the dummy's local matrix
dummy.updateMatrix();
// Apply the matrix to the instance at index i
instancedMesh.setMatrixAt(i, dummy.matrix);
// Optional: Set a custom color for each instance
const color = new THREE.Color(Math.random(), Math.random(), Math.random());
instancedMesh.setColorAt(i, color);
}
// 4. Notify Three.js that the instance matrices and colors need an update
instancedMesh.instanceMatrix.needsUpdate = true;
if (instancedMesh.instanceColor) {
instancedMesh.instanceColor.needsUpdate = true;
}
// 5. Add the instanced mesh to the scene
scene.add(instancedMesh);Updating Instances Dynamically
If you need to move, rotate, or scale the instances dynamically inside your animation loop, you must repeat the matrix update process for the target instances inside your loop and set the update flag to true on every frame:
function animate() {
requestAnimationFrame(animate);
// Example: Modify the first instance's position dynamically
dummy.position.set(0, Math.sin(Date.now() * 0.001), 0);
dummy.updateMatrix();
instancedMesh.setMatrixAt(0, dummy.matrix);
// Inform Three.js to upload the new matrices to the GPU
instancedMesh.instanceMatrix.needsUpdate = true;
renderer.render(scene, camera);
}Key Benefits of InstancedMesh
- Single Draw Call: Reduces CPU-GPU communication bottleneck down to a single instruction.
- Shared Memory: Reuses geometry and material data in GPU memory instead of duplicating it.
- Individual Control: Allows you to define unique positions, rotations, scales, and colors for every single instance.