PhysX Vehicle Project

The best Physics with love from PhysX

This project was both challenging and immensely satisfying which made it the perfect endeavor for me as a programmer. The intent was to integrate Nvidia PhysX SDK with Prodigy, my personal C++ game engine. For the purpose of this project, I used the static libraries provided by Nvidia to use the various features provided by PhysX.

Project Details

  • Role: Physics and Gameplay Programmer
  • Engine: Prodigy C++ Engine
  • Development Time: 2 Months
  • Language/Tools: C++ programming on the Prodigy Engine
Screenshot captured in ProtoPhysX project

The Integration Process

To integrate PhysX in my engine, I decided to use the static libraries provided and linked them as part of the third party libraries framework in my engine. This involved creating a custom PxPhysicsSystem class to serve as an interface for the rest of my engine to use.

I ended up using the DEBUG build configuration of PhysX with the DEBUG build setting of my engine project and used the RELEASE build configuration of PhysX with the RELEASE setting on my engine. Although PhysX also provides a CHECKED and PROFILE configuration, I found the need for only the DEBUG and RELEASE configurations for all intents of my project.

The only other setting I had to change was the Runtime Library setting for both my engine and game projects to use /MT and /MTd for the RELEASE and DEBUG builds configurations to work with the PhysX libraries.

Engine project properties in Visual Studio 2017

Rigid body Collisions

Once I got the libraries integrated and got the project compiling, the next step was naturally to make a bunch of rigid bodies to play with! I started off by trying to emulate the sample PhysX project in my engine using my DirectX 11 renderer. This was fairly straightforward but I was adamant about using my engine’s math classes as opposed to the PhysX math classes and struct. This meant that I had to write some code to implement some math conversions from my Vector 3 class to the PhysX Vector 3 class and vice versa. Since I handle all transformations in my engine using matrices, I found the need to also write conversions that gave me a Quaternion from a matrix so I could easily convert my transforms into transforms that could be provided to PhysX for use.

Below is the startup code I implemented to work in my PxPhysicsSystem. This allowed me to start up physics with the required instances of classes created and some sane defaults being used during setup.

void PhysXSystem::StartUp()
{
      //PhysX starts off by setting up a Physics Foundation
      m_PxFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, m_PxAllocator, m_PXErrorCallback);
      //Setup PhysX cooking if you need convex hull cooking support
      m_PxCooking = PxCreateCooking(PX_PHYSICS_VERSION, *m_PxFoundation, PxCookingParams(PxTolerancesScale()));
      //Create the PhysX Visual Debugger by giving it the current foundation
      m_Pvd = PxCreatePvd(*m_PxFoundation);
      
      //The PVD needs connection via a socket. It will run on the Address defined, in our case it's our machine
      PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(m_pvdIPAddress.c_str(), m_pvdPortNumber, m_pvdTimeOutSeconds);
      m_Pvd->connect(*transport, PxPvdInstrumentationFlag::eALL);
      //Create Physics! This creates an instance of the PhysX SDK
      m_PhysX = PxCreatePhysics(PX_PHYSICS_VERSION, *m_PxFoundation, PxTolerancesScale(), true, m_Pvd);
      PxInitExtensions(*m_PhysX, m_Pvd);
      //What is the description of this PhysX scene?
      PxSceneDesc sceneDesc(m_PhysX->getTolerancesScale());
      sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
      //This creates CPU dispatcher threads or worker threads. We will make 2
      m_PxDispatcher = PxDefaultCpuDispatcherCreate(2);
      sceneDesc.cpuDispatcher = m_PxDispatcher;
      sceneDesc.filterShader = PxDefaultSimulationFilterShader;
      //Create the scene now by passing the scene's description
      m_PxScene = m_PhysX->createScene(sceneDesc);
      PxPvdSceneClient* pvdClient = m_PxScene->getScenePvdClient();
      if (pvdClient)
      {
        //I have a PVD client, so set some flags that it needs
        pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
        pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
        pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
      }
      m_PxDefaultMaterial = m_PhysX->createMaterial(m_defaultStaticFriction, m_defaultDynamicFriction, m_defaultRestitution);
}
physx::PxRigidDynamic* PhysXSystem::CreateDynamicObject(const PxGeometry& pxGeometry, const Vec3& velocity, const Matrix44& matrix, float materialDensity)
{
	if (materialDensity < 0.f)
	{
		materialDensity = m_defaultDensity;
	}

	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();

	PxVec3 pxVelocity = PxVec3(velocity.x, velocity.y, velocity.z);
	Vec3 position = matrix.GetTBasis();
	PxVec3 pxPosition = VecToPxVector(position);

	PxTransform pxTransform(pxPosition);
	pxTransform.q = MakeQuaternionFromMatrix(matrix);

	PxRigidDynamic* dynamic = PxCreateDynamic(*physX, pxTransform, pxGeometry, *m_PxDefaultMaterial, materialDensity);
    dynamic->setAngularDamping(m_defaultAngularDamping);
	dynamic->setLinearVelocity(pxVelocity);
	pxScene->addActor(*dynamic);

	return dynamic;
}
void Game::CreatePhysXStack(const Vec3& position, uint size, float halfExtent)
{
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();

	PxTransform pxTransform = PxTransform(PxVec3(position.x, position.y, position.z));

	//We are going to make a stack of boxes
	PxBoxGeometry box = PxBoxGeometry((PxReal)halfExtent, (PxReal)halfExtent, (PxReal)halfExtent);
	PxMaterial* pxMaterial = physX->createMaterial(0.5f, 0.5f, 0.6f);
	PxShape* shape = physX->createShape(box, *pxMaterial);
	
	//Loop to stack everything in a pyramid shape
	for (PxU32 layerIndex = 0; layerIndex < size; layerIndex++)
	{
		for (PxU32 indexInLayer = 0; indexInLayer < size - layerIndex; indexInLayer++)
		{
			PxTransform localTm(PxVec3(PxReal(indexInLayer * 2) - PxReal(size - layerIndex), PxReal(layerIndex * 2 + 1), 0) * halfExtent);
			PxRigidDynamic* body = physX->createRigidDynamic(pxTransform.transform(localTm));
			body->attachShape(*shape);
			PxRigidBodyExt::updateMassAndInertia(*body, 10.0f);
			pxScene->addActor(*body);
		}
	}

	//release the shape now, we don't need it anymore since everything has been added to the PhysX scene
	shape->release();
}

Very quickly I was able to implement an interface that started up physics, provided me some math conversions as required and some functions to create and use rigid bodies. I was able to then create a simple scene with a bunch of boxes stacked in the scene similar to the sample provided by PhysX. I also took the liberty to add a convex hull in the scene to ensure I was able to support some convex hull collisions in my system.

Optimizing the renderer

While setting up PhysX in Prodigy, I found that my original renderer was set up to create any shape desired at the origin and make use of a model matrix to place the object at the required position in the scene. This meant that each object would have to be its own draw call that performed the binding of the model matrix, vertex, and index buffers and then perform the draw. With hundreds of objects, this meant that the CPU would have to wait to send data to the GPU for each object in the scene making my renderer very un-optimal.

To change this I re-wrote some code to handle the addition of vertices simpler and also allowed me to create a CPU side mesh that could be easily manipulated. The CPU mesh can then be transformed to the required location and then used to create a GPU side mesh that would be bound to the GPU before the draw. This allowed me to send all the boxes in 1 single draw call as 1 single mesh with a whole bunch of vertices placed at required positions in the scene.

Below is the code used by the game to render the PhysX actors in the scene.

void Game::RenderPhysXActors(const std::vector<PxRigidActor*> actors, int numActors, Rgba& color) const
{
	//Look for maximum of 10 shapes to draw per actor
	PxShape* shapes[10] = { nullptr };
	CPUMesh boxMesh;
	CPUMesh sphereMesh;
	CPUMesh cvxMesh;
	CPUMesh capMesh;
	for (int actorIndex = 0; actorIndex < numActors; actorIndex++)
	{
		const int numShapes = actors[actorIndex]->getNbShapes();
		actors[actorIndex]->getShapes(shapes, numShapes);
		const bool sleeping = actors[actorIndex]->is() ? actors[actorIndex]->is()->isSleeping() : false;
		for (int shapeIndex = 0; shapeIndex < numShapes; shapeIndex++)
		{
			int type = shapes[shapeIndex]->getGeometryType();
			switch (type)
			{
			case PxGeometryType::eBOX:
			{
				color = GetColorForGeometry(type, sleeping);
				AddMeshForPxCube(boxMesh, *actors[actorIndex], *shapes[shapeIndex], color);
			}
			break;
			case PxGeometryType::eSPHERE:
			{
				color = GetColorForGeometry(type, sleeping);
				AddMeshForPxSphere(sphereMesh, *actors[actorIndex], *shapes[shapeIndex], color);
			}
			break;
			case PxGeometryType::eCONVEXMESH:
			{
				color = GetColorForGeometry(type, sleeping);
				AddMeshForConvexMesh(cvxMesh, *actors[actorIndex], *shapes[shapeIndex], color);
			}
			break;
			case PxGeometryType::eCAPSULE:
			{
				color = GetColorForGeometry(type, sleeping);
				AddMeshForPxCapsule(capMesh, *actors[actorIndex], *shapes[shapeIndex], color);
			}
			break;
			default:
				break;
			}
		}
	}
	g_renderContext->SetModelMatrix(Matrix44::IDENTITY);
	if (boxMesh.GetVertexCount() > 0)
	{
		m_pxCube->CreateFromCPUMesh(&boxMesh, GPU_MEMORY_USAGE_STATIC);
		g_renderContext->DrawMesh(m_pxCube);
	}
	if (sphereMesh.GetVertexCount() > 0)
	{
		m_pxSphere->CreateFromCPUMesh(&sphereMesh, GPU_MEMORY_USAGE_STATIC);
		g_renderContext->BindShader(m_shader);
		g_renderContext->BindTextureViewWithSampler(0U, m_sphereTexture);
		g_renderContext->DrawMesh(m_pxSphere);
	}
	if (cvxMesh.GetVertexCount() > 0)
	{
		m_pxConvexMesh->CreateFromCPUMesh(&cvxMesh, GPU_MEMORY_USAGE_STATIC);
		g_renderContext->DrawMesh(m_pxConvexMesh);
	}
	if (capMesh.GetVertexCount() > 0)
	{
		m_pxCapMesh->CreateFromCPUMesh(&capMesh, GPU_MEMORY_USAGE_STATIC);
		g_renderContext->DrawMesh(m_pxCapMesh);
	}
}

PhysX Joints and Articulation

The next step was to add PhysX Joints and Articulation in my engine. This allowed me to test the various joints that were provided by PhysX in my project.

Here’s a video of the joints and articulation demo running in the scene. You can notice the drop in frame rate compared to the previous scene due to the added complexity of Joints and Articulation being added to the system.

Video showing RB physics, joints and articulation

Here is the interface after adding methods to create a few of the joints that are supported by Nvidia PhysX:

//Accessors for Vehicle SDK
	VehicleSceneQueryData*	GetVehicleSceneQueryData() const;
	PxVehicleDrivableSurfaceToTireFrictionPairs* GetVehicleTireFrictionPairs() const;

	//Rigid body Functions
	PxRigidDynamic*		CreateDynamicObject(const PxGeometry& pxGeometry, const Vec3& velocity, const Matrix44& matrix, float materialDensity = -1.f);

	//Convex Hull
	void				CreateRandomConvexHull(std::vector<Vec3>& vertexArray, int gaussMapLimit, bool directInsertion);
	PxConvexMesh*		CreateConvexMesh(const PxVec3* verts, const PxU32 numVerts, PxPhysics& physics, PxCooking& cooking);
	PxConvexMesh*		CreateWedgeConvexMesh(const PxVec3& halfExtents, PxPhysics& physics, PxCooking& cooking);
	PxConvexMesh*		CreateCuboidConvexMesh(const PxVec3& halfExtents, PxPhysics& physics, PxCooking& cooking);
	PxConvexMeshCookingType::Enum	GetPxConvexMeshCookingType(PhysXConvexMeshCookingTypes_T meshType);

	//PhysX Joints
	PxJoint*			CreateJointSimpleSpherical(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB);
	PxJoint*			CreateJointLimitedSpherical(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB
												, float yAngleLimit, float zAngleLimit, float contactDistance = -1.f);

	PxJoint*			CreateJointSimpleFixed(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB);
	PxJoint*			CreateJointBreakableFixed(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB
												, float breakForce = FLT_MAX, float breakTorque = FLT_MAX);

	PxJoint*			CreateJointDampedD6(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB
												, float driveStiffness, float driveDamping, float driveForceLimit = FLT_MAX, bool isDriveAcceleration = false);

	//PhysX Chains
	void				CreateSimpleSphericalChain(const Vec3& position, int length, const PxGeometry& geometry, float separation);
	void				CreateLimitedSphericalChain(const Vec3& position, int length, const PxGeometry& geometry, float separation
												, float yConeAngleLimit, float zConeAngleLimit, float coneContactDistance = -1.f);

	void				CreateSimpleFixedChain(const Vec3& position, int length, const PxGeometry& geometry, float separation);
	void				CreateBreakableFixedChain(const Vec3& position, int length, const PxGeometry& geometry, float separation
												, float breakForce = FLT_MAX, float breakTorque = FLT_MAX);

	void				CreateDampedD6Chain(const Vec3& position, int length, const PxGeometry& geometry, float separation
												, float driveStiffness, float driveDamping, float driveForceLimit = FLT_MAX, bool isDriveAcceleration = false);
	
	//PhysX Vehicle Utils
	PxRigidStatic*		AddStaticObstacle(const PxTransform& transform, const PxU32 numShapes, PxTransform* shapeTransforms, PxGeometry** shapeGeometries, PxMaterial** shapeMaterials);
	PxRigidDynamic*		AddDynamicObstacle(const PxTransform& transform, const PxF32 mass, const PxU32 numShapes, PxTransform* shapeTransforms, PxGeometry** shapeGeometries, PxMaterial** shapeMaterials);
	
void Game::CreatePhysXChains(const Vec3& position, int length, const PxGeometry& geometry, float separation)
{
	Vec3 offsetZ = Vec3(0.f, 0.f, 20.f);
	Vec3 offsetY = Vec3(0.f, 20.f, 0.f);

	g_PxPhysXSystem->CreateSimpleSphericalChain(position, length, geometry, separation);
	g_PxPhysXSystem->CreateLimitedSphericalChain(position + offsetY, length, geometry, separation, m_defaultConeFreedomY, m_defaultConeFreedomZ, m_defaultContactDistance);

	g_PxPhysXSystem->CreateSimpleFixedChain(position + offsetZ, length, geometry, separation);
	g_PxPhysXSystem->CreateBreakableFixedChain(position + offsetZ + offsetY, length, geometry, separation, m_defaultBreakForce, m_defaultBreakTorque);

	g_PxPhysXSystem->CreateDampedD6Chain(position + (offsetZ * 2.f), length, geometry, separation, m_defaultDriveStiffness, m_defaultDriveDamping, m_defaultDriveForceLimit, m_isDriveAccelerating);
}
//--------------------------------------------------------------------------------
// NOTE: Interface asks for angles in degrees but PhysX needs angles in Radians
//--------------------------------------------------------------------------------
physx::PxJoint* PhysXSystem::CreateJointLimitedSpherical(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB, float yAngleLimit, float zAngleLimit, float contactDistance)
{
	PxTransform transformA(VecToPxVector(positionA));
	PxTransform transformB(VecToPxVector(positionB));

	PxSphericalJoint* joint = PxSphericalJointCreate(*m_PhysX, actorA, transformA, actorB, transformB);
	joint->setLimitCone(PxJointLimitCone(DegreesToRadians(yAngleLimit), DegreesToRadians(zAngleLimit), contactDistance));
	joint->setSphericalJointFlag(PxSphericalJointFlag::eLIMIT_ENABLED, true);
	joint->setConstraintFlag(PxConstraintFlag::ePROJECTION, true);
	return joint;
}

//--------------------------------------------------------------------------------
// Unbreakable Fixed Joint
//--------------------------------------------------------------------------------
physx::PxJoint* PhysXSystem::CreateJointSimpleFixed(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB)
{
	PxTransform transformA(VecToPxVector(positionA));
	PxTransform transformB(VecToPxVector(positionB));

	PxFixedJoint* joint = PxFixedJointCreate(*m_PhysX, actorA, transformA, actorB, transformB);
	joint->setBreakForce(FLT_MAX, FLT_MAX);
	joint->setConstraintFlag(PxConstraintFlag::eDRIVE_LIMITS_ARE_FORCES, true);
	joint->setConstraintFlag(PxConstraintFlag::eDISABLE_PREPROCESSING, true);
	return joint;
}

//--------------------------------------------------------------------------------
// Unrestricted Spherical Joint (Rotation permitted to 180 degrees in y and z)
// NOTE: PhysX needs the angles in Radians, in the simple case we pass it PxHalfPi for y and z cone limits
//--------------------------------------------------------------------------------
physx::PxJoint* PhysXSystem::CreateJointSimpleSpherical(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB)
{
	PxTransform transformA(VecToPxVector(positionA));
	PxTransform transformB(VecToPxVector(positionB));

	PxSphericalJoint* joint = PxSphericalJointCreate(*m_PhysX, actorA, transformA, actorB, transformB);
	joint->setLimitCone(PxJointLimitCone(PxHalfPi, PxHalfPi));
	joint->setSphericalJointFlag(PxSphericalJointFlag::eLIMIT_ENABLED, true);
	return joint;
}

//--------------------------------------------------------------------------------
// NOTE: This creates a fixed joint which we set a break force to.
// By default the breakable joint is unbreakable(max float for break force)
//--------------------------------------------------------------------------------
physx::PxJoint* PhysXSystem::CreateJointBreakableFixed(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB, float breakForce, float breakTorque)
{
	PxTransform transformA(VecToPxVector(positionA));
	PxTransform transformB(VecToPxVector(positionB));

	PxFixedJoint* joint = PxFixedJointCreate(*m_PhysX, actorA, transformA, actorB, transformB);
	joint->setBreakForce(breakForce, breakTorque);
	joint->setConstraintFlag(PxConstraintFlag::eDRIVE_LIMITS_ARE_FORCES, true);
	joint->setConstraintFlag(PxConstraintFlag::eDISABLE_PREPROCESSING, true);
	return joint;
}

//--------------------------------------------------------------------------------
// NOTE: By default the max drive force limit is set to FLT_MAX and the drive is force dependent (Not acceleration dependent)
//--------------------------------------------------------------------------------
physx::PxJoint* PhysXSystem::CreateJointDampedD6(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB, float driveStiffness, float driveDamping, float driveForceLimit, bool isDriveAcceleration)
{
	PxTransform transformA(VecToPxVector(positionA));
	PxTransform transformB(VecToPxVector(positionB));

	PxD6Joint* joint = PxD6JointCreate(*m_PhysX, actorA, transformA, actorB, transformB);
	joint->setMotion(PxD6Axis::eSWING1, PxD6Motion::eFREE);
	joint->setMotion(PxD6Axis::eSWING2, PxD6Motion::eFREE);
	joint->setMotion(PxD6Axis::eTWIST, PxD6Motion::eFREE);
	joint->setDrive(PxD6Drive::eSLERP, PxD6JointDrive(driveStiffness, driveDamping, driveForceLimit, isDriveAcceleration));
	return joint;
}

void PhysXSystem::CreateSimpleSphericalChain(const Vec3& position, int length, const PxGeometry& geometry, float separation)
{
	PxTransform transform(PhysXSystem::VecToPxVector(position));
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();
	PxMaterial* pxMat = g_PxPhysXSystem->GetDefaultPxMaterial();

	PxVec3 offsetPx(separation / 2.f, 0, 0);
	Vec3 offset(separation / 2.f, 0.f, 0.f);
	PxTransform localTm(offsetPx);
	PxRigidDynamic* prev = nullptr;

	for (int i = 0; i < length; i++)
	{
		PxRigidDynamic* current = PxCreateDynamic(*physX, transform * localTm, geometry, *pxMat, 1.0f);

		if (prev == nullptr)
		{
			CreateJointSimpleSpherical(prev, position, current, offset * -1.f);
		}
		else
		{
			CreateJointSimpleSpherical(prev, offset, current, offset * -1.f);
		}

		pxScene->addActor(*current);
		prev = current;
		localTm.p.x += separation;
	}
}

//--------------------------------------------------------------------------------
void PhysXSystem::CreateSimpleFixedChain(const Vec3& position, int length, const PxGeometry& geometry, float separation)
{
	PxTransform transform(PhysXSystem::VecToPxVector(position));
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();
	PxMaterial* pxMat = g_PxPhysXSystem->GetDefaultPxMaterial();

	PxVec3 offsetPx(separation / 2.f, 0, 0);
	Vec3 offset(separation / 2.f, 0.f, 0.f);
	PxTransform localTm(offsetPx);
	PxRigidDynamic* prev = nullptr;

	for (int i = 0; i < length; i++)
	{
		PxRigidDynamic* current = PxCreateDynamic(*physX, transform * localTm, geometry, *pxMat, 1.0f);

		if (prev == nullptr)
		{
			CreateJointSimpleFixed(prev, position, current, offset * -1.f);
		}
		else
		{
			CreateJointSimpleFixed(prev, offset, current, offset * -1.f);
		}

		pxScene->addActor(*current);
		prev = current;
		localTm.p.x += separation;
	}
}

