I'm doing some character locomotion with a rigid body sphere and noticed that sliding a sphere along a flat heightfield was a bumpy experience and I could never push the sphere in a straight line by applying a force. Of course, this is the classical case of the sphere catching the edges of the tessellation.
In past physics engines I simply ignored or reassigned the normals of internal edges to be that of the face and noticed that there has been previous work on this with triangle meshes, specifically:
http://bulletphysics.org/Bullet/phpBB3/ ... f=9&t=1814
http://code.google.com/p/bullet/issues/detail?id=27
However, this doesn't work for heightfields (and note that uncondtionally modifying the contact normal without also providing a better contact point will lead to fast moving objects falling through the world as the counter-impulse is a more of a spin). I've had a hack at fixing this myself using as little changes to Bullet as I can, maintaining the ability to dynamically modify the heightfield and not storing any connectivity information (it's implicit for the heightfield plane). Before I continue, can anybody see any easier solutions or anything that looks inherently crazy in my solution?
An ideal solution would involve a much higher level refactor (not being able to tell the difference between an edge/face collision, or even get the index of the edge involved in the collision is a real pain).
Starting at the top, I'm adding a custom "contact added callback":
Code: Select all
// Initialisation
btSetContactAddedCallback(CustomContactAddedCallback);
rigid_body->bullet->setCollisionFlags(rigid_body->bullet->getCollisionFlags() | btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK);
bool CustomContactAddedCallback(btManifoldPoint& cp, const btCollisionObject* colObj0, int partId0, int index0, const btCollisionObject* colObj1, int partId1, int index1)
{
// Determine which, if any of the shapes are a heightfield
if (colObj0->getRootCollisionShape()->getShapeType() == TERRAIN_SHAPE_PROXYTYPE)
return ProcessHeightfieldCollision(cp,
cp.m_localPointA,
(btHeightfieldTerrainShape*)colObj0->getRootCollisionShape(),
(btTriangleShape*)colObj0->getCollisionShape(),
partId0, index0);
if (colObj1->getRootCollisionShape()->getShapeType() == TERRAIN_SHAPE_PROXYTYPE)
return ProcessHeightfieldCollision(cp,
cp.m_localPointB,
(btHeightfieldTerrainShape*)colObj1->getRootCollisionShape(),
(btTriangleShape*)colObj1->getCollisionShape(),
partId1, index1);
return true;
}
Code: Select all
btScalar SegmentSqrDistance(const btVector3& from, const btVector3& to,const btVector3 &p)
{
btVector3 diff = p - from;
btVector3 v = to - from;
btScalar t = v.dot(diff);
if (t > 0)
{
btScalar dotVV = v.dot(v);
if (t < dotVV)
diff -= (t / dotVV) * v;
else
diff -= v;
}
return diff.dot(diff);
}
bool ProcessHeightfieldCollision(btManifoldPoint& cp, btVector3& cppos, btHeightfieldTerrainShape* heightfield_shape, btTriangleShape* triangle_shape, int part_id, int index)
{
// This can be classified as an edge collision if the contact normal differs from the triangle normal
btVector3 triangle_normal;
triangle_shape->calcNormal(triangle_normal);
if (cp.m_normalWorldOnB.dot(triangle_normal) < 1 - SIMD_EPSILON)
{
// Need to find the edge that matches this contact normal
int ev0 = -1, ev1 = -1;
btVector3* vertices = &triangle_shape->getVertexPtr(0);
for (int v0 = 2, v1 = 0; v1 < 3; v0 = v1, v1++)
{
float d = SegmentSqrDistance(vertices[v0], vertices[v1], cppos);
if (d < SIMD_EPSILON)
{
ev0 = v0;
ev1 = v1;
}
}
// Leave contact point alone if the edge can't be found
if (ev0 == -1)
return true;
// Get the triangle that shares this edge. If this is a boundary edge, the contact normal is
// perfectly valid
btVector3 neighbour_vertices[3];
if (!heightfield_shape->getNeighbourTriangle(part_id, index, ev0, neighbour_vertices))
return true;
// If the two faces have similar normals, use the face normal and ignore the edge normal
btVector3 other_normal = (neighbour_vertices[1]-neighbour_vertices[0]).cross(neighbour_vertices[2]-neighbour_vertices[0]);
other_normal.normalize();
if (triangle_normal.dot(other_normal) > 1 - SIMD_EPSILON)
cp.m_normalWorldOnB = triangle_normal;
}
return true;
}
* I'm having to calculate the triangle normal and do a dot product to check if this is a collision with an edge.
* Because I don't have the edge index, I'm doing a search to see which edge the contact point is on.
It would be nice if there were quicker ways to do this. Anyhoo, that's as far as I got without having to make changes to the core Bullet code. The getNeighbourTriangle function is implemented as two functions:
Code: Select all
bool btHeightfieldTerrainShape::getTriangle(int x, int j, int subIndex, btVector3 vertices[3]) const
{
// Reject triangles that are outside the bounds of the heightfield
if (x < 0 || x >= m_heightStickWidth)
return false;
if (j < 0 || j >= m_heightStickLength)
return false;
if (m_flipQuadEdges || (m_useDiamondSubdivision && !((j+x) & 1)))
{
//first triangle
if (subIndex == 0)
{
getVertex(x,j,vertices[0]);
getVertex(x+1,j,vertices[1]);
getVertex(x+1,j+1,vertices[2]);
}
//second triangle
else
{
getVertex(x,j,vertices[0]);
getVertex(x+1,j+1,vertices[1]);
getVertex(x,j+1,vertices[2]);
}
}
else
{
//first triangle
if (subIndex == 0)
{
getVertex(x,j,vertices[0]);
getVertex(x,j+1,vertices[1]);
getVertex(x+1,j,vertices[2]);
}
//second triangle
else
{
getVertex(x+1,j,vertices[0]);
getVertex(x,j+1,vertices[1]);
getVertex(x+1,j+1,vertices[2]);
}
}
return true;
}
bool btHeightfieldTerrainShape::getNeighbourTriangle(int partId, int triangleIndex, int edgeIndex, btVector3 vertices[3]) const
{
// Second triangle?
if (triangleIndex & 0x40000000)
{
triangleIndex &= ~0x40000000;
switch (edgeIndex)
{
case (0):
// Shared edge between first and second triangle in this quad
return getTriangle(partId, triangleIndex, 0, vertices);
case (1):
// First triangle of the quad in the row below
return getTriangle(partId, triangleIndex + 1, 0, vertices);
case (2):
// First triangle of the quad in the next column
return getTriangle(partId + 1, triangleIndex, 0, vertices);
}
}
else
{
// It's the first triangle
switch (edgeIndex)
{
case (0):
// Second triangle of the quad in the previous column
return getTriangle(partId - 1, triangleIndex, 1, vertices);
case (1):
// Shared edge between first and second triangle in this quad
return getTriangle(partId, triangleIndex, 1, vertices);
case (2):
// Second triangle of the quad in the row above
return getTriangle(partId, triangleIndex - 1, 1, vertices);
}
}
return false;
}
Any thoughts?
Cheers,
- Don