Proper Physics Time Step

YO @Cedric … I am having an issue with my driving physics simulation (Raycast Vehicle) and time step.

First i was using the UseDeltaAsTimeStep Option new Babylon.AmmoJSPlugin(true). Using that options
run smooth on my PC that tops out at 60 FPS… But others folks are having issues with the falling thru ground meshes or just really weird contacts, on some of their machines. Come to find out the machines being really weird are WAY faster. I assume their delta time is throwing things off.

So i switched the scene setup to use fixed time step. They said that fixed the weird contact issues but the car moves around at super speed on their fast pc when i fix the time step to 1/60.

What, if any, is the proper way of calculating what the fixed time step should be per machine ?

Hard to say without code in the hands. Can you do a repro in a PG? Is it running at 60FPS on all PCs? Did you try using subtimestep ? PhysicsEngine | Babylon.js Documentation

I think i got faster pc issue fixed using animation ratio:

scene.getPhysicsEngine().setTimeStep((1/60) * scene.getAnimationRatio())

Seems to be working nicely, what do you think about using animation ratio :slight_smile:

P.S. i still get some ghost collisions using Ammo and raycast vehicle… When driving along a mesh… sometimes it seems like it just hit something invisible in the road and throws the car… But that is another issue i will work on and post a question for you once i get more info :slight_smile:

Sounds like the well-known internal edges collision issue. This isn’t isolated to Ammo, but is an issue stemming from finding the minimum penetration vector. If the engine determines that the shortest distance is to simple resolve the collision to one of the edges of the triangle, you’ll get this weird behavior. The usual fix is to replace the collision normal with that of the triangle normal before resolving the collision.

Yo @Raggar … you got any specifics on using the triangle normal instead of the collision normal ??

FYI… I saw previous about the internal edge problem and was trying a solution if found using generateInternalEdgeInfo … so i was setting that up like so

const world:any = BABYLON.SceneManager.GetPhysicsWorld(scene);
if (world != null && world.generateInternalEdgeInfo) {
   const collisionShape:any = colobj.getCollisionShape();
   if (collisionShape != null && collisionShape.getShapeType) {
       const shapeType:number = collisionShape.getShapeType();
       if (shapeType === 21) { // TRIANGLE_MESH_SHAPE_PROXYTYPE
           const triangleShape:any = Ammo.castObject(collisionShape, Ammo.btBvhTriangleMeshShape);
           colobj.triangleMapInfo = new Ammo.btTriangleInfoMap();
           world.generateInternalEdgeInfo(triangleShape, colobj.triangleMapInfo);
        }
    }
}

Then i use the Ammo.addFunction to setup a collision call back to adjust the internal edge info

    const addFunctionSignature:string = "iiiiiiii";
    const contactAddedCallbackPtr:any = Ammo.addFunction((cp:any, colObj0Wrap:any, partId0:number, index0:number, colObj1Wrap:any, partId1:number, index1:number) => {
        if (physicsplugin.world.adjustInternalEdgeContacts) {
            physicsplugin.world.adjustInternalEdgeContacts(cp, colObj1Wrap, colObj0Wrap, partId1, index1);
            return true;
        } else {
            return false;
        }
    }, addFunctionSignature);

    if (contactAddedCallbackPtr != null) {
        physicsplugin.world.setContactAddedCallback(contactAddedCallbackPtr);
        BABYLON.Tools.Log("Ammo.js internal edges supported");
    }

But apparently that is not working…

But you said something about replacing the collision normal with the triangle normal…

Well i just so happen to have the triangle normal available … I am using that with me btSmoothRaycastVehicle… i use barry coordinates to smooth out mesh collision contact… is Pretty kool take a look

Here is my header file for btSmoothVehicleRaycaster

class btSmoothVehicleRaycaster : public btVehicleRaycaster
{
	btDynamicsWorld*	m_dynamicsWorld;
public:
	short int m_collisionFilterGroup;
	short int m_collisionFilterMask;
	bool m_interpolateNormals;
	bool m_shapeTestingMode;
	int m_testPointCount;
	btScalar m_sweepPenetration;
	btScalar m_shapeTestingSize;