//--------------------------------------------------------------------------------
void PhysXSystem::CreateLimitedSphericalChain(const Vec3& position, int length, const PxGeometry& geometry, float separation, float yConeAngleLimit, float zConeAngleLimit, float coneContactDistance /*= -1.f*/)
{
	PxTransform transform(PhysXSystem::VecToPxVector(position));
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();
	PxMaterial* pxMat = g_PxPhysXSystem->GetDefaultPxMaterial();

	PxVec3 offsetPx(separation / 2.f, 0, 0);
	Vec3 offset(separation / 2.f, 0.f, 0.f);
	PxTransform localTm(offsetPx);
	PxRigidDynamic* prev = nullptr;

	for (int i = 0; i < length; i++)
	{
		PxRigidDynamic* current = PxCreateDynamic(*physX, transform * localTm, geometry, *pxMat, 1.0f);

		if (prev == nullptr)
		{
			CreateJointLimitedSpherical(prev, position, current, offset * -1.f, yConeAngleLimit, zConeAngleLimit, coneContactDistance);
		}
		else
		{
			CreateJointLimitedSpherical(prev, offset, current, offset * -1.f, yConeAngleLimit, zConeAngleLimit, coneContactDistance);
		}

		pxScene->addActor(*current);
		prev = current;
		localTm.p.x += separation;
	}
}

//--------------------------------------------------------------------------------
void PhysXSystem::CreateBreakableFixedChain(const Vec3& position, int length, const PxGeometry& geometry, float separation, float breakForce /*= FLT_MAX*/, float breakTorque /*= FLT_MAX*/)
{
	PxTransform transform(PhysXSystem::VecToPxVector(position));
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();
	PxMaterial* pxMat = g_PxPhysXSystem->GetDefaultPxMaterial();

	PxVec3 offsetPx(separation / 2.f, 0, 0);
	Vec3 offset(separation / 2.f, 0.f, 0.f);
	PxTransform localTm(offsetPx);
	PxRigidDynamic* prev = nullptr;

	for (int i = 0; i < length; i++)
	{
		PxRigidDynamic* current = PxCreateDynamic(*physX, transform * localTm, geometry, *pxMat, 1.0f);

		if (prev == nullptr)
		{
			CreateJointBreakableFixed(prev, position, current, offset * -1.f, breakForce, breakTorque);
		}
		else
		{
			CreateJointBreakableFixed(prev, offset, current, offset * -1.f, breakForce, breakTorque);
		}

		pxScene->addActor(*current);
		prev = current;
		localTm.p.x += separation;
	}
}

//--------------------------------------------------------------------------------
void PhysXSystem::CreateDampedD6Chain(const Vec3& position, int length, const PxGeometry& geometry, float separation, float driveStiffness, float driveDamping, float driveForceLimit /*= FLT_MAX*/, bool isDriveAcceleration /*= false*/)
{
	PxTransform transform(PhysXSystem::VecToPxVector(position));
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxScene* pxScene = g_PxPhysXSystem->GetPhysXScene();
	PxMaterial* pxMat = g_PxPhysXSystem->GetDefaultPxMaterial();

	PxVec3 offsetPx(separation / 2.f, 0, 0);
	Vec3 offset(separation / 2.f, 0.f, 0.f);
	PxTransform localTm(offsetPx);
	PxRigidDynamic* prev = nullptr;

	for (int i = 0; i < length; i++)
	{
		PxRigidDynamic* current = PxCreateDynamic(*physX, transform * localTm, geometry, *pxMat, 1.0f);

		if (prev == nullptr)
		{
			CreateJointDampedD6(prev, position, current, offset * -1.f, driveStiffness, driveDamping, driveForceLimit, isDriveAcceleration);
		}
		else
		{
			CreateJointDampedD6(prev, offset, current, offset * -1.f, driveStiffness, driveDamping, driveForceLimit, isDriveAcceleration);
		}

		pxScene->addActor(*current);
		prev = current;
		localTm.p.x += separation;
	}
}

Below is a section of code I wrote to handle the creation of the D6 joint using a spring drive:

physx::PxJoint* PhysXSystem::CreateJointDampedD6(PxRigidActor* actorA, const Vec3& positionA, PxRigidActor* actorB, const Vec3& positionB, float driveStiffness, float driveDamping, float driveForceLimit, bool isDriveAcceleration)
{
	PxTransform transformA(VecToPxVector(positionA));
	PxTransform transformB(VecToPxVector(positionB));
	PxD6Joint* joint = PxD6JointCreate(*m_PhysX, actorA, transformA, actorB, transformB);
	joint->setMotion(PxD6Axis::eSWING1, PxD6Motion::eFREE);
	joint->setMotion(PxD6Axis::eSWING2, PxD6Motion::eFREE);
	joint->setMotion(PxD6Axis::eTWIST, PxD6Motion::eFREE);
	joint->setDrive(PxD6Drive::eSLERP, PxD6JointDrive(driveStiffness, driveDamping, driveForceLimit, isDriveAcceleration));
	return joint;
}

Vehicle SDK and some cool drifts!

The vehicle SDK was the most challenging part of this project. About 4 weeks of the 8 weeks spent on this project were spent trying to use the Vehicle SDK with my code base and attempting to create a drivable 4 wheeled car in the scene.

