top of page
Search
cedarcantab

Studying Box2D-Lite in Javascript, Part 9: Contact Solver

Updated: Mar 1


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();



2 views0 comments

Recent Posts

See All

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2...

sopiro motor constranit

import { Matrix2, Vector2 } from "./math.js"; import { RigidBody } from "./rigidbody.js"; import { Settings } from "./settings.js";...

Comments


記事: Blog2_Post
bottom of page