Shelves and Walls

Modeling BookNest's Shelves and Room Walls

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:

BookNest's storefront, bookcases and books, seen from the streetHTML
<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>
Browser output of Listing 7.21
Browser output of 21

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).

BookNest's room with two merged bookcases, six books on a shelf and a storefront wall with a window and doorHTMLLive
<!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>