Walkthrough Camera

Choosing BookNest's Walkthrough Camera

BookNest's visitors should browse as in a real shop: at eye height, on foot, unable to walk through walls. ArcRotateCamera floats and FollowCamera needs a target, so the choice is a UniversalCamera with collisions, gravity and WASD keys, added to bookstore.js as BookNestStore.createWalkCamera(scene, canvas):

bookstore.js: the walkthrough cameraJavaScript
function createWalkCamera(scene, canvas) {         // Section 7.3.5: walk at eye height
  const start = new BABYLON.Vector3(0, 1.6, -2.2);  // eye height, near the open front
  const camera = new BABYLON.UniversalCamera('walk', start, scene);
  camera.setTarget(new BABYLON.Vector3(0, 1.4, 3));
  Object.assign(camera, {
    minZ: 0.05,                                     // the default 1 m clips nearby shelves
    speed: 0.05, angularSensibility: 4000,          // about 1.6 m/s, slower mouse look
    ellipsoid: new BABYLON.Vector3(0.3, 0.8, 0.3),  // a visitor 1.6 m tall, 0.6 m wide
    checkCollisions: true, applyGravity: true,
    needMoveForGravity: true,                       // fall even while no key is held
  });
  camera.keysUp.push(87); camera.keysDown.push(83);     // W and S beside the arrows
  camera.keysLeft.push(65); camera.keysRight.push(68);  // A and D
  scene.gravity = new BABYLON.Vector3(0, -9.81 / 60, 0);  // units per frame, not m/s^2
  scene.collisionsEnabled = true;
  const size = { width: ROOM.width, height: ROOM.height, depth: ROOM.wall };
  const glass = BABYLON.MeshBuilder.CreateBox('frontCollider', size, scene);
  glass.position.set(0, ROOM.height / 2, -ROOM.depth / 2);
  glass.isVisible = false;                          // the open front still stops you
  for (const name of ['floor', 'backWall', 'leftWall', 'rightWall', 'frontCollider']) {
    scene.getMeshByName(name).checkCollisions = true;
  }
  if (canvas) camera.attachControl(canvas, true);
  scene.activeCamera = camera;
  return camera;
}

Speed 0.05 moved the camera 1.6 m in one second of holding the up arrow, a walking pace. The invisible frontCollider closes the open side until Shelves and Walls builds a real front wall. With the camera attached, the page becomes a first-person view you can walk with the keyboard and turn with the mouse:

The empty bookstore through the walkthrough cameraHTML
<canvas id="view" style="width: 620px; height: 210px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="bookstore.js"></script>
<script>
{
  const engine = new BABYLON.Engine(view, true, {}, true);
  const scene = BookNestStore.createScene(engine);
  BookNestStore.createRoom(scene);
  BookNestStore.createWalkCamera(scene, view);
  BookNestStore.renderWhenReady(engine, scene, 30);    // let gravity settle the visitor
}
</script>
Browser output of Listing 7.15
Browser output of 15

The eye is 1.6 m above the floor, facing the back wall where shelves will stand. In virtual reality, WebXR swaps in the WebXR camera, and the headset supplies the eye height.

BookNest's walkthrough camera: a UniversalCamera at eye height with gravity, collisions and WASDHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 340px; display: block; touch-action: none; outline: none; }
  p { max-width: 600px; margin: 4px 8px; }
</style>
<canvas id="view" tabindex="0"></canvas>
<p id="info">Click the view, then walk with WASD or the arrow keys and look around by dragging.</p>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const ROOM = { width: 8, depth: 6, height: 3, wall: 0.15 };
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.4), scene)
  .groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
const paint = (mesh, hex) => {
  mesh.material = new BABYLON.StandardMaterial(hex, scene);
  mesh.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
  mesh.material.specularColor = BABYLON.Color3.Black();
  return mesh;
};

// The room: floor and three walls, as in the first render
paint(BABYLON.MeshBuilder.CreateGround('floor', { width: ROOM.width, height: ROOM.depth }, scene), '#b08d6a');
for (const [name, width, x, z] of [['backWall', 8, 0, 3], ['leftWall', 6, -4, 0], ['rightWall', 6, 4, 0]]) {
  const wall = paint(BABYLON.MeshBuilder.CreateBox(name, { width, height: 3, depth: ROOM.wall }, scene), '#fff3e0');
  wall.position.set(x, 1.5, z);
  wall.rotation.y = x ? Math.PI / 2 : 0;
}
// A table in the middle of the room to bump into
const table = paint(BABYLON.MeshBuilder.CreateBox('table', { width: 1.6, height: 0.8, depth: 0.9 }, scene), '#6d4c33');
table.position.set(0, 0.4, 1);
table.checkCollisions = true;
['#1f5f8b', '#e09a10', '#b5452f'].forEach((hex, i) => {
  const book = paint(BABYLON.MeshBuilder.CreateBox('book', { width: 0.3, height: 0.05, depth: 0.42 }, scene), hex);
  book.position.set(-0.4 + i * 0.4, 0.83, 1);
});

function createWalkCamera(scene, canvas) {         // walk at eye height
  const start = new BABYLON.Vector3(0, 1.6, -2.2);  // eye height, near the open front
  const camera = new BABYLON.UniversalCamera('walk', start, scene);
  camera.setTarget(new BABYLON.Vector3(0, 1.4, 3));
  Object.assign(camera, {
    minZ: 0.05,                                     // the default 1 m clips nearby shelves
    speed: 0.05, angularSensibility: 4000,          // about 1.6 m/s, slower mouse look
    ellipsoid: new BABYLON.Vector3(0.3, 0.8, 0.3),  // a visitor 1.6 m tall, 0.6 m wide
    checkCollisions: true, applyGravity: true,
    needMoveForGravity: true,                       // fall even while no key is held
  });
  camera.keysUp.push(87); camera.keysDown.push(83);     // W and S beside the arrows
  camera.keysLeft.push(65); camera.keysRight.push(68);  // A and D
  scene.gravity = new BABYLON.Vector3(0, -9.81 / 60, 0);  // units per frame, not m/s^2
  scene.collisionsEnabled = true;
  const size = { width: ROOM.width, height: ROOM.height, depth: ROOM.wall };
  const glass = BABYLON.MeshBuilder.CreateBox('frontCollider', size, scene);
  glass.position.set(0, ROOM.height / 2, -ROOM.depth / 2);
  glass.isVisible = false;                          // the open front still stops you
  for (const name of ['floor', 'backWall', 'leftWall', 'rightWall', 'frontCollider']) {
    scene.getMeshByName(name).checkCollisions = true;
  }
  if (canvas) camera.attachControl(canvas, true);
  scene.activeCamera = camera;
  return camera;
}
const camera = createWalkCamera(scene, canvas);

const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
  const p = camera.position;
  info.textContent = `Click the view, then walk with WASD or the arrows, drag to look. Eye at ` +
    `(${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)}): walls, the table and the open front stop you.`;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>