Here is the reproduction case. I made it last night but got knocked offline so I had to wait until now to post it. It is based on the hello world example. There is a ground (trimesh) and a box (0.15 halfwidth) resting on the ground. The box is shoved along the ground and flies up into the air when it crosses a triangle boundary. There are no graphics but the (x,y) coords of the box are printed to stdout. The y output should be 0.15 constantly but there are several severe knocks in the air that are caused by trimesh edges.
If NORMAL_HACK is defined, code that attempts to adjust the normal is turned on. This vastly improves the behaviour of the box (although it still isn't perfect). I tried a number of things but I didn't have net access at the time so this is all home-made and not quite the same as the example given earlier.
The code is below, and also a graph of the (x,y) coords. The green line is with NORMAL_HACK, the red line is without. You can see that the green line is almost flat as it should be but the red line is knocked into the air several times as the box goes in the direction of +x. The white line is 0 so it gets knocked up by its own height -- quite noticable.
Code: Select all
#include <cstdlib>
#include <cstdio>
#include <iostream>
#include <vector>
#include <btBulletDynamicsCommon.h>
// Used to initialise the trimesh
typedef btAlignedObjectArray<btVector3> Vertexes;
struct Face {
Face (int v1_, int v2_, int v3_, unsigned int flag_)
: v1(v1_), v2(v2_), v3(v3_), flag(flag_) { }
int v1, v2, v3;
unsigned long flag; // unused in this repro case but don't want to remove it
};
typedef std::vector<Face> Faces;
//#define NORMAL_HACK
#ifdef NORMAL_HACK
std::ostream &operator<<(std::ostream &o, btVector3 &v) {
o << "("<<v.x()<<", "<<v.y()<<", "<<v.z()<<")";
return o;
}
btVector3 get_face_normal (const btStridingMeshInterface *mesh, int face) {
PHY_ScalarType vertexes_type, indexes_type;
const unsigned char *vertexes;
int num_vertexes;
int vertexes_stride;
const unsigned char *indexes;
int num_faces;
int face_stride;
mesh->getLockedReadOnlyVertexIndexBase(&vertexes, num_vertexes, vertexes_type, vertexes_stride,
&indexes, face_stride, num_faces, indexes_type);
assert(vertexes_type == PHY_FLOAT);
assert(indexes_type == PHY_INTEGER);
const int *indexes2 = reinterpret_cast<const int *>(indexes + face_stride*face);
int i1=indexes2[0], i2=indexes2[1], i3=indexes2[2];
btVector3 v1 = *reinterpret_cast<const btVector3 *>(vertexes + vertexes_stride * i1);
btVector3 v2 = *reinterpret_cast<const btVector3 *>(vertexes + vertexes_stride * i2);
btVector3 v3 = *reinterpret_cast<const btVector3 *>(vertexes + vertexes_stride * i3);
btVector3 r;
r = (v2-v1).cross(v3-v1);
r.normalize();
return r;
}
// Attempts to fix it
extern ContactAddedCallback gContactAddedCallback;
void customCallbackObj(btManifoldPoint& cp, const btCollisionObject* colObj, int partId, int index)
{
(void) partId;
const btCollisionShape *shape = colObj->getCollisionShape();
if (shape->getShapeType() != TRIANGLE_SHAPE_PROXYTYPE) return;
const btCollisionShape *parent = colObj->getRootCollisionShape();
if (parent == NULL) return;
if (parent->getShapeType() != TRIANGLE_MESH_SHAPE_PROXYTYPE) return;
const btTriangleMeshShape *parent2 = static_cast<const btTriangleMeshShape*>(parent);
const btStridingMeshInterface *mesh = parent2->getMeshInterface();
btVector3 face_normal = get_face_normal(mesh,index);
float dot = face_normal.dot(cp.m_normalWorldOnB);
cp.m_normalWorldOnB = dot > 0 ? face_normal : -face_normal;
}
bool customCallback(btManifoldPoint& cp, const btCollisionObject* colObj0,int partId0,int index0,const btCollisionObject* colObj1,int partId1,int index1)
{
customCallbackObj(cp, colObj0, partId0, index0);
customCallbackObj(cp, colObj1, partId1, index1);
return true;
}
#endif
int main (void)
{
#ifdef NORMAL_HACK
gContactAddedCallback = customCallback;
#endif
/* STANDARD STUFF */
btVector3 worldAabbMin(-10000,-10000,-10000);
btVector3 worldAabbMax(10000,10000,10000);
int maxProxies = 1024;
btAxisSweep3* broadphase = new btAxisSweep3(worldAabbMin,worldAabbMax,maxProxies);
btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration();
btCollisionDispatcher* dispatcher = new btCollisionDispatcher(collisionConfiguration);
btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver;
btDiscreteDynamicsWorld* dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher,broadphase,solver,collisionConfiguration);
dynamicsWorld->setGravity(btVector3(0,-10,0));
/* MAKE ME A TRIANGLE MESH WORTHY OF MORDOR */
Vertexes *vertexes = new Vertexes();
int sz = 11;
vertexes->reserve(sz*sz);
for (int z=-5 ; z<=5 ; z++) {
for (int x=-5 ; x<=5 ; x++) {
// sz * sz grid of 10m squares
vertexes->push_back(btVector3(10*x, 0, 10*z));
}
}
Faces *faces = new Faces();
int num_faces = (sz-1) * (sz-1) * 2; // the 2 turns quads into triangles
faces->reserve(num_faces);
for (int z=0 ; z<sz-1 ; z++) {
for (int x=0 ; x<sz-1 ; x++) {
int top_left = z*sz + x; // (x, z)
int top_rght = z*sz + x+1; // (x+1, z)
int bot_left = (z+1)*sz + x; // (x, z+1)
int bot_rght = (z+1)*sz + x+1; // (x+1, z+1)
/*
+---------+
|\ |
| \ |
| \ | Clockwise!
| \ |
| \|
+---------+
*/
faces->push_back(Face(top_left,top_rght,bot_rght,0x0));
faces->push_back(Face(top_left,bot_rght,bot_left,0x0));
}
}
btTriangleIndexVertexArray *v = new btTriangleIndexVertexArray(
faces->size(), &((*faces)[0].v1), sizeof(Face),
vertexes->size(), &((*vertexes)[0][0]), sizeof(btVector3));
/* STANDARD STUFF */
btCollisionShape* groundShape = new btBvhTriangleMeshShape(v,true,true);
groundShape->setMargin(0);
btCollisionShape* fallShape = new btBoxShape(btVector3(0.15,0.15,0.15));
// put the ground at -0.5 so that the box centre will be at height 0
btDefaultMotionState* groundMotionState = new btDefaultMotionState(btTransform(btQuaternion(0,0,0,1),btVector3(0,0,0)));
btRigidBody::btRigidBodyConstructionInfo
groundRigidBodyCI(0,groundMotionState,groundShape,btVector3(0,0,0));
btRigidBody* groundRigidBody = new btRigidBody(groundRigidBodyCI);
dynamicsWorld->addRigidBody(groundRigidBody);
groundRigidBody->setFriction(0);
btDefaultMotionState* fallMotionState =
new btDefaultMotionState(btTransform(btQuaternion(0,0,0,1),btVector3(3,0.15,3)));
btScalar mass = 20;
btVector3 fallInertia(0,0,0);
fallShape->calculateLocalInertia(mass,fallInertia);
btRigidBody::btRigidBodyConstructionInfo fallRigidBodyCI(mass,fallMotionState,fallShape,fallInertia);
btRigidBody* fallRigidBody = new btRigidBody(fallRigidBodyCI);
fallRigidBody->setFriction(0);
dynamicsWorld->addRigidBody(fallRigidBody);
#ifdef NORMAL_HACK
fallRigidBody->setCollisionFlags(fallRigidBody->getCollisionFlags() | btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK);
#endif
for (int i=0 ; i<500 ; i++) {
dynamicsWorld->stepSimulation(1/60.f,10);
if (i==60) {
/* GIVE IT A SHOVE */
fallRigidBody->applyCentralImpulse(btVector3(100,0,60));
}
btTransform trans;
fallRigidBody->getMotionState()->getWorldTransform(trans);
printf("%0.4f %0.4f\n", trans.getOrigin().getX(), trans.getOrigin().getY());
}
dynamicsWorld->removeRigidBody(fallRigidBody);
delete fallRigidBody->getMotionState();
delete fallRigidBody;
dynamicsWorld->removeRigidBody(groundRigidBody);
delete groundRigidBody->getMotionState();
delete groundRigidBody;
delete fallShape;
delete groundShape;
delete vertexes;
delete faces;
delete dynamicsWorld;
delete solver;
delete collisionConfiguration;
delete dispatcher;
delete broadphase;
return 0;
}
// vim: shiftwidth=8:tabstop=8:expandtab
However when I tried to reproduce this in my game I ran into a separate set of problems. While the normal hack does fix this particular problem, it causes boxes to get jammed when they are fired at trimesh edges. Gimpact triangle meshes usually just fall straight through the floor. I tried a number of things including trying to preserve the direction (up/down) of the normal and its magnitude but I couldn't find anything that was stable enough to be a real fix for the original problem.
In conclusion, "correcting" the normals does work, but the side-effects make it unsuitable as a general solution.