Babylon.js Animation & Physics — Bring Your 3D Scene to Life
Babylon.js provides two powerful systems for bringing 3D scenes to life: a keyframe-based animation engine with smooth easing curves, and a full physics simulator via Havok that makes objects fall, bounce, collide, and react to forces — just like the real world.
What You’ll Learn
By the end of this tutorial, you’ll create keyframe animations for position, rotation, and color, use easing functions for natural motion (bounce, elastic, ease-in-out), group animations for coordinated playback, animate skinned models with skeletal animation, integrate the Havok physics engine, set up physics impostors (box, sphere, plane, mesh), apply forces and impulses, detect collisions, and use the physics debug renderer.
Why Animation and Physics Matter
A static 3D scene is a screenshot. Animated scenes are movies. Physics makes them interactive — objects fall when dropped, bounce when they hit the ground, and stack on top of each other.
Without a physics engine, you’d have to calculate all of this manually: “if object A falls 1 meter per frame squared and hits ground at y=0, then…” It’s complex and error-prone. Havok handles all the math so you can focus on building the experience.
Real-world use: Durga Antivirus Pro uses Babylon.js physics in its security training simulator. New analysts drop virtual servers into a 3D network topology, and the physics engine ensures they stack correctly. Animated threat alerts pulse with color animations, bouncing to draw attention to critical breaches.
Where This Fits in Your Learning Path
flowchart LR
A["Babylon.js Getting Started"] --> B["Materials & Textures"]
B --> C["**Animation & Physics**"]
C --> D["GUI & Interaction"]
D --> E["Importing & Assets"]
style C fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#22c55e
Keyframe Animation
Animation in Babylon.js works like a flipbook. You define keyframes — snapshots of a property at specific times — and Babylon.js calculates the in-between frames.
The Animation Object
const anim = new BABYLON.Animation(
'boxAnim', // name (for debugging)
'position.y', // property to animate
60, // frames per second
BABYLON.Animation.ANIMATIONTYPE_FLOAT, // data type
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE // loop behavior
)
// Keyframes: at frame 0, y=0. At frame 30, y=3. At frame 60, y=0.
anim.setKeys([
{ frame: 0, value: 0 },
{ frame: 30, value: 3 },
{ frame: 60, value: 0 },
])
// Attach the animation to a mesh
box.animations.push(anim)
// Play it: mesh, from frame, to frame, loop
scene.beginAnimation(box, 0, 60, true)What happens here:
- Frame 0: box is at y=0 (on the ground)
- Frame 30: box rises to y=3 (3 units up)
- Frame 60: box returns to y=0
Babylon.js generates all 60 intermediate positions. The box bobs up and down smoothly.
Animation Types
| Type Constant | Use For |
|---|---|
ANIMATIONTYPE_FLOAT | Single values: position.y, rotation.x, alpha |
ANIMATIONTYPE_VECTOR3 | Position, rotation, scale |
ANIMATIONTYPE_COLOR3 | Color transitions (diffuse, emissive) |
ANIMATIONTYPE_QUATERNION | Smooth rotational interpolation |
ANIMATIONTYPE_MATRIX | Full transform matrix |
Loop Modes
| Mode | Behavior |
|---|---|
ANIMATIONLOOPMODE_CYCLE | Restart from beginning (default) |
ANIMATIONLOOPMODE_CONSTANT | Hold last keyframe value |
ANIMATIONLOOPMODE_RELATIVE | Continue from last value (additive) |
Converting Seconds to Frames
At 60 fps: frames = seconds * 60. So 2 seconds = 120 frames.
const durationFrames = 2 * 60 // 2 seconds at 60fps
Easing Functions — Natural Motion
Without easing, animations are robotic — objects move at constant speed, starting and stopping instantly. Easing functions make motion feel natural by accelerating and decelerating.
Think of a bouncing ball. It doesn’t move at constant speed. It slows down at the top of its arc, speeds up as it falls, compresses on impact, and bounces back. Easing simulates this.
// Bounce ease — ball bounces at the end
const bounceEase = new BABYLON.BounceEase(4, 2) // 4 bounces, bounciness 2
anim.setEasingFunction(bounceEase)
// Elastic ease — spring-like oscillation
const elasticEase = new BABYLON.ElasticEase(10, 0.5) // oscillations, springiness
anim.setEasingFunction(elasticEase)
// Back ease — overshoots then settles
const backEase = new BABYLON.BackEase(0.5) // overshoot amount
anim.setEasingFunction(backEase)Easing Modes
Each easing function supports three modes:
elasticEase.easingMode = BABYLON.EasingFunction.EASINGMODE_EASEINOUT| Mode | Effect |
|---|---|
EASINGMODE_EASEIN | Slow start, fast end |
EASINGMODE_EASEOUT | Fast start, slow end |
EASINGMODE_EASEINOUT | Slow start and end, fast middle |
AnimationGroup — Coordinated Playback
When you need multiple objects to animate together, use AnimationGroup:
const animGroup = new BABYLON.AnimationGroup('group1')
// Box rotates
const rotAnim = new BABYLON.Animation('rot', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT)
rotAnim.setKeys([{ frame: 0, value: 0 }, { frame: 60, value: Math.PI * 2 }])
animGroup.addTargetedAnimation(rotAnim, box)
// Sphere floats up and down
const posAnim = new BABYLON.Animation('pos', 'position.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT)
posAnim.setKeys([{ frame: 0, value: 1 }, { frame: 30, value: 3 }, { frame: 60, value: 1 }])
animGroup.addTargetedAnimation(posAnim, sphere)
// Control all at once
animGroup.play()
animGroup.pause()
animGroup.stop()
animGroup.speedRatio = 2 // 2x speed
Skeletal Animation
For character models with bones (loaded from glTF/GLB), you animate the skeleton, not the mesh:
// After loading a model with a skeleton
scene.beginAnimation(skeleton, 0, 100, true, 1.0)
// Switch between animation ranges
const walkRange = skeleton.getAnimationRange('walk')
const runRange = skeleton.getAnimationRange('run')
// Cross-fade between animations
scene.beginWeightedAnimation(skeleton, walkRange.from, walkRange.to, 0.5, true)Skeletal animation is how game characters walk, run, jump, and idle. Each “animation range” is a named segment of the timeline (e.g., frames 0-30 = walk, frames 31-60 = run).
Havok Physics
Havok is a high-performance physics engine that simulates gravity, collisions, and forces in real time.
Setup
<script src="https://cdn.babylonjs.com/havok/HavokPhysics.js"></script>await HavokPhysics().then((havokInstance) => {
const havokPlugin = new BABYLON.HavokPlugin(true, havokInstance)
scene.enablePhysics(new BABYLON.Vector3(0, -9.81, 0), havokPlugin)
})Why await? Havok loads asynchronously (it’s a WebAssembly module). You must wait for it to initialize before adding physics.
What is -9.81? That’s Earth’s gravity in m/s² on the Y axis (downward). Change it for different effects: 0 for space, -1.62 for moon gravity, positive values for reverse gravity.
Physics Impostors
An impostor defines the physical shape of a mesh. Think of it as the invisible collision body that the physics engine uses, separate from the visible mesh:
// Box impostor (falling crate)
const boxImpostor = new BABYLON.PhysicsImpostor(
box,
BABYLON.PhysicsImpostor.BoxImpostor,
{ mass: 1, restitution: 0.6, friction: 0.5 },
scene
)
// Sphere impostor (bouncing ball)
const sphereImpostor = new BABYLON.PhysicsImpostor(
sphere,
BABYLON.PhysicsImpostor.SphereImpostor,
{ mass: 2, restitution: 0.8 },
scene
)
// Static ground (mass=0 means it doesn't move)
const groundImpostor = new BABYLON.PhysicsImpostor(
ground,
BABYLON.PhysicsImpostor.BoxImpostor,
{ mass: 0, restitution: 0.5 },
scene
)Impostor Type Reference
| Impostor | Best For |
|---|---|
BoxImpostor | Crates, walls, buildings |
SphereImpostor | Balls, projectiles, marbles |
PlaneImpostor | Ground, walls (infinite plane) |
CylinderImpostor | Barrels, pipes |
MeshImpostor | Custom shapes (convex hull) |
CapsuleImpostor | Characters (better than cylinder) |
Mass Rules
- mass = 0: Static object — never moves. Use for ground, walls, buildings.
- mass > 0: Dynamic object — affected by gravity and forces. Heavier objects push lighter ones more.
Applying Forces and Impulses
Impulse — instant velocity change (like a cannonball hit):
sphereImpostor.applyImpulse(
new BABYLON.Vector3(0, 5, 10), // direction and strength
sphere.getAbsolutePosition() // where to apply it
)Force — continuous push (like wind or a thruster):
boxImpostor.applyForce(
new BABYLON.Vector3(0, 0, 20), // direction and strength
box.getAbsolutePosition()
)Set velocity directly:
sphereImpostor.setLinearVelocity(new BABYLON.Vector3(5, 2, 0))
sphereImpostor.setAngularVelocity(new BABYLON.Vector3(0, 1, 0))Collision Detection
sphereImpostor.onCollideEvent.add((collider, collidedWith) => {
console.log('Collision between', collider.object.name, 'and', collidedWith.object.name)
})Physics Debug Renderer
Visualize physics bodies as wireframes — very helpful for debugging:
const debugRenderer = new BABYLON.PhysicsDebugRenderer(scene)
debugRenderer.enabled = trueCommon Mistakes Beginners Make
1. Forgetting await on HavokPhysics
// Wrong — Havok not initialized
const havokPlugin = new BABYLON.HavokPlugin(true, HavokPhysics())
scene.enablePhysics(gravity, havokPlugin)
// Correct — wait for WASM to load
const havokInstance = await HavokPhysics()
const havokPlugin = new BABYLON.HavokPlugin(true, havokInstance)2. Physics Object Falls Through Ground
// Wrong — ground needs mass=0 and an impostor
ground.physicsImpostor = new BABYLON.PhysicsImpostor(
ground, BABYLON.PhysicsImpostor.BoxImpostor,
{ mass: 0 }, // mass=0 makes it static
scene
)3. Animation Frame Timing Confusion
Keyframes are in frame numbers, not seconds. At 60 fps, frame 60 = 1 second. Always convert: frames = seconds * fps.
4. applyImpulse Not Moving the Object
The impostor’s mass must be > 0. A mass-0 object is static and ignores forces and impulses.
5. High Mass Differences Cause Instability
Keep mass ratios below 10:1 between colliding objects. A mass-1 box hitting a mass-10000 wall will jitter or pass through.
Practice Questions
What is the difference between
applyForceandapplyImpulse?applyForceapplies continuous force each frame (like gravity or wind).applyImpulseapplies an instant velocity change (like a collision). Use impulse for one-shot events, force for ongoing effects.What does
mass = 0mean for a physics impostor? The object is static — it never moves, but other objects can collide with it. Use for ground, walls, and immovable objects.How do you convert seconds to animation frames at 60 fps?
frames = seconds * 60. So 2 seconds = 120 frames.What does an easing function do in animation? It changes the interpolation curve between keyframes, making motion accelerate, decelerate, bounce, or overshoot — creating more natural movement.
Why does Havok need
await? Havok is a WebAssembly module that loads asynchronously. You must wait for it to initialize before creating impostors.
Challenge
Create a physics playground with:
- A ground plane and four enclosing walls
- A “Spawn Cube” button that drops cubes with random sizes and colors
- A “Spawn Sphere” button with random sizes
- A “Launch Sphere” button that fires a sphere in the camera’s direction
- Gravity slider (0 to -30)
- Bounciness slider (restitution 0-1)
- A physics debug renderer toggle
- Auto-cleanup of launched projectiles after 5 seconds
FAQ
Try It Yourself: Physics Playground
Drop cubes and spheres, launch projectiles, adjust gravity and bounciness, and see the physics debug renderer in action.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Babylon.js — Physics Playground</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: system-ui, sans-serif; }
#renderCanvas { width: 100%; height: 100%; display: block; }
#ui {
position: absolute; top: 16px; left: 16px;
background: rgba(0,0,0,0.78); color: #fff;
padding: 16px 20px; border-radius: 8px; font-size: 14px;
backdrop-filter: blur(4px); user-select: none;
display: flex; flex-direction: column; gap: 10px; min-width: 200px;
}
#ui label { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
#ui input[type="range"] { width: 100px; }
#ui button { background: #4a6cf7; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
#ui button:hover { background: #5f7cf7; }
#ui .row { display: flex; gap: 8px; }
#ui hr { border: none; border-top: 1px solid #444; margin: 4px 0; }
.val { font-family: monospace; min-width: 28px; text-align: right; }
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<div id="ui">
<strong>Physics Playground</strong>
<label>Gravity <input type="range" id="gravity" min="0" max="30" step="0.5" value="12" /><span class="val" id="gravityVal">12</span></label>
<label>Bounciness <input type="range" id="restitution" min="0" max="1" step="0.05" value="0.4" /><span class="val" id="restitutionVal">0.40</span></label>
<hr />
<div class="row"><button id="spawnCube">Spawn Cube</button><button id="spawnSphere">Spawn Sphere</button></div>
<div class="row"><button id="launchSphere">Launch Sphere!</button><button id="clearAll">Clear All</button></div>
<hr />
<label><input type="checkbox" id="debugPhysics" /> Debug Render</label>
</div>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/havok/HavokPhysics.js"></script>
<script>
(async function () {
const canvas = document.getElementById('renderCanvas')
const engine = new BABYLON.Engine(canvas, true, { stencil: true })
const scene = new BABYLON.Scene(engine)
scene.clearColor = new BABYLON.Color3(0.06, 0.06, 0.1)
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 3, Math.PI / 3, 18, new BABYLON.Vector3(0, 3, 0), scene)
camera.lowerRadiusLimit = 5; camera.upperRadiusLimit = 40; camera.attachControl(canvas, true)
const hemi = new BABYLON.HemisphericLight('hemi', new BABYLON.Vector3(0, 1, 0), scene); hemi.intensity = 0.4
const dir = new BABYLON.DirectionalLight('dir', new BABYLON.Vector3(-1, -2, -1), scene); dir.position = new BABYLON.Vector3(10, 20, 10)
const ground = BABYLON.MeshBuilder.CreateGround('ground', { width: 20, height: 20 }, scene)
const gMat = new BABYLON.StandardMaterial('gMat', scene); gMat.diffuseColor = new BABYLON.Color3(0.2, 0.2, 0.25); gMat.specularColor = BABYLON.Color3.Black(); ground.material = gMat
function createWall(name, w, h, pos, rotY) {
const wall = BABYLON.MeshBuilder.CreateBox(name, { width: w, height: h, depth: 0.3 }, scene)
wall.position.copyFrom(pos); wall.rotation.y = rotY || 0
const wMat = new BABYLON.StandardMaterial(name + 'Mat', scene); wMat.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.8); wMat.alpha = 0.6; wall.material = wMat
return wall
}
const walls = [createWall('wall1', 20, 2, new BABYLON.Vector3(0, 1, -10), 0), createWall('wall2', 20, 2, new BABYLON.Vector3(0, 1, 10), 0), createWall('wall3', 20, 2, new BABYLON.Vector3(-10, 1, 0), Math.PI / 2), createWall('wall4', 20, 2, new BABYLON.Vector3(10, 1, 0), Math.PI / 2)]
const havokInstance = await HavokPhysics()
const havokPlugin = new BABYLON.HavokPlugin(true, havokInstance)
scene.enablePhysics(new BABYLON.Vector3(0, -12, 0), havokPlugin)
let debugRenderer = null
function toggleDebug(enabled) { if (enabled) { if (!debugRenderer) debugRenderer = new BABYLON.PhysicsDebugRenderer(scene); debugRenderer.enabled = true } else if (debugRenderer) debugRenderer.enabled = false }
ground.physicsImpostor = new BABYLON.PhysicsImpostor(ground, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, restitution: 0.5 }, scene)
walls.forEach(w => { w.physicsImpostor = new BABYLON.PhysicsImpostor(w, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, restitution: 0.3 }, scene) })
function randomColor() { return new BABYLON.Color3(Math.random(), Math.random(), Math.random()) }
function randomRange(min, max) { return Math.random() * (max - min) + min }
function spawnCube() {
const size = randomRange(0.5, 1.2); const mesh = BABYLON.MeshBuilder.CreateBox('cube', { size }, scene)
mesh.position.set(randomRange(-4, 4), 5 + randomRange(0, 3), randomRange(-4, 4))
const mat = new BABYLON.StandardMaterial('cMat', scene); mat.diffuseColor = randomColor(); mat.specularColor = new BABYLON.Color3(0.2, 0.2, 0.2); mesh.material = mat
mesh.physicsImpostor = new BABYLON.PhysicsImpostor(mesh, BABYLON.PhysicsImpostor.BoxImpostor, { mass: size, restitution: parseFloat(document.getElementById('restitution').value), friction: 0.3 }, scene)
}
function spawnSphere() {
const r = randomRange(0.3, 0.7); const mesh = BABYLON.MeshBuilder.CreateSphere('sphere', { diameter: r * 2, segments: 24 }, scene)
mesh.position.set(randomRange(-4, 4), 5 + randomRange(0, 3), randomRange(-4, 4))
const mat = new BABYLON.StandardMaterial('sMat', scene); mat.diffuseColor = randomColor(); mat.specularColor = new BABYLON.Color3(0.5, 0.5, 0.5); mat.specularPower = 32; mesh.material = mat
mesh.physicsImpostor = new BABYLON.PhysicsImpostor(mesh, BABYLON.PhysicsImpostor.SphereImpostor, { mass: r * 2, restitution: parseFloat(document.getElementById('restitution').value) }, scene)
}
function launchSphere() {
const mesh = BABYLON.MeshBuilder.CreateSphere('projectile', { diameter: 0.6, segments: 24 }, scene)
mesh.position.set(0, 2, -8)
const mat = new BABYLON.StandardMaterial('pMat', scene); mat.diffuseColor = new BABYLON.Color3(1, 0.8, 0.2); mat.emissiveColor = new BABYLON.Color3(0.3, 0.2, 0); mesh.material = mat
mesh.physicsImpostor = new BABYLON.PhysicsImpostor(mesh, BABYLON.PhysicsImpostor.SphereImpostor, { mass: 1, restitution: 0.3 }, scene)
const forward = camera.getTarget().subtract(camera.position).normalize()
mesh.physicsImpostor.setLinearVelocity(forward.scale(15).add(new BABYLON.Vector3(0, 3, 0)))
setTimeout(() => { if (mesh.physicsImpostor) mesh.physicsImpostor.dispose(); mesh.dispose() }, 5000)
}
function clearAll() {
const toRemove = []; scene.meshes.forEach(m => { if (m !== ground && !walls.includes(m) && m.physicsImpostor) toRemove.push(m) })
toRemove.forEach(m => { if (m.physicsImpostor) m.physicsImpostor.dispose(); m.dispose() })
}
document.getElementById('spawnCube').addEventListener('click', spawnCube)
document.getElementById('spawnSphere').addEventListener('click', spawnSphere)
document.getElementById('launchSphere').addEventListener('click', launchSphere)
document.getElementById('clearAll').addEventListener('click', clearAll)
document.getElementById('gravity').addEventListener('input', function () { const v = parseFloat(this.value); document.getElementById('gravityVal').textContent = v; scene.getPhysicsEngine().setGravity(new BABYLON.Vector3(0, -v, 0)) })
document.getElementById('restitution').addEventListener('input', function () { document.getElementById('restitutionVal').textContent = parseFloat(this.value).toFixed(2) })
document.getElementById('debugPhysics').addEventListener('change', function () { toggleDebug(this.checked) })
for (let i = 0; i < 5; i++) spawnCube()
for (let i = 0; i < 3; i++) spawnSphere()
engine.runRenderLoop(() => { scene.render() })
window.addEventListener('resize', () => engine.resize())
})()
</script>
</body>
</html>What to expect: A 3D arena with falling cube and sphere objects, a launch projectile button, and adjustable gravity and bounciness. Toggle the physics debug renderer to see collision wireframes.
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| GUI & Interaction | 2D UI overlays, mesh picking, ActionManager |
| Importing & Assets | glTF/GLB import, asset containers, optimization |
Related topics: Babylon.js Materials & Textures, Game Physics.
What’s Next
Congratulations on completing this Babylonjs Animation Physics tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro