Remember, the contact manager takes care of coming up with the contact information as a b2Contact object (or null if there is no contact).
Having obtained the contact information, the contact constraint equation must be created and solve. These are the responsbilities for the ContactSolver class, which, like the ContactManager must be instantiated by the world at the start.
(A very) High level overview of Box2D b2ContactSolver class
After the contact points have been identified, the creation of the contact constraints and the solving of those constraints is handled by the ContactSolver class.
I have set out below what I understand to be some of the key properties of this class, together with the various objects that are critical to Box2D contact constraint resolution process.
m_count: the number of contacts
m_contacts: array of all the contacts, in the form of b2Contact object. This is essentially the basic building block of the contact graph
The key properties of b2Contact are:
m_fixtureA, m_fixtureB
m_indexA, m_indexB
m_friction, m_restitution
m_manifold (b2Manifold object). This is the manifold for two touching convex shapes. Much of the information is in the local space of the fixtures:
points: an array of (up to 2) b2ManifoldPoint objects. b2ManifoldPoint is holds the most basic information about a contact point:
localPoint
normalImpulse, tangentImpulse
id: b2ContactID - to uniquely identify a contact point between two shapes
localNormal
localPoint
b2Contact also houses some useful methods for manipulating the more basic contact point information:
Update(): // Update the contact manifold and touching status
m_velocityConstraints: this is an array of b2ContactVelocityConstraint objects, which in turn is populated with, inter alia, the following data:
points: array (up to 2) of b2VelocityConstraintPoint objects:
rA, rB,
normalImpulse, tangentImpulse
normalMass, tangentMass
velocityBias
normal: collision normal
normalMass,
indexA, indexB (references to the bodies)
invMassA, invMassB
invIA, invIB
friction, resitution
pointCount
Upon receiving the contacts data from the broadphase / narrowphase collision detection, the following methods are key:
InitializeVelocityConstraints(): this loops through all the contacts, initializing the velocity constraint equations (vc = b2ContactVelocityConstraint objects, as well as the contact points, vcp = b2VelocityConstraintPoint - up to 2 for each vc ).
Because the contact point information is held in local space, as part of this initialization process, another object b2WorldManifold, is created to hold the contact point information in world space:
b2WorldManifold worldManifold; worldManifold.Initialize(manifold, xfA, radiusA, xfB, radiusB);
the familiar stuff of setting up the velocity constraint equation are carried out here, including:
vcp->rA = worldManifold.points[j] - cA;
vcp->rB = worldManifold.points[j] - cB;
vcp->normalMass = kNormal > 0.0f ? 1.0f / kNormal : 0.0f;
vcp->tangentMass = kTangent > 0.0f ? 1.0f / kTangent : 0.0f;
vcp->velocityBias = -vc->restitution * vRel;
WarmStart(): this does exactly what it says. if there are persistent contacts from the previous frame, the carried over accumulated impulse is applied to the velocity
SolveVelocityConstraints(): this loops through all the contacts and the asscoiated velocity constraint equations (b2ContactVelocityConstraint objects) and solves both the normal and tangent portions
StoreImpulses(): the
(The ContactSolver class also has properties and methods for correcting positional errors - I will not delve into that here).
By examining the above, you can begin to understand the complexity of Box2D implementation.
Contact Solver class
Let:s look at the contact solver class itself.
class b2ContactSolver {
constructor() {
this.m_contacts = new Map();
}
}
ContactSolver: intiailize method
We have delved into this in quite some eth when translating the Lite implementation. There are important differences.
initializeConstraints(timeStep) {
// perform pre-steps
for (const c of this.m_contacts.values()) {
const inv_dt = timeStep.invdt;
const allowedPenetration = 0.01;
const biasFactor = 0.2;
const mA = c.m_fixtureA.invMass;
const mB = c.m_fixtureB.invMass;
const iA = c.m_fixtureA.invI;
const iB = c.m_fixtureB.invI;
const cA = c.m_fixtureA.m_body.position;
const cB = c.m_fixtureB.m_body.position;
const pointCount = c.m_pointCount;
for (let i = 0; i < pointCount; i++) {
const vc = c.constraints[i];
const vcp = c.m_worldManifold.points[i]; // contact point in world space
const vcps = c.m_worldManifold.separation[i]; // separation between fixtures at contact point
const vcpn = c.m_worldManifold.normal; // collision normal of contact
Vec2.Subtract(vcp, cA, vc.rA); // radii from COM to contact
Vec2.Subtract(vcp, cB, vc.rB); // radii from COM to contact
// Precompute normal mass, tangent mass, and bias.
let rnA = b2Cross(vc.rA, vcpn);
let rnB = b2Cross(vc.rB, vcpn);
let kNormal = mA + mB + iA rnA rnA + iB rnB rnB;
vc.normalMass = kNormal > 0 ? 1 / kNormal : 0;
let tangent = b2Cross(vcpn, 1); // vector2
let rtA = b2Cross(vc.rA, tangent);
let rtB = b2Cross(vc.rB, tangent);
let kTangent = mA + mB + iA rtA rtA + iB rtB rtB;
vc.tangentMass = kTangent > 0 ? 1 / kTangent : 0;
vc.velocityBias = -biasFactor inv_dt Math.min(0, vcps + allowedPenetration);
}
}
}
warmStart() {
// Warm start.
for (const c of this.m_contacts.values()) {
const pointCount = c.m_pointCount;
const bodyA = c.m_fixtureA.m_body;
const bodyB = c.m_fixtureB.m_body;
for (let i = 0; i < pointCount; i++) {
const vc = c.constraints[i];
const vcpn = c.m_worldManifold.normal; // collision normal of contact
let tangent = b2Cross(vcpn, 1); // vector2
// Apply normal + friction impulse
let P = vcpn.clone().scale(vc.normalImpulse).addScaled(tangent, vc.tangentImpulse);
b2Constraint.applyImpulse(P, bodyA, bodyB, vc.rA, vc.rB);
}
}
}
ContactSolver: solve method
solve() {
// perform iterations
for (const c of this.m_contacts.values()) {
const pointCount = c.m_pointCount;
const bodyA = c.m_fixtureA.m_body;
const bodyB = c.m_fixtureB.m_body;
// Solve tangent constraints first because non-penetration is more important than friction.
for (let i = 0; i < pointCount; i++) {
const vc = c.constraints[i];
const normal = c.m_worldManifold.normal; // collision normal of contact
var tangent = b2Cross(normal, 1.0);
// Relative velocity at contact
let dv = Constraint.getRelativeVelocity(bodyA, bodyB, vc.rA, vc.rB);
// Compute tangent force
var vt = dv.dot(tangent);
var lambda = vc.tangentMass (-vt);
// Clamp friction
var maxFriction = c.m_friction vc.normalImpulse;
let newImpulse = Phaser.Math.Clamp(vc.tangentImpulse + lambda, -maxFriction, maxFriction);
lambda = newImpulse - vc.tangentImpulse;
vc.tangentImpulse = newImpulse;
// Apply contact impulse
let P = Vec2.Scale(tangent, lambda) ;
Constraint.applyImpulse(P, bodyA, bodyB, vc.rA, vc.rB)
}
// Solve normal constraints
for (let i = 0; i < c.m_pointCount; i++) {
const vc = c.constraints[i];
const normal = c.m_worldManifold.normal; // collision normal of contact
var tangent = b2Cross(normal, 1.0);
// Relative velocity at contact
let dv = Constraint.getRelativeVelocity(bodyA, bodyB, vc.rA, vc.rB);
// Compute normal impulse
var vn = dv.dot(normal);
let lambda = vc.normalMass * (-vn + vc.velocityBias); // incremental impulse
// Clamp the accumulated impulse
let newImpulse = Math.max(vc.normalImpulse + lambda, 0);
lambda = newImpulse - vc.normalImpulse;
vc.normalImpulse = newImpulse;
// Apply contact impulse
let P = Vec2.Scale(normal, lambda);
Constraint.applyImpulse(P, bodyA, bodyB, vc.rA, vc.rB);
}
}
}
class b2Constraint {
static getRelativeVelocity(bodyA, bodyB, rA, rB) {
return Vec2.Subtract(Vec2.Add(bodyB.linearVelocity, b2Cross(bodyB.angularVelocity, rB)), Vec2.Add(bodyA.linearVelocity, b2Cross(bodyA.angularVelocity, rA)))
}
static applyImpulse(impulse, bodyA, bodyB, rA, rB) {
const mA = bodyA.m_shape.invMass;
const mB = bodyB.m_shape.invMass;
const iA = bodyA.m_shape.invI;
const iB = bodyB.m_shape.invI;
bodyA.linearVelocity.subtractScaled(impulse, mA) ;
bodyA.angularVelocity -= iA b2Cross(rA, impulse);
bodyB.linearVelocity.addScaled(impulse, mB );
bodyB.angularVelocity += iB b2Cross(rB, impulse);
}
}
let newArb = this.m_contactManager.create(bodyA.getFixtureList(), bodyB.getFixtureList());
if (newArb) {
let oldArb = this.m_contactSolver.m_contacts.get(key);
if (oldArb) {
newArb.update(oldArb, timeStep);
}
this.m_contactSolver.m_contacts.set(key, newArb);
}
else {
this.m_contactSolver.m_contacts.delete(key);
}
which is called frmo the world.step method, in order to set up the contact constraint equations, and solve them.
step(dt) {
////////
// perform pre-steps
this.m_contactSolver.initializeConstraints(this.m_timeStep);
if (this.warmStarting) {
this.m_contactSolver.warmStart();
};
/////
this.m_contactSolver.solve();
Comments