Navigate
Search topics across all sections
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.
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.
<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.
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.
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.
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.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)// 1000 meshes = 1000 draw calls = slideshow
{Array.from({ length: 1000 }, (_, i) => (
<mesh key={i} position={[i, 0, 0]}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
))}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}.