A container is also a template: instantiateModelsToScene(nameFunction, cloneMaterials, options) copies its hierarchy into the scene as often as you like, cloning skeletons and animation groups so each copy animates on its own. Meshes become clones that share vertex buffers and (unless cloneMaterials) materials, or, with doNotInstantiate: false, instances drawn in one call per source mesh (Instances and Thin Instances):
const container = await BABYLON.LoadAssetContainerAsync('booknest-books.glb', scene);
BookNestStore.createWalkCamera(scene); // the shop's camera (Section 7.3.5)
const stats = new BABYLON.SceneInstrumentation(scene);
stats.captureDrawCalls = true;
for (const doNotInstantiate of [true, false]) { // true is the default
const copies = [1, 2, 3].map((shelf) => container.instantiateModelsToScene(
(name) => `${name}s${shelf}`, false, { doNotInstantiate }));
copies.forEach((entries, i) => { entries.rootNodes[0].position.y = 0.5 + i * 0.5; });
await scene.whenReadyAsync();
scene.render();
const kind = copies[0].rootNodes[0].getChildMeshes()[0].getClassName();
console.log(`${kind}: ${stats.drawCallsCounter.current} draw calls`);
copies.forEach((entries) => entries.dispose()); // clear the shelf for the next run
}Output
Mesh: 22 draw calls InstancedMesh: 10 draw calls
The room's floor and walls take four draw calls, so 18 clones cost 18 and 18 instances 6. Instances share everything, material included, and skinned meshes cannot be instanced; BookNest highlights one book at a time (Actions and Picking), so it keeps clones.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 300px; display: block; touch-action: none; outline: none; }
#bar { max-width: 600px; display: flex; gap: 12px; align-items: center; padding: 6px 8px; } #info { font-family: monospace; }
</style>
<canvas id="view"></canvas>
<div id="bar"><label><input type="checkbox" id="instances"> doNotInstantiate: false (instances)</label><span id="info">Loading…</span></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-loaders@9.28.0/babylonjs.loaders.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-serializers@9.28.0/babylonjs.serializers.min.js"></script>
<script>
// ---- BookNest's bookstore: the helpers the book keeps in bookstore.js ----
const ROOM = { width: 8, depth: 6, height: 3, wall: 0.15 };
const BOOKS = [
{ id: 1, title: 'The Quiet Harbor', author: 'Mara Ellison', color: '#1f5f8b', pages: 312, price: 14.99, rating: 4.6, inStock: true },
{ id: 2, title: 'Patterns of the Deep Web', author: 'Tomas Reyes', color: '#5b3f99', pages: 428, price: 32.5, rating: 4.4, inStock: true },
{ id: 3, title: 'Salt and Saffron', author: 'Priya Nair', color: '#e09a10', pages: 256, price: 24, rating: 4.8, inStock: false },
{ id: 4, title: 'Small Steps to Big Summits', author: 'Dana Whitfield', color: '#3f7d3a', pages: 198, price: 18.5, rating: 4.3, inStock: true },
{ id: 5, title: "The Clockmaker's Paradox", author: 'Julian Voss', color: '#b5452f', pages: 344, price: 16.75, rating: 4.7, inStock: true },
{ id: 6, title: 'Gardens in Glass', author: 'Lena Hart', color: '#2a9d8f', pages: 176, price: 21.3, rating: 4.5, inStock: true },
];
function createScene(engine) {
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
const sky = new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
sky.groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
return scene;
}
function paint(scene, name, hex) { // a matte color
const material = new BABYLON.StandardMaterial(name, scene);
material.diffuseColor = BABYLON.Color3.FromHexString(hex);
material.specularColor = BABYLON.Color3.Black();
return material;
}
function createRoom(scene) { // floor and three walls, front open
const room = new BABYLON.TransformNode('room', scene);
const floor = BABYLON.MeshBuilder.CreateGround('floor', { width: ROOM.width, height: ROOM.depth }, scene);
floor.material = paint(scene, 'oak', '#b08d6a');
floor.parent = room;
const plaster = paint(scene, 'plaster', '#fff3e0');
for (const [name, width, x, z] of [['backWall', 8, 0, 3], ['leftWall', 6, -4, 0], ['rightWall', 6, 4, 0]]) {
const wall = BABYLON.MeshBuilder.CreateBox(name, { width, height: ROOM.height, depth: ROOM.wall }, scene);
wall.position.set(x, ROOM.height / 2, z);
wall.rotation.y = x ? Math.PI / 2 : 0;
wall.material = plaster;
wall.parent = room;
}
return room;
}
function createBookcase(scene, name, x) { // 8 boxes merged into 1 mesh
const [w, h, d, t] = [3, 2.2, 0.35, 0.04];
const parts = [[t, h, d, -w / 2, h / 2, 0], [t, h, d, w / 2, h / 2, 0], [w, h, t, 0, h / 2, d / 2 - t / 2]];
for (let i = 0; i < 5; i++) parts.push([w, t, d, 0, 0.1 + i * 0.5, 0]);
const bookcase = BABYLON.Mesh.MergeMeshes(parts.map(([width, height, depth, px, py, pz]) => {
const part = BABYLON.MeshBuilder.CreateBox('part', { width, height, depth }, scene);
part.position.set(px, py, pz);
return part;
}), true);
bookcase.name = name;
bookcase.material = paint(scene, 'walnut', '#6d4c33');
bookcase.position.set(x, 0, ROOM.depth / 2 - ROOM.wall / 2 - d / 2); // against the back wall
return bookcase;
}
function drawCover(scene, book) { // cover art painted on a canvas
const tex = new BABYLON.DynamicTexture('cover' + book.id, { width: 256, height: 384 }, scene);
const c = tex.getContext();
c.fillStyle = book.color; c.fillRect(0, 0, 256, 384);
c.fillStyle = 'rgba(0,0,0,0.4)'; c.fillRect(0, 0, 18, 384); // the dark left edge
c.strokeStyle = '#fff8e1'; c.lineWidth = 3; c.strokeRect(34, 24, 202, 336);
c.fillStyle = '#fff8e1'; c.textAlign = 'center'; c.font = 'bold 30px Georgia';
const lines = [''];
for (const word of book.title.split(' ')) {
const test = (lines[lines.length - 1] + ' ' + word).trim();
if (c.measureText(test).width > 180) lines.push(word); else lines[lines.length - 1] = test;
}
lines.forEach((line, i) => c.fillText(line, 135, 110 + i * 38));
c.font = '20px Georgia'; c.fillText(book.author, 135, 330);
tex.update();
return tex;
}
function placeBooks(scene, bookcase, shelf = 2) { // six books, spines out, covers on +x
const strip = new BABYLON.Vector4(0, 0, 0.07, 1);
const faceUV = [strip, strip, new BABYLON.Vector4(0, 0, 1, 1), strip, strip, strip];
let x = -0.35; // left end of the row, local to the bookcase
return BOOKS.map((record) => {
const width = record.pages / 4000 + 0.02; // spine thickness from page count
const book = BABYLON.MeshBuilder.CreateBox(`book${record.id}`, { width, height: 0.3, depth: 0.22, faceUV, wrap: true }, scene);
book.parent = bookcase;
book.position.set(x + width / 2, 0.12 + shelf * 0.5 + 0.15, -0.02);
x += width + 0.012;
book.material = new BABYLON.StandardMaterial(record.title, scene);
book.material.diffuseTexture = drawCover(scene, record);
book.material.specularColor = new BABYLON.Color3(0.15, 0.15, 0.15);
book.metadata = record; // the record, for picking
return book;
});
}
// The book cuts the storefront with CSG2 (Manifold WebAssembly); this sandbox cannot download
// the .wasm, so the older pure-JavaScript BABYLON.CSG makes the same cuts here.
function createFrontWall(scene) {
const box = (width, height, depth, x, y) => {
const mesh = BABYLON.MeshBuilder.CreateBox('cut', { width, height, depth }, scene);
mesh.position.set(x, y, -ROOM.depth / 2);
return mesh;
};
const solid = box(ROOM.width, ROOM.height, ROOM.wall, 0, ROOM.height / 2);
const windowHole = box(3.2, 1.6, 1, -1.4, 1.6), doorHole = box(1.1, 2.2, 1, 2.3, 1.1);
const wall = BABYLON.CSG.FromMesh(solid).subtract(BABYLON.CSG.FromMesh(windowHole))
.subtract(BABYLON.CSG.FromMesh(doorHole)).toMesh('frontWall', scene.getMaterialByName('plaster'), scene);
[solid, windowHole, doorHole].forEach((mesh) => mesh.dispose());
return wall;
}
// ---- end of the bookstore helpers ----
// The book loads booknest-books.glb over HTTP. This sandboxed preview cannot fetch files, so it
// first writes the same GLB in memory (six books with PBR covers and their records as glTF
// extras) with the serializers package, then hands those bytes to the loader.
async function createBooksGlb(engine) {
const scratch = new BABYLON.Scene(engine);
const shelf = new BABYLON.TransformNode('shelf', scratch); // a parent that is not exported
const books = placeBooks(scratch, shelf, 0); // shelf 0: books stand at y 0.27
books.forEach((book) => { // PBR covers -> glTF materials
const cover = new BABYLON.PBRMaterial(book.material.name, scratch);
Object.assign(cover, { albedoTexture: book.material.diffuseTexture, metallic: 0, roughness: 0.5 });
book.material = cover;
});
await scratch.whenReadyAsync();
const glb = await BABYLON.GLTF2Export.GLBAsync(scratch, 'booknest-books', {
shouldExportNode: (node) => node.name.startsWith('book'), // the six books only
metadataSelector: (metadata) => metadata, // BOOKS[i] -> glTF extras
});
const bytes = new Uint8Array(await glb.files['booknest-books.glb'].arrayBuffer());
scratch.dispose();
return bytes; // pass with { pluginExtension: '.glb' }
}
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true, {}, true);
const scene = createScene(engine);
createRoom(scene);
const bookcase = createBookcase(scene, 'bookcase', 0);
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.25, 1.35, 3.4, new BABYLON.Vector3(-0.1, 1.2, 2.5), scene);
camera.minZ = 0.05;
camera.attachControl(canvas, true);
const stats = new BABYLON.SceneInstrumentation(scene);
stats.captureDrawCalls = true;
const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
if (copies.length) info.textContent = `${copies[0].rootNodes[0].getChildMeshes()[0].getClassName()}: ` +
`${stats.drawCallsCounter.current} draw calls`;
});
engine.runRenderLoop(() => scene.render());
let container = null, copies = [];
function stock(doNotInstantiate) {
copies.forEach((entries) => entries.dispose()); // clear the shelves first
copies = [1, 2, 3].map((shelf) => container.instantiateModelsToScene(
(name) => `${name}s${shelf}`, false, { doNotInstantiate })); // name function, cloneMaterials, options
copies.forEach((entries, i) => {
const root = entries.rootNodes[0];
root.parent = bookcase;
root.position.y = (i + 1) * 0.5; // the file's books stand on shelf 0
});
}
(async () => {
const bytes = await createBooksGlb(engine);
container = await BABYLON.LoadAssetContainerAsync(bytes, scene, { pluginExtension: '.glb' });
stock(true); // clones: the default
document.getElementById('instances').addEventListener('change', (e) => stock(!e.target.checked));
})();
addEventListener('resize', () => engine.resize());
</script>