Skip to content

Three.js Physics — Adding Physics Engines (Cannon-es, Ammo.js)

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Three.js Physics. We cover key concepts, practical examples, and best practices to help you master this topic.

Three.js physics engines like Cannon-es and Ammo.js add real-world forces such as gravity, friction, and collision response to 3D scenes for realistic simulation.

What You'll Learn

By the end of this guide, you will integrate Cannon-es with Three.js, create physics bodies from meshes, apply forces and impulses, detect collisions, manage compound shapes, and build a physics playground.

Why Physics Matters

Static 3D scenes feel dead. Physics brings them to life. Durga Antivirus Pro uses physics for its particle explosion effect when threats are neutralized — debris scatters with gravity and bounces off walls. Physics also powers the training simulations where users practice defense tactics in realistic environments.

flowchart LR
    A[Three.js Scene] --> B[Physics World]
    C[Mesh Geometry] --> D[Physics Body]
    A --> D
    B --> D
    D --> E[Forces & Gravity]
    E --> F[Physics Step]
    F --> G[Sync Body to Mesh]
    G --> H[Render Frame]

Setting Up Cannon-es

Cannon-es is a maintained fork of the Cannon.js physics engine, compatible with modern JavaScript.

import * as THREE from 'three';
import * as CANNON from 'cannon-es';

var scene = new THREE.Scene();
var world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
world.allowSleep = true;

Why gravity: The -9.82 value represents Earth's gravity in meters per second squared. Set to 0 for zero-G or adjust for game-specific physics.

Creating Physics Bodies

Every physics object needs a body with a shape. The shape roughly matches the mesh geometry.

var sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
var sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff6600 });
var sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphereMesh.position.y = 5;
scene.add(sphereMesh);

var sphereShape = new CANNON.Sphere(1);
var sphereBody = new CANNON.Body({ mass: 1 });
sphereBody.addShape(sphereShape);
sphereBody.position.set(0, 5, 0);
world.addBody(sphereBody);

Expected output: A 3D sphere hovering at y=5 that drops to the ground on simulation start, bouncing slightly on impact.

Syncing Physics With Render

The physics simulation runs at a fixed timestep. Sync body positions to meshes every frame.

var clock = new THREE.Clock();

function animate() {
    var delta = clock.getDelta();
    world.step(1 / 60, delta, 3);

    sphereMesh.position.copy(sphereBody.position);
    sphereMesh.quaternion.copy(sphereBody.quaternion);

    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}
animate();

Expected output: The sphere mesh follows the physics simulation exactly, matching position and rotation.

Why fixed timestep: Physics is sensitive to varying frame rates. A fixed 1/60 step ensures deterministic simulation regardless of display refresh rate.

Ground Plane and Collision

var groundGeo = new THREE.PlaneGeometry(20, 20);
var groundMat = new THREE.MeshStandardMaterial({ color: 0x333344, side: THREE.DoubleSide });
var groundMesh = new THREE.Mesh(groundGeo, groundMat);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.position.y = -1;
scene.add(groundMesh);

var groundShape = new CANNON.Plane();
var groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);

Expected output: A ground plane visible in Three.js with a matching physics body that objects collide with and rest upon.

Why mass = 0: Static bodies (mass = 0) never move. They act as immovable collision surfaces.

Applying Forces and Impulses

sphereBody.applyForce(
    new CANNON.Vec3(5, 0, 0),
    sphereBody.position
);

sphereBody.applyImpulse(
    new CANNON.Vec3(0, 10, 0),
    sphereBody.position
);

Expected output: The sphere experiences a continuous force pushing it right and a sudden upward impulse, launching it in an arc.

Force vs Impulse: Forces apply continuously (like wind). Impulses apply instantaneously (like a punch).

Compound Shapes

Complex objects need multiple collision shapes combined.

var compoundBody = new CANNON.Body({ mass: 5 });

// Main body
var boxShape = new CANNON.Box(new CANNON.Vec3(1, 0.5, 0.5));
compoundBody.addShape(boxShape);

// Wheels
var wheelShape = new CANNON.Sphere(0.3);
compoundBody.addShape(wheelShape, new CANNON.Vec3(-0.8, -0.5, 0.6));
compoundBody.addShape(wheelShape, new CANNON.Vec3(0.8, -0.5, 0.6));
compoundBody.addShape(wheelShape, new CANNON.Vec3(-0.8, -0.5, -0.6));
compoundBody.addShape(wheelShape, new CANNON.Vec3(0.8, -0.5, -0.6));

world.addBody(compoundBody);

Expected output: A box body with four wheel-shaped collision spheres attached at the corners, acting as a single physics object.

Common Mistakes

1. Shape and Mesh Mismatch

If the physics shape is much larger or smaller than the visual mesh, objects appear to float above or sink into surfaces. Always match shape dimensions to mesh dimensions.

2. Forgetting to Sync Positions

Without copying body position/quaternion to mesh each frame, the visual mesh stays at its initial position while the physics body moves invisibly.

3. Massless Dynamic Bodies

Dynamic bodies need mass > 0. Mass = 0 means static (immovable). Trying to move a mass = 0 body with forces has no effect.

4. Using step With Variable Delta

Always use a fixed timestep (1/60). Passing the real delta causes physics instability when frame rate fluctuates.

5. Creating Physics Bodies for Every Small Object

For small, visually insignificant objects, skip physics. Use simple animation instead to save CPU.

6. Memory Leaks From Unremoved Bodies

