The Dark Art of tweaking Bullet Physics

posted in Merc Tactics
Published February 09, 2023
Advertisement

I thought I'd share some of the tips and tricks I learned using the Bullet Physics SDK in my game.

(This is a work in progress. There might be some things in this blog that are incorrect ?. If you spot anything let me know. I will be updating.)

In the game loop you have to call btDynamicsWorld::stepSimulation, so if your game runs at 60 FPS, the physics simulation will be update every 16ms. This is the main problem with all physics libraries, because this update will never be frequent enough. For instance, if an object is moving very fast it can go through thin objects. If it's moving extremely fast is will also go through thick objects. And then some objects will get embedded inside other objects.

There are various special parameters that you can tweak for fix these problems. For instance you can set a margin: btCollisionShape::setMargin. However, tweaking the parameter may fix one problem, but it will cause another problem. In the case of setMargin, setting it too low will cause objects to sometimes go through (or imbed themselves within) other objects, and setting it too high will cause object to appear to float above the ground.

I was trying to simulate the following scenario in my game: soldier throws a grenade at a door, which bounces on ground and then explodes and blows a door up (https://vimeo.com/manage/videos/757654546).

Lets go through all the problems I had.

First thing, the grenade sometimes bounced at strange angles that looked weird. This was caused because the mesh of the grenade was slightly irregular shaped, so I used the native shapes btSphereShape and btCapsuleShapeZ, rather than custom meshes.

Then, the grenade would sometimes embed itself into the ground and not bounce. There is a weird hack that fixes that problem:

 m_pItemBody->setCcdMotionThreshold(pGrenade->GetRadius());
m_pItemBody->setCcdSweptSphereRadius(pGrenade->GetRadius() * 0.4f);

What this does is it caps the velocity so, it cannot travel more than distance of radius of the grenade in one frame (At least, that's what I think it does)

Then, blowing up the door. That entails swapping out the door mesh with a group of splinter meshes, which are each given a velocity and (random) angular velocity. Because it's a violent explosion the velocity has to be high and that caused some splinters to embed theirselves into other splinters. I solved by explicitly calling stepSimulation(0.001f, 1, 0.001f) right after the explosion, with an extemely small time duration.

The next problem I had was that sometimes the splinters didn't fly apart like they should do. This was because the start off very close together and they have m_friction = 0.7f (which is typical for wood against concrete).

The solution to this is to temporarily set the friction to 0. This makes the splinters slippery and they will fly apart easier, but then you have to set the friction back to 0.7, otherwise they will later slide across the ground too much. This is how I do that in my update routine:

void Sharpnel::Update(int timeElapsed)
{
if (m_pItemBody && m_pItemBody->getActivationState() != ISLAND_SLEEPING)
{
 m_age += timeElapsed;
 Matrix ATTRIBUTE_ALIGNED16(matrix);
 m_pItemBody->getWorldTransform().getOpenGLMatrix(matrix.m);
 m_pMesh->UpdateRenderItem(matrix, &m_renderItem);
 // we set friction zero, to stop it rubbing against the other pieces initially
 // then set to normal wood against concrete friction
 if (m_age > 60 && m_pItemBody->getFriction() == 0)
  m_pItemBody->setFriction(0.7f);
 }
}
} // Update

Next problem I had with so called kinematic objects. These are objects that bullet does not move (the user moves them), but can interact with dynamic objects (for instance, the splinters and the grenade). But really, kinematic and dynamic objects are the same thing. What makes them behave differently are flags set in the object. You set a kinematic like this:

  m_pRigidBody->setCollisionFlags(m_pRigidBody->getCollisionFlags()  | btCollisionObject::CF_KINEMATIC_OBJECT);
  m_pRigidBody->setActivationState(DISABLE_DEACTIVATION);

There is an internal flag btCollisionObject::m_activationState1 which controls if objects move, or not and this flag is very important. For dynamic objects, the flag is initially set to ACTIVE_TAG. ACTIVE_TAG means the object can interact with other objects. After not moving for some time Bullet will change the state to ISLAND_SLEEPING and that means it won't move anymore (unless something hits it). This is an optimisation thing within bullet. For kinematic objects you need to set the activation state to DISABLE_DEACTIVATION, to prevent it from sleeping.

To move a kinematic object you need to directly modify both the world transform and the motionState (setting the world transform without modifying the motionState will cause interactions between kinematic and dynamic objects to behave very strangely)

  btTransform& t = m_pRigidBody->getWorldTransform();
 t.setOrigin(btVector3(m_x+m_xOffset, m_y+m_yOffset, m_z));
 m_pRigidBody->getMotionState()->setWorldTransform(t);

Also the position change only happens after btDynamicsWorld::stepSimulation is called.

If you need the object to move without btDynamicsWorld::stepSimulation being called you can call pWorld->updateSingleAabb(m_pRigidBody);

Note: If you look at my Sharpnel::Update routine again, you will see I first checked to see if the rigid body is in ISLAND_SLEEPING state before updating. This is an optimisation. I hope this now makes sense)

