scene.enablePhysics(gravity, plugin) creates a PhysicsEngineV2 (gravity, time step, world queries), and the plugin, an IPhysicsEnginePluginV2, translates every call for the native engine. A simulation consists of PhysicsBody objects (motion type, mass, velocity) on TransformNodes, PhysicsShapes with a material (friction, restitution), which many bodies can share, and PhysicsConstraints joining pairs of bodies. Each scene.render() runs onBeforePhysicsObservable, the optional pre-step (node pose into body), one Havok 516,349 step, the copy of body poses back to their nodes, and onAfterPhysicsObservable, then draws.

Physics V2 arrived with Babylon 6.0 in April 2023. V1's impostors (mesh.physicsImpostor) still work with plugins for Ammo.js 4,573 (zlib licence), cannon.js 5,002 and Oimo.js 3,175 (MIT, idle since 2023 and 2021), but only V2 has the character controller and shape casts. Three.js 30,815 has no physics API; Rapier and cannon-es synced Rapier by hand.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 360px; display: block; touch-action: none; outline: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.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('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.1, 1.4, 9.8, new BABYLON.Vector3(0.4, 1.8, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.6), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const slab = (name, text, width, y, hex, x = 0) => {
const box = BABYLON.MeshBuilder.CreateBox(name, { width, height: 0.55, depth: 1.6 }, scene);
box.position.set(x, y, 0);
box.material = new BABYLON.StandardMaterial(name, scene);
box.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
const label = new BABYLON.GUI.TextBlock('', text);
Object.assign(label, { color: 'white', fontSize: 12, fontWeight: 'bold', resizeToFit: true, outlineWidth: 2, outlineColor: '#333' });
ui.addControl(label);
label.linkWithMesh(box);
return box;
};
// Bottom to top: what you create, what runs it, what translates it, what simulates it
slab('body', 'PhysicsBody\nmass, velocity', 2.6, 0.3, '#1f5f8b', -2.8);
slab('shape', 'PhysicsShape\n+ material', 2.6, 0.3, '#5b3f99', 0);
slab('constraint', 'PhysicsConstraint\njoins two bodies', 2.6, 0.3, '#2a9d8f', 2.8);
slab('engine', 'PhysicsEngineV2 (scene.enablePhysics): gravity, step, queries', 8.2, 1.3, '#3f7d3a');
slab('plugin', 'HavokPlugin translates every call', 8.2, 2.3, '#e09a10');
slab('wasm', 'HavokPhysics.wasm: the native engine', 8.2, 3.3, '#b5452f');
// A marker climbing the stack once per frame: the order of one scene.render()
const STEPS = ['onBeforePhysicsObservable', 'pre-step: node pose → body', 'one Havok step',
'body poses → nodes', 'onAfterPhysicsObservable', 'draw'];
const token = BABYLON.MeshBuilder.CreateSphere('token', { diameter: 0.35 }, scene);
token.material = new BABYLON.StandardMaterial('tokenMat', scene);
token.material.emissiveColor = BABYLON.Color3.FromHexString('#ffe082');
const stepLabel = new BABYLON.GUI.TextBlock('', '');
Object.assign(stepLabel, { color: '#333', fontSize: 14, height: '26px', top: '-10px',
verticalAlignment: BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM });
ui.addControl(stepLabel);
const HEIGHTS = [1.3, 2.3, 3.3, 2.3, 1.3, 0.3];
scene.onBeforeRenderObservable.add(() => {
const t = (performance.now() / 900) % STEPS.length;
const i = Math.floor(t), f = t - i;
const y0 = HEIGHTS[i], y1 = HEIGHTS[(i + 1) % STEPS.length];
token.position.set(4.5,y0 + (y1 - y0) * Math.min(f * 2, 1), 0);
stepLabel.text = `each scene.render(): ${i + 1}. ${STEPS[i]}`;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>