Navigate

Search topics across all sections

GitHub
Meshes & Objects

Instanced Mesh

Need to render a forest of 10,000 trees? A galaxy of stars? A swarm of particles? Placing 10,000 individual meshes would grind your app to a halt. InstancedMesh lets you stamp out thousands of copies in a single draw call -- same geometry, same material, different positions and colors.

You build a beautiful particle field with 5,000 individual <mesh> components. It looks amazing in your head. Then you run it. Your fan spins up, your FPS counter reads "4", and your laptop starts warming your coffee.

terminal
WARNING: 5000 draw calls detected. Frame time: 243ms (4 FPS).
CPU bottleneck: the GPU is idle, waiting for the CPU to finish
issuing draw commands. Consider using InstancedMesh.

Real-world

Think of a cookie cutter. Instead of hand-sculpting 1,000 cookies one by one (1,000 draw calls), you grab one cookie cutter and stamp them all out from a single sheet of dough (1 draw call). Every cookie has the same shape, but you can place them anywhere on the baking sheet and frost each one a different color.

That is exactly what InstancedMesh does. One shape, one material, thousands of stamps -- and the GPU barely breaks a sweat.

How Instancing Works

Create InstancedMesh

Define shape + material + count

Position each copy

Set transform via dummy Object3D

Bake the matrix

dummy.updateMatrix()

Upload to GPU

needsUpdate = true

1 draw call

GPU renders all copies at once

See It In Action

The demo below renders hundreds of instances with a single draw call. Each one has its own position and color, but they all share one geometry and one material.

Building It Step by Step

Step 1 -- Declare the InstancedMesh

Tell R3F what shape to stamp, what material to use, and how many copies you need. Pass undefined for geometry and material since you provide them as children.

Forest.tsxTSX
<instancedMesh ref={meshRef} args={[undefined, undefined, 1000]}>
  <boxGeometry args={[0.5, 0.5, 0.5]} />
  <meshStandardMaterial color="#6c5ce7" />
</instancedMesh>

The third value in args is the instance count. This tells the GPU how much buffer space to allocate up front.

Step 2 -- Position each instance with a dummy Object3D

You cannot set position directly on each instance. Instead, use a temporary Object3D as a stamp -- set its position, bake its matrix, and hand that matrix to the InstancedMesh.

Forest.tsxTSX
const dummy = useMemo(() => new THREE.Object3D(), [])

useEffect(() => {
  for (let i = 0; i < 1000; i++) {
    dummy.position.set(
      (Math.random() - 0.5) * 20,
      (Math.random() - 0.5) * 20,
      (Math.random() - 0.5) * 20
    )
    dummy.updateMatrix()
    meshRef.current.setMatrixAt(i, dummy.matrix)
  }
  meshRef.current.instanceMatrix.needsUpdate = true
}, [dummy])

Notice the two critical lines at the end: dummy.updateMatrix() bakes position/rotation/scale into a 4x4 matrix, and needsUpdate = true ships that data to the GPU.

Step 3 -- Give each instance its own color

Optionally, you can assign a unique color to every instance using setColorAt(). Just remember to flip the color buffer's needsUpdate flag too.

Forest.tsxTSX
const color = useMemo(() => new THREE.Color(), [])

useEffect(() => {
  for (let i = 0; i < 1000; i++) {
    color.setHSL(i / 1000, 0.8, 0.5)
    meshRef.current.setColorAt(i, color)
  }
  meshRef.current.instanceColor.needsUpdate = true
}, [color])

One gotcha: the base material color multiplies with instance colors. If your material is black, everything stays black. Use white or omit the color prop on the material.

What you just learned

InstancedMesh renders thousands of identical shapes in 1 draw call

A dummy Object3D acts as your stamp -- set its transform, bake the matrix, hand it over

Always call dummy.updateMatrix() before setMatrixAt()

Always set instanceMatrix.needsUpdate = true after a batch of updates

Per-instance color is optional via setColorAt() + instanceColor.needsUpdate

Question

What if you need instances with different geometries -- say, a mix of cubes and spheres? InstancedMesh requires all instances to share the same geometry. For mixed shapes, you would use multiple InstancedMesh components (one per shape) or look into drei's <Merged> component which batches different geometries into a single draw call using geometry merging.

Think about it...

You have 2,000 instances that never move after the initial setup. Where should you set their matrices?

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Set count to 100 vs 1000 -- can you feel the performance difference?

Try This!

Beginner

Max out waveHeight -- watch the cubes fly!

Try This!

Beginner

Set cubeSize to 0.5 -- chunky cubes!

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Forgetting instanceMatrix.needsUpdate
Instances don't appear or stay stuck at the origin
Don't do this
Instances.tsxTSX
for (let i = 0; i < COUNT; i++) {
  dummy.position.set(Math.random() * 10, 0, 0)
  dummy.updateMatrix()
  meshRef.current.setMatrixAt(i, dummy.matrix)
}
// Oops! GPU never gets the memo.
setMatrixAt writes to a CPU-side buffer. The GPU still has old data until you flip needsUpdate to true. Think of it like editing a spreadsheet offline -- you have to hit 'sync' before the cloud copy updates.
2
Forgetting dummy.updateMatrix()
All instances pile up at the origin
Don't do this
Instances.tsxTSX
dummy.position.set(i * 2, 0, 0)
dummy.rotation.set(0, i * 0.1, 0)
// dummy.matrix is still the identity!
meshRef.current.setMatrixAt(i, dummy.matrix)
Position, rotation, and scale are separate properties. The matrix is only recalculated when you call updateMatrix(). Skip it and you hand over a stale identity matrix every time.
3
Using 1,000 individual meshes instead of instancing
Frame rate tanks because the CPU issues 1,000 draw calls
Don't do this
Forest.tsxTSX
// 1000 meshes = 1000 draw calls = slideshow
{Array.from({ length: 1000 }, (_, i) => (
  <mesh key={i} position={[i, 0, 0]}>
    <boxGeometry />
    <meshStandardMaterial />
  </mesh>
))}
Each separate mesh is a separate conversation between CPU and GPU. InstancedMesh bundles the entire conversation into one message. The GPU handles thousands of copies just as easily as one.

Best Practices

Keep geometry simple

Total vertices = instance count x vertices per shape. Low-poly geometry is key for massive instance counts.

Reuse the dummy Object3D

Create one dummy with useMemo and reuse it in every loop. Never create Object3D inside a loop.

Set matrices once for static scenes

If instances never move, set their matrices in useEffect and never touch them again. Zero per-frame cost.

Disable frustum culling for spread-out instances

Three.js culls the entire InstancedMesh based on one bounding box. If instances are spread far apart, set frustumCulled={false}.