	btSmoothVehicleRaycaster(btDynamicsWorld* world)
		: m_dynamicsWorld(world),
		m_interpolateNormals(false),
		m_shapeTestingMode(false),
		m_testPointCount(32), 				// shall be enough, but can be higher
		m_shapeTestingSize(btScalar(0.2f)), // should be half of wheel radius
		m_sweepPenetration(btScalar(0.0f)), // default to zero
		m_collisionFilterGroup(btBroadphaseProxy::DefaultFilter),
		m_collisionFilterMask(btBroadphaseProxy::StaticFilter) { }

	virtual void* castRay(const btVector3& from,const btVector3& to, btVehicleRaycasterResult& result);
	void* performLineTest(const btVector3& from,const btVector3& to, btVehicleRaycasterResult& result);
	void* performShapeTest(const btVector3& from,const btVector3& to, btVehicleRaycasterResult& result);
};

The main thing is the performLineTest function

void* btSmoothVehicleRaycaster::performLineTest(const btVector3& from,const btVector3& to, btVehicleRaycasterResult& result)
{
	SmoothRayCastResultCallback rayCallback(from, to);
	rayCallback.m_collisionFilterMask = this->m_collisionFilterMask;
	rayCallback.m_collisionFilterGroup = this->m_collisionFilterGroup;

	// If this is a rigid body, m_collision_object is NULL, and the
    // rigid body is the actual collision object.
    // btCollisionWorld::rayTestSingle(from, to, m_collision_object ? m_collision_object : body, m_collision_shape, world_trans, ray_callback);
	m_dynamicsWorld->rayTest(from, to, rayCallback);
	if (rayCallback.hasHit())
	{
		const btRigidBody* body = btRigidBody::upcast(rayCallback.m_collisionObject);
        if (body && body->hasContactResponse())
		{
			result.m_distFraction = rayCallback.m_closestHitFraction;
			result.m_hitPointInWorld = rayCallback.m_hitPointWorld;
			result.m_hitNormalInWorld = rayCallback.m_hitNormalWorld;
			result.m_hitNormalInWorld.normalize();

			// printf("Perform Line Test - Hit Fraction (%f) -> Hit Point (%f x %f x %f) -> Hit Normal: (%f x %f x %f)\n", result.m_distFraction,
			//	result.m_hitPointInWorld.x(), result.m_hitPointInWorld.y(), result.m_hitPointInWorld.z(),
			//	result.m_hitNormalInWorld.x(), result.m_hitNormalInWorld.y(), result.m_hitNormalInWorld.z()
			// );

			if (this->m_interpolateNormals == true)
			{
				btCollisionShape* shape = (btCollisionShape*)body->getCollisionShape();
				if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)
				{
					btTriangleMeshShape *mesh_shape = static_cast<btTriangleMeshShape *>(shape);
					btStridingMeshInterface *mesh_interface = mesh_shape->getMeshInterface();
					btSmoothTriangleMesh *mesh_smoothing = dynamic_cast<btSmoothTriangleMesh*>(mesh_interface);
					if (mesh_smoothing != NULL && mesh_smoothing->hasVertexNormals())
					{
						// btVector3 n1 = btVector3(result.m_hitNormalInWorld.x(), result.m_hitNormalInWorld.y(), result.m_hitNormalInWorld.z());
						// ..
						result.m_hitNormalInWorld = mesh_smoothing->interpolateMeshNormal(body->getWorldTransform(), mesh_interface, rayCallback.m_shapePart, rayCallback.m_triangleIndex, rayCallback.m_hitPointWorld);
						// ..
						// btVector3 n2 = btVector3(result.m_hitNormalInWorld.x(), result.m_hitNormalInWorld.y(), result.m_hitNormalInWorld.z());
						// printf("Perform Line Test - Hit Normal (%f x %f x %f) -> Bary Normal: (%f x %f x %f)\n", n1.x(), n1.y(), n1.z(), n2.x(), n2.y(), n2.z());
					}
				}
			}

			return (void*)body;
		}
	}
	return 0;
}

And this is the actual interoplation functions that make all the driving Smooth as a baby’s but

