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