Camera Collisions

Camera Collisions, Limits and Inputs

Free cameras can collide with meshes without a physics engine. Turn on scene.collisionsEnabled, set checkCollisions on the camera and on each mesh it may hit, and give the camera an ellipsoid, the half-sizes of the body around the eye (default 0.5, 1, 0.5). The engine then slides that ellipsoid along the requested movement and stops it at the first contact:

The same push toward a wall, without and with collisionsJavaScript
const camera = new BABYLON.UniversalCamera('walk', new BABYLON.Vector3(0, 1.6, -2), scene);
camera.setTarget(new BABYLON.Vector3(0, 1.6, 3));       // face the back wall
scene.collisionsEnabled = true;
scene.getMeshByName('backWall').checkCollisions = true;
camera.ellipsoid = new BABYLON.Vector3(0.3, 0.8, 0.3);
for (const collide of [false, true]) {
  camera.position.set(0, 1.6, -2);
  camera.checkCollisions = collide;
  for (let i = 0; i < 60; i++) {                        // hold "forward" for 60 frames
    camera.cameraDirection.z += 0.2;
    scene.render();
  }
  console.log(`checkCollisions ${collide}: z = ${camera.position.z.toFixed(3)}`);
}
Output
BJS - [15:51:01]: Babylon.js v9.28.0 - WebGL2 - Parallel shader compilation
checkCollisions false: z = 100.032
checkCollisions true: z = 2.622

Without collisions the camera went 102 m, through the wall; with them it stopped 0.3 m, the ellipsoid's radius, short of the wall's inner face (z = 2.925). Collisions test triangles, so give complex meshes an invisible box stand-in (Walkthrough Camera), and use physics (Physics with Havok) when objects must push each other.

Limits bound the orbit cameras (lowerRadiusLimit, upperBetaLimit and the rest, ArcRotateCamera); collisions and gravity bound free cameras. Inputs are plug-ins in camera.inputs: removeByType('FreeCameraTouchInput'), addMouseWheel(), or your own class with checkInputs(). ArcRotateCamera has moved to a declarative camera.movement.input, where setInteraction('pointer', { button: 0 }, 'pan') remaps a gesture in one call.

Two walking cameras pushed at a wall: without collisions one passes through, with an ellipsoid the other stopsHTMLLive
<!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; }
  #info { max-width: 600px; padding: 4px 8px; font-family: monospace; white-space: pre; }
</style>
<canvas id="view"></canvas>
<div id="info"></div>
<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);
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);
const paint = (mesh, hex, alpha = 1) => {
  mesh.material = new BABYLON.StandardMaterial(hex + alpha, scene);
  mesh.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
  mesh.material.alpha = alpha;
  return mesh;
};
paint(BABYLON.MeshBuilder.CreateGround('floor', { width: 8, height: 8 }, scene), '#b08d6a');
const backWall = paint(BABYLON.MeshBuilder.CreateBox('backWall', { width: 8, height: 3, depth: 0.15 }, scene), '#fff3e0');
backWall.position.set(0, 1.5, 3);

// Collisions without physics: the scene, the camera and each mesh opt in
scene.collisionsEnabled = true;
backWall.checkCollisions = true;

// Two walkers with the same body; only the second checks collisions
const walkers = [false, true].map((collide, i) => {
  const camera = new BABYLON.UniversalCamera('walk' + i, new BABYLON.Vector3(i ? 1.2 : -1.2, 1.6, 0), scene);
  camera.setTarget(new BABYLON.Vector3(camera.position.x, 1.6, 3));   // face the back wall
  camera.ellipsoid = new BABYLON.Vector3(0.3, 0.8, 0.3);               // half-sizes of the body
  camera.checkCollisions = collide;
  camera.minZ = 0.05;
  // A visible ellipsoid showing the body (the collider sits 0.8 m below the eye)
  const body = paint(BABYLON.MeshBuilder.CreateSphere('body' + i, { diameterX: 0.6, diameterY: 1.6, diameterZ: 0.6 }, scene),
    collide ? '#3f7d3a' : '#b5452f', 0.8);
  return { camera, body, collide };
});

// We watch from an overview camera; the walkers are updated by hand each frame
const overview = new BABYLON.ArcRotateCamera('overview', -Math.PI / 2 - 0.8, 0.95, 10, new BABYLON.Vector3(0, 0.5, 1.5), scene);
overview.attachControl(canvas, true);
scene.activeCamera = overview;

const info = document.getElementById('info');
let held = 0;
scene.onBeforeRenderObservable.add(() => {
  held++;
  for (const w of walkers) {
    w.camera.cameraDirection.z += 0.012;                // hold "forward"
    w.camera.update();                                  // apply the move (and the collisions)
    w.body.position.copyFrom(w.camera.position).y -= 0.8;
    if (held > 240) w.camera.position.set(w.camera.position.x, 1.6, 0);   // start over
  }
  if (held > 240) held = 0;
  info.textContent = walkers.map((w, i) => `${i ? 'green' : 'red  '}  ` +
    `checkCollisions ${String(w.collide).padEnd(5)}  z = ${w.camera.position.z.toFixed(2)}`).join('\n') +
    '\nwall inner face z = 2.925, ellipsoid radius 0.3';
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>