Three.js InstancedMesh: How to Reduce Draw Calls

Rendering thousands of 3D objects in Three.js without dropping frames can be challenging due to CPU-to-GPU overhead known as draw calls. This article explains what an InstancedMesh is, how it solves performance bottlenecks by merging multiple identical objects into a single draw call, and how to implement it to optimize your WebGL projects.

The Problem: What are Draw Calls?

In WebGL and Three.js, every time you want to render an object on the screen, the CPU must send a command to the GPU. This command is called a draw call.

If you create 5,000 individual THREE.Mesh objects—such as a forest of trees or a crowd of people—the CPU has to prepare and send 5,000 separate draw calls to the GPU every frame. Even if the 3D models are low-polygon, the communication overhead between the CPU and GPU quickly bottlenecks your application, resulting in a low frame rate (FPS).

The Solution: What is InstancedMesh?

InstancedMesh is a specialized class in Three.js designed to render a large number of objects that share the exact same geometry and material, but have different transformations (such as position, rotation, scale, or color).

Instead of sending thousands of separate draw calls, InstancedMesh allows you to send the geometry and material to the GPU only once, along with an array of transformation matrices for each instance. The GPU then renders all of those instances in a single draw call.

This drastically reduces the CPU overhead, allowing you to render tens of thousands of objects smoothly at 60 FPS.

How to Implement InstancedMesh in Three.js

To use InstancedMesh, you define the shared geometry, the shared material, and the maximum number of instances you want to render.

Here is a step-by-step example of how to implement it:

import * as THREE from 'three';

// 1. Create the shared geometry and material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

// 2. Define the number of instances
const count = 1000;

// 3. Create the InstancedMesh
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

// 4. Position each instance using a 4x4 Matrix
const dummy = new THREE.Object3D();

for (let i = 0; i < count; i++) {
    // Set random position, rotation, and scale on a dummy object
    dummy.position.set(
        Math.random() * 50 - 25,
        Math.random() * 50 - 25,
        Math.random() * 50 - 25
    );
    dummy.rotation.set(
        Math.random() * Math.PI,
        Math.random() * Math.PI,
        0
    );
    dummy.updateMatrix();

    // Apply the dummy's transformation matrix to the specific instance
    instancedMesh.setMatrixAt(i, dummy.matrix);
    
    // Optional: Set a unique color for each instance
    const randomColor = new THREE.Color(Math.random(), Math.random(), Math.random());
    instancedMesh.setColorAt(i, randomColor);
}

// 5. Notify Three.js that the matrices and colors need an update
instancedMesh.instanceMatrix.needsUpdate = true;
if (instancedMesh.instanceColor) {
    instancedMesh.instanceColor.needsUpdate = true;
}

// 6. Add the InstancedMesh to the scene
scene.add(instancedMesh);

When Should You Use InstancedMesh?

InstancedMesh is highly effective, but it is not a silver bullet for every scenario.