Three.js Physics



Focus

This tutorial is about the creation of motions based on Physics laws within Three.js applications. Physics encompasses the ability of moving 3D objects as objects in real life that are subject to gravity (force in general), damping, friction, bounce…

Due to Three.js has no enhanced support for Physics, a Physics engine is required, for instance, cannon-es.

Application

Installation and use

For ease of installation and use, you may download the TypeScript application with all the necessary settings: .

Once unzipped, you can execute the following statements:

cd Physics.ts
npm i
npm run Physics

Be careful about running things locally, you need a local Web server to load Physics.html (further explanation here…). Typically, if you use Visual Studio Code then you benefit from installing Live Server and running Physics.html within it.

Theme

A warehouse model in GLTF format (license “CC Attribution”, credit to AurĂ©lien Martel) downloaded from Sketchfab includes elements like floor, ceil, windows, beams, walls…

The Three.js application with Physics assigns a physical body with no mass to the floor, i.e., it is not subject to gravity created by means of cannon-es (lines 3-5).

While the floor is still, a “Window1” 3D object is equipped with a body having a mass. A shown above in the picture, “Window1” is beforehand shifted inside the warehouse so that it may later fall down on the floor.

class Physics {
    …
    private readonly _world = new CANNON.World({ // cannon-es
        gravity: new CANNON.Vec3(0, -1, 0), // Some gravity on Y axis (down)...
    });
    private _floor_body: CANNON.Body | null = null;
    private _window1_body: CANNON.Body | null = null;
    static readonly Floor = "Plane_0"; // Original name in GLTF model...
    private _floor_mesh: null | THREE.Mesh = null;
    static readonly Window1 = "TexturesCom_WindowsBacklit0019_12_M_0"; // Original name in GLTF model...
    private _window1_mesh: null | THREE.Mesh = null;
    …
}

Create floor body from 3D object in GLTF model

private _initialization(): void { // As instance method of 'Physics' class...
    this._floor_mesh = this._scene.getObjectByName(Physics.Floor) as THREE.Mesh;
    /** 'Physics._Position', 'Physics._Quaternion' and 'Physics._Scale' are Three.js predefined static 3D/4D vectors to avoid the creation
     of local vectors that may diminish performance */
    this._floor_mesh.getWorldPosition(Physics._Position);
    this._floor_mesh.getWorldQuaternion(Physics._Quaternion);
    // The floor has no motion, so:
    this._floor_mesh.matrixAutoUpdate = false; // 'this._floor_mesh.updateMatrix();' is required if one wants to introduce motions later on...

    // CANNON planes are infinite (mass is zero by default; it prevents any motion due to gravity especially):
    this._floor_body = new CANNON.Body({material: Physics.FLOOR_BODY_MATERIAL, shape: new CANNON.Plane()}); // Mass is zero by default...
    this._world.addBody(this._floor_body);
    // this._floor_body.allowSleep = true; // This is default...
    this._floor_body.position.set(Physics._Position.x, Physics._Position.y, Physics._Position.z);
    this._floor_body.quaternion.set(Physics._Quaternion.x, Physics._Quaternion.y, Physics._Quaternion.z, Physics._Quaternion.w);
    …
}

Create “Window1” body from 3D object in GLTF model

