Each visible mesh costs at least one draw call, the usual bottleneck of web scenes (Bottlenecks). BABYLON.Mesh.MergeMeshes(meshes, disposeSource) bakes each mesh's world transform into its vertices and joins them into one mesh and one draw call. BookNest's bookcase merges two sides, a back and five boards:
function createBookcase(scene, name, x) { // Section 7.4.4: 8 boxes, 1 mesh
const [w, h, d, t] = [3, 2.2, 0.35, 0.04]; // width, height, depth, board thickness
const box = (width, height, depth, px, py, pz) => {
const part = BABYLON.MeshBuilder.CreateBox('part', { width, height, depth }, scene);
part.position.set(px, py, pz);
return part;
};
const parts = [box(t, h, d, -w / 2, h / 2, 0), box(t, h, d, w / 2, h / 2, 0), // sides
box(w, h, t, 0, h / 2, d / 2 - t / 2)]; // back
for (let i = 0; i < 5; i++) parts.push(box(w, t, d, 0, 0.1 + i * 0.5, 0)); // boards
const bookcase = BABYLON.Mesh.MergeMeshes(parts, true); // dispose the parts
bookcase.name = name;
bookcase.material = paint(scene, 'walnut', '#6d4c33');
bookcase.position.set(x, 0, ROOM.depth / 2 - ROOM.wall / 2 - d / 2); // against back wall
bookcase.checkCollisions = true;
return bookcase;
}A SceneInstrumentation's drawCallsCounter read 8 draw calls for the separate parts and 1 for the merged bookcase (192 vertices, 288 indices). Merged parts can no longer move or be picked alone; mixed materials need subdivideWithSubMeshes and multiMultiMaterials, and over 65,536 vertices allow32BitsIndices.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 320px; display: block; touch-action: none; }
#bar { max-width: 600px; display: flex; gap: 12px; align-items: center; padding: 6px 8px; }
#info { font-family: monospace; }
button { font: inherit; padding: 4px 12px; }
</style>
<canvas id="view"></canvas>
<div id="bar"><button id="merge">Merge the left bookcase</button><span id="info"></span></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene)
.groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.25, 1.25, 7.5, new BABYLON.Vector3(0, 1.1, 0), scene);
camera.attachControl(canvas, true);
const walnut = new BABYLON.StandardMaterial('walnut', scene);
walnut.diffuseColor = BABYLON.Color3.FromHexString('#6d4c33');
walnut.specularColor = BABYLON.Color3.Black();
function bookcaseParts(x) { // two sides, a back and five boards
const [w, h, d, t] = [3, 2.2, 0.35, 0.04]; // width, height, depth, board thickness
const box = (width, height, depth, px, py, pz) => {
const part = BABYLON.MeshBuilder.CreateBox('part', { width, height, depth }, scene);
part.position.set(x + px, py, pz);
part.material = walnut;
return part;
};
const parts = [box(t, h, d, -w / 2, h / 2, 0), box(t, h, d, w / 2, h / 2, 0), // sides
box(w, h, t, 0, h / 2, d / 2 - t / 2)]; // back
for (let i = 0; i < 5; i++) parts.push(box(w, t, d, 0, 0.1 + i * 0.5, 0)); // boards
return parts;
}
const leftParts = bookcaseParts(-1.7);
// Bake each part's world transform into its vertices and join them: one mesh, one draw call
const right = BABYLON.Mesh.MergeMeshes(bookcaseParts(1.7), true); // true: dispose the parts
right.name = 'bookcaseRight';
// SceneInstrumentation counts the draw calls issued each frame
const instrumentation = new BABYLON.SceneInstrumentation(scene);
instrumentation.captureRenderTime = false;
const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
info.textContent = `${scene.meshes.length} meshes, ${instrumentation.drawCallsCounter.current} draw calls ` +
`(merged bookcase: ${right.getTotalVertices()} vertices, ${right.getTotalIndices()} indices)`;
});
document.getElementById('merge').addEventListener('click', (event) => {
BABYLON.Mesh.MergeMeshes(leftParts, true).name = 'bookcaseLeft';
event.target.disabled = true;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>