When destroying objects, remove the physics body with world.removeBody(body) and dispose the mesh resources.

Practice Questions

Q1: Why does the physics simulation use a fixed timestep? A: Physics is sensitive to timing variations. A fixed timestep ensures deterministic, stable simulation regardless of frame rate fluctuations.

Q2: What happens when mass is set to 0? A: The body becomes static (immovable). It participates in collisions but gravity and forces do not affect it.

Q3: What is the difference between applyForce and applyImpulse? A: applyForce applies a continuous push over time. applyImpulse applies an instantaneous change in momentum.

Q4: How do you create a compound physics object? A: Create a single Body and call addShape() multiple times with different shapes and offsets.

Q5: Why must body position be copied to the mesh every frame? A: The physics simulation updates body positions internally. Without syncing, the visual mesh stays in its initial position while the invisible physics body moves.

Challenge: Build a physics simulation of a tower of blocks. Stack 10 boxes in alternating orientations, then shoot a sphere at the tower to knock it down. Count how many blocks fall off the platform.

FAQ

What is the difference between Cannon-es and Ammo.js?

Cannon-es is a pure JavaScript physics engine, lighter and easier to set up. Ammo.js is a port of Bullet Physics, more powerful but larger and requires WASM.

Can I use other physics engines with Three.js?

Yes. Popular integrations include PhysX via three-physx, Jolt Physics via jolt-physics, and Rapier via @dimforge/rapier3d.

How do I detect collisions in Cannon-es?

Use contact events: world.addEventListener('postStep', checkContacts) and iterate world.contacts for collision pairs.

What is the SAPBroadphase?

Sweep and Prune broadphase. It efficiently finds potential collision pairs by sorting bounding boxes along axes. Better than the default for most scenes.

How do I make an object kinematic?

Set body.type = CANNON.Body.KINEMATIC. Kinematic bodies move under user control but still push dynamic bodies out of the way.

What is allowSleep?

It lets bodies that have stopped moving temporarily pause physics calculations, saving CPU. Waking happens automatically when another body hits the sleeping body.

Try It Yourself — Physics Playground

Build a complete page with a ground plane, falling shapes of various types, and click-to-throw interaction. Physics runs at 60fps with Cannon-es.

<!DOCTYPE html>
<html>
<head>
    <title>Three.js Physics Playground</title>
    <style>
        body { margin: 0; overflow: hidden; font-family: sans-serif; }
        #controls { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.8); color: white; padding: 15px; border-radius: 8px; }
        #controls button { padding: 6px 12px; margin: 4px; cursor: pointer; }
    </style>
</head>
<body>
<div id="controls">
    <button onclick="dropSphere()">Drop Sphere</button>
    <button onclick="dropBox()">Drop Box</button>
    <button onclick="resetScene()">Reset</button>
</div>
<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
    }
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import * as CANNON from "https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.js";

var scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
var camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(8, 6, 10);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

var controls = new OrbitControls(camera, renderer.domElement);

var world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.SAPBroadphase(world);
world.allowSleep = true;

var ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;
scene.add(dirLight);

var groundGeo = new THREE.PlaneGeometry(20, 20);
var groundMat = new THREE.MeshStandardMaterial({ color: 0x333355, side: THREE.DoubleSide });
var groundMesh = new THREE.Mesh(groundGeo, groundMat);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
scene.add(groundMesh);

var groundShape = new CANNON.Plane();
var groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);

var physicsObjects = [];

function createPhysicsMesh(geo, mat, shape, pos, mass) {
    var mesh = new THREE.Mesh(geo, mat);
    mesh.position.copy(pos);
    mesh.castShadow = true;
    scene.add(mesh);

    var body = new CANNON.Body({ mass: mass || 1 });
    body.addShape(shape);
    body.position.set(pos.x, pos.y, pos.z);
    world.addBody(body);

    physicsObjects.push({ mesh, body });
    return { mesh, body };
}

window.dropSphere = function() {
    var pos = new THREE.Vector3(
        (Math.random() - 0.5) * 4,
        8 + Math.random() * 3,
        (Math.random() - 0.5) * 4
    );
    createPhysicsMesh(
        new THREE.SphereGeometry(0.5, 32, 32),
        new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }),
        new CANNON.Sphere(0.5),
        pos, 1
    );
};

window.dropBox = function() {
    var pos = new THREE.Vector3(
        (Math.random() - 0.5) * 4,
        8 + Math.random() * 3,
        (Math.random() - 0.5) * 4
    );
    createPhysicsMesh(
        new THREE.BoxGeometry(0.8, 0.8, 0.8),
        new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }),
        new CANNON.Box(new CANNON.Vec3(0.4, 0.4, 0.4)),
        pos, 1
    );
};

window.resetScene = function() {
    physicsObjects.forEach(function(obj) {
        scene.remove(obj.mesh);
        world.removeBody(obj.body);
    });
    physicsObjects = [];
};

var clock = new THREE.Clock();
function animate() {
    world.step(1 / 60, clock.getDelta(), 3);
    physicsObjects.forEach(function(obj) {
        obj.mesh.position.copy(obj.body.position);
        obj.mesh.quaternion.copy(obj.body.quaternion);
    });
    controls.update();
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}
animate();

window.addEventListener("resize", function() {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

What's Next

Add spatial audio to your 3D scenes for immersive experiences.

Audio — Positional audio in Three.js. VR AR — Virtual and augmented reality with Three.js.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro