planck-js
Version:
2D JavaScript physics engine for cross-platform HTML5 game development
1,230 lines (991 loc) • 34.5 kB
JavaScript
/*
* Planck.js
* The MIT License
* Copyright (c) 2021 Erin Catto, Ali Shakiba
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var _DEBUG = typeof DEBUG === 'undefined' ? false : DEBUG;
var _ASSERT = typeof ASSERT === 'undefined' ? false : ASSERT;
var DEBUG_SOLVER = false;
var common = require('./util/common');
var Math = require('./common/Math');
var Vec2 = require('./common/Vec2');
var Transform = require('./common/Transform');
var Mat22 = require('./common/Mat22');
var Rot = require('./common/Rot');
var Settings = require('./Settings');
var Manifold = require('./Manifold');
var Distance = require('./collision/Distance');
module.exports = Contact;
/**
* A contact edge is used to connect bodies and contacts together in a contact
* graph where each body is a node and each contact is an edge. A contact edge
* belongs to a doubly linked list maintained in each attached body. Each
* contact has two contact nodes, one for each attached body.
*
* @prop {Contact} contact The contact
* @prop {ContactEdge} prev The previous contact edge in the body's contact list
* @prop {ContactEdge} next The next contact edge in the body's contact list
* @prop {Body} other Provides quick access to the other body attached.
*/
function ContactEdge(contact) {
this.contact = contact;
this.prev;
this.next;
this.other;
};
/**
* @function Contact~evaluate
*
* @param manifold
* @param xfA
* @param fixtureA
* @param indexA
* @param xfB
* @param fixtureB
* @param indexB
*/
/**
* The class manages contact between two shapes. A contact exists for each
* overlapping AABB in the broad-phase (except if filtered). Therefore a contact
* object may exist that has no contact points.
*
* @param {Fixture} fA
* @param {int} indexA
* @param {Fixture} fB
* @param {int} indexB
* @param {Contact~evaluate} evaluateFcn
*/
function Contact(fA, indexA, fB, indexB, evaluateFcn) {
// Nodes for connecting bodies.
this.m_nodeA = new ContactEdge(this);
this.m_nodeB = new ContactEdge(this);
this.m_fixtureA = fA;
this.m_fixtureB = fB;
this.m_indexA = indexA;
this.m_indexB = indexB;
this.m_evaluateFcn = evaluateFcn;
this.m_manifold = new Manifold();
this.m_prev = null;
this.m_next = null;
this.m_toi = 1.0;
this.m_toiCount = 0;
// This contact has a valid TOI in m_toi
this.m_toiFlag = false;
this.m_friction = mixFriction(this.m_fixtureA.m_friction,
this.m_fixtureB.m_friction);
this.m_restitution = mixRestitution(this.m_fixtureA.m_restitution,
this.m_fixtureB.m_restitution);
this.m_tangentSpeed = 0.0;
// This contact can be disabled (by user)
this.m_enabledFlag = true;
// Used when crawling contact graph when forming islands.
this.m_islandFlag = false;
// Set when the shapes are touching.
this.m_touchingFlag = false;
// This contact needs filtering because a fixture filter was changed.
this.m_filterFlag = false;
// This bullet contact had a TOI event
this.m_bulletHitFlag = false;
this.v_points = []; // VelocityConstraintPoint[maxManifoldPoints]
this.v_normal = Vec2.zero();
this.v_normalMass = new Mat22();
this.v_K = new Mat22();
this.v_pointCount;
this.v_tangentSpeed;
this.v_friction;
this.v_restitution;
this.v_invMassA;
this.v_invMassB;
this.v_invIA;
this.v_invIB;
this.p_localPoints = [] // Vec2[maxManifoldPoints];
this.p_localNormal = Vec2.zero();
this.p_localPoint = Vec2.zero();
this.p_localCenterA = Vec2.zero();
this.p_localCenterB = Vec2.zero();
this.p_type; // Manifold.Type
this.p_radiusA;
this.p_radiusB;
this.p_pointCount;
this.p_invMassA;
this.p_invMassB;
this.p_invIA;
this.p_invIB;
}
Contact.prototype.initConstraint = function(step) {
var fixtureA = this.m_fixtureA;
var fixtureB = this.m_fixtureB;
var shapeA = fixtureA.getShape();
var shapeB = fixtureB.getShape();
var bodyA = fixtureA.getBody();
var bodyB = fixtureB.getBody();
var manifold = this.getManifold();
var pointCount = manifold.pointCount;
_ASSERT && common.assert(pointCount > 0);
this.v_invMassA = bodyA.m_invMass;
this.v_invMassB = bodyB.m_invMass;
this.v_invIA = bodyA.m_invI;
this.v_invIB = bodyB.m_invI;
this.v_friction = this.m_friction;
this.v_restitution = this.m_restitution;
this.v_tangentSpeed = this.m_tangentSpeed;
this.v_pointCount = pointCount;
this.v_K.setZero();
this.v_normalMass.setZero();
this.p_invMassA = bodyA.m_invMass;
this.p_invMassB = bodyB.m_invMass;
this.p_invIA = bodyA.m_invI;
this.p_invIB = bodyB.m_invI;
this.p_localCenterA = Vec2.clone(bodyA.m_sweep.localCenter);
this.p_localCenterB = Vec2.clone(bodyB.m_sweep.localCenter);
this.p_radiusA = shapeA.m_radius;
this.p_radiusB = shapeB.m_radius;
this.p_type = manifold.type;
this.p_localNormal = Vec2.clone(manifold.localNormal);
this.p_localPoint = Vec2.clone(manifold.localPoint);
this.p_pointCount = pointCount;
for (var j = 0; j < pointCount; ++j) {
var cp = manifold.points[j]; // ManifoldPoint
var vcp = this.v_points[j] = new VelocityConstraintPoint();
if (step.warmStarting) {
vcp.normalImpulse = step.dtRatio * cp.normalImpulse;
vcp.tangentImpulse = step.dtRatio * cp.tangentImpulse;
} else {
vcp.normalImpulse = 0.0;
vcp.tangentImpulse = 0.0;
}
vcp.rA.setZero();
vcp.rB.setZero();
vcp.normalMass = 0.0;
vcp.tangentMass = 0.0;
vcp.velocityBias = 0.0;
this.p_localPoints[j] = Vec2.clone(cp.localPoint);
}
};
/**
* Get the contact manifold. Do not modify the manifold unless you understand
* the internals of the library.
*/
Contact.prototype.getManifold = function() {
return this.m_manifold;
}
/**
* Get the world manifold.
*
* @param {WorldManifold} [worldManifold]
*/
Contact.prototype.getWorldManifold = function(worldManifold) {
var bodyA = this.m_fixtureA.getBody();
var bodyB = this.m_fixtureB.getBody();
var shapeA = this.m_fixtureA.getShape();
var shapeB = this.m_fixtureB.getShape();
return this.m_manifold.getWorldManifold(worldManifold, bodyA.getTransform(),
shapeA.m_radius, bodyB.getTransform(), shapeB.m_radius);
}
/**
* Enable/disable this contact. This can be used inside the pre-solve contact
* listener. The contact is only disabled for the current time step (or sub-step
* in continuous collisions).
*/
Contact.prototype.setEnabled = function(flag) {
this.m_enabledFlag = !!flag;
}
/**
* Has this contact been disabled?
*/
Contact.prototype.isEnabled = function() {
return this.m_enabledFlag;
}
/**
* Is this contact touching?
*/
Contact.prototype.isTouching = function() {
return this.m_touchingFlag;
}
/**
* Get the next contact in the world's contact list.
*/
Contact.prototype.getNext = function() {
return this.m_next;
}
/**
* Get fixture A in this contact.
*/
Contact.prototype.getFixtureA = function() {
return this.m_fixtureA;
}
/**
* Get fixture B in this contact.
*/
Contact.prototype.getFixtureB = function() {
return this.m_fixtureB;
}
/**
* Get the child primitive index for fixture A.
*/
Contact.prototype.getChildIndexA = function() {
return this.m_indexA;
}
/**
* Get the child primitive index for fixture B.
*/
Contact.prototype.getChildIndexB = function() {
return this.m_indexB;
}
/**
* Flag this contact for filtering. Filtering will occur the next time step.
*/
Contact.prototype.flagForFiltering = function() {
this.m_filterFlag = true;
}
/**
* Override the default friction mixture. You can call this in
* ContactListener.preSolve. This value persists until set or reset.
*/
Contact.prototype.setFriction = function(friction) {
this.m_friction = friction;
}
/**
* Get the friction.
*/
Contact.prototype.getFriction = function() {
return this.m_friction;
}
/**
* Reset the friction mixture to the default value.
*/
Contact.prototype.resetFriction = function() {
this.m_friction = mixFriction(this.m_fixtureA.m_friction,
this.m_fixtureB.m_friction);
}
/**
* Override the default restitution mixture. You can call this in
* ContactListener.preSolve. The value persists until you set or reset.
*/
Contact.prototype.setRestitution = function(restitution) {
this.m_restitution = restitution;
}
/**
* Get the restitution.
*/
Contact.prototype.getRestitution = function() {
return this.m_restitution;
}
/**
* Reset the restitution to the default value.
*/
Contact.prototype.resetRestitution = function() {
this.m_restitution = mixRestitution(this.m_fixtureA.m_restitution,
this.m_fixtureB.m_restitution);
}
/**
* Set the desired tangent speed for a conveyor belt behavior. In meters per
* second.
*/
Contact.prototype.setTangentSpeed = function(speed) {
this.m_tangentSpeed = speed;
}
/**
* Get the desired tangent speed. In meters per second.
*/
Contact.prototype.getTangentSpeed = function() {
return this.m_tangentSpeed;
}
/**
* Called by Update method, and implemented by subclasses.
*/
Contact.prototype.evaluate = function(manifold, xfA, xfB) {
this.m_evaluateFcn(manifold, xfA, this.m_fixtureA, this.m_indexA, xfB,
this.m_fixtureB, this.m_indexB);
};
/**
* Updates the contact manifold and touching status.
*
* Note: do not assume the fixture AABBs are overlapping or are valid.
*
* @param {function} listener.beginContact
* @param {function} listener.endContact
* @param {function} listener.preSolve
*/
Contact.prototype.update = function(listener) {
// Re-enable this contact.
this.m_enabledFlag = true;
var touching = false;
var wasTouching = this.m_touchingFlag;
var sensorA = this.m_fixtureA.isSensor();
var sensorB = this.m_fixtureB.isSensor();
var sensor = sensorA || sensorB;
var bodyA = this.m_fixtureA.getBody();
var bodyB = this.m_fixtureB.getBody();
var xfA = bodyA.getTransform();
var xfB = bodyB.getTransform();
// Is this contact a sensor?
if (sensor) {
var shapeA = this.m_fixtureA.getShape();
var shapeB = this.m_fixtureB.getShape();
touching = Distance.testOverlap(shapeA, this.m_indexA, shapeB,
this.m_indexB, xfA, xfB);
// Sensors don't generate manifolds.
this.m_manifold.pointCount = 0;
} else {
// TODO reuse manifold
var oldManifold = this.m_manifold;
this.m_manifold = new Manifold();
this.evaluate(this.m_manifold, xfA, xfB);
touching = this.m_manifold.pointCount > 0;
// Match old contact ids to new contact ids and copy the
// stored impulses to warm start the solver.
for (var i = 0; i < this.m_manifold.pointCount; ++i) {
var nmp = this.m_manifold.points[i];
nmp.normalImpulse = 0.0;
nmp.tangentImpulse = 0.0;
for (var j = 0; j < oldManifold.pointCount; ++j) {
var omp = oldManifold.points[j];
if (omp.id.key == nmp.id.key) { // ContactID.key
nmp.normalImpulse = omp.normalImpulse;
nmp.tangentImpulse = omp.tangentImpulse;
break;
}
}
}
if (touching != wasTouching) {
bodyA.setAwake(true);
bodyB.setAwake(true);
}
}
this.m_touchingFlag = touching;
if (wasTouching == false && touching == true && listener) {
listener.beginContact(this);
}
if (wasTouching == true && touching == false && listener) {
listener.endContact(this);
}
if (sensor == false && touching && listener) {
listener.preSolve(this, oldManifold);
}
}
Contact.prototype.solvePositionConstraint = function(step) {
return this._solvePositionConstraint(step, false);
}
Contact.prototype.solvePositionConstraintTOI = function(step, toiA, toiB) {
return this._solvePositionConstraint(step, true, toiA, toiB);
}
Contact.prototype._solvePositionConstraint = function(step, toi, toiA, toiB) {
var fixtureA = this.m_fixtureA;
var fixtureB = this.m_fixtureB;
var bodyA = fixtureA.getBody();
var bodyB = fixtureB.getBody();
var velocityA = bodyA.c_velocity;
var velocityB = bodyB.c_velocity;
var positionA = bodyA.c_position;
var positionB = bodyB.c_position;
var localCenterA = Vec2.clone(this.p_localCenterA);
var localCenterB = Vec2.clone(this.p_localCenterB);
var mA = 0.0;
var iA = 0.0;
if (!toi || (bodyA == toiA || bodyA == toiB)) {
mA = this.p_invMassA;
iA = this.p_invIA;
}
var mB = 0.0;
var iB = 0.0;
if (!toi || (bodyB == toiA || bodyB == toiB)) {
mB = this.p_invMassB;
iB = this.p_invIB;
}
var cA = Vec2.clone(positionA.c);
var aA = positionA.a;
var cB = Vec2.clone(positionB.c);
var aB = positionB.a;
var minSeparation = 0.0;
// Solve normal constraints
for (var j = 0; j < this.p_pointCount; ++j) {
var xfA = Transform.identity();
var xfB = Transform.identity();
xfA.q.set(aA);
xfB.q.set(aB);
xfA.p = Vec2.sub(cA, Rot.mulVec2(xfA.q, localCenterA));
xfB.p = Vec2.sub(cB, Rot.mulVec2(xfB.q, localCenterB));
// PositionSolverManifold
var normal, point, separation;
switch (this.p_type) {
case Manifold.e_circles:
var pointA = Transform.mulVec2(xfA, this.p_localPoint);
var pointB = Transform.mulVec2(xfB, this.p_localPoints[0]);
normal = Vec2.sub(pointB, pointA);
normal.normalize();
point = Vec2.combine(0.5, pointA, 0.5, pointB);
separation = Vec2.dot(Vec2.sub(pointB, pointA), normal) - this.p_radiusA
- this.p_radiusB;
break;
case Manifold.e_faceA:
normal = Rot.mulVec2(xfA.q, this.p_localNormal);
var planePoint = Transform.mulVec2(xfA, this.p_localPoint);
var clipPoint = Transform.mulVec2(xfB, this.p_localPoints[j]);
separation = Vec2.dot(Vec2.sub(clipPoint, planePoint), normal)
- this.p_radiusA - this.p_radiusB;
point = clipPoint;
break;
case Manifold.e_faceB:
normal = Rot.mulVec2(xfB.q, this.p_localNormal);
var planePoint = Transform.mulVec2(xfB, this.p_localPoint);
var clipPoint = Transform.mulVec2(xfA, this.p_localPoints[j]);
separation = Vec2.dot(Vec2.sub(clipPoint, planePoint), normal)
- this.p_radiusA - this.p_radiusB;
point = clipPoint;
// Ensure normal points from A to B
normal.mul(-1);
break;
}
var rA = Vec2.sub(point, cA);
var rB = Vec2.sub(point, cB);
// Track max constraint error.
minSeparation = Math.min(minSeparation, separation);
var baumgarte = toi ? Settings.toiBaugarte : Settings.baumgarte;
var linearSlop = Settings.linearSlop;
var maxLinearCorrection = Settings.maxLinearCorrection;
// Prevent large corrections and allow slop.
var C = Math.clamp(baumgarte * (separation + linearSlop),
-maxLinearCorrection, 0.0);
// Compute the effective mass.
var rnA = Vec2.cross(rA, normal);
var rnB = Vec2.cross(rB, normal);
var K = mA + mB + iA * rnA * rnA + iB * rnB * rnB;
// Compute normal impulse
var impulse = K > 0.0 ? -C / K : 0.0;
var P = Vec2.mul(impulse, normal);
cA.subMul(mA, P);
aA -= iA * Vec2.cross(rA, P);
cB.addMul(mB, P);
aB += iB * Vec2.cross(rB, P);
}
positionA.c.set(cA);
positionA.a = aA;
positionB.c.set(cB);
positionB.a = aB;
return minSeparation;
}
// TODO merge with ManifoldPoint
function VelocityConstraintPoint() {
this.rA = Vec2.zero();
this.rB = Vec2.zero();
this.normalImpulse = 0;
this.tangentImpulse = 0;
this.normalMass = 0;
this.tangentMass = 0;
this.velocityBias = 0;
}
Contact.prototype.initVelocityConstraint = function(step) {
var fixtureA = this.m_fixtureA;
var fixtureB = this.m_fixtureB;
var bodyA = fixtureA.getBody();
var bodyB = fixtureB.getBody();
var velocityA = bodyA.c_velocity;
var velocityB = bodyB.c_velocity;
var positionA = bodyA.c_position;
var positionB = bodyB.c_position;
var radiusA = this.p_radiusA;
var radiusB = this.p_radiusB;
var manifold = this.getManifold();
var mA = this.v_invMassA;
var mB = this.v_invMassB;
var iA = this.v_invIA;
var iB = this.v_invIB;
var localCenterA = Vec2.clone(this.p_localCenterA);
var localCenterB = Vec2.clone(this.p_localCenterB);
var cA = Vec2.clone(positionA.c);
var aA = positionA.a;
var vA = Vec2.clone(velocityA.v);
var wA = velocityA.w;
var cB = Vec2.clone(positionB.c);
var aB = positionB.a;
var vB = Vec2.clone(velocityB.v);
var wB = velocityB.w;
_ASSERT && common.assert(manifold.pointCount > 0);
var xfA = Transform.identity();
var xfB = Transform.identity();
xfA.q.set(aA);
xfB.q.set(aB);
xfA.p.setCombine(1, cA, -1, Rot.mulVec2(xfA.q, localCenterA));
xfB.p.setCombine(1, cB, -1, Rot.mulVec2(xfB.q, localCenterB));
var worldManifold = manifold.getWorldManifold(null, xfA, radiusA, xfB, radiusB);
this.v_normal.set(worldManifold.normal);
for (var j = 0; j < this.v_pointCount; ++j) {
var vcp = this.v_points[j]; // VelocityConstraintPoint
vcp.rA.set(Vec2.sub(worldManifold.points[j], cA));
vcp.rB.set(Vec2.sub(worldManifold.points[j], cB));
var rnA = Vec2.cross(vcp.rA, this.v_normal);
var rnB = Vec2.cross(vcp.rB, this.v_normal);
var kNormal = mA + mB + iA * rnA * rnA + iB * rnB * rnB;
vcp.normalMass = kNormal > 0.0 ? 1.0 / kNormal : 0.0;
var tangent = Vec2.cross(this.v_normal, 1.0);
var rtA = Vec2.cross(vcp.rA, tangent);
var rtB = Vec2.cross(vcp.rB, tangent);
var kTangent = mA + mB + iA * rtA * rtA + iB * rtB * rtB;
vcp.tangentMass = kTangent > 0.0 ? 1.0 / kTangent : 0.0;
// Setup a velocity bias for restitution.
vcp.velocityBias = 0.0;
var vRel = Vec2.dot(this.v_normal, vB)
+ Vec2.dot(this.v_normal, Vec2.cross(wB, vcp.rB))
- Vec2.dot(this.v_normal, vA)
- Vec2.dot(this.v_normal, Vec2.cross(wA, vcp.rA));
if (vRel < -Settings.velocityThreshold) {
vcp.velocityBias = -this.v_restitution * vRel;
}
}
// If we have two points, then prepare the block solver.
if (this.v_pointCount == 2 && step.blockSolve) {
var vcp1 = this.v_points[0]; // VelocityConstraintPoint
var vcp2 = this.v_points[1]; // VelocityConstraintPoint
var rn1A = Vec2.cross(vcp1.rA, this.v_normal);
var rn1B = Vec2.cross(vcp1.rB, this.v_normal);
var rn2A = Vec2.cross(vcp2.rA, this.v_normal);
var rn2B = Vec2.cross(vcp2.rB, this.v_normal);
var k11 = mA + mB + iA * rn1A * rn1A + iB * rn1B * rn1B;
var k22 = mA + mB + iA * rn2A * rn2A + iB * rn2B * rn2B;
var k12 = mA + mB + iA * rn1A * rn2A + iB * rn1B * rn2B;
// Ensure a reasonable condition number.
var k_maxConditionNumber = 1000.0;
if (k11 * k11 < k_maxConditionNumber * (k11 * k22 - k12 * k12)) {
// K is safe to invert.
this.v_K.ex.set(k11, k12);
this.v_K.ey.set(k12, k22);
this.v_normalMass.set(this.v_K.getInverse());
} else {
// The constraints are redundant, just use one.
// TODO_ERIN use deepest?
this.v_pointCount = 1;
}
}
positionA.c.set(cA);
positionA.a = aA;
velocityA.v.set(vA);
velocityA.w = wA;
positionB.c.set(cB);
positionB.a = aB;
velocityB.v.set(vB);
velocityB.w = wB;
};
Contact.prototype.warmStartConstraint = function(step) {
var fixtureA = this.m_fixtureA;
var fixtureB = this.m_fixtureB;
var bodyA = fixtureA.getBody();
var bodyB = fixtureB.getBody();
var velocityA = bodyA.c_velocity;
var velocityB = bodyB.c_velocity;
var positionA = bodyA.c_position;
var positionB = bodyB.c_position;
var mA = this.v_invMassA;
var iA = this.v_invIA;
var mB = this.v_invMassB;
var iB = this.v_invIB;
var vA = Vec2.clone(velocityA.v);
var wA = velocityA.w;
var vB = Vec2.clone(velocityB.v);
var wB = velocityB.w;
var normal = this.v_normal;
var tangent = Vec2.cross(normal, 1.0);
for (var j = 0; j < this.v_pointCount; ++j) {
var vcp = this.v_points[j]; // VelocityConstraintPoint
var P = Vec2.combine(vcp.normalImpulse, normal, vcp.tangentImpulse, tangent);
wA -= iA * Vec2.cross(vcp.rA, P);
vA.subMul(mA, P);
wB += iB * Vec2.cross(vcp.rB, P);
vB.addMul(mB, P);
}
velocityA.v.set(vA);
velocityA.w = wA;
velocityB.v.set(vB);
velocityB.w = wB;
};
Contact.prototype.storeConstraintImpulses = function(step) {
var manifold = this.m_manifold;
for (var j = 0; j < this.v_pointCount; ++j) {
manifold.points[j].normalImpulse = this.v_points[j].normalImpulse;
manifold.points[j].tangentImpulse = this.v_points[j].tangentImpulse;
}
};
Contact.prototype.solveVelocityConstraint = function(step) {
var bodyA = this.m_fixtureA.m_body;
var bodyB = this.m_fixtureB.m_body;
var velocityA = bodyA.c_velocity;
var positionA = bodyA.c_position;
var velocityB = bodyB.c_velocity;
var positionB = bodyB.c_position;
var mA = this.v_invMassA;
var iA = this.v_invIA;
var mB = this.v_invMassB;
var iB = this.v_invIB;
var vA = Vec2.clone(velocityA.v);
var wA = velocityA.w;
var vB = Vec2.clone(velocityB.v);
var wB = velocityB.w;
var normal = this.v_normal;
var tangent = Vec2.cross(normal, 1.0);
var friction = this.v_friction;
_ASSERT && common.assert(this.v_pointCount == 1 || this.v_pointCount == 2);
// Solve tangent constraints first because non-penetration is more important
// than friction.
for (var j = 0; j < this.v_pointCount; ++j) {
var vcp = this.v_points[j]; // VelocityConstraintPoint
// Relative velocity at contact
var dv = Vec2.zero();
dv.addCombine(1, vB, 1, Vec2.cross(wB, vcp.rB));
dv.subCombine(1, vA, 1, Vec2.cross(wA, vcp.rA));
// Compute tangent force
var vt = Vec2.dot(dv, tangent) - this.v_tangentSpeed;
var lambda = vcp.tangentMass * (-vt);
// Clamp the accumulated force
var maxFriction = friction * vcp.normalImpulse;
var newImpulse = Math.clamp(vcp.tangentImpulse + lambda, -maxFriction,
maxFriction);
lambda = newImpulse - vcp.tangentImpulse;
vcp.tangentImpulse = newImpulse;
// Apply contact impulse
var P = Vec2.mul(lambda, tangent);
vA.subMul(mA, P);
wA -= iA * Vec2.cross(vcp.rA, P);
vB.addMul(mB, P);
wB += iB * Vec2.cross(vcp.rB, P);
}
// Solve normal constraints
if (this.v_pointCount == 1 || step.blockSolve == false) {
for (var i = 0; i < this.v_pointCount; ++i) {
var vcp = this.v_points[i]; // VelocityConstraintPoint
// Relative velocity at contact
var dv = Vec2.zero();
dv.addCombine(1, vB, 1, Vec2.cross(wB, vcp.rB));
dv.subCombine(1, vA, 1, Vec2.cross(wA, vcp.rA));
// Compute normal impulse
var vn = Vec2.dot(dv, normal);
var lambda = -vcp.normalMass * (vn - vcp.velocityBias);
// Clamp the accumulated impulse
var newImpulse = Math.max(vcp.normalImpulse + lambda, 0.0);
lambda = newImpulse - vcp.normalImpulse;
vcp.normalImpulse = newImpulse;
// Apply contact impulse
var P = Vec2.mul(lambda, normal);
vA.subMul(mA, P);
wA -= iA * Vec2.cross(vcp.rA, P);
vB.addMul(mB, P);
wB += iB * Vec2.cross(vcp.rB, P);
}
} else {
// Block solver developed in collaboration with Dirk Gregorius (back in
// 01/07 on Box2D_Lite).
// Build the mini LCP for this contact patch
//
// vn = A * x + b, vn >= 0, , vn >= 0, x >= 0 and vn_i * x_i = 0 with i =
// 1..2
//
// A = J * W * JT and J = ( -n, -r1 x n, n, r2 x n )
// b = vn0 - velocityBias
//
// The system is solved using the "Total enumeration method" (s. Murty).
// The complementary constraint vn_i * x_i
// implies that we must have in any solution either vn_i = 0 or x_i = 0.
// So for the 2D contact problem the cases
// vn1 = 0 and vn2 = 0, x1 = 0 and x2 = 0, x1 = 0 and vn2 = 0, x2 = 0 and
// vn1 = 0 need to be tested. The first valid
// solution that satisfies the problem is chosen.
//
// In order to account of the accumulated impulse 'a' (because of the
// iterative nature of the solver which only requires
// that the accumulated impulse is clamped and not the incremental
// impulse) we change the impulse variable (x_i).
//
// Substitute:
//
// x = a + d
//
// a := old total impulse
// x := new total impulse
// d := incremental impulse
//
// For the current iteration we extend the formula for the incremental
// impulse
// to compute the new total impulse:
//
// vn = A * d + b
// = A * (x - a) + b
// = A * x + b - A * a
// = A * x + b'
// b' = b - A * a;
var vcp1 = this.v_points[0]; // VelocityConstraintPoint
var vcp2 = this.v_points[1]; // VelocityConstraintPoint
var a = Vec2.neo(vcp1.normalImpulse, vcp2.normalImpulse);
_ASSERT && common.assert(a.x >= 0.0 && a.y >= 0.0);
// Relative velocity at contact
var dv1 = Vec2.zero().add(vB).add(Vec2.cross(wB, vcp1.rB)).sub(vA).sub(Vec2.cross(wA, vcp1.rA));
var dv2 = Vec2.zero().add(vB).add(Vec2.cross(wB, vcp2.rB)).sub(vA).sub(Vec2.cross(wA, vcp2.rA));
// Compute normal velocity
var vn1 = Vec2.dot(dv1, normal);
var vn2 = Vec2.dot(dv2, normal);
var b = Vec2.neo(vn1 - vcp1.velocityBias, vn2 - vcp2.velocityBias);
// Compute b'
b.sub(Mat22.mulVec2(this.v_K, a));
var k_errorTol = 1e-3;
// NOT_USED(k_errorTol);
for (;;) {
//
// Case 1: vn = 0
//
// 0 = A * x + b'
//
// Solve for x:
//
// x = - inv(A) * b'
//
var x = Mat22.mulVec2(this.v_normalMass, b).neg();
if (x.x >= 0.0 && x.y >= 0.0) {
// Get the incremental impulse
var d = Vec2.sub(x, a);
// Apply incremental impulse
var P1 = Vec2.mul(d.x, normal);
var P2 = Vec2.mul(d.y, normal);
vA.subCombine(mA, P1, mA, P2);
wA -= iA * (Vec2.cross(vcp1.rA, P1) + Vec2.cross(vcp2.rA, P2));
vB.addCombine(mB, P1, mB, P2);
wB += iB * (Vec2.cross(vcp1.rB, P1) + Vec2.cross(vcp2.rB, P2));
// Accumulate
vcp1.normalImpulse = x.x;
vcp2.normalImpulse = x.y;
if (DEBUG_SOLVER) {
// Postconditions
dv1 = vB + Vec2.cross(wB, vcp1.rB) - vA - Vec2.cross(wA, vcp1.rA);
dv2 = vB + Vec2.cross(wB, vcp2.rB) - vA - Vec2.cross(wA, vcp2.rA);
// Compute normal velocity
vn1 = Dot(dv1, normal);
vn2 = Dot(dv2, normal);
_ASSERT && common.assert(Abs(vn1 - vcp1.velocityBias) < k_errorTol);
_ASSERT && common.assert(Abs(vn2 - vcp2.velocityBias) < k_errorTol);
}
break;
}
//
// Case 2: vn1 = 0 and x2 = 0
//
// 0 = a11 * x1 + a12 * 0 + b1'
// vn2 = a21 * x1 + a22 * 0 + b2'
//
x.x = -vcp1.normalMass * b.x;
x.y = 0.0;
vn1 = 0.0;
vn2 = this.v_K.ex.y * x.x + b.y;
if (x.x >= 0.0 && vn2 >= 0.0) {
// Get the incremental impulse
var d = Vec2.sub(x, a);
// Apply incremental impulse
var P1 = Vec2.mul(d.x, normal);
var P2 = Vec2.mul(d.y, normal);
vA.subCombine(mA, P1, mA, P2);
wA -= iA * (Vec2.cross(vcp1.rA, P1) + Vec2.cross(vcp2.rA, P2));
vB.addCombine(mB, P1, mB, P2);
wB += iB * (Vec2.cross(vcp1.rB, P1) + Vec2.cross(vcp2.rB, P2));
// Accumulate
vcp1.normalImpulse = x.x;
vcp2.normalImpulse = x.y;
if (DEBUG_SOLVER) {
// Postconditions
var dv1B = Vec2.add(vB, Vec2.cross(wB, vcp1.rB));
var dv1A = Vec2.add(vA, Vec2.cross(wA, vcp1.rA));
var dv1 = Vec2.sub(dv1B, dv1A);
// Compute normal velocity
vn1 = Vec2.dot(dv1, normal);
_ASSERT && common.assert(Math.abs(vn1 - vcp1.velocityBias) < k_errorTol);
}
break;
}
//
// Case 3: vn2 = 0 and x1 = 0
//
// vn1 = a11 * 0 + a12 * x2 + b1'
// 0 = a21 * 0 + a22 * x2 + b2'
//
x.x = 0.0;
x.y = -vcp2.normalMass * b.y;
vn1 = this.v_K.ey.x * x.y + b.x;
vn2 = 0.0;
if (x.y >= 0.0 && vn1 >= 0.0) {
// Resubstitute for the incremental impulse
var d = Vec2.sub(x, a);
// Apply incremental impulse
var P1 = Vec2.mul(d.x, normal);
var P2 = Vec2.mul(d.y, normal);
vA.subCombine(mA, P1, mA, P2);
wA -= iA * (Vec2.cross(vcp1.rA, P1) + Vec2.cross(vcp2.rA, P2));
vB.addCombine(mB, P1, mB, P2);
wB += iB * (Vec2.cross(vcp1.rB, P1) + Vec2.cross(vcp2.rB, P2));
// Accumulate
vcp1.normalImpulse = x.x;
vcp2.normalImpulse = x.y;
if (DEBUG_SOLVER) {
// Postconditions
var dv2B = Vec2.add(vB, Vec2.cross(wB, vcp2.rB));
var dv2A = Vec2.add(vA, Vec2.cross(wA, vcp2.rA));
var dv1 = Vec2.sub(dv2B, dv2A);
// Compute normal velocity
vn2 = Vec2.dot(dv2, normal);
_ASSERT && common.assert(Math.abs(vn2 - vcp2.velocityBias) < k_errorTol);
}
break;
}
//
// Case 4: x1 = 0 and x2 = 0
//
// vn1 = b1
// vn2 = b2;
//
x.x = 0.0;
x.y = 0.0;
vn1 = b.x;
vn2 = b.y;
if (vn1 >= 0.0 && vn2 >= 0.0) {
// Resubstitute for the incremental impulse
var d = Vec2.sub(x, a);
// Apply incremental impulse
var P1 = Vec2.mul(d.x, normal);
var P2 = Vec2.mul(d.y, normal);
vA.subCombine(mA, P1, mA, P2);
wA -= iA * (Vec2.cross(vcp1.rA, P1) + Vec2.cross(vcp2.rA, P2));
vB.addCombine(mB, P1, mB, P2);
wB += iB * (Vec2.cross(vcp1.rB, P1) + Vec2.cross(vcp2.rB, P2));
// Accumulate
vcp1.normalImpulse = x.x;
vcp2.normalImpulse = x.y;
break;
}
// No solution, give up. This is hit sometimes, but it doesn't seem to
// matter.
break;
}
}
velocityA.v.set(vA);
velocityA.w = wA;
velocityB.v.set(vB);
velocityB.w = wB;
};
/**
* Friction mixing law. The idea is to allow either fixture to drive the
* restitution to zero. For example, anything slides on ice.
*/
function mixFriction(friction1, friction2) {
return Math.sqrt(friction1 * friction2);
}
/**
* Restitution mixing law. The idea is allow for anything to bounce off an
* inelastic surface. For example, a superball bounces on anything.
*/
function mixRestitution(restitution1, restitution2) {
return restitution1 > restitution2 ? restitution1 : restitution2;
}
var s_registers = [];
/**
* @param fn function(fixtureA, indexA, fixtureB, indexB) Contact
*/
Contact.addType = function(type1, type2, callback) {
s_registers[type1] = s_registers[type1] || {};
s_registers[type1][type2] = callback;
}
Contact.create = function(fixtureA, indexA, fixtureB, indexB) {
var typeA = fixtureA.getType(); // Shape.Type
var typeB = fixtureB.getType(); // Shape.Type
// TODO: pool contacts
var contact, evaluateFcn;
if (evaluateFcn = s_registers[typeA] && s_registers[typeA][typeB]) {
contact = new Contact(fixtureA, indexA, fixtureB, indexB, evaluateFcn);
} else if (evaluateFcn = s_registers[typeB] && s_registers[typeB][typeA]) {
contact = new Contact(fixtureB, indexB, fixtureA, indexA, evaluateFcn);
} else {
return null;
}
// Contact creation may swap fixtures.
fixtureA = contact.getFixtureA();
fixtureB = contact.getFixtureB();
indexA = contact.getChildIndexA();
indexB = contact.getChildIndexB();
var bodyA = fixtureA.getBody();
var bodyB = fixtureB.getBody();
// Connect to body A
contact.m_nodeA.contact = contact;
contact.m_nodeA.other = bodyB;
contact.m_nodeA.prev = null;
contact.m_nodeA.next = bodyA.m_contactList;
if (bodyA.m_contactList != null) {
bodyA.m_contactList.prev = contact.m_nodeA;
}
bodyA.m_contactList = contact.m_nodeA;
// Connect to body B
contact.m_nodeB.contact = contact;
contact.m_nodeB.other = bodyA;
contact.m_nodeB.prev = null;
contact.m_nodeB.next = bodyB.m_contactList;
if (bodyB.m_contactList != null) {
bodyB.m_contactList.prev = contact.m_nodeB;
}
bodyB.m_contactList = contact.m_nodeB;
// Wake up the bodies
if (fixtureA.isSensor() == false && fixtureB.isSensor() == false) {
bodyA.setAwake(true);
bodyB.setAwake(true);
}
return contact;
}
Contact.destroy = function(contact, listener) {
var fixtureA = contact.m_fixtureA;
var fixtureB = contact.m_fixtureB;
var bodyA = fixtureA.getBody();
var bodyB = fixtureB.getBody();
if (contact.isTouching()) {
listener.endContact(contact);
}
// Remove from body 1
if (contact.m_nodeA.prev) {
contact.m_nodeA.prev.next = contact.m_nodeA.next;
}
if (contact.m_nodeA.next) {
contact.m_nodeA.next.prev = contact.m_nodeA.prev;
}
if (contact.m_nodeA == bodyA.m_contactList) {
bodyA.m_contactList = contact.m_nodeA.next;
}
// Remove from body 2
if (contact.m_nodeB.prev) {
contact.m_nodeB.prev.next = contact.m_nodeB.next;
}
if (contact.m_nodeB.next) {
contact.m_nodeB.next.prev = contact.m_nodeB.prev;
}
if (contact.m_nodeB == bodyB.m_contactList) {
bodyB.m_contactList = contact.m_nodeB.next;
}
if (contact.m_manifold.pointCount > 0 && fixtureA.isSensor() == false
&& fixtureB.isSensor() == false) {
bodyA.setAwake(true);
bodyB.setAwake(true);
}
var typeA = fixtureA.getType(); // Shape.Type
var typeB = fixtureB.getType(); // Shape.Type
var destroyFcn = s_registers[typeA][typeB].destroyFcn;
if (typeof destroyFcn === 'function') {
destroyFcn(contact);
}
};