Twelve hundred instances: squeezing a garden into a few draw calls

Six hundred blades of grass, a hundred and twenty flowers, eighty-five mossy stones, forty-two shrubs. Density comes from InstancedMesh, not from cutting the count.

The garden holds roughly twelve hundred objects. One mesh each would mean twelve hundred draw calls for that patch alone; it takes four or five.

One kind of thing, one InstancedMesh

Grass blades are cones, flower stems are cylinders, mossy stones are icosahedra. Each kind gets one geometry, one material, one InstancedMesh, and every instance’s position, scale and rotation goes into a matrix:

const dummy = new THREE.Object3D();
transforms.forEach((item, index) => {
  dummy.position.set(...item.position);
  dummy.scale.set(...(item.scale ?? [1, 1, 1]));
  dummy.rotation.set(...(item.rotation ?? [0, 0, 0]));
  dummy.updateMatrix();
  object.setMatrixAt(index, dummy.matrix);
  if (item.color) object.setColorAt(index, new THREE.Color(item.color));
});

setColorAt is the important line. Grass has three greens, flower heads have three colours, stones have three greys. Bake the colour into the material and you need one InstancedMesh per colour, which halves the benefit immediately.

Pick the positions too

Sprinkling over the whole disc is not good enough. Grass generation excludes three areas: the pool (an ellipse), the stream (a rectangle) and the pavilion’s plinth. The conditions are a string of boolean checks — not elegant to read, but much faster than patching afterwards:

if (x * x + z * z > 16.7) continue;
if (((x - 0.48) ** 2) / 2.7 + ((z - 1.26) ** 2) / 1.5 < 1) continue;
if (x > 1 && x < 2.6 && z > 1.5) continue;

When not to instance

The petals do not use instancing — they need their matrices rewritten every frame: falling, rotating, respawning. You can do that with an InstancedMesh (set instanceMatrix.needsUpdate = true afterwards), but something that only drifts slowly, like the fireflies, is cheaper as Points.

The rule is simple: anything whose full matrix set is rewritten per frame should be Points or Sprites; anything written once should be an InstancedMesh.

Shadows count too

Instancing projects shadows in one go, but with enough instances shadow rendering gets heavy as well. Here the whole scene’s shadow map is computed once, so a growing instance count no longer affects the per-frame cost.

Closing

The point of instancing is not “a bit faster” — it turns density from a cost question into a design question. Whether you can plant six hundred blades of grass stops being up to the renderer and becomes up to you.

← Back to all posts

Comments

…