Here’s what that ended up looking like:

I started off trying to keep this simple and only implementing the sample vehicle from the PhysX sample projects. This soon evolved into a major overhaul of that sample to create a fun drivable car interacting with obstacles in the scene and driving over ramps!

Here is some of the implementation:

PxVehicleDrive4W* PhysXSystem::StartUpVehicleSDK()
{
	//--------------------------------------------------------------------------------
	// Vehicle SDK Setup
	//--------------------------------------------------------------------------------
	m_vehicleKitEnabled = true;

	m_PxScene->release();
	m_PxScene = nullptr;
	PxSceneDesc sceneDesc(m_PhysX->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	m_PxDispatcher = PxDefaultCpuDispatcherCreate(1);
	sceneDesc.cpuDispatcher = m_PxDispatcher;
	//sceneDesc.filterShader = PxDefaultSimulationFilterShader;
	sceneDesc.filterShader = VehicleFilterShader;
	sceneDesc.contactModifyCallback = &gWheelContactModifyCallback;			//Enable contact modification
	sceneDesc.ccdContactModifyCallback = &gWheelCCDContactModifyCallback;	//Enable ccd contact modification
	sceneDesc.flags |= PxSceneFlag::eENABLE_CCD;							//Enable ccd
	m_PxScene = m_PhysX->createScene(sceneDesc);

	PxInitVehicleSDK(*m_PhysX);
	PxVehicleSetBasisVectors(PxVec3(0, 1, 0), PxVec3(0, 0, 1));
	PxVehicleSetUpdateMode(PxVehicleUpdateMode::eVELOCITY_CHANGE);
	PxVehicleSetSweepHitRejectionAngles(POINT_REJECT_ANGLE, NORMAL_REJECT_ANGLE);
	PxVehicleSetMaxHitActorAcceleration(MAX_ACCELERATION);

	//Create the batched scene queries for the suspension sweeps.
	//Use the post-filter shader to reject hit shapes that overlap the swept wheel at the start pose of the sweep.
	PxQueryHitType::Enum(*sceneQueryPreFilter)(PxFilterData, PxFilterData, const void*, PxU32, PxHitFlags&);
	PxQueryHitType::Enum(*sceneQueryPostFilter)(PxFilterData, PxFilterData, const void*, PxU32, const PxQueryHit&);
#if BLOCKING_SWEEPS
	sceneQueryPreFilter = &WheelSceneQueryPreFilterBlocking;
	sceneQueryPostFilter = &WheelSceneQueryPostFilterBlocking;
#else
	sceneQueryPreFilter = &WheelSceneQueryPreFilterNonBlocking;
	sceneQueryPostFilter = &WheelSceneQueryPostFilterNonBlocking;
#endif 

	//Create the batched scene queries for the suspension raycasts.
	m_vehicleSceneQueryData = VehicleSceneQueryData::allocate(m_numberOfVehicles, PX_MAX_NB_WHEELS, gNumQueryHitsPerWheel, m_numberOfVehicles, sceneQueryPreFilter, sceneQueryPostFilter, m_PxAllocator);
	m_batchQuery = VehicleSceneQueryData::setUpBatchedSceneQuery(0, *m_vehicleSceneQueryData, m_PxScene);

	//Create the friction table for each combination of tire and surface type.
	m_frictionPairs = createFrictionPairs(m_PxDefaultMaterial);

	//Create a plane to drive on.
	PxFilterData groundPlaneSimFilterData(COLLISION_FLAG_GROUND, COLLISION_FLAG_GROUND_AGAINST, 0, 0);
	m_drivableGroundPlane = createDrivablePlane(groundPlaneSimFilterData, m_PxDefaultMaterial, m_PhysX);
	m_PxScene->addActor(*m_drivableGroundPlane);
	
	//Create a vehicle that will drive on the plane.
	PxFilterData chassisSimFilterData(COLLISION_FLAG_CHASSIS, COLLISION_FLAG_GROUND, 0, 0);
	PxFilterData wheelSimFilterData(COLLISION_FLAG_WHEEL, COLLISION_FLAG_WHEEL, PxPairFlag::eDETECT_CCD_CONTACT | PxPairFlag::eMODIFY_CONTACTS, 0);
	VehicleDesc vehicleDesc = InitializeVehicleDescription(chassisSimFilterData, wheelSimFilterData);
	PxVehicleDrive4W* vehicleReference = createVehicle4W(vehicleDesc, m_PhysX, m_PxCooking);
	PxTransform startTransform(PxVec3(0, (vehicleDesc.chassisDims.y*0.5f + vehicleDesc.wheelRadius + 1.0f), 0), PxQuat(PxIdentity));
	vehicleReference->getRigidDynamicActor()->setGlobalPose(startTransform);
	vehicleReference->getRigidDynamicActor()->setRigidBodyFlag(PxRigidBodyFlag::eENABLE_CCD, true);
	m_PxScene->addActor(*vehicleReference->getRigidDynamicActor());

	//Set the vehicle to rest in first gear.
	//Set the vehicle to use auto-gears.
	vehicleReference->setToRestState();
	vehicleReference->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
	vehicleReference->mDriveDynData.setUseAutoGears(true);

	//Return the reference to the car that was created
	return vehicleReference;
}
void Game::CreatePhysXVehicleRamp()
{
	PxPhysics* physX = g_PxPhysXSystem->GetPhysXSDK();
	PxCooking* pxCooking = g_PxPhysXSystem->GetPhysXCookingModule();
	PxMaterial* pxMaterial = g_PxPhysXSystem->GetDefaultPxMaterial();

	//Add a really big ramp to jump over the car stack.
	{
		PxVec3 halfExtentsRamp(5.0f, 1.9f, 7.0f);
		PxConvexMeshGeometry geomRamp(g_PxPhysXSystem->CreateWedgeConvexMesh(halfExtentsRamp, *physX, *pxCooking));
		PxTransform shapeTransforms[1] = { PxTransform(PxIdentity) };
		PxMaterial* shapeMaterials[1] = { pxMaterial };
		PxGeometry* shapeGeometries[1] = { &geomRamp };

		Matrix44 bigRampModel;
		bigRampModel.MakeTranslation3D(Vec3(-10.f, 0.f, 0.f));
		Matrix44 rotation = Matrix44::MakeYRotationDegrees(180.f);
		bigRampModel = bigRampModel.AppendMatrix(rotation);

		PxTransform tRamp(g_PxPhysXSystem->VecToPxVector(bigRampModel.GetTBasis()), g_PxPhysXSystem->MakeQuaternionFromMatrix(bigRampModel) );
		g_PxPhysXSystem->AddStaticObstacle(tRamp, 1, shapeTransforms, shapeGeometries, shapeMaterials);
	}

	//Add two ramps side by side with a gap in between
	{
		PxVec3 halfExtents(3.0f, 1.5f, 3.5f);
		PxConvexMeshGeometry geometry(g_PxPhysXSystem->CreateWedgeConvexMesh(halfExtents, *physX, *pxCooking));
		PxTransform shapeTransforms[1] = { PxTransform(PxIdentity) };
		PxMaterial* shapeMaterials[1] = { pxMaterial };
		PxGeometry* shapeGeometries[1] = { &geometry };
		PxTransform t1(PxVec3(-60.f, 0.f, 0.f), PxQuat(0.000013f, -0.406322f, 0.000006f, 0.913730f));
		g_PxPhysXSystem->AddStaticObstacle(t1, 1, shapeTransforms, shapeGeometries, shapeMaterials);
		PxTransform t2(PxVec3(-80, 0.f, 0.f), PxQuat(0.000013f, -0.406322f, 0.000006f, 0.913730f));
		g_PxPhysXSystem->AddStaticObstacle(t2, 1, shapeTransforms, shapeGeometries, shapeMaterials);
	}
}

Taking it to the next level

Okay so clearly I know that I have a car in the scene that I can drive! I now need the car to be cool. To do that I added a new car mesh, wrote a nicer car controller, added a car camera that implemented some lerping to create the effect of a smooth follow. Finally, I added some more obstacles and drivable ramps in the scene.

Video of the final Demo project

The car controller was designed to use the Xbox controller to provide PhysX with analog input that could be used in driving the car. I bound the right trigger to be the throttle, the left analog stick to steer the car and the A key to be the brakes, while B key was the hand brake.

Below is the interface I created for the Car Controller that allowed me to create a nice smooth follow camera for the car:

class CarController
{
public:
	CarController();
	~CarController();

	void	SetupVehicle();
	void	SetDigitalControlMode(bool digitalControlEnabled);

	bool	IsDigitalInputEnabled() const;

	void	Update(float deltaTime);
	void	UpdateInputs();
	void	VehiclePhysicsUpdate(float deltaTime);

	//Vehicle Getters
	PxVehicleDrive4W* GetVehicle() const;
	PxVehicleDrive4WRawInputData* GetVehicleInputData() const;
	Vec3	GetVehiclePosition() const;
	Vec3	GetVehicleForwardBasis() const;

	//Vehicle Controls
	void	AccelerateForward(float analogAcc = 0.f);
	void	AccelerateReverse(float analogAcc = 0.f);
	void	Brake();

	void	Steer(float analogSteer = 0.f);
	void	Handbrake();

	void	ReleaseAllControls();
	void	ReleaseVehicle();

private:
	bool		m_digitalControlEnabled = false;
	bool		m_isVehicleInAir = false;

	PxVehicleDrive4W*					m_vehicle4W = nullptr;
	PxVehicleDrive4WRawInputData*		m_vehicleInputData = nullptr;
}

The car camera is also fairly simple. It only makes sure I am at a certain distance away from the car. The update function takes the car’s forward direction as a parameter to determine how far along the negative of the direction and how high up in the world up direction it needs to be. This focus point was then lerped to with time to create a smooth follow effect.

Below you can see the interface for the CarCamera

class CarCamera : public Camera
{
public:

	CarCamera();
	~CarCamera();

	void Update(const Vec3& carForward, float deltaTime);
	void SetFocalPoint(Vec3 const &pos);
	void SetZoom(float zoom); //Manipulates distance
	void SetAngleOffset(float angleOffset); // really is setting an angle offset
	void SetZoomDelta(float delta);

	void SetTiltValue(float tilt);
	void SetAngleValue(float angle);
	void SetHeightValue(float height);
	void SetDistanceValue(float distance);
	void SetLerpSpeed(float lerpSpeed);

	float GetAngleValue() const;
	float GetTiltValue() const;
	float GetHeightValue() const;
	float GetDistanceValue() const;
	float GetLerpSpeed() const;

private:
	Vec3			m_focalPoint = Vec3::ZERO;
	float			m_distance = 5.f;
	float			m_height = 2.f;

	float			m_currentZoom = 0.f;

	// configuration - mess with these numbers to get a view you like; 
	float			m_minDistance = 1.0f;
	float			m_maxDistance = 32.0f;

	float			m_defaultAngle = 90.0f;
	float			m_defaultTilt = -70.0f;
	float			m_lerpSpeed = 6.f;

	Vec2			m_tiltBounds = Vec2(10.f, 40.f);

	Vec3			m_camPosition = Vec3::ZERO;
	Vec3			m_targetPosition = Vec3::ZERO;

	Matrix44		m_modelMatrix = Matrix44::IDENTITY;

	//The actual tilt and angle for the camera
	float			m_tilt;
	float			m_angle;
};
void Game::UpdateImGUIPhysXWidget()
{
	//Use this place to create/update info for imGui

	//Read Cam Position
	ui_camPosition[0] = m_camPosition.x;
	ui_camPosition[1] = m_camPosition.y;
	ui_camPosition[2] = m_camPosition.z;

	ui_dirLight[0] = m_directionalLightPos.x;
	ui_dirLight[1] = m_directionalLightPos.y;
	ui_dirLight[2] = m_directionalLightPos.z;

	ui_dynamicSpawnPos[0] = m_dynamicSpawnPos.x;
	ui_dynamicSpawnPos[1] = m_dynamicSpawnPos.y;
	ui_dynamicSpawnPos[2] = m_dynamicSpawnPos.z;

	ui_dynamicVelocity[0] = m_dynamicDropVelocity.x;
	ui_dynamicVelocity[1] = m_dynamicDropVelocity.y;
	ui_dynamicVelocity[2] = m_dynamicDropVelocity.z;

	Vec3 cameraAngle = m_mainCamera->GetEuler();
	float cameraAngleFloat[3];
	cameraAngleFloat[0] = cameraAngle.x;
	cameraAngleFloat[1] = cameraAngle.y;
	cameraAngleFloat[2] = cameraAngle.z;

	ui_camAngle = m_carCamera->GetAngleValue();
	ui_camTilt = m_carCamera->GetTiltValue();
	ui_camHeight = m_carCamera->GetHeightValue();
	ui_camDistance = m_carCamera->GetDistanceValue();
	ui_camLerpSpeed = m_carCamera->GetLerpSpeed();

	ImGui::Begin("PhysX Scene Controls");

	ImGui::ColorEdit3("Clear Color", (float*)&ui_cameraClearColor); 
	ImGui::DragFloat3("Main Camera Position", ui_camPosition);
	ImGui::DragFloat3("Main Camera Angle", cameraAngleFloat);
	ImGui::DragFloat3("Light Direction", ui_dirLight);
	ImGui::DragFloat3("Dynamic Spawn Position", ui_dynamicSpawnPos);
	ImGui::DragFloat3("Dynamic Spawn velocity", ui_dynamicVelocity);

	ImGui::DragFloat("Camera Angle", &ui_camAngle);
	ImGui::DragFloat("Camera Tilt", &ui_camTilt);
	ImGui::DragFloat("Camera Height", &ui_camHeight);
	ImGui::DragFloat("Camera Distance", &ui_camDistance);
	ImGui::DragFloat("Camera Lerp Speed", &ui_camLerpSpeed);

	ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);

	//Write CamPos
	m_camPosition.x = ui_camPosition[0];
	m_camPosition.y = ui_camPosition[1];
	m_camPosition.z = ui_camPosition[2];

	m_directionalLightPos.x = ui_dirLight[0];
	m_directionalLightPos.y = ui_dirLight[1];
	m_directionalLightPos.z = ui_dirLight[2];

	m_dynamicSpawnPos.x = ui_dynamicSpawnPos[0];
	m_dynamicSpawnPos.y = ui_dynamicSpawnPos[1];
	m_dynamicSpawnPos.z = ui_dynamicSpawnPos[2];

	m_dynamicDropVelocity.x = ui_dynamicVelocity[0];
	m_dynamicDropVelocity.y = ui_dynamicVelocity[1];
	m_dynamicDropVelocity.z = ui_dynamicVelocity[2];

	m_directionalLightPos.Normalize();

	m_carCamera->SetAngleValue(ui_camAngle);
	m_carCamera->SetTiltValue(ui_camTilt);
	m_carCamera->SetHeightValue(ui_camHeight);
	m_carCamera->SetDistanceValue(ui_camDistance);
	m_carCamera->SetLerpSpeed(ui_camLerpSpeed);

	ImGui::End();
}

The final implementation includes some interesting obstacles and ramps that created a nice Vehicle Zoo to use with the car that was created.

Retrospectives:

Things that went well! 🙂

  • Integrated both PhysX and Vehicle SDKs successfully with the codebase
  • Implemented core features with scope for future expansion and feature addition
  • Visual Debugger setup early on saved time during debugging
  • Setting up ImGUI made debugging a lot easier and allowed finding good defaults faster

Things that went wrong 🙁

  • Feature creep with joints and articulation took away time from other core features
  • Over-scoped project early on resulting in cuts on game features
  • Poor understanding of filter shaders resulted with valuable time lost towards the end stage
  • Lack of support in the current interface for height fields meant the use of only flat surfaces

Things I would do differently the next time 🙂

  • Concentrate on 1 core feature rather than building an interface supporting multiple features
  • Take a more goal-oriented approach to development.
  • Spend time compiling source rather than using the static libraries to gain a deeper understanding of the framework