private _setup_window1_physics(): void { // As instance method of 'Physics' class...
    this._window1_mesh = this._scene.getObjectByName(Physics.Window1) as THREE.Mesh;
    console.assert(this._window1_mesh.isMesh);
    …
    // Backward "Window1" on Z axis (separation of warehouse) for better visualization (physics simulation):
    this._window1_mesh!.translateZ(-0.5);
    // Compute physical body from "Window1" 3D properties:
    this._window1_mesh!.geometry.computeBoundingBox();
    console.assert(this._window1_mesh!.geometry.boundingBox!.isBox3);
    // Compute physical body from surrounding box:
    const x_min = this._window1_mesh!.geometry.boundingBox!.min.x;
    const x_max = this._window1_mesh!.geometry.boundingBox!.max.x;
    const y_min = this._window1_mesh!.geometry.boundingBox!.min.y;
    const y_max = this._window1_mesh!.geometry.boundingBox!.max.y;
    const z_min = this._window1_mesh!.geometry.boundingBox!.min.z;
    const z_max = this._window1_mesh!.geometry.boundingBox!.max.z;
    this._window1_mesh!.getWorldScale(Physics._Scale);
    const shape = new CANNON.Vec3((x_max - x_min) / 2 * Physics._Scale.x, (y_max - y_min) / 2 * Physics._Scale.y, (z_max - z_min) / 2 * Physics._Scale.z);
    /* CAUTION: '1' or greater values for 'linearDamping' create irrational behaviors for bodies. */
    this._window1_body = new CANNON.Body({
        // angularDamping: 0.5,
        linearDamping: 0.1, /* This slows the motion (increased "air" friction) compared to default value, which is '0.01' */
        material: Physics.WINDOW1_BODY_MATERIAL, // For controlled collision management..
        mass: 1, // Some mass to have motion...
        shape: new CANNON.Box(shape)
    });
    this._world.addBody(this._window1_body);
    this._window1_mesh!.getWorldPosition(Physics._Position);
    this._window1_mesh!.getWorldQuaternion(Physics._Quaternion);
    this._window1_body.position.set(Physics._Position.x, Physics._Position.y, Physics._Position.z);
    this._window1_body.quaternion.set(Physics._Quaternion.x, Physics._Quaternion.y, Physics._Quaternion.z, Physics._Quaternion.w);
    // Set up collision features:
    this._world.addContactMaterial(Physics.FLOOR_WINDOW1_CONTACT_MATERIAL);
    this._window1_body.addEventListener(CANNON.Body.COLLIDE_EVENT_NAME, (event: { type: any, body: CANNON.Body, contact: CANNON.ContactMaterial, target: CANNON.Body }) => { // 'Object.getOwnPropertyNames(event)'
        console.info("Collision between " + event.body.material!.name + " and " + event.target.material!.name);
        BEEP.play();
    });
}

The _initialization, _setup_window1_physics, and _animate instance methods of the Physics class are called in sequence once an instance is constructed. Please, note that the DOM and the GLTF model must be loaded before such an instance accesses resources in general (see the overall code in downloadable ZIP file above).

The _animate function aims at updating (before rendering) the location and orientation of “Window1” Three.js 3D object. Update comes from motion evolution of “Window1” cannon-es body.

Subtlety is the fact that “Window1” has a relative position to the group of 3D objects (warehouse structure) it belongs to in the GLTF model. To set the absolute position of “Window1” 3D object (lines 6-13) from its body, one must then use a subterfuge, which is explained here

private _animate = () => {
    this._requestAnimationFrame_id = window.requestAnimationFrame(this._animate);
    // Apply physical laws:
    // this._world.step(0.05); // 20 frames per sec. (i.e., '1/20 === 0.05')
    this._world.fixedStep(); // By default, 60 frames per sec.
    // Update "Window1" location (this is based on https://stackoverflow.com/questions/12547701/three-js-changing-the-world-position-of-a-child-3d-object):
    this._scene.attach(this._window1_mesh!); // Detach from parent and add to current scene...
    /** It's a bad idea to rely on some interoperability between 'THREE.Vector3'
     and 'CANNON.Vec3'. While this may work in JavaScript, TypeScript forbids this at compilation time: */
    this._window1_mesh!.position.set(this._window1_body!.position.x, this._window1_body!.position.y, this._window1_body!.position.z);
    this._window1_mesh!.parent!.attach(this._window1_mesh!); // Reattach to original parent...
    // Update "Window1" orientation:
    this._window1_mesh!.quaternion.set(this._window1_body!.quaternion.x, this._window1_body!.quaternion.y, this._window1_body!.quaternion.z, this._window1_body!.quaternion.w);
    …    
    this._renderer.render(this._scene, this._camera);
};

Exercise

Introduce a similar gravity-based motion for a “Wall1” 3D object whose original name in the GLTF model is “Plane004_0”.

Some note on cannon-es

Computing the body of a Three.js 3D object relies (in the simpler case) on computing a bounding box (as done above for “Window1”: this._window1_mesh!.geometry.computeBoundingBox();) or a bounding sphere surrounding this object's geometry.

Consequently, starting from a bounding box of a Three.js 3D object, one must setup its body shape's depth as follows: (z_max - z_min) / 2 * Physics._Scale.z (i.e., getting “scale” means that some enlargement or narrowing may exist) while const z_min = this._window1_mesh!.geometry.boundingBox!.min.z; and const z_max = this._window1_mesh!.geometry.boundingBox!.max.z;. The same spirit applies for width on the X axis and height on the Y axis…

As an illustration below, on the Z axis, a cannon-es-based lips 3D object's depth is equal to body.shapes[0].halfExtents.z * 2 once the shaped body variable is defined.