// shape, subpart and triangle come from the ray callback.
// transform is the mesh shape's world transform
// position is the world space hit point of the ray
btVector3 btSmoothTriangleMesh::interpolateMeshNormal(const btTransform &transform, btStridingMeshInterface *mesh_interface, int subpart, int triangle, const btVector3 &position)
{
	const unsigned char *vertexbase;
	int num_verts;
	PHY_ScalarType type;
	int stride;

	const unsigned char *indexbase;
	int indexstride;
	int numfaces;
	PHY_ScalarType indicestype;

	int numverts = 0;
	mesh_interface->getLockedReadOnlyVertexIndexBase(&vertexbase, numverts, type, stride, &indexbase, indexstride, numfaces, indicestype, subpart);

	// Calculate new barycentric coordinates
	const unsigned int *indices = (const unsigned int *)(indexbase + triangle * indexstride);
	unsigned int i = indices[0], j = indices[1], k = indices[2];
	StrideVertexAccessor positions(vertexbase, stride, 0);
	btVector3 barry = this->barycentricCoordinates(transform.invXform(position), positions[i], positions[j], positions[k]);
	// ..
	// btVector3 n1 = this->getVertexNormal(i);
	// btVector3 n2 = this->getVertexNormal(j);
	// btVector3 n3 = this->getVertexNormal(k);
	// printf("SmoothVehicleRaycaster: Trace Mesh Vertex Normals I: (%f x %f x %f) -> J: (%f x %f x %f) -> K: (%f x %f x %f)\n", n1.x(), n1.y(), n1.z(), n2.x(), n2.y(), n2.z(), n3.x(), n3.y(), n3.z());
	// ..
	// Interpolate from barycentric coordinates
	// ..
	btVector3 result = barry.x() * this->getVertexNormal(i) + barry.y() * this->getVertexNormal(j) + barry.z() * this->getVertexNormal(k);
	// ..
	// Transform back into world space
	// ..
	result = transform.getBasis() * result;
	result.normalize();
	mesh_interface->unLockReadOnlyVertexBase(subpart);
	return result;
}

btVector3 btSmoothTriangleMesh::barycentricCoordinates(const btVector3 &position, const btVector3 &p1, const btVector3 &p2, const btVector3 &p3)
{
	btVector3 edge1 = p2 - p1;
	btVector3 edge2 = p3 - p1;
	// Area of triangle ABC
	btScalar p1p2p3 = edge1.cross(edge2).length2();
	// Area of BCP
	btScalar p2p3p = (p3 - p2).cross(position - p2).length2(); // Area of CAP
	btScalar p3p1p = edge2.cross(position - p3).length2();
	btScalar s = btSqrt(p2p3p / p1p2p3);
	btScalar t = btSqrt(p3p1p / p1p2p3);
	btScalar w = 1.0f - s - t;
	return btVector3(s, t, w);
}

So you can see i have the vertex normal (triangle normal) right here

btVector3 result = barry.x() * this->getVertexNormal(i) + barry.y() * this->getVertexNormal(j) + barry.z() * this->getVertexNormal(k);

So you are saying somehow (if you can help with the how)… use that vertex normal instead of the collision normal… How and where would i do that ?

For you reference i am including a copy of the modified bullet classes i made so you can look them over… All my stuff starts with btSmooth at the bottom of each file

btRacycastVehicle.zip (11.7 KB)

1 Like

Yo @Raggar … Just trying to figure out what that means… are you saying just return the mesh vertex normal as the hit.normal ???

Unfortunately I haven’t played with Bullet for quite some time, as I’m using another physics engine for the time being. I’m not entirely sure, but I believe I used some of the modifications from STK, as I’ve used quite a few of their changes: Incorrect normal in collision with static triangle mesh · Issue #676 · bulletphysics/bullet3 · GitHub

Yo @Raggar … Thanks a bunch bro… That pointed me in the right direction. As i stated in post above. I create a btSmoothTriangleMesh that is basically btTriangleMesh with support for storing the mesh normal per vertex. I then use the triangle mesh normal to smooth out the raycast hit point using barycentric interpolation. So i simply added another function to btSmoothTriangleMesh to get the raw triangle normal as use that for the pre collision normal… And Bob’s Your Uncle :slight_smile:
Works great… I modified what super kart was doing in the btGjkPairDetector.cpp with this:

Work great thus far :slight_smile:

Take a look at what was happening (I lowered car body to exaggerate the collision with ground)

And using my Use Triangle Normals flag in the toolkit!

Screen Shot 2021-03-22 at 9.35.29 AM|690x386

The fixed internal edge issue

Thanks again :slight_smile:

3 Likes