Box2D C++ tutorials - Introduction
Last edited: July 24 2013Halt! You should have a good understanding of
the basic tutorials before venturing further.
Physics-driven particles
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 fixtures that collided. This topic will look at how we can use the relative velocity of the two fixtures at the contact point, combined with the friction between them, to generate particles like smoke, dust, sparks etc.
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. Even with only four material types, there are 10 possible combinations of material pairs, so this type of management can grow large very quickly. In this topic we will only generate particles in four cases:
steel | rubber | concrete | dirt | |
dirt | dirt | dirt | - | - |
concrete | sparks | smoke | - | |
rubber | - | - | ||
steel | - |
The general procedure for keeping track of particle-generating contacts can be done like this:
- 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 fixtures at the contact point to see if particles should be generated
Sets of contacts
We need some type of list to store all the active contacts, for which I find std::set to be quite useful.
1 2 3 4 5 | //references to currently active contacts set<b2Contact*> m_steelToConcreteContacts; set<b2Contact*> m_rubberToConcreteContacts; set<b2Contact*> m_steelToDirtContacts; set<b2Contact*> m_rubberToDirtContacts; |
Next we need to check in BeginContact/EndContact to see if each contact is one that we're interested in. For example if one fixture is steel and the other is concrete, we'll add/remove that contact in m_steelToConcreteContacts. In the contact listener, either one of fixtureA or fixtureB could be the steel, so we have to make checks like:
if (( A is steel and B is concrete ) or ( A is concrete and B is steel ))
For a large number of material combinations this can become quite messy and tedious, especially when the "is steel" part of the check also involves a lot of code, and checking user data etc. There are various ways you could tackle this problem, although none of them are very pleasant. You might like to read some discussion about this on Stack Overflow: How can I track all of my Box2D collisions in a clean, manageable manner?
For this example I used a nasty pre-processor macro to help out, so I will mention that here. It is a handy solution, but don't take it as a recommendation (it's only applicable for C/C++ anyway). First, a function is made for each material type, to decide whether a fixture is made from that material:
1 2 3 4 | bool fixtureIsSteel(b2Fixture* f) { ... } bool fixtureIsConcrete(b2Fixture* f) { ... } bool fixtureIsDirt(b2Fixture* f) { ... } bool fixtureIsRubber(b2Fixture* f) { ... } |
Now we can write a function to decide whether a contact is steelVsConcrete:
1 2 3 4 5 6 7 8 9 | bool contactIsSteelVsConcrete(b2Contact* contact) { b2Fixture* fA = contact->GetFixtureA(); b2Fixture* fB = contact->GetFixtureB(); if ( fixtureIsSteel(fA) && fixtureIsConcrete(fB) ) return true; if ( fixtureIsSteel(fB) && fixtureIsConcrete(fA) ) return true; return false; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #define CHECK_MAT_VS_MAT(mat1, mat2)\ bool contactIs##mat1##Vs##mat2(b2Contact* contact) {\ b2Fixture* fA = contact->GetFixtureA();\ b2Fixture* fB = contact->GetFixtureB();\ if ( fixtureIs##mat1(fA) && fixtureIs##mat2(fB) )\ return true;\ if ( fixtureIs##mat1(fB) && fixtureIs##mat2(fA) )\ return true;\ return false;\ } CHECK_MAT_VS_MAT(Steel, Concrete) CHECK_MAT_VS_MAT(Rubber, Concrete) CHECK_MAT_VS_MAT(Steel, Dirt) CHECK_MAT_VS_MAT(Rubber, Dirt) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void BeginContact(b2Contact* contact) { if ( contactIsSteelVsConcrete(contact) ) m_steelToConcreteContacts.insert(contact); if ( contactIsRubberVsConcrete(contact) ) m_rubberToConcreteContacts.insert(contact); if ( contactIsSteelVsDirt(contact) ) m_steelToDirtContacts.insert(contact); if ( contactIsRubberVsDirt(contact) ) m_rubberToDirtContacts.insert(contact); } void EndContact(b2Contact* contact) { if ( contactIsSteelVsConcrete(contact) ) m_steelToConcreteContacts.erase(contact); if ( contactIsRubberVsConcrete(contact) ) m_rubberToConcreteContacts.erase(contact); if ( contactIsSteelVsDirt(contact) ) m_steelToDirtContacts.erase(contact); if ( contactIsRubberVsDirt(contact) ) m_rubberToDirtContacts.erase(contact); } |
Generating particles
Now that we have a list of the current contacts for each pair of material types, we can check those contacts every time step to see if there is enough movement and friction between them to generate some particles. If you're using std::set, the outer part of the loop to do this would look like:
1 2 3 4 5 6 7 8 | // replace "theSet" with eg. m_steelToConcreteContacts etc for (set<b2Contact*>::iterator it = theSet.begin(); it != theSet.end(); ++it) { b2Contact* contact = *it; if ( contact->GetManifold()->pointCount < 1 ) continue; // particle generation goes here } |
Now, the main particle generation part goes like this: we get the contact point from the manifold, which is a point in world coordinates. Then we use that point to find the velocity of each of the two fixtures at that point. Comparing those velocities tells us how fast the fixtures are rubbing against each other. We also look at the friction of the two fixtures, to come up with single number to represent a total umm... 'intensity' value. If the intensity is above a certain threshold, we generate a particle, otherwise we don't.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | b2Fixture* fA = contact->GetFixtureA(); b2Fixture* fB = contact->GetFixtureB(); b2Body* bA = fA->GetBody(); b2Body* bB = fB->GetBody(); // get the contact point in world coordinates b2WorldManifold worldManifold; contact->GetWorldManifold( &worldManifold ); b2Vec2 worldPoint = worldManifold.points[0]; // find the relative speed of the fixtures at that point b2Vec2 velA = bA->GetLinearVelocityFromWorldPoint(worldPoint); b2Vec2 velB = bB->GetLinearVelocityFromWorldPoint(worldPoint); float relativeSpeed = (velA - velB).Length(); // overall friction of contact float totalFriction = fA->GetFriction() * fB->GetFriction(); // check if this speed and friction is enough to generate particles float intensity = relativeSpeed * totalFriction; if ( intensity > threshold ) spawnParticle( worldPoint, velA, velB, intensity ); |
The creation and management of particles is not really the subject of this tutorial. Much has already been written about particle systems, and there are many ready-made solutions out there to use. The spawnParticle function in the code above is just to show how you might use these values from the Box2D world to tell the particle system what to do.
In the accompanying source code for this topic, you can find a rudimentary particle system using this structure:
1 2 3 4 | struct simpleParticle { b2Body* body; float life; }; |
Problems
As you may have noticed from the anatomy of a collision topic, the contact point is actually inside both of the fixtures involved in the collision, like this: Depending on what type of particles you are generating, this could be a problem. For example, in the sample source code for this topic the particles are using Box2D bodies, and if the body for the particle starts embedded in another body, it can affect the starting velocity. Or even worse, if one of the fixtures is an edge/chain fixture, the particle can start on the wrong side of the chain and never be able to come back.
I can't think of a nice way to solve this, other than raycasting to find the desired surface, and making sure the particle starts on the outside of it. For example in the case above, if the circle was a car tire you would want the particle to start on the outside of the polygon fixture, so you could raycast from the circle center to the contact point to find an appropriate position: (In the source code below, particles are only ever generated on the ground, so a raycast vertically downwards from the sky is used to find the start position.)
Other considerations
This topic and the accompanying source code 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 (for polygon vs polygon)
- you could consider tangential relative velocity only
- you could use a better friction mix, eg. b2MixFriction
- you could use PostSolve to consider the pressure between the fixtures
- different thresholds for each material pair
Source code
Here is the source code for those who would like to try it out for themselves. This is a 'test' for the testbed, based on Box2D v2.3.0.
Testbed test: iforce2d_physicsDrivenParticles.zip
*** To compile this, you will need to make the b2World::DrawShape function public ***
Linux 32-bit binary
Linux 64-bit binary
Windows binary
MacOSX binary
RUBE file for the test scene: physicsDrivenParticlesRUBEScene.zip
YouTube video