Skip to content
Babylon.js Animation & Physics — Bring Your 3D Scene to Life

Babylon.js Animation & Physics — Bring Your 3D Scene to Life

DodaTech Updated Jun 6, 2026 13 min read

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.

Prerequisites: You should know how to create a scene with meshes and lights.

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:

  1. Frame 0: box is at y=0 (on the ground)
  2. Frame 30: box rises to y=3 (3 units up)
  3. Frame 60: box returns to y=0

Babylon.js generates all 60 intermediate positions. The box bobs up and down smoothly.

Animation Types

Type ConstantUse For
ANIMATIONTYPE_FLOATSingle values: position.y, rotation.x, alpha
ANIMATIONTYPE_VECTOR3Position, rotation, scale
ANIMATIONTYPE_COLOR3Color transitions (diffuse, emissive)
ANIMATIONTYPE_QUATERNIONSmooth rotational interpolation
ANIMATIONTYPE_MATRIXFull transform matrix

Loop Modes

ModeBehavior
ANIMATIONLOOPMODE_CYCLERestart from beginning (default)
ANIMATIONLOOPMODE_CONSTANTHold last keyframe value
ANIMATIONLOOPMODE_RELATIVEContinue 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
ModeEffect
EASINGMODE_EASEINSlow start, fast end
EASINGMODE_EASEOUTFast start, slow end
EASINGMODE_EASEINOUTSlow 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

ImpostorBest For
BoxImpostorCrates, walls, buildings
SphereImpostorBalls, projectiles, marbles
PlaneImpostorGround, walls (infinite plane)
CylinderImpostorBarrels, pipes
MeshImpostorCustom shapes (convex hull)
CapsuleImpostorCharacters (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 = true

Common 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

  1. What is the difference between applyForce and applyImpulse? applyForce applies continuous force each frame (like gravity or wind). applyImpulse applies an instant velocity change (like a collision). Use impulse for one-shot events, force for ongoing effects.

  2. What does mass = 0 mean 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.

  3. How do you convert seconds to animation frames at 60 fps? frames = seconds * 60. So 2 seconds = 120 frames.

  4. 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.

  5. 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

How do I convert seconds to frames in Babylon.js?

Multiply seconds by the animation FPS (default 60): frames = seconds * 60. So 2 seconds = 120 frames.

Can I animate any property?

Yes — position, rotation, scaling, material color, light intensity, camera position, bone transforms, and even custom properties. The property string uses dot notation: "material.diffuseColor.r".

Why does my physics simulation jitter?

Increase the physics time step: scene.getPhysicsEngine().setTimeStep(1/120) (default is 1/60). A smaller step = more stable physics but more CPU usage.

How do I create a ragdoll?

Create multiple box impostors as bones, connect them with PhysicsConstraint (hinge or ball-and-socket), attach them to a character skeleton, and apply impulses on impact.

Can I disable physics for specific objects at runtime?

Set impostor.mass = 0 to make it static, or impostor.isDisabled = true. Re-enable by setting mass > 0.

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

TutorialWhat You’ll Learn
GUI & Interaction2D UI overlays, mesh picking, ActionManager
Importing & AssetsglTF/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