Also you should bear in mind, if you want to apply a velocity to a dynamic object, for example you must activate it afterwards, because it might have gone into ISLAND_SLEEPING state:

   m_pRigidBody->setLinearVelocity(btVector3(vector.m[0], vector.m[1], vector.m[2]));
   m_pRigidBody->activate(); 

Other problems that I encountered:

You can use an externally created mesh to create a btConvexHullShape, which in turn is used by btRigidBody to simulate a dynamic object. It is very important that the pivot of this mesh is positioned where the center of gravity should be. If it's in the wrong place, then the object is going to behave strangely (like there is a large lead weight at one end of the object).

There are some “magic” parameters in btRigidBodyConstructionInfo and in btRigidBody:

rbInfo.m_rollingFriction 
rbInfo.m_spinningFriction 
rbInfo.m_angularSleepingThreshold;
m_pRigidBody->setSleepingThresholds();
m_pRigidBody->setDamping();
m_pRigidBody->setDeactivationTime();

These parameters are only for resolving some very specific problems. I made the mistake of tweaking these and it actually made everything a lot worse. I recommend not touching them and using the default values.

Calling stepSimulation

You are meant to call btDiscreteDynamicsWorld::stepSimulation in the render loop of you game with the time of the last frame. I made the mistake of using the default parameters for this function.

You should call stepSimulation like this:

pWorld->stepSimulation(timeInSeconds, 1, timeInSeconds);

The first parameter is the time that has passed since the last frame. The third parameter is the internal time that bullet uses, which defaults to 16ms (i.e. 60 FPS). (Forget the second parameter, just use 1).

It's a bad idea to use the default value, because it will make physics run very slowly if the PC is running less than 60 FPS. For instance, if you are getting only 30 FPS, then physics will run twice as slow as it should. Also, you should consider capping the third parameter to a 16ms maximum, otherwise you run the risk of physics to often and slowing the whole game down.

Edit:

I corrected some facts above about kinematic objects.

Note: setting m_rollingFriction in fact, causes cylinder and btCapsuleShape objects to behave very oddly, but it's ok to use it for btSphereShape.

1 likes 4 comments

Comments

pedrohbr154

Hey Ed, nice post! We too are trying to use Bullet in our project. We are facing an issue right now related to setting the transform of a rigid body.
Let's say we set the position of the rigid body to btVector3(0.0f, 0.0f, 50.0f). The rigid body will fall to the ground and bounce. However, if we do this repeatedly the simulation will diverge everytime - the body will not bounce the same way that it did in the previous setTransform.
We really need full determinism, the same exact simulation everytime. Do you think that's possible? We can fix that by completely deleting the bullet world and re-creating it after setting the transform, but that's too expensive.

February 10, 2023 07:52 PM
sgold131

@pedrohbr154 In btDispatcher.h there's a field named m_deterministicOverlappingPairs that might help.

February 11, 2023 02:31 AM
Ed Welch

@pedrohbr154

Sorry, I have no idea. It's not something that I need to do for my game.

February 11, 2023 11:23 AM
pedrohbr154

Thanks for your answers guys, I'm gonna check it out!

February 11, 2023 11:36 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement