⚛️ THREE.js + cannon-es Physics Lesson

Explore a 3D physics simulation with a box falling onto a plane using cannon-es. Reset the physics anytime!

Lesson: Building a Physics Simulation

In this lesson, we will create a simple physics simulation using THREE.js for rendering and cannon-es for physics calculations.

Step 1: Setting Up the Scene

We start by creating a THREE.js scene, adding a camera, and setting up lighting. This provides the foundation for rendering 3D objects.


    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 6, 12);
    camera.lookAt(0, 0, 0);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(10, 15, 10);
    scene.add(directionalLight);
    

Step 2: Adding Physics with cannon-es

Next, we set up a physics world using cannon-es. This includes defining gravity, materials, and creating physics bodies for the ground and a box.


    const world = new CANNON.World({
      gravity: new CANNON.Vec3(0, -9.82, 0)
    });

    const groundBody = new CANNON.Body({
      mass: 0,
      shape: new CANNON.Plane()
    });
    groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
    world.addBody(groundBody);

    const boxBody = new CANNON.Body({
      mass: 5,
      shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)),
      position: new CANNON.Vec3(0, 10, 0)
    });
    world.addBody(boxBody);
    

Step 3: Synchronizing Physics and Rendering

We synchronize the physics world with the THREE.js scene by updating the positions and rotations of the 3D objects based on the physics bodies.


    function animate() {
      requestAnimationFrame(animate);
      world.step(1 / 60);

      boxMesh.position.copy(boxBody.position);
      boxMesh.quaternion.copy(boxBody.quaternion);

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

Step 4: Adding Interactivity

We add a button to reset the simulation, allowing users to restart the physics world and reposition the box.


    document.getElementById('restartBtn').addEventListener('click', () => {
      world.removeBody(boxBody);
      boxBody.position.set(0, 10, 0);
      world.addBody(boxBody);
    });
    

Step 5: Enhancing the Scene

Finally, we add materials, textures, and additional objects to make the scene more visually appealing.

Step 6: Collision Detection

We can detect collisions between objects in the physics world using event listeners. For example, we can log a message when the box hits the ground:


    boxBody.addEventListener('collide', (event) => {
      console.log('Box collided with:', event.body);
    });
    

This is useful for triggering game logic or visual effects when objects interact.

Step 7: Debugging the Physics World

To debug the physics world, we can use a helper library like cannon-es-debugger to visualize physics bodies:


    import CannonDebugger from 'https://cdn.jsdelivr.net/npm/cannon-es-debugger/dist/cannon-es-debugger.js';

    const cannonDebugger = new CannonDebugger(scene, world);
    function animate() {
      requestAnimationFrame(animate);
      world.step(1 / 60);
      cannonDebugger.update(); // Update the debugger
      renderer.render(scene, camera);
    }
    

This helps identify issues like incorrect body positions or shapes.

Step 8: Adding More Objects

We can add more objects to the scene and physics world, such as spheres or custom shapes:


    const sphereShape = new CANNON.Sphere(1);
    const sphereBody = new CANNON.Body({
      mass: 3,
      shape: sphereShape,
      position: new CANNON.Vec3(2, 8, 0),
    });
    world.addBody(sphereBody);

    const sphereGeo = new THREE.SphereGeometry(1, 32, 32);
    const sphereMat = new THREE.MeshStandardMaterial({ color: 0xff5733 });
    const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
    scene.add(sphereMesh);

    function animate() {
      requestAnimationFrame(animate);
      world.step(1 / 60);

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

      renderer.render(scene, camera);
    }
    

Step 9: Optimizing Performance

For better performance, we can reduce the number of physics steps or use simpler shapes for collision detection:


    world.step(1 / 30); // Reduce physics update frequency
    

Additionally, avoid adding too many high-polygon meshes to the scene.

Step 10: Exporting the Scene

We can export the scene or physics data for use in other applications:


  // Export scene to JSON
  const sceneData = scene.toJSON();
  console.log(JSON.stringify(sceneData));

  // Export physics world
  const physicsData = world.bodies.map(body => ({
    position: body.position,
    velocity: body.velocity,
    shape: body.shapes[0].type,
  }));
  console.log(JSON.stringify(physicsData));
    

This allows us to save and reload simulations.

Cannon.js Cheatsheet