FollowCamera

FollowCamera for Trailing a Target

FollowCamera chases a mesh, its lockedTarget. Each frame it accelerates toward a goal point set by radius (distance), heightOffset (height above the target) and rotationOffset (degrees around it), limited by cameraAcceleration and maxCameraSpeed, so it lags on turns the way a chase camera in a racing game does. Below, a book cart circles First Render's room (from bookstore.js), seen through two viewports of one canvas:

A FollowCamera chasing a book cart, beside an overview 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);
  const cart = BABYLON.MeshBuilder.CreateBox('cart', { width: 0.8, height: 0.9 }, scene);
  cart.material = BookNestStore.paint(scene, 'cart', '#b5452f');
  const follow = new BABYLON.FollowCamera('follow', new BABYLON.Vector3(-3, 3, -2), scene);
  Object.assign(follow, { lockedTarget: cart, radius: 2.5, heightOffset: 1.2,
    rotationOffset: 180, cameraAcceleration: 0.03, maxCameraSpeed: 4 });
  const overview = new BABYLON.ArcRotateCamera('overview', -Math.PI / 2, 0.9, 11,
    BABYLON.Vector3.Zero(), scene);
  overview.viewport = new BABYLON.Viewport(0, 0, 0.5, 1);          // x, y, width, height
  follow.viewport = new BABYLON.Viewport(0.5, 0, 0.5, 1);
  scene.activeCameras = [overview, follow];
  let t = 0;
  scene.onBeforeRenderObservable.add(() => {
    t += engine.getDeltaTime() / 1000;
    cart.position.set(Math.sin(t) * 2.5, 0.45, Math.cos(t) * 1.5);  // an ellipse
    cart.rotation.y = t + Math.PI / 2;
  });
  scene.executeWhenReady(() => engine.runRenderLoop(() => scene.render()));
  scene.onAfterRenderObservable.add(() => { window.__done = scene.getFrameId() > 90; });
}
</script>
Browser output of Listing 7.12
Browser output of 12

The follow camera (right) trails the cart from behind and above. The activeCameras and viewport pattern also gives split screens and minimaps, at one scene pass per camera. Babylon 89,603 's ArcFollowCamera is a stiffer alternative that keeps fixed alpha, beta and radius offsets without easing.

A FollowCamera trailing a book cart, beside an overview camera in a second viewportHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; }
  canvas { width: 100%; max-width: 600px; height: 260px; 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>
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;
};

// BookNest's room with a few bookcases to drive past
paint(BABYLON.MeshBuilder.CreateGround('floor', { width: 8, height: 6 }, scene), '#b08d6a');
for (const [name, width, x, z] of [['back', 8, 0, 3], ['left', 6, -4, 0], ['right', 6, 4, 0]]) {
  const wall = paint(BABYLON.MeshBuilder.CreateBox(name, { width, height: 3, depth: 0.15 }, scene), '#fff3e0');
  wall.position.set(x, 1.5, z);
  wall.rotation.y = x ? Math.PI / 2 : 0;
}
for (const [x, z] of [[-1.8, 2.7], [1.8, 2.7], [0, 0]]) {
  paint(BABYLON.MeshBuilder.CreateBox('case', { width: 2.2, height: 1.8, depth: 0.4 }, scene), '#6d4c33')
    .position.set(x, 0.9, z);
}

// The cart: a red box with a stack of books on top
const cart = paint(BABYLON.MeshBuilder.CreateBox('cart', { width: 0.8, height: 0.9, depth: 0.5 }, scene), '#b5452f');
['#1f5f8b', '#e09a10', '#3f7d3a'].forEach((hex, i) => {
  const book = paint(BABYLON.MeshBuilder.CreateBox('b' + i, { width: 0.5, height: 0.1, depth: 0.35 }, scene), hex);
  book.parent = cart;
  book.position.y = 0.5 + i * 0.1;
});

// The chase camera: accelerates toward a point behind and above its lockedTarget
const follow = new BABYLON.FollowCamera('follow', new BABYLON.Vector3(-3, 3, -2), scene);
Object.assign(follow, { lockedTarget: cart, radius: 2.5, heightOffset: 1.2,
  rotationOffset: 180, cameraAcceleration: 0.03, maxCameraSpeed: 4 });
const overview = new BABYLON.ArcRotateCamera('overview', -Math.PI / 2, 0.9, 11,
  BABYLON.Vector3.Zero(), scene);
overview.viewport = new BABYLON.Viewport(0, 0, 0.5, 1);          // x, y, width, height
follow.viewport = new BABYLON.Viewport(0.5, 0, 0.5, 1);
scene.activeCameras = [overview, follow];                         // one scene pass each

let t = 0;
scene.onBeforeRenderObservable.add(() => {
  t += engine.getDeltaTime() / 1000;
  cart.position.set(Math.sin(t) * 2.5, 0.45, Math.cos(t) * 1.5);  // an ellipse
  cart.rotation.y = t + Math.PI / 2;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>