Drawing lots and lots of cubes with react-three-fiber

Drawing lots and lots of cubes with react-three-fiber

Building a Three.JS scene that runs well can be hard. One thing that's a very common cause of slow down is just having too many objects to track. The problem is that sometimes you just want to have a lot of objects. Thankfully Three.JS provides a solution - instancing.

Instancing is a way of building up a single mesh from lots of copies of another mesh. This makes the scene run much faster than if there were actually lots of individual meshes because the GPU can draw everything at the same time. The mesh itself doesn't have to be static. Once it's loaded on to the GPU it can be modified just like any other mesh. This affords some interesting possibilities for making scenes with lots of copies of a single mesh that all move dynamically.

Lots of little cubes

To demonstrate why instancing is a useful technique for getting better performance out of Three.JS and r3f it's useful to see an example;

In this scene there is a grid of cubes each being rotated on every tick of the render loop. That's easy to do without instancing (especially with r3f - just change the rotate-y prop) but performance on my laptop sinks below 60fps at a relatively modest 2000 cubes. In this scene there are 122,500 and the performance is awesome.

Actually making an instanced mesh is quite straightforward. It relies on Three.JS's appropriately named InstancedMesh geometry.


const tempBoxes = new THREE.Object3D();

const Boxes = ({ i, j }) => {
  const material = new THREE.MeshLambertMaterial({ color: "red" });
  const boxesGeometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5);
  const ref = useRef();

  useFrame(({ clock }) => {
    let counter = 0;
    const t = clock.oldTime * 0.001;
    for (let x = 0; x < i; x++) {
      for (let z = 0; z < j; z++) {
        const id = counter++;
        tempBoxes.position.set(i / 2 - x, 0, j / 2 - z);
        tempBoxes.rotation.y = t;
        tempBoxes.updateMatrix();
        ref.current.setMatrixAt(id, tempBoxes.matrix);
      }
    }
    ref.current.instanceMatrix.needsUpdate = true;
  });

  return <instancedMesh ref={ref} args={[boxesGeometry, material, i * j]} />;
};

This component takes two props, i and j, for each of the grid dimensions. It creates the geometry and a material to use to render them, and returns an <instancedMesh> with a ref (so we can access it later), and args that instancedMesh expects. These are simply the geometry to instance, the material to use, and the number of instances to draw.

The render loop in the useFrame hook is where the good stuff lives. Each geometry in the instanced mesh has its own ID, and inside the loop React iterates over all 122,500 of them to update the matrix that has that ID. This makes each cube rotate. Those rotations are then updated in the instancedGeometry as well. There's no reason why every cube has to receive the same updated. The matrixes could be updated independently with different values if necessary - for example, if the cubes were following a path or rotating at different speeds.

Finally the instancedMesh's needsUpdate flag is set to true to tell Three.JS to update it on the next render.

Instancing is a really powerful technique. If you have a lot of meshes in a scene that all use the same geometry and material it's definitely worth a closer look.