Collision Detection In Javascript 3D Physics using Ammo.js and Three.js

Blue Magnificent
14 min readAug 4, 2020

--

This is the third article in my Javascript 3D Physics tutorials. Before proceeding it is assumed that you have gone through the first two articles. If you haven’t, I strongly suggest you do. The first article is “Intro to JavaScript 3D Physics using Ammo.js and three.js” and the second “Moving Objects in Javascript 3D Physics using Ammo.js and three.js

Introduction

Having objects collide and interact in the physics world is really fun; filtering who collides and who doesn’t using masks and all that. However there are situations where you might want to detect when there is collision between objects or just between a key object and any other object. You might also want to get additional information of the collision such as the objects involved, their velocity and even the position of the contacts made. For example in a game where the character runs through the stage collecting coins, you would want to detect when the characters makes contact with the coin so as to credit the character’s coin inventory or points.

Fortunately, ammo.js provides concepts to aide in collision detection. We have

  • Contact Manifold Check
  • ContactTest
  • ContactPairTest
  • Ghost Object

For simplicity we would only be treating Contact Manifold Check, ContactTest and ContactPairTest.

Before we start, do not forget to setup your workspace. This is clearly stated in the first tutorial and is quoted below:

First, obtain the libraries for three.js and ammo.js. Three.js can be gotten from https://threejs.org/build/three.js while for ammo.js, download the repo from https://github.com/kripken/ammo.js, then go to the build folder for the ammo.js file.

Create your project folder and give it any name of your choice. In it create an index.html file and a “js” folder that is to contain the three.js and ammo.js files.

Note that this tutorial as well as the previous ones draws a lot of information from the bullet user manual and the now defunct bullet physics wiki. This tutorial, in particular, contains some code snippets gotten from three.js physics convex break example.

Heads-up: There is going to be a lot of copying and pasting if you are to follow along with this tutorial. And don’t be scared, this article is not as voluminous as it looks.

Contact Manifold Check

To help us better understand contact manifold check, we would explain some concepts.

Contact Point

Just as the name says, this is the point of contact between two collision objects. Contact point is represented as btManifoldPoint in ammo.js and provides helpful information about the contact such as the impulse and position. It also provides the distance between the two objects. The assumption would be that for there to be a contact the distance between the objects will always be zero, however it might be greater or less.

Contact Manifold

A contact manifold is a cache that contains all contact points between pairs of collision objects”. For two colliding object, contact manifold contains all the contact points between them. It is represented in ammo.js by btPersistentManifold and exposes some useful methods, for example, to retrieve the two colliding objects, the number of contacts points between them and also a specific contact point by index.

Broadphase

Broadphase is definitely not new to us, we first encountered it in the first tutorial and it has been part of the structures we use in initializing our physics world:

overlappingPairCache = new Ammo.btDbvtBroadphase()

However, we will get to understand it a little bit more. Broadphase provides a fast and optimized way to eliminate collision object pairs based on their axis aligned bounding box (AABB) overlap. Basically, for each pair of collision objects in the world, the broadphase algorithm checks if their AABB overlaps. The pair is retained if there is an overlap or rejected otherwise thereby generating an approximate list of colliding pairs. There are however, pairs that have overlapping AABB but are still not close enough to collide. These are handled later by more specific acting algorithms.

Two main broadphase acceleration structures are available in ammo.js btDbvtBroadphase (Dynamic AABB Tree) and btAxisSweep3 (Sweep and Prune or SAP).

btDbvtBroadphaseadapts dynamically to the dimensions of the world and its contents. It is very well optimized and a very good general purpose broadphase. It handles dynamic worlds where many objects are in motion, and object addition and removal is faster than SAP”.

“Dbvt” in the name stands for dynamic bounding volume tree

btAxisSweep3 on the other hand “is also a good general purpose broadphase, with a limitation that it requires a fixed world size, known in advance. This broadphase has the best performance for typical dynamics worlds, where most objects have little or no motion”.

Dispatcher

We have also been using a dispatcher object all along:

dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration)

After the broadphase is done removing non overlapping AABB, the dispatcher “iterates over each pair, searches for a matching collision algorithm based on the types of objects involved and executes the collision algorithm computing contact points”.

To bring all these together, here is a simplified summary of how they all fit.
At every simulation step, the broadphase (btDbvtBroadphase or btAxisSweep3) checks every pair of collision objects in the world filtering off those without overlapping AABB. The dispatcher (btCollisionDispatcher) follows next by applying detailed collision algorithm on each pair giving us the contacts manifolds (btPersistentManifold) which contains one or more contact points (btManifoldPoint).

