Cutting Draw Calls with Babylon.js InstancedMesh: A Practical Guide
Learn how InstancedMesh reduces draw calls for thousands of identical objects, see a worked example, and understand the trade‑offs you’ll hit in production.
18 Mar 2026, 10:21 UTC

The problem: too many draw calls
Rendering a forest, a crowd, or a particle field with thousands of separate Mesh objects forces the GPU to issue a draw call for each one. On the CPU side this means building a command buffer per object, which quickly becomes the bottleneck before the GPU even sees the geometry.
Why InstancedMesh helps
InstancedMesh lets you draw many copies of a single mesh with one draw call. All instances share the same vertex buffers (geometry) and the same material. Per‑instance data — position, rotation, scale, color, or any custom attribute — lives in instance buffers that the vertex shader reads via instanceID. The CPU only submits one draw command, so the overhead scales with the number of instances, not the number of meshes.
Worked example: 1 000 boxes
The following playground‑style snippet creates two scenes side‑by‑side: one with 1 000 individual MeshBuilder.CreateBox calls, the other with a single InstancedMesh. Run it in the Babylon.js Playground or a local dev server (requires a recent Babylon.js ≥ 6.x build).
// 1️⃣ Scene with 1 000 separate meshes
const sceneA = new BABYLON.Scene(engine);
for (let i = 0; i < 1000; i++) {
const box = BABYLON.MeshBuilder.CreateBox("box" + i, {size: 1}, sceneA);
box.position.x = (i % 50) * 2;
box.position.z = Math.floor(i / 50) * 2;
}
// 2️⃣ Scene with InstancedMesh
const sceneB = new BABYLON.Scene(engine);
const source = BABYLON.MeshBuilder.CreateBox("source", {size: 1}, sceneB);
source.setEnabled(false); // hide the template
const instances = new BABYLON.InstancedMesh("boxes", source);
const count = 1000;
instances.thinInstanceSetBuffer("matrix", new Float32Array(count * 16));
for (let i = 0; i < count; i++) {
const m = BABYLON.Matrix.Translation(
(i % 50) * 2,
0,
Math.floor(i / 50) * 2
);
instances.thinInstanceSetMatrixAt(i, m);
}
// Enable debug layer on both scenes to see draw calls
sceneA.debugLayer.show();
sceneB.debugLayer.show();
How to verify (run in Chrome DevTools → “Performance” or the Babylon.js Debug Layer “Statistics” tab):
- Open the page, let both scenes render a few frames.
- In the Debug Layer, locate Draw Calls under “Engine”.
- Scene A should report ~1 000 draw calls; Scene B should report 1 (plus any UI overlays).
If you prefer the classic InstancedMesh API (pre‑thin‑instance), replace the buffer setup with:
const instances = new BABYLON.InstancedMesh("boxes", source);
for (let i = 0; i < count; i++) {
const inst = instances.createInstance("inst" + i);
inst.position.x = (i % 50) * 2;
inst.position.z = Math.floor(i / 50) * 2;
}
Both approaches collapse the draw calls; the thin‑instance path avoids creating a JavaScript object per instance, further reducing GC pressure.
Trade‑offs and limitations
- Shared material: All instances use the same
Material. If you need per‑instance textures or shader parameters, you must pack them into a texture atlas or use a custom vertex buffer. - Picking:
scene.pickon anInstancedMeshreturns the parent mesh, not the instance index. You must compute the instance yourself — e.g., by reading the instance buffer in a custom shader or by performing a second pass with an ID buffer. - Buffer updates: Uploading a new matrix buffer every frame shifts work back to the CPU. Batch updates, reuse the same
Float32Array, and only callthinInstanceSetMatrixAtfor instances that actually moved. - Profiler visibility: Some GPU profilers show the
InstancedMeshas a single drawable, hiding per‑instance vertex‑shader cost. Complement draw‑call counts with GPU time metrics.
Actionable next steps
- Clone the snippet into the Babylon.js Playground and confirm the draw‑call drop on your target browser.
- If you need per‑instance colors, add a second thin‑instance buffer:
instances.thinInstanceSetBuffer("color", new Float32Array(count * 4));and update it alongside the matrix buffer. - For picking, implement a lightweight ID render pass: render instances to an off‑screen texture where the fragment shader outputs
float(instanceID), then read the pixel under the pointer. - Profile the full frame (CPU + GPU) before committing; the draw‑call win is real, but vertex‑shader work still scales with instance count.
InstancedMesh is a powerful lever for scenes with massive repetition. Use it when geometry and material are truly shared, and pair it with thin‑instance buffers and careful update strategies to keep both CPU and GPU happy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.