Warning: You should have a good understanding of the basic tutorials before venturing further.

In the anatomy of a collision topic we looked at how to get information about collisions. One piece of information was the contact point, where the impulse will be applied to separate the two shapes that collided. This topic will look at how we can use the relative velocity of the two shapes at the contact point, combined with the friction between them, to generate particles like smoke, dust, sparks etc.

a box2d car with smoke, sparks and gravel

The basics

We need to keep track of different sets of contacts, for each pair of material types. For this example we will have four material types: steel, rubber, concrete and dirt. The combinations of these rubbing on each other will generate three particle types: smoke, sparks and dirt.

Materials combinations table

The general procedure for keeping track of particle-generating contacts:

  • Have a set of contacts for each particle type (smoke, sparks, dirt)
  • When BeginContact occurs, add the contact to the appropriate set
  • When EndContact occurs, remove the contact from the set
  • Every time step, look at each contact in the set and check the relative velocity of the shapes at the contact point to see if particles should be generated

Sets of contacts

NOTE: this document was converted from the Box2D v2.X C++ web-site. Some of the approaches are less than ideal when converted into JavaScript, this is one of them. I would suggest that an associative array of callbacks where the keys are the material names is probably more elegant (as one choice, there are many others).

We need some type of list to store all the active contacts:

// References to currently active contacts using Sets
const steelToConcreteContacts = new Set();
const rubberToConcreteContacts = new Set();
const steelToDirtContacts = new Set();
const rubberToDirtContacts = new Set();

For material type checking:

function shapeIsSteel(shape) { /* ... */ }
function shapeIsConcrete(shape) { /* ... */ }
function shapeIsDirt(shape) { /* ... */ }
function shapeIsRubber(shape) { /* ... */ }

Of course, you would replace the ... with whatever you need to decide which material the fixture is, for example checking the user data etc.

Contact checking function:

function contactIsSteelVsConcrete(contact) {
    const shapeA = b2Contact_GetFixtureA(contact);
    const shapeB = b2Contact_GetFixtureB(contact);
    if (shapeIsSteel(shapeA) && shapeIsConcrete(shapeB))
        return true;
    if (shapeIsSteel(shapeB) && shapeIsConcrete(shapeA))
        return true;
    return false;
}

Material check functions:

function contactIsSteelVsConcrete(contact) {
    const shapeA = b2Contact_GetFixtureA(contact);
    const shapeB = b2Contact_GetFixtureB(contact);
    return (shapeIsSteel(shapeA) && shapeIsConcrete(shapeB)) ||
           (shapeIsSteel(shapeB) && shapeIsConcrete(shapeA));
}

function contactIsRubberVsConcrete(contact) {
    const shapeA = b2Contact_GetFixtureA(contact);
    const shapeB = b2Contact_GetFixtureB(contact);
    return (shapeIsRubber(shapeA) && shapeIsConcrete(shapeB)) ||
           (shapeIsRubber(shapeB) && shapeIsConcrete(shapeA));
}

function contactIsSteelVsDirt(contact) {
    const shapeA = b2Contact_GetFixtureA(contact);
    const shapeB = b2Contact_GetFixtureB(contact);
    return (shapeIsSteel(shapeA) && shapeIsDirt(shapeB)) ||
           (shapeIsSteel(shapeB) && shapeIsDirt(shapeA));
}

function contactIsRubberVsDirt(contact) {
    const shapeA = b2Contact_GetFixtureA(contact);
    const shapeB = b2Contact_GetFixtureB(contact);
    return (shapeIsRubber(shapeA) && shapeIsDirt(shapeB)) ||
           (shapeIsRubber(shapeB) && shapeIsDirt(shapeA));
}

Contact listener functions:

function BeginContact(contact) {
    if (contactIsSteelVsConcrete(contact))  steelToConcreteContacts.add(contact);
    if (contactIsRubberVsConcrete(contact)) rubberToConcreteContacts.add(contact);
    if (contactIsSteelVsDirt(contact))      steelToDirtContacts.add(contact);
    if (contactIsRubberVsDirt(contact))     rubberToDirtContacts.add(contact);
}

function EndContact(contact) {
    if (contactIsSteelVsConcrete(contact))  steelToConcreteContacts.delete(contact);
    if (contactIsRubberVsConcrete(contact)) rubberToConcreteContacts.delete(contact);
    if (contactIsSteelVsDirt(contact))      steelToDirtContacts.delete(contact);
    if (contactIsRubberVsDirt(contact))     rubberToDirtContacts.delete(contact);
}

Generating particles

Contact checking loop:

// Replace contactSet with steelToConcreteContacts etc
for (const contact of contactSet) {
    const manifold = b2Contact_GetManifold(contact);
    if (manifold.pointCount < 1)
        continue;

    // particle generation goes here
}

Particle generation code:

const shapeA = b2Contact_GetFixtureA(contact);
const shapeB = b2Contact_GetFixtureB(contact);
const bodyA = b2Fixture_GetBody(shapeA);
const bodyB = b2Fixture_GetBody(shapeB);

// Get the contact point in world coordinates
const worldManifold = b2Contact_GetWorldManifold(contact);
const worldPoint = worldManifold.points[0];

// Find the relative speed of the shapes at that point
const velA = b2Body_GetLinearVelocityFromWorldPoint(bodyA, worldPoint);
const velB = b2Body_GetLinearVelocityFromWorldPoint(bodyB, worldPoint);
const relativeSpeed = b2Vec2.Length(b2Vec2.Subtract(velA, velB));

// Overall friction of contact
const totalFriction = b2Fixture_GetFriction(shapeA) * b2Fixture_GetFriction(shapeB);

// Check if this speed and friction is enough to generate particles
const intensity = relativeSpeed * totalFriction;
if (intensity > threshold)
    spawnParticle(worldPoint, velA, velB, intensity);

Simple particle structure:

class SimpleParticle {
    constructor() {
        this.body = null;
        this.life = 0;
    }
}

Problems

contact points inside

  • Contact points are inside shapes
  • May need raycasting to find proper particle spawn positions
  • Particles can get trapped behind edge/chain shapes

Other considerations

This topic shows a really basic example of detecting where and how to generate particles. There are so many ways you can tweak and improve things that you could spend all day on it, but this tutorial is mainly to cover the Box2D part of the procedure. So I will just briefly mention some areas where improvements could be made:

  • Both manifold points could be checked
  • Consider tangential relative velocity only
  • Use better friction mix (e.g., b2MixFriction)
  • Different thresholds for each material pair

Credits

This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.