In this tutorial, you'll learn how to use Physi.js to add game physics to a 3D scene created using Three.js. We'll create a simple game in which we drive a cart around collecting items, using basic physics shapes and physics constraints.
This tutorial will be building on top of the concepts shared in my previous Three.js tutorial. I would request you to read it if you are new to Three.js and to its 3D scene creation.
Due to a technical limitation in hosting web worker based solutions on JSFiddle, we are unable to embed the interactive game in this tutorial page. Please use the provided source code to check out the working example on any cloud-based IDEs or by self-hosting.
1. 3D Physics on the Web
There are multiple frameworks and engines currently available which can be used to create 3D content for the web with physics. Some of those worth mentioning are Turbulenz, BabylonJS, PlayCanvas, and the obvious Unity WebGL build. But when it comes to popularity and ease of use, most people like to use Three.js for their experiments. As Three.js is a rendering engine and does not have integrated physics, we need to explore additional frameworks to add the physics capability.
A very popular JavaScript physics solution is Ammo.js, which is a direct port of Bullet physics. While directly using Ammo.js is possible, it is not very beginner friendly and has a lot of boilerplate code for each aspect. Also, as it is not manually written but ported using Emscripten, the code is not easy to understand.
An alternative solution is to use Cannon.js or Physijs. The interesting thing with Physijs is that the focus is always on making things easier, which makes it the ideal choice for beginners. It is built based on Ammo.js and even has a Cannon.js branch. This tutorial uses Physijs and Three.js to build a working game prototype with physics capabilities.
Yet another option, although oversimplified, would be to use the Whitestorm framework, which is a component-based framework based on Three.js and Physijs.
2. Setting Up Physijs
We need to have the ammo.js, physi.js, physijs_worker.js and three.js files within our folder structure or coding environment to use Physijs. Physijs uses a web worker to use different threads for physics calculations. So the first step in the integration process is to set up the web worker as below.
Physijs.scripts.worker = 'lib/physijs_worker.js'; Physijs.scripts.ammo = 'ammo.js';
At this point, the setup is complete, and we can start using the physics framework. Physijs has made sure that it followed the coding style of Three.js, and most of the concepts are simple replacements of the corresponding Three.js concept.
Basic Steps
Instead of THREE.Scene
, we need to use Physijs.Scene
.
There are multiple meshes available in Physijs which need to be used in place of THREE.Mesh
. The available options are PlaneMesh
, BoxMesh
, SphereMesh
, CylinderMesh
, ConeMesh
, CapsuleMesh
, ConvexMesh
, ConcaveMesh
, and HeighfieldMesh
.
We need to call the scene.simulate
method to do the physics calculations either in the render
method or within frequent intervals. Let me remind you that the physics calculations happen in a different thread and will not be in sync or as fast as the scene render loop.
Even the next call to scene.simulate
may happen while the previous calculations are still running. In order to make it properly in sync with the physics calculations, we could use the Physijs scene's update
event.
scene.addEventListener( 'update', function() { //your code. physics calculations have done updating });
In order to register a collision on a Physijs mesh object named arbitrarily as cube
, we can listen to the collision
event.
cube.addEventListener( 'collision', function( objCollidedWith, linearVelOfCollision, angularVelOfCollision ) { });
Within the above method, this
will refer to cube
, while objCollidedWith
is the object cube
has collided with.
3. Example Game Prototype
For this tutorial, we will be creating a very simple physics-based game where we will use physics constraints to create a vehicle. The player can use arrow keys to drive the vehicle and collect a bouncing ball which randomly appears in the play area.
Interestingly, Physijs already has a special vehicle
feature which can be directly used for creating vehicles, but we won't be using it.
The Game World
Our game world is a vast ground with walls on its four sides as below.
We use Physijs.BoxMesh
for the ground and the four walls as shown in the code below.
ground_material = Physijs.createMaterial( new THREE.MeshStandardMaterial( { color: 0x00ff00 } ),friction, .9 // low restitution ); // Ground ground = new Physijs.BoxMesh(new THREE.BoxGeometry(150, 1, 150),ground_material,0 // mass ); ground.receiveShadow = true; scene.add( ground );
Notice the usage of Physijs.createMaterial
to create the necessary physics materials by passing a friction value and a restitution value. The friction value determines the grip on the ground, and the restitution value determines the bounciness. One important thing to note is that when we provide a mass value of 0
, we create a stationary mesh object.
The Vehicle
We are going to create a special vehicle which has two connected parts. The front part, which has three wheels, acts as the engine, and the rear part, having two wheels, will act as a carriage. The carriage part is connected to the engine part using a hinge joint implemented using a Physijs.HingeContraint
.
The wheels use Physijs.DOFConstraint
, which is a degree of freedom constraint to be attached to the vehicle's body while retaining the ability to rotate independently. I would invite you to read the official documentation on the various constraints available in Physijs.
The engine body and carriage body are simple BoxMesh
objects like the ground
shown above, but with a definite mass value. They are connected to each other using a hinge joint, as shown in the following code. A hinge joint restricts the motion of the connected object like that of a normal door.
car.carriage_constraint = new Physijs.HingeConstraint( car.carriage, // First object to be constrained car.body, // constrained to this new THREE.Vector3( 6, 0, 0 ), // at this point new THREE.Vector3( 0, 1, 0 ) // along this axis ); scene.addConstraint( car.carriage_constraint ); car.carriage_constraint.setLimits( -Math.PI / 3, // minimum angle of motion, in radians Math.PI / 3, // maximum angle of motion, in radians 0, // applied as a factor to constraint error 0 // controls bounce at limit (0.0 == no bounce) );
The second part of the code applies limits to the rotation of the hinge, which in this case is between -Math.PI/3
and Math.PI/3
.
The wheels use a degree of freedom constraint which can be used to set limits on both linear motion and angular motion in all three axes. A method addWheel
is created for adding wheels, which takes in multiple parameters.
The parameters are the position of the wheel, the weight of the wheel, whether the wheel is big or small, and the wheel reference object. The method returns a newly created DOFConstraint
, which is used for driving the vehicle.
function addWheel(wheel, pos, isBig, weight){ var geometry=wheel_geometry; if(isBig){ geometry=big_wheel_geometry; } wheel = new Physijs.CylinderMesh( geometry, wheel_material, weight ); wheel.name="cart"; wheel.rotation.x = Math.PI / 2; wheel.position.set(pos.x,pos.y,pos.z); wheel.castShadow = true; scene.add( wheel ); wheel.setDamping(0,damping); var wheelConstraint = new Physijs.DOFConstraint( wheel, car.body, pos ); if(isBig){ wheelConstraint = new Physijs.DOFConstraint( wheel, car.carriage, pos); } scene.addConstraint( wheelConstraint ); wheelConstraint.setAngularLowerLimit({ x: 0, y: 0, z: 0 }); wheelConstraint.setAngularUpperLimit({ x: 0, y: 0, z: 0 }); return wheelConstraint; }
The big wheels need to be attached to the carriage, and the small wheels are attached to the engine. The wheels are given a damping value so that their angular velocity gets reduced when no external force is being applied. This makes sure that the vehicle slows down when we release the accelerator.
We will explore the driving logic in a later section. Each wheel mesh along with the car meshes are assigned the name of cart
for identification purposes during the collision call back. Each wheel constraint has different angular limits, which are set independently once they are created. For example, here is the code for the front middle wheel of the engine, car.wheel_fm
, and the corresponding constraint, car.wheel_fm_constraint
.
car.wheel_fm_constraint=addWheel(car.wheel_fm, new THREE.Vector3( -7.5, 6.5, 0 ),false,300); car.wheel_fm_constraint.setAngularLowerLimit({ x: 0, y: -Math.PI / 8, z: 1 }); car.wheel_fm_constraint.setAngularUpperLimit({ x: 0, y: Math.PI / 8, z: 0 });
The Ball
The ball is a Physijs.SphereMesh
object with a lower mass value of 20
. We use the releaseBall
method to position the ball randomly within our game area whenever it gets collected.
function addBall(){ var ball_material = Physijs.createMaterial(new THREE.MeshStandardMaterial({ color: 0x0000ff ,shading:THREE.FlatShading}),friction,.9 // good restitution ); var ball_geometry = new THREE.SphereGeometry( 2,16,16); ball = new Physijs.SphereMesh(ball_geometry,ball_material,20); ball.castShadow = true; releaseBall(); scene.add( ball ); ball.setDamping(0,0.9); ball.addEventListener( 'collision', onCollision); } function releaseBall(){ var range =10+Math.random()*30; ball.position.y = 16; ball.position.x = ((2*Math.floor(Math.random()*2))-1)*range; ball.position.z = ((2*Math.floor(Math.random()*2))-1)*range; ball.__dirtyPosition = true;//force new position // You also need to cancel the object's velocity ball.setLinearVelocity(new THREE.Vector3(0, 0, 0)); ball.setAngularVelocity(new THREE.Vector3(0, 0, 0)); }
One thing worth noticing is the fact that we need to override the position values set by the physics simulation in order to reposition our ball. For this, we use the __dirtyPosition
flag, which makes sure that the new position is used for further physics simulation.
The ball gets collected when it collides with any part of the vehicle which happens in the onCollision
listener method.
function onCollision(other_object, linear_velocity, angular_velocity ){ if(other_object.name==="cart"){ score++; releaseBall(); scoreText.innerHTML=score.toString(); } }
Driving the Vehicle
We add event listeners for the onkeydown
and onkeyup
events of the document, where we determine the keyCode
to set the values of the corresponding wheel constraints. The theory is that the single front wheel of the engine controls the turning of our vehicle, and the two wheels on the rear of the engine control the acceleration and deceleration. The wheels on the carriage do not play any part in the driving.
document.onkeydown = handleKeyDown; document.onkeyup = handleKeyUp; function handleKeyDown(keyEvent){ switch( keyEvent.keyCode ) { case 37: // Left car.wheel_fm_constraint.configureAngularMotor( 1, -Math.PI / 3, Math.PI / 3, 1, 200 ); car.wheel_fm_constraint.enableAngularMotor( 1 ); break; case 39: // Right car.wheel_fm_constraint.configureAngularMotor( 1, -Math.PI / 3, Math.PI / 3, -1, 200 ); car.wheel_fm_constraint.enableAngularMotor( 1 ); break; case 38: // Up car.wheel_bl_constraint.configureAngularMotor( 2, 1, 0, 6, 2000 ); car.wheel_br_constraint.configureAngularMotor( 2, 1, 0, 6, 2000 ); car.wheel_bl_constraint.enableAngularMotor( 2 ); car.wheel_br_constraint.enableAngularMotor( 2 ); break; case 40: // Down car.wheel_bl_constraint.configureAngularMotor( 2, 1, 0, -6, 2000 ); car.wheel_br_constraint.configureAngularMotor( 2, 1, 0, -6, 2000 ); car.wheel_bl_constraint.enableAngularMotor( 2 ); car.wheel_br_constraint.enableAngularMotor( 2 ); break; } } function handleKeyUp(keyEvent){ switch( keyEvent.keyCode ) { case 37: // Left car.wheel_fm_constraint.disableAngularMotor( 1 ); break; case 39: // Right car.wheel_fm_constraint.disableAngularMotor( 1 ); break; case 38: // Up car.wheel_bl_constraint.disableAngularMotor( 2 ); car.wheel_br_constraint.disableAngularMotor( 2 ); break; case 40: // Down car.wheel_bl_constraint.disableAngularMotor( 2 ); car.wheel_br_constraint.disableAngularMotor( 2 ); break; } }
The DOFConstraint
uses the enableAngularMotor
method to apply angular velocity on the wheel which turns the wheel based on the axis value provided as a parameter. Basically, we only turn the wheels, and the motion of the vehicle happens due to the frictional response of the ground, as in the real world.
We are unable to embed the working game in this page as mentioned at the start of the tutorial. Please go through the complete source to understand how everything is wired together.
Conclusion
This is a very simple introduction to implementing physics in your Three.js 3D world. The Physijs documentation is sorely lacking, but there are many examples already available which are worth looking into. Physijs is a very beginner-friendly framework, like Three.js, when you consider the complexity that is present under the hood.
JavaScript is clearly popular for game development as well as web development. It’s not without its learning curves, and there are plenty of frameworks and libraries to keep you busy, as well. If you’re looking for additional resources to study or to use in your work, check out what we have available in the Envato marketplace.
Hope this tutorial helps you get started in exploring the interesting world of 3D web game physics.