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:
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)}`);
}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.
<!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>