I was almost obsessed with converting Box2D-Lite into Javascript. It's been over a year since I completed the project, and I had not touched the project until now. However, the urge to tinker with physics engines re-emerged(!) such is the "addictiveness" of playing with physics engines. So I decided to brush away the cobwebs and see if I can add new functionalities to Box2D-Lite by looking at the full version of Box2D, to come up with Box2D-Lite kai!
Note there are complete ports of Box2D in Javascript, so this project is just for fun!
What's are the differences between Box2D-Lite and Box2D?
In Box2D-Lite, collision detection and contact point generation were carried out by (i) separating axis theorem and (ii) clipping method respectively.
The implementation was focused on handling collision between boxes. On the other hand, the "full" version Box2D can deal with the following geometries:
Polygon (including rectangles)
Circles
Edge (i.e. line segment)
Chain (series of edges)
In addition, whereas Box2D-Lite came with the revolute joint (point-to-point) constraint, full version comes equipped with numerous other types of joints including the following:
Distant joint
Gear joint
Motor joint
Pully joint
Weld joint
Wheel joint
As well these obvious differences there are many other more subtle differences including:
Ability to attach multiple "fixtures" (i.e. geometries) to a single "body" (in Box2D-Lite, there is no distinction between a body and a fixture - there is only "box").
The Revolute joint has many more functionality, such as limits, and motor
Block Solver to solve two contact constraints simultaneously.
Broad-phase collision detection (dynamic AABB tree)
Position correction based on NGS, as opposed to the simple Baumgarte stabilization.
Islands
Continuous collision detection
Where to start?
The first thing I did before implementing new functionality was to make the following key changes to the existing implementation of Box2D-Lite, to make the addition of new functionality a bit easier.
Introduce a new object called Transform, which is a combination of world position Vec2 and world rotation matrix. This makes the transformation of bodies easier to implement.
Separate the "body" from its geometry
Transform Object
In the original Lite implementation, the Body class (which also contained the geometry - ie box - properties) held its position in world space via 2 properties:
this.position - a Vec2 to define the linear position in world coordinates, and
this.rotation - a number to hold the orientation of the body in radians
To move a body required you to manipulate the linear and angular components separately.
Transform object combines the above two properties and comes with it a bunch of methods for transforming from one space to another.
The constructor of the class is shown below.
class Transform {
/// Initialize using a position vector and a rotation.
constructor(position = new Vec2(), angle = 0) {
this.p = position; // vector2
this.q = new Rot(angle); // Rot
}
Instead of the actual angle, the Transform object holds the sine and cosine of the angle as another object called Rot. The object together with some of its key methods are shown below.
class Rot {
constructor(angle=-0) {
this.s = Math.sin(angle);
this.c = Math.cos(angle);
}
set(angle) {
this.s = Math.sin(angle);
this.c = Math.cos(angle);
}
/// Set to the identity rotation
setIdentity() {
this.s = 0.0;
this.c = 1.0;
}
/// Get the angle in radians
getAngle() {
return Math.atan2(this.s, this.c);
}
Using Transform object to map between spaces
Functions are created to "apply" a transform to the linear position or orientation, in order to make mapping between different spaces easier.
For example, you can "multiply" a Vec2 by a Transform object, to map from one space to another, using the following function.
function b2Mul(a, b, out) {
if (arguments[0] instanceof Transform && arguments[1] instanceof Vec2) {
if (out === undefined) out = new Vec2();
out.x = (a.q.c * b.x - a.q.s * b.y) + a.p.x;
out.y = (a.q.s * b.x + a.q.c * b.y) + a.p.y;
return out;
}
Separating the geometry from the body
Box2D-Lite implementation had the Body class which contained the position of the body (ie the translation and angle in world space), as well as the size of the box and mass information. This class has now been split into two separate classes:
Body class (essentially a point), and
Polygon class to hold the information relating to the box (in fact, Box2D has a base Fixture class, which in turn holds information on the geometry, in a Shape class).
Unlike before, to instantiate a game object, we now need to:
instantiate a body,
instantiate a polygon,
add the polygon to the body,
finally add the body to the world
like so:
const b1 = new Body(0,4,0);
const s1 = new Polygon(1, 1, 200);
b1.createFixture(s1);
world.addBody(b1);
This is of course, totally unnecessary when dealing with one geometry. This is preparation for when we come to add other geometries.
BoxShape ( PolygonShape ) classes
We will implement a BoxShape class (no such thing in the full Box2D) to start with, and expand it to handle polygons later.
Whereas in the Lite version, the shape property was just the width and height, in this version we will hold the 4 vertices (defined in local space) as well as the normals of the 4 faces, which point outwards (again in local space).
We will not develop this class too much since this is just to get us started. The constructor function are as follows.
this.radius may seem curious for a box class. This radius creates a skin around the polygon. According to the official documentation, the skin is used in stacking scenarios to keep polygons slightly separated. This allows continuous collision to work against the core polygon.
The polygon skin helps prevent tunneling by keeping the polygons separated. This results in small gaps between the shapes. This will become clearer when we come to look at the collision detection code.
class b2PolygonShape extends b2Shape {
constructor() {
super();
this.m_type = b2Shape.e_polygon;
this.m_vertices =[];
this.m_normals = [];
this.m_radius = b2_polygonRadius;
this.m_count;
this.m_centroid = new b2Vec2();
this.density = 1;
}
////
}
and the method to create a box is as follows.
/// Build vertices to represent an oriented box.
/// @param hx the half-width.
/// @param hy the half-height.
/// @param center the center of the box in local coordinates.
/// @param angle the rotation of the box in local coordinates.
setAsBox( hx, hy, center = new b2Vec2(), angle = 0) {
b2Assert(IsValid(hx) && hx > 0.0);
b2Assert(IsValid(hy) && hy > 0.0);
this.width = hx * 2;
this.height = hy * 2;
this.m_count = 4;
this.m_vertices[0] = new b2Vec2(-hx, -hy);
this.m_vertices[1] = new b2Vec2( hx, -hy);
this.m_vertices[2] = new b2Vec2( hx, hy);
this.m_vertices[3] = new b2Vec2(-hx, hy);
this.m_normals[0] = new b2Vec2(0.0, -1.0);
this.m_normals[1] = new b2Vec2(1.0, 0.0);
this.m_normals[2] = new b2Vec2(0.0, 1.0);
this.m_normals[3] = new b2Vec2(-1.0, 0.0);
this.m_centroid = center;
let xf = new b2Transform();
xf.p = center;
xf.q.set(angle);
// Transform vertices and normals.
for (let i = 0; i < this.m_count; i++) {
this.m_vertices[i] = b2Mul(xf, this.m_vertices[i]);
this.m_normals[i] = b2Mul(xf.q, this.m_normals[i]);
}
return this;
}
Base Class for Geometries
In Box2D, there is something called the Fixture class. Fixture is actually the object that is used to hold the material properties (such as density and friction) and the geometry itself.
There is also a similarly named class called FixtureDef. FixtureDef is basically a repository for holding all properties of a Fixture. It is used to pass all the properties required to define a fixture in one object. It also makes manipulating the properties of the Fixture cleaner as you do not need to directly amend the properties of the Fixture object; rather you set up a FixtureDef as you like, then pass it along to createFixture function, where the properties are copied to a clean fresh Fixture object.
I pondered sometime as to whether I should replicate these classes in my Javascript experiment. In the end, I decided not to implement these, and hold the material properties and the geometry in a base class called Shape. There are certainly good reasons for having these additional classes, but I felt (at least for now) that these classes would make my things too complicated for me - perhaps once I manage to get the functions of Box2D-Lite going in this new version, I shall ponder some more.
The constructor function for the base Shape class is shown below.
class b2Shape {
constructor(shapeDef = {}) {
this.density = shapeDef.density || 1;
this.friction = shapeDef.friction || 0.2;
this.restitution = shapeDef.restitution || 0;
this.next = null;
this.body = null;
}
Computing the mass
With a shape, we need a method to calculate the mass. In the case of a box, this is simple.
A massdata object is used to "transport" mass and ineria data as one object between classes.
// calculate the mass data and store in the massData object
computeMass(massData) {
console.log("POLYGONSHAPE COMPUTEMASS CALLED!")
massData.mass = this.width * this.height * this.density;
massData.I = massData.mass * (this.width * this.width + this.height * this.height) / 12;
}
Body Class
In Lite, there were two types of bodies: (i) static body, as defined with mass of zero, and (ii) dynamic bodies.
In Box2D, there are 3 different body types. The extra body type is kinematicBody. In short kinematic bodies differ from dynamic bodies in the way they are handled by the world.step function. Specifically kinematic bodies move only based on its previous velocity and not affected by gravity, applied forces / impulses, masses, damping and the restitution values of the fixture when they experience collision. They are useful for simulating things like moving platforms in a platform game.
const BodyType = {
staticBody: 0,
kinematicBody: 1,
dynamicBody: 2
};
The constructor for the new body class is as follows. The basic properties should be familiar.
class b2Body {
constructor( x, y, r = 0, type = BodyType.staticBody) {
this.xf = new b2Transform(new b2Vec2(x,y), r)
this.type = type;
this.linearVelocity = new b2Vec2();
this.angularVelocity = 0;
this.force = new b2Vec2();/** The current force */
this.torque = 0;/** The current torque */
this.mass = 0.0;
this.invMass = 0.0;
this.I = 0.0;
this.invI = 0.0;
this.localCenter = new b2Vec2();
this.fixtureList = null;
this.fixtureCount = 0;
this.bodyId;
// WORKING VARIABLES
this.assData = new b2MassData(); // working variable used to calculate mass of each fixture
}
Adding Fixtures to Body
Also, as outlined earlier, Box2D allows you one to add more than one fixture to a single body, which makes the code considerably more complex.
The actual code of the method (of the Body class) to add a fixture to a body is shown below. Fixtures are added to the body in a linked list (I guess you could use an array in JavaScript but I tried to keep the code structure similar to the original).
createFixture(fixture) {
fixture.body = this;
fixture.next = this.fixtureList;
this.fixtureList = fixture;
++this.fixtureCount;
if (this.type == BodyType.staticBody || this.type == BodyType.kinematicBody) {
return
}
if (fixture.density) {
this.resetMass();
}
}
Computing the body mass
The createFixture method calls the computeMass method. This method loops through all the fixtures attached to the body, adds them up to arrive at the total mass for the body. The tricky part of having multiple bodies (particularly if you start attaching non-symmetric polygons) is that the center of mas of the body may not be zero (in local space). In Lite, because we were only ever dealing with one box, the center of mass in local space was always zero and the world space of the body's center of mass was always this.position.
The overall center of mass of the body is held in a new property called this.localCenter. This is necessary because so many of the dynamics calculations are done with respect to the center of mass. For example, in order to calculate the "arm" from the contact point in the collision response function, in Lite, this was simply the contact point in world space minus the body's position in world space. However, the center of mass of the body is not the same as this.position of the body. For this reason, Box2D stores the contact point in local space, and the center of mass of the body is also stored in local space, separately from this.position. Hence the arm is calculated in a different way, as we will see later.
Finally, if a body is experiencing angular motion, the linear velocity varies depending on which part of the body you are talking about. As a result of attaching a fixture, if the center of mass is changed, then the linear velocity might need to be adjusted.
resetMass() {
this.mass = 0.0;
this.invMass = 0.0;
this.I = 0.0;
this.invI = 0.0;
this.localCenter.setZero();
// Static and kinematic bodies have zero mass.
if (this.type === BodyType.staticBody || this.type === BodyType.kinematicBody) {
return
}
b2Assert(this.type = BodyType.dynamicBody);
// accumulate mass over all shapes
for (let f = this.fixtureList; f; f = f.next) {
if (f.density === 0) {
continue;
}
f.computeMass(this.assData);
this.mass += this.assData.mass;
b2Vec2.AddScaled(this.localCenter, this.assData.center, this.assData.mass, this.localCenter)
this.I +=this.assData.I;
}
// compute center of mass
if (this.mass > 0) {
this.invMass = 1 / this.mass;
b2Vec2.Scale(this.localCenter, this.invMass, this.localCenter)
}
if (this.I > 0) {
this.I -= this.mass * this.localCenter.dot(this.localCenter)
b2Assert(this.I > 0);
this.invI = 1 / this.I;
}
else {
this.I = 0;
this.invI = 0;
}
// move the position to line-up with center of mass
this.position = b2Mul(this.xf, this.localCenter)
}
Sample Code
Yes, the sample code just shows a falling box with no collision detection.
The new box shape structure requires a complete rewrite of the collision detection and collision response methods...so it might be a while before the next post!
Comments