Jitter/deviation with flat heightfield and sphere

dwilliamson
Posts: 6
Joined: Fri Oct 07, 2011 2:41 pm
Location: Brighton, UK

Jitter/deviation with flat heightfield and sphere

Post by dwilliamson »

Hi,

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;
}
ProcessHeightfieldCollision does the job of figuring out if this is an edge collision and whether the edge collision is significant enough to retain the edge normal. In the case of a locally flat plane, the contact normal is changed to that of the faces to ensure smooth, straight-line movement:

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;
}
There are a couple things to note, here:

* 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;
}
I OR the value of 0x40000000 into the call to ProcessTriangle for the second triangle in processAllTriangles to tell the difference between the two. I think these functions are quite a reasonable price to pay for not having to store more explicit connectivity information for the heightfield. It would be nice to record the "sharpness" of each heightfield edge, but that would add some post-processing requirements to the heightfield object whenever you modified it (as well as requiring more memory).

Any thoughts?

Cheers,
- Don