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.
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 runningPhysics.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 thePhysics
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”.
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) whileconst z_min = this._window1_mesh!.geometry.boundingBox!.min.z;
andconst z_max = this._window1_mesh!.geometry.boundingBox!.max.z;
. The same spirit applies for width on theX
axis and height on theY
axis…As an illustration below, on the
Z
axis, a cannon-es-based lips 3D object's depth is equal tobody.shapes[0].halfExtents.z * 2
once the shapedbody
variable is defined.