Here is how I would do it:
Rather than apply impulse I would implement a custom "action" (e.g. derive UprightCharacterAction from btActionInterface) which behaves like an "angular spring" and slams the velocity of the character toward an upright position. The main reason I don't like to operate in impulse space is because the impulse required to achieve a desired deltaAngularVelocity must be scaled by the inverse inertia tensor, else you get odd results and even instabilities.
So your
UprightCharacterAction class might look something like this:
Code: Select all
const btScalar MIN_ANGLE(0.017453); // 0.017453 radians ~= 1 degree
const btScalar MIN_DOT_PRODUCT = btCos(MIN_ANGLE);
const btScalar SUBSTEP_DURATION(1.0 / 60.0);
const btScalar MIN_TIMESCALE = 2.0 * SUBSTEP_DURATIOB; // for stability
const btVector3 DEFAULT_UP_AXIS(0.0, 1.0, 0.0);
class UprightCharacterAction : public btActionInterface {
public:
UprightCharacterAction(btRigidBody* body, const btVector3& up = DEFAULT_UP_AXIS) : m_body(body) {}
void setWorldUp(const btVector3& worldUp) {
m_worldUp = worldUp;
m_worldUp.normalize();
}
void setLocalUp(const btVector3& localUp) {
m_localUp = localUp;
m_localUp.normalize();
}
void setSpringTimescale(btScalar timescale) { m_springTimescale = btMax(MIN_TIMESCALE, timescale); }
void setBlendTimescale(btScalar timescale) { m_blendTimescale = btMax(MIN_TIMESCALE, timescale); }
void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep) override {
if (m_body->isActive()) {
// compute localUpInWorldFrame
btTransform worldTransform = m_body->getWorldTransform();
worldTransform.setOrigin(btVector3(0.0, 0.0, 0.0));
btVector3 localUpInWorldFrame = worldTransform * m_localUp;
// compute targetAngularVelocity
btVector3 targetAngularVelocity(0.0, 0.0, 0.0);
// Checking the dotProduct is cheaper/faster than computing the angle, so we do that first.
// (When two normalized vectors are parallel their dotProduct is 1.0. Otherwise it is less.)
btScalar dotProduct = localUpInWorldFrame.dot(m_worldUp);
if (dotProduct < MIN_DOT_PRODUCT) {
btVector3 axis = localUpInWorld.cross(m_worldUp).normalize();
btScalar angle = btAcos(dotProduct);
// NOTE: for very fast upright behavior set m_springTimescale very small (two substeps is as low as allowed)
// for very slow upright behavior set m_springTimescale longer (several seconds would be very slow)
targetAngularVelocity = (angle / m_springTimescale) * axis;
}
// decompose currentAngularVelocity into spin and upright parts
// because we only want to modify the part that orients the character toward UP
btVector3 currentAngularVelocity = m_body->getAngularVelocity;
btVector3 spinVelocity = currentAngularVelocity.dot(m_worldUp) * m_worldUp;
btVector3 uprightVelocity = currentAngularVelocity - spinVelocity;
// blend targetAngularVelocity into uprightAngularVelocity
// NOTE: for very agressive upright behavior set the m_blendTimescale very small (two substeps is as low as allowed)
// for weak upright behavior set it longer (several seconds would be very weak)
btScalar del = btMin(btScalar(0.99), deltaTimStep / m_blendTimescale); // must clamp for stability
btVector3 newUprightVelocity = (btScalar(1.0) - del) * uprightAngularVelocity + del * targetAngularVelocity;
// set newAngularVelocity
btVector3 newAngularVelocity = spinVelocity + newUprightVelocity;
m_body->setAngularVelocity(spinVelocity + newUprightVelocity);
}
}
private:
btVector3 m_worldUp { DEFAULT_UP_AXIS };
btVector3 m_localUp { DEFAULT_UP_AXIS };
btScalar m_springTimescale { 0.5 };
btScalar m_blendTimescale { 0.5 };
btRigidBody* m_body;
};
Note: I wrote that from scratch and did not test. I don't know if it compiles and there may very well be errors, but it should be close.
What is that class doing?
There are two "timescales". Each timescale is used to perform an exponential attraction toward a target. You can tune the behavior of your action by adjusting the timescales. I picked some default likely values but they might not be what you want.
The
m_springTimescale determines the "uprighting" velocity as if you had a "critically damped angular spring with zero velocity history" constantly pulling the character upright. To make this spring fast you would reduce the timescale to be short. To make the spring slow and lazy you would use longer values. A good way to tune this is to ask yourself the following question:
"Ignoring any external bumps, how many seconds should it take for the character to upright from sideways?"
Suppose the answer was "two seconds". Then you would divide your answer by three to get a good timescale:
Code: Select all
uprightAction.setSpringTimescale(timeToGetUp / 3.0);
The reason is because the timescale dictates exponential decay and the delta angle decreases to
1/e of its original value after each timescale. After three timescales the delta angle would be
(1/e)^3 which is pretty small.
The
m_blendTimescale is used to prevent the upright behavior from completely overwriting whatever current angular velocity it already has, since otherwise the action would completely erase any external bumps. If you make springTimescale very short but the blendTimescale longer then you tend to get a bouncy spring. If you make the blendTimescale very short then you just get the exponential decay of the spring and all external bumps tend to get "erased". Tuning this value is a little trickier. My advice: set it to the
m_springTimescale and then adjust up/down to see how it affects things.
Finally I will mention how the
btActionInterface works: In this specific example you would need to instantiate one
UprightCharacterAction, passing it a pointer to the RigidBody on which it is supposed to act, and then give it to the world:
Code: Select all
UprightCharacterAction* action = new UprightCharacterAction(characterBody);
world->addAction(action);
The world will then call
action->updateAction(world, dt) once every substep. It is the developer's duty to implement that method to do the Right Thing.
I hope that helps. Good luck.