Choosing Between Instancing, Thin Instances, and SPS for Thousands of Meshes in Babylon.js
Guide to picking Instancing, Thin Instances, or SPS in Babylon.js for tens of thousands of similar meshes, with constraints, trade‑offs, a 50k‑box Thin Instance example, and validation steps.
30 Aug 2026, 06:45 UTC

Problem Statement
When a scene contains tens of thousands of identical or nearly identical objects – for example a forest of 50 000 trees or a swarm of 30 000 rocks – the rendering technique you pick determines memory usage, frame rate, and the ability to interact with each instance. Babylon.js provides three built‑in options that can handle this load: Instancing, Thin Instances, and the Solid Particle System (SPS). Each has a distinct API, memory model, and performance profile.
Decision Constraints
- Target platform: modern desktop browsers (WebGL 2.0) vs. older mobile devices that may only support WebGL 1.0.
- Per‑instance features required: animation, material changes, physics impostors, or custom shaders.
- Memory footprint acceptable for GPU and CPU.
- Draw‑call count – lower is better for GPU pipeline efficiency.
- Development effort – how much custom shader or boilerplate code is acceptable.
Comparison Table
| Technique | Typical Memory Overhead | Draw Calls (ideal) | Per‑Instance Animation | Per‑Instance Material | Physics Support | WebGL Requirement |
|---|---|---|---|---|---|---|
| Thin Instances | One 4×4 float matrix per instance (≈ 64 bytes) plus optional attribute buffers | 1 (plus extra draws if multiple materials are used) | Supported via GPU‑driven vertex shader animation or skeleton textures | Not supported via standard PBRMaterial; requires custom shader or NodeMaterial that reads an instance‑index buffer | Not directly supported; use Instancing for per‑instance physics | Works on WebGL 1.0 through ANGLE_instanced_arrays and on WebGL 2.0 natively |
| Instancing | Per‑instance buffers for world matrix, color, etc. (≈ 100 + bytes per instance depending on attributes) | 1 (plus extra draws if multiple materials are used) | Supported – each instance can have its own skeleton | Supported – per‑instance material overrides via material slots (requires enabling material.enableInstances and setting per‑instance material indices) |
Supported – physics impostors can be created per instance and aggregated | WebGL 1.0 and 2.0 |
| SPS (Solid Particle System) | All geometry merged into a single vertex/index buffer; no per‑instance transform storage beyond the particle data itself | 1 (plus one draw per distinct material if used) | Not supported – all particles share the same skeleton (if any) | Not supported – each particle inherits the same material; per‑particle color requires a custom vertex buffer | Not supported – no per‑particle physics impostors | WebGL 1.0 and 2.0 |
Trade‑Off Analysis
- Thin Instances give the lowest memory cost and best draw‑call efficiency for static or GPU‑animated scenes. They are ideal for large forests, crowds, or any situation where the geometry is identical and only the transform changes.
- Instancing is the most flexible. It allows per‑instance skeletons, material overrides, and physics. However, the per‑instance buffers grow linearly with the number of instances, so memory can become a bottleneck at very high counts.
- SPS is fastest for fully static geometry with no per‑instance variation. The downside is the loss of per‑instance control – no animation, no material changes, no physics.
Concrete Implementation: 50 000 Boxes with Thin Instances
The following example shows how to create 50 000 boxes using the Thin Instances API, enable picking, and verify the result. It assumes Babylon.js 5.x or later is already loaded in the page.
// 1. Set up engine, scene, camera and light
const canvas = document.getElementById('renderCanvas');
const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true });
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera('cam', Math.PI / 2, Math.PI / 3, 200, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0, 1, 0), scene);
// 2. Create a source box that provides the geometry
const sourceBox = BABYLON.MeshBuilder.CreateBox('sourceBox', { size: 1 }, scene);
sourceBox.setEnabled(false); // hide the source mesh
// 3. Create a Thin Instance mesh that will hold all instances
const thinMesh = new BABYLON.Mesh('thinBoxes', scene);
// Copy vertex and index data from the source box
thinMesh.setVerticesData(BABYLON.VertexBuffer.PositionKind, sourceBox.getVerticesData(BABYLON.VertexBuffer.PositionKind));
thinMesh.setVerticesData(BABYLON.VertexBuffer.NormalKind, sourceBox.getVerticesData(BABYLON.VertexBuffer.NormalKind));
thinMesh.setIndices(sourceBox.getIndices());
// 4. Allocate space for 50 000 world matrices
const instanceCount = 50000;
thinMesh.setInstanceCount(instanceCount); // informs the mesh how many instances to expect
// 5. Fill the matrix buffer (random positions for demo)
const matrices = new Float32Array(16 * instanceCount);
for (let i = 0; i < instanceCount; i++) {
const pos = new BABYLON.Vector3(Math.random() * 200 - 100, 0, Math.random() * 200 - 100);
const mat = BABYLON.Matrix.Translation(pos.x, pos.y, pos.z);
mat.copyToArray(matrices, i * 16);
}
// Upload the matrix buffer as the per‑instance world matrix
thinMesh.thinInstanceSetBuffer('matrix', matrices, 16); // 16 floats per matrix
// 6. Enable picking – no extra call is required; the engine will fill pickInfo.thinInstanceIndex
// 7. Render loop
engine.runRenderLoop(() => {
scene.render();
});
// 8. Handle resize
window.addEventListener('resize', () => engine.resize());
// 9. Example: log the index of the picked instance
canvas.addEventListener('click', (evt) => {
const pickInfo = scene.pick(evt.clientX, evt.clientY, (mesh) => mesh === thinMesh);
if (pickInfo.hit) {
console.log('Picked thin instance index:', pickInfo.thinInstanceIndex);
}
});
Key points in the snippet:
- All 50 000 boxes share the same geometry; only the world‑matrix changes per instance.
setInstanceCountreserves GPU memory for the instance buffers.thinInstanceSetBufferuploads an array of 4×4 matrices; the engine uses this buffer for the world transform of each instance.- Picking works without any extra enable call;
pickInfo.thinInstanceIndexgives the zero‑based instance that was hit. - Because Thin Instances use a single vertex buffer, the draw‑call count stays at one (plus any extra draws for multiple materials).
Validation & Performance Measurement
- Draw‑Call Count
- Open Chrome DevTools → Performance tab → start recording → interact with the scene → stop recording.
- In the timeline, look for
drawElementsInstancedcalls. Thin Instances should show a single call per frame (or one per material if you split by material).
- GPU Memory
- In DevTools → Memory → GPU Memory, locate the buffer named something like
thinInstanceMatrixBuffer. Its size should be roughlyinstanceCount × 64 bytes(16 floats × 4 bytes). - For 50 000 instances, expect ≈ 3.2 MB for the matrix buffer alone.
- In DevTools → Memory → GPU Memory, locate the buffer named something like
- Frame Time
- Use the Performance tab to capture a few seconds of runtime. Thin Instances typically maintain >30 fps for 10 000+ instances on a mid‑range GPU.
- To compare with Instancing, replace the Thin Instance creation with
sourceBox.createInstances('instBox', instanceCount)and observe the increase in GPU memory (~1 KB per instance for a world matrix + color).
- Picking Accuracy
- Click on a visible box. The console should log an integer between 0 and 49 999. If the value is undefined or out of range, verify that the matrix buffer was uploaded with
thinInstanceSetBufferand that the mesh used inscene.pickis the thin instance mesh.
- Click on a visible box. The console should log an integer between 0 and 49 999. If the value is undefined or out of range, verify that the matrix buffer was uploaded with
Limitations & Troubleshooting
- WebGL 1.0 Path – Thin Instances rely on the ANGLE_instanced_arrays extension; on very old browsers that lack it, Babylon will fall back to standard Instancing automatically. You can check
engine.getCaps().webglVersionto confirm the path being used. - Per‑Instance Material Overrides – Not supported via the standard PBRMaterial. To vary color or other parameters per instance, create a custom shader (or NodeMaterial) that reads a per‑instance buffer (e.g., a color array) using the
instanceIdattribute. - Physics – Thin Instances do not support per‑instance physics impostors. For physics‑heavy scenes, group instances into static clusters and use Instancing for the moving parts that need impostors.
- GPU Buffer Size – The maximum number of instances is limited by the size of the vertex buffer that holds the per‑instance data (typically governed by the GPU’s maximum vertex buffer size). If you exceed this limit, split the instances across multiple Thin Instance meshes.
Conclusion
For the majority of large‑scale static or GPU‑animated scenes, Thin Instances provide the best balance of memory efficiency and draw‑call performance, especially on devices that support hardware instancing. Choose Instancing when you need per‑instance skeletons, material overrides, or physics. Reserve SPS for fully static geometry where you can accept a single draw call and do not require per‑instance variation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.