Contact manifold check approach of collision detection therefore involves iterating over all contact manifolds and extracting out information from them and their contact points. This should be done after each simulation step.

An example would explain better.

Create a new file in your workspace and name it contact_manifold_check.html. Copy the below code into it for bootstrapping

Preview the file in your browser and you should have something similar to the below image.

What you have here is a grid wall. The mouse can be moved around and pressing the left mouse button shoots out a ball from the cursor’s position. Only one ball at a time can be shot out with a lifespan of three (3) seconds and there is no gravity. Equally we have added tags to the three.js mesh objects through their userData property: wall.userData.tag = "wall"; and ball.userData.tag = "ball"; . This is to help us identify them along the process.

Feel free to explore the code, we will be building upon it for the reminder of this section.

Our implementation of contact manifold check will be in two steps. First we’ll detect that a collision has occurred. Next we’ll identify the participating physics objects along with their respective three.js object and equally obtain vital information about the collision such as the velocity of the objects and their impact points.

To detect if collision has occurred we will after each simulation:

  • Get the dispatcher.
  • From the dispatcher get the number of contact manifolds generated.
  • Iterate to get each contact manifold.
  • For each contact manifold get the number of contact points contained.
  • Iterate to get each contact point.
  • For each contact point get the distance.
  • Lastly log these information to the console.

Let’s go back to the code for some modification.

After updatePhysics() definition paste the below

The above method, detectCollision(), is almost a line by line implementation of the steps listed earlier. Add a call to this method as the last line of updatePhysics(), with updatePhysics() now looking like:

Go ahead and preview the work in the browser. Open the browser’s console to see what is printed out as the ball hits the wall.

A clear observation from the logged output shows that some of the distances displayed are greater than zero. Yes that is because the two objects might be very close to be taken that there is contact but there is still a very tiny distance between them. We will filter off all such cases, that is, every contacts point whose distance is greater than zero.

Inside detectCollision(), just above the line that logs to the console, add

if( distance > 0.0 ) continue;

Preview once more in the browser and notice that we now see only logs with distance less than or equal to zero.

Your code should be looking like this.

Our next step is to identify the participating objects and retrieve information from their contact points. Before we do that, permit me to make some explanations.

In this article and the two previous ones, we have been making use of the userData property of three.js objects. This makes it possible for a user to add additional properties to a three.js object to be retrieved or used later. We’ve been using this to store a reference to ammo.js objects in their respective three.js objects;

ball.userData.physicsBody = ballPhysicsBody;

With this we can retrieve the physics object later as is done in updatePhysics() .

Ammo.js on the other hand, deriving from its parent project bullet, also has a way to add the reference of a user object to a physics object. That mean we can also have our physics objects bear reference to their corresponding three.js object. This is achieved using the setUserPointer() and getUserPointer() methods of btCollisionObject ( parent class of btRigidBody ). With this we are able to retrieve our three.js objects if we have the physics objects at hand.

That is how it is supposed to be. However we will leverage on the nature of javascript by adding our three.js object directly as a property of the physics objects without setUserPointer() and retrieving them just as easy. If you don’t understand any of this just continue with the tutorial, you won’t miss out on anything.

Back to work.

Remember that now we are to identify the participating objects and to retrieve information from their contact points. What we’ll do is

  • Set the three.js objects as property of their corresponding physics objects.
  • In detectCollision(), for each contact manifold retrieve the two participating physics objects and their associated three.js object.
  • For each contact point of the contact manifold get the velocity of the participating objects as well as their contact position.
  • Finally log these information to the console.

Over to the code. Proceed to createWall() and add the below line of code as its last line

body.threeObject = wall;

Also in createBall() add the below code before the return statement

body.threeObject = ball;

This sets the reference of our three.js objects as properties of their corresponding physics objects.

Now move to detectCollision(), immediately after the line that has

let contactManifold = dispatcher.getManifoldByIndexInternal( i );

add

let rb0 = Ammo.castObject( contactManifold.getBody0(), Ammo.btRigidBody );
let rb1 = Ammo.castObject( contactManifold.getBody1(), Ammo.btRigidBody );
let threeObject0 = rb0.threeObject;
let threeObject1 = rb1.threeObject;
if ( ! threeObject0 && ! threeObject1 ) continue;let userData0 = threeObject0 ? threeObject0.userData : null;
let userData1 = threeObject1 ? threeObject1.userData : null;
let tag0 = userData0 ? userData0.tag : "none";
let tag1 = userData1 ? userData1.tag : "none";

