Choosing Between InstancedMesh and Thin Instances in Babylon.js 6.x
Decide between Babylon.js InstancedMesh and Thin Instances for large-scale identical objects. Compare CPU overhead, draw calls, picking, animation support, and memory with a concrete 50k-box example and validation steps.
25 Jul 2025, 01:58 UTC

The problem: rendering thousands of identical objects without killing the CPU
When a scene contains many copies of the same mesh — trees, bullets, crowd agents — creating a full Mesh per copy adds a TransformNode, a world matrix update, and a draw call for each instance. Babylon.js offers two instancing paths that share geometry and material but differ in CPU overhead, pickability, and flexibility. The decision hinges on whether you need per-instance interaction (picking, physics, bone animation) or raw throughput for static or uniformly animated crowds.
Quick takeaway
- InstancedMesh – lightweight clones with individual world matrices, colors, and full picking support. Best when instances move independently or must be selectable.
- Thin Instances – GPU-only buffers storing matrices (and optional custom attributes). No JavaScript objects per instance, single draw call, minimal CPU work. Best for massive static or uniformly animated sets where picking is handled separately.
Side-by-side comparison
| Aspect | InstancedMesh | Thin Instances |
|---|---|---|
| CPU objects per instance | ~200 bytes (TransformNode) | None (GPU buffer only) |
| Draw calls | One per source mesh (instances batched) | One per source mesh (all instances in one call) |
| Per-instance transform update | Set instanceMatrix on each clone | Update GPU buffer via thinInstanceSetMatrixAt or thinInstanceBuffer |
| Picking | scene.pick returns pickInfo.instanceId | Not built-in; requires color-ID buffer or proxy mesh |
| Skeleton / bone animation | Supported with useBones = true | Not supported |
| Custom shader attributes | Via instanceColor or material uniforms | thinInstanceAddAttribute + matching vertex shader input |
| Memory (100k instances) | ~20 MB CPU + GPU | ~6 MB GPU only |
| WebGL1 fallback | Non-instanced rendering if instancedArrays false | Same fallback |
When to pick InstancedMesh
- Instances need independent movement, rotation, or scaling each frame.
- You rely on
scene.pickor raycasting to select individual copies (e.g., UI hover, shooting). - Characters use skeletal animation; each instance may be in a different pose.
- Instance count is moderate (under 20k) and the CPU budget allows per-frame matrix writes.
When to pick Thin Instances
- Instance count exceeds 50k and most transforms are static or driven by a uniform shader (e.g., wind-affected grass).
- Picking is handled by a separate pass (color-ID texture) or not required.
- You want the smallest possible CPU footprint and a single draw call.
- You can live without per-instance physics bodies; each dynamic object still needs its own physics impostor.
Concrete implementation: 50,000 boxes
InstancedMesh version
// Run in a Babylon.js Playground or any page with a WebGL2 engine
const createInstanced = (scene, count = 50000) => {
const source = BABYLON.MeshBuilder.CreateBox("box", {size: 1}, scene);
source.isVisible = false; // hide the template
const instances = [];
for (let i = 0; i < count; i++) {
const inst = source.createInstance(`box_${i}`);
inst.position.set(
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 200
);
instances.push(inst);
}
return {source, instances};
};
// After creation, verify draw calls:
console.log(engine.getCounters().drawCalls); // should be 1 for the box mesh
console.log(scene.getActiveMeshes().length); // includes each instanceWhere to run: Browser console on a page that has already created a BABYLON.Engine (WebGL2). No special permissions. Placeholders: count for number of instances, source mesh name. Expected check: drawCalls stays at 1, activeMeshes grows with instance count. Risk: Each instance adds a TransformNode; memory rises linearly.
Thin Instances version
const createThin = (scene, count = 50000) => {
const source = BABYLON.MeshBuilder.CreateBox("boxThin", {size: 1}, scene);
source.isVisible = false;
// Enable thin instancing and allocate buffers
source.thinInstanceEnable();
const matrices = new Float32Array(count * 16);
for (let i = 0; i < count; i++) {
const m = BABYLON.Matrix.Translation(
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 200
);
m.copyToArray(matrices, i * 16);
}
source.thinInstanceBuffer = new BABYLON.VertexBuffer(
engine,
matrices,
"matrix", // attribute name expected by the default shader
16, // stride (4x4 matrix)
false,
false,
1 // instance divisor
);
// Or use the helper:
// source.thinInstanceSetMatrixAt(i, m); // repeated for each i
return source;
};
// Verify:
console.log(engine.getCounters().drawCalls); // still 1
console.log(scene.getActiveMeshes().length); // only the source meshWhere to run: Same environment as above. Placeholders: count, matrix generation logic. Expected check: Draw calls unchanged, active meshes count does not increase. Risk: Very large buffers can hit WebGL buffer size limits; split into multiple source meshes if needed.
Validation steps you can run today
- Open the Babylon.js Inspector (call
scene.debugLayer.show()or open it from the Playground toolbar). - Note Draw calls and Total vertices before and after each implementation.
- Take a Chrome DevTools Performance > Memory heap snapshot for both versions; compare retained size of
TransformNodeobjects. - Test picking: click an instance in the InstancedMesh scene;
pickInfo.instanceIdshould be a valid index. For Thin Instances, implement a color-ID render target or raycast against a low-poly proxy mesh. - Confirm WebGL2 caps:
engine.getCaps().instancedArrays === true. If false, both paths fall back to non-instanced rendering.
Limitations and gotchas
- Thin Instances cannot use skeletal animation; stick with InstancedMesh or separate meshes for skinned characters.
- Custom per-instance attributes (e.g., random hue) require
thinInstanceAddAttributeand a matching vertex shader input — not automatic. - Physics engines (Havok, Ammo) do not simulate Thin Instances automatically; each dynamic instance still needs its own physics body.
- Very large buffers may exceed WebGL vertex buffer limits; split into multiple source meshes.
- WebGL1 contexts (
instancedArrays === false) disable hardware instancing for both approaches.
How to verify the choice in production
Add a small telemetry snippet that logs engine.getCounters().drawCalls and performance.now() frame time every 60 frames. Compare the numbers after deploying each variant. If draw calls stay at 1 and frame time drops below your budget (e.g., 16 ms for 60 fps), the instancing path is working. For Thin Instances, also monitor GPU memory and buffer sizes to ensure you stay within WebGL limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.