The bookstore gets two bookcases (Merging Meshes and Vertex Data) and the storefront (CSG2). A new function, BookNestStore.placeBooks(scene, bookcase, shelf = 2), stands the six books on a shelf as boxes 0.3 m tall and pages / 4000 + 0.02 m thick, 12 mm apart, each with its palette color, parented to the bookcase so they move with it, and with its record in mesh.metadata for picking (Actions and Picking). A camera outside shows the result:
<canvas id="view" style="width: 620px; height: 230px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="bookstore.js"></script>
<script>
(async () => {
const engine = new BABYLON.Engine(view, true, {}, true);
const scene = BookNestStore.createScene(engine);
BookNestStore.createRoom(scene);
const left = BookNestStore.createBookcase(scene, 'bookcaseLeft', -1.8);
BookNestStore.createBookcase(scene, 'bookcaseRight', 1.8);
BookNestStore.placeBooks(scene, left);
await BookNestStore.createFrontWall(scene);
new BABYLON.ArcRotateCamera('shopper', -1.72, 1.5, 10,
new BABYLON.Vector3(-0.2, 1.3, 2.6), scene); // outside, looking in at the window
BookNestStore.renderWhenReady(engine, scene, 30);
})();
</script>
The window and door frame the bookcases, with the six spines on the left case's middle shelf: 13 meshes, ready for covers (Cover Textures), lights (Lights and Shadows) and picking (Actions and Picking).
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; }
canvas { width: 100%; max-width: 600px; height: 340px; display: block; touch-action: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
// The book builds the front wall with CSG2 (Manifold WebAssembly); the sandboxed preview cannot
// download the .wasm, so the same subtraction uses the older pure-JavaScript BABYLON.CSG here.
const ROOM = { width: 8, depth: 6, height: 3, wall: 0.15 };
const BOOKS = [['The Quiet Harbor', '#1f5f8b', 312], ['Patterns of the Deep Web', '#5b3f99', 428],
['Salt and Saffron', '#e09a10', 256], ['Small Steps to Big Summits', '#3f7d3a', 198],
["The Clockmaker's Paradox", '#b5452f', 344], ['Gardens in Glass', '#2a9d8f', 176]];
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true, {}, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.6), scene)
.groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
const paint = (name, hex) => {
const m = new BABYLON.StandardMaterial(name, scene);
m.diffuseColor = BABYLON.Color3.FromHexString(hex);
m.specularColor = BABYLON.Color3.Black();
return m;
};
const plaster = paint('plaster', '#fff3e0');
// Room: floor and three walls
BABYLON.MeshBuilder.CreateGround('floor', { width: ROOM.width, height: ROOM.depth }, scene).material = paint('oak', '#b08d6a');
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;
}
function createBookcase(name, x) { // 8 boxes merged into 1 mesh
const [w, h, d, t] = [3, 2.2, 0.35, 0.04];
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), box(w, h, t, 0, h / 2, d / 2 - t / 2)];
for (let i = 0; i < 5; i++) parts.push(box(w, t, d, 0, 0.1 + i * 0.5, 0));
const bookcase = BABYLON.Mesh.MergeMeshes(parts, true);
bookcase.name = name;
bookcase.material = paint('walnut', '#6d4c33');
bookcase.position.set(x, 0, ROOM.depth / 2 - ROOM.wall / 2 - d / 2); // against the back wall
return bookcase;
}
function placeBooks(bookcase, shelf = 2) { // six spines, thickness from page count
let x = -1.3;
return BOOKS.map(([title, hex, pages], i) => {
const width = pages / 4000 + 0.02;
const book = BABYLON.MeshBuilder.CreateBox(`book${i + 1}`, { width, height: 0.3, depth: 0.22 }, scene);
book.material = paint(title, hex);
book.parent = bookcase; // moves with the bookcase
book.position.set(x + width / 2, 0.12 + shelf * 0.5 + 0.15, -0.02);
book.metadata = { title, pages }; // the record, for picking later
x += width + 0.012;
return book;
});
}
function createFrontWall() { // window and door cut from a solid wall
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);
const 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', plaster, scene);
[solid, windowHole, doorHole].forEach((mesh) => mesh.dispose());
return wall;
}
const left = createBookcase('bookcaseLeft', -1.8);
createBookcase('bookcaseRight', 1.8);
placeBooks(left);
createFrontWall();
const camera = new BABYLON.ArcRotateCamera('shopper', -1.72, 1.5, 8.6,
new BABYLON.Vector3(-0.6, 1.3, 2.6), scene); // outside, looking in at the window
camera.attachControl(canvas, true);
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>