In the above code snippet, the physics objects are retrieved using the getBody0() and getBody1() after which their corresponding three.js object are obtained. For these three.js object we retrieve their tag value. I would like to point out that both getBody0() and getBody1() return a btCollisionObject hence the reason why we cast them to btRigidBody.

Still in detectCollision(), after the line of code that has

if( distance > 0.0 ) continue;

add

let velocity0 = rb0.getLinearVelocity();
let velocity1 = rb1.getLinearVelocity();
let worldPos0 = contactPoint.get_m_positionWorldOnA();
let worldPos1 = contactPoint.get_m_positionWorldOnB();
let localPos0 = contactPoint.get_m_localPointA();
let localPos1 = contactPoint.get_m_localPointB();

Finally, replace the console log line which is

console.log({manifoldIndex: i, contactIndex: j, distance: distance});

with

console.log({
manifoldIndex: i,
contactIndex: j,
distance: distance,
object0:{
tag: tag0,
velocity: {x: velocity0.x(), y: velocity0.y(), z: velocity0.z()},
worldPos: {x: worldPos0.x(), y: worldPos0.y(), z: worldPos0.z()},
localPos: {x: localPos0.x(), y: localPos0.y(), z: localPos0.z()}
},
object1:{
tag: tag1,
velocity: {x: velocity1.x(), y: velocity1.y(), z: velocity1.z()},
worldPos: {x: worldPos1.x(), y: worldPos1.y(), z: worldPos1.z()},
localPos: {x: localPos1.x(), y: localPos1.y(), z: localPos1.z()}
}
});

Your work should look like this

Pause for a bit of reflection. The code explains itself pretty well.

View what you’ve done so far in the browser, shoot a ball against the wall and note the logged information. You should now be able to see further details of the collision such as the two objects involved, their collision points and their velocities.

That is it for contact manifold check. Its now up to you to add further implementations to suit your need.

ContactTest

Ammo.js lets you “perform an instant query on the world (btCollisionWorld or btDiscreteDynamicsWorld) using the contactTest query. The contactTest query will perform a collision test against all overlapping objects in the world, and produces the results using a callback. The query object doesn’t need to be part of the world”.

Basically, with contactTest you can just check if a particular physics object, (query object or target object) is colliding or making contact with any other object in the world. Being a method of the physics world, contactTest takes as its parameters the query object and a callback object to handle contact results.

An advantage of this method is that you can perform collision tests at a reduced temporal resolution if you do not need collision tests at every physics tic”. For example you might want to check for collision only on mouse click or on key down. Perhaps in your game you don’t want the character to jump when airborne, so whenever the jump key is pressed contactTest is invoked to check if the character is in contact with any physics object.

However, a downside is that collision detection is being duplicated for the target object (if it already exists in the world), so frequent or widespread collision tests may become less efficient than iterating over previously generated collision pairs”. Nothing stops you from calling contactTest after each simulation step or even multiple times withing a second. But if the query object is part of the world and you want to check for contact or collision every simulation step, then you are better off using contact manifold check.

ConcreteContactResultCallback

As was mentioned earlier, contactTest takes a callback object as part of its parameters to handle contact results. This callback object in ammo.js is ConcreteContactResultCallback. To use it you would have to add an implementation for its method addSingleResult() method which is what gets called when there is contact.

Let’s workout an example.

Create a new file named contact_test.html then copy and paste the below code:

Previewing the file in the browser would show something like

What we have here is a floor made up of four colored tiles and a ball that can be moved around using the WASD keys. Be careful not to fall off the edge.

Going through the code, you will notice that the physics objects already have a reference to their three.js counterparts added as threeObject property. You will also notice that we added a key down handler for T-key that calls the checkContact() which for now does nothing.

Our target is to identify the tile currently underneath the ball together with its local and world positions. This might sound very simple but it’s enough to demonstrate the basics of contactTest. Don’t forget to open up your browser’s console, we will be displaying information there.

First let’s create our ConcreteContactResultCallback object and add an implementation for its addSingleResult() method. Head to the variable declaration section at the top of the code and add

let cbContactResult;

This will be the handle to our ConcreteContactResultCallback object.

Next. Before moveBall() definition add

Pause. Let’s make some explanations. Note our implementation of addSingleResult() above. The required method signature is

float addSingleResult( 
[Ref] btManifoldPoint cp,
[Const] btCollisionObjectWrapper colObj0Wrap,
long partId0,
long index0,
[Const] btCollisionObjectWrapper colObj1Wrap,
long partId1,
long index1 )

For each contact made, addSingleResult() will be called by the physics world with values for the parameters (the return value is insignificant). cp is a handle to the contact point, while colObj0Wrap and colObj1Wrap are handles to collision object wrappers from which we can retrieve the participating objects. Due to ammo.js being ported from bullet with the help of emscripten, these values which are supposed to be ammo.js objects are actually passed as what we can fairly refer to as pointers. In-order to get the real values we would have to employ the wrapPointer() method that comes with ammo.js ( you can read more about it here ).

The remaining parameters partId0, index0, partId1 and index1 have no real use for us. To be honest, the closest I know of their use is for something around “per-triangle material / custom material combiner” and I’m very sure we wouldn’t be needing that.

Our implementation of addSingleResult() is straight forward and understandable:

  • Get distance from the contact point and exit if the distance is greater than zero.
  • Obtain the participating physics objects.
  • From them get their respective three.js objects.
  • Bearing in mind we are just after the tiles, we check for the three.js object that is not the ball and assign variables appropriately.
  • Finally, with some formatting, we log the information to the console.

It’s not yet preview time. Go to start() method, after the call to createBall() add:

setupContactResultCallback();

Move over to checkContact() and add

physicsWorld.contactTest( ball.userData.physicsBody , cbContactResult );

This calls the contactTest() of the physics world to perform collision detection against the ball’s physics object. Any result will be passed to addSingleResult() of cbContactResult.

With all said and done, you should have a code similar to this .

Preview in browser, move the ball around, press T-key and observe the information that is displayed on the console.

That is all you need to know about contactTest, take your time to go through what we’ve done. As always, its left for you to implement whatever you want, what we’ve done is to show you how.

Remember that your query object ( in our case the ball ) does not have to be part of the physics world for contactTest to work. However you'd have to update the transform yourself to get accurate results.

ContactPairTest

This is similar to contactTest except that you will have to supply two physics objects to check for contact between them. The parameters of contactPairTest are the two collision objects and a callback object to handle contact results.

As with contactTest, the two physics objects do not need to be part of the physics world.

To demonstrate contact pair test, we will continue with our code from contactTest.

Our goal is for the ball to jump when J-key is pressed, but only on the red tile. To achieve this, in the jump function, we'd first query the physics world for contact specifically between the ball and the red tile. If any exists, we’ll make the ball jump otherwise nothing happens.

Return back to our code. Right up at the variable declaration section below the line that has let cbContactResult; add

let redTile, cbContactPairResult;

redTile will be the handle to the red tile while cbContactPairResult just like cbContactResult will be our callback object for contactPairTest.

To set the handle of the red tile, we would make some not so clean but pardonable modifications. Move to createFloorTiles() method, at the last line of the for..of loop add

if( tile.name == "red"){  mesh.userData.physicsBody = body;
redTile = mesh;
}

Next, let’s create our callback. After setupContactResultCallback() definition copy and paste

All we want is for addSingleResult() to set the value of hasContact whenever there is contact. Please note that hasContact does not exist in ConcreteContactResultCallback we just added it ourselves as a property.

Before moving to the next modification add a call to setupContactPairResultCallback() inside start() just after setupContactResultCallback().

Now to the jump function. Paste the below code before updatePhysics() definition

In the method above, we first reset the hasContact property of cbContactPairResult to false then called the physics world’s contactPairTest(). The rest of the code speaks for itself.

Lastly we need to modify the key-down handler to call jump() when J-key is pressed. Add the below as a new case entry to the switch..case structure of handleKeyDown(), preferably make it the last

case 74://J
jump();
break;

handleKeyDown() should now look like

Your final code is supposed to look like this.

Preview the work in a browser, move the ball around and press the J-key to jump. Notice that the ball only jumps when it’s on the red tile.

There you have it; an implementation of contactPairTest.

Conclusion

Wow!! What a ride.

Feel free to modify the codes, explore and experiment as you like. Do not forget to consult ammo.idl, included in ammo.js repo, to know more about the classes and interfaces provided by ammo.js .

If there is any mistake do let me know, I will be delighted to make corrections. That is all for now and I hope it helped.

Cheers.

--

--

Blue Magnificent
Blue Magnificent

Written by Blue Magnificent

A Software Engineer, PolyMath, with strong passion for CGI/3D

Responses (4)