Browse Source

Ability to override gravity for vehicles (#1107)

This allows creating vehicles that can e.g. stick to the surface of a track and drive upside down.
Added a new vehicle test scene called 'Loop'.
Jorrit Rouwe 1 year ago
parent
commit
6e9df72234

+ 5 - 0
Docs/ReleaseNotes.md

@@ -24,6 +24,11 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 
 
 * Added CharacterBaseSettings::mEnhancedInternalEdgeRemoval (default false) that allows smoother movement for both the Character and CharacterVirtual class.
 * Added CharacterBaseSettings::mEnhancedInternalEdgeRemoval (default false) that allows smoother movement for both the Character and CharacterVirtual class.
 
 
+
+#### Vehicles
+
+* Added ability to override the gravity vector per vehicle. This allows creating vehicles that can e.g. stick to the surface of a track and drive upside down. See VehicleConstraint::OverrideGravity.
+
 #### Various
 #### Various
 
 
 * Replaced std::vector with a custom Array class. std::vector initializes memory to zero which causes an undesired performance overhead.
 * Replaced std::vector with a custom Array class. std::vector initializes memory to zero which causes an undesired performance overhead.

+ 1 - 1
Jolt/Physics/Vehicle/MotorcycleController.cpp

@@ -146,7 +146,7 @@ void MotorcycleController::PreCollide(float inDeltaTime, PhysicsSystem &inPhysic
 	// TurnRadius = WheelBase / (Sin(SteerAngle) * Cos(CasterAngle))
 	// TurnRadius = WheelBase / (Sin(SteerAngle) * Cos(CasterAngle))
 	// => SteerAngle = ASin(WheelBase * Tan(LeanAngle) * Gravity / (Velocity^2 * Cos(CasterAngle))
 	// => SteerAngle = ASin(WheelBase * Tan(LeanAngle) * Gravity / (Velocity^2 * Cos(CasterAngle))
 	// The caster angle is different for each wheel so we can only calculate part of the equation here
 	// The caster angle is different for each wheel so we can only calculate part of the equation here
-	float max_steer_angle_factor = wheel_base * Tan(mMaxLeanAngle) * inPhysicsSystem.GetGravity().Length();
+	float max_steer_angle_factor = wheel_base * Tan(mMaxLeanAngle) * (mConstraint.IsGravityOverridden()? mConstraint.GetGravityOverride() : inPhysicsSystem.GetGravity()).Length();
 
 
 	// Calculate forward velocity
 	// Calculate forward velocity
 	float velocity = body->GetLinearVelocity().Dot(forward);
 	float velocity = body->GetLinearVelocity().Dot(forward);

+ 18 - 2
Jolt/Physics/Vehicle/VehicleConstraint.cpp

@@ -165,8 +165,24 @@ void VehicleConstraint::OnStep(float inDeltaTime, PhysicsSystem &inPhysicsSystem
 	if (mPreStepCallback != nullptr)
 	if (mPreStepCallback != nullptr)
 		mPreStepCallback(*this, inDeltaTime, inPhysicsSystem);
 		mPreStepCallback(*this, inDeltaTime, inPhysicsSystem);
 
 
-	// Calculate new world up vector by inverting gravity
-	mWorldUp = (-inPhysicsSystem.GetGravity()).NormalizedOr(mWorldUp);
+	if (mIsGravityOverridden)
+	{
+		// If gravity is overridden, we replace the normal gravity calculations
+		if (mBody->IsActive())
+		{
+			MotionProperties *mp = mBody->GetMotionProperties();
+			mp->SetGravityFactor(0.0f);
+			mBody->AddForce(mGravityOverride / mp->GetInverseMass());
+		}
+
+		// And we calculate the world up using the custom gravity
+		mWorldUp = (-mGravityOverride).NormalizedOr(mWorldUp);
+	}
+	else
+	{
+		// Calculate new world up vector by inverting gravity
+		mWorldUp = (-inPhysicsSystem.GetGravity()).NormalizedOr(mWorldUp);
+	}
 
 
 	// Callback on our controller
 	// Callback on our controller
 	mController->PreCollide(inDeltaTime, inPhysicsSystem);
 	mController->PreCollide(inDeltaTime, inPhysicsSystem);

+ 10 - 0
Jolt/Physics/Vehicle/VehicleConstraint.h

@@ -108,6 +108,12 @@ public:
 	const StepCallback &		GetPostStepCallback() const					{ return mPostStepCallback; }
 	const StepCallback &		GetPostStepCallback() const					{ return mPostStepCallback; }
 	void						SetPostStepCallback(const StepCallback &inPostStepCallback) { mPostStepCallback = inPostStepCallback; }
 	void						SetPostStepCallback(const StepCallback &inPostStepCallback) { mPostStepCallback = inPostStepCallback; }
 
 
+	/// Override gravity for this vehicle. Note that overriding gravity will set the gravity factor of the vehicle body to 0 and apply gravity in the PhysicsStepListener instead.
+	void						OverrideGravity(Vec3Arg inGravity)			{ mGravityOverride = inGravity; mIsGravityOverridden = true; }
+	bool						IsGravityOverridden() const					{ return mIsGravityOverridden; }
+	Vec3						GetGravityOverride() const					{ return mGravityOverride; }
+	void						ResetGravityOverride()						{ mIsGravityOverridden = false; mBody->GetMotionProperties()->SetGravityFactor(1.0f); } ///< Note that resetting the gravity override will restore the gravity factor of the vehicle body to 1.
+
 	/// Get the local space forward vector of the vehicle
 	/// Get the local space forward vector of the vehicle
 	Vec3						GetLocalForward() const						{ return mForward; }
 	Vec3						GetLocalForward() const						{ return mForward; }
 
 
@@ -198,6 +204,10 @@ private:
 	// Calculate the constraint properties for mPitchRollPart
 	// Calculate the constraint properties for mPitchRollPart
 	void						CalculatePitchRollConstraintProperties(RMat44Arg inBodyTransform);
 	void						CalculatePitchRollConstraintProperties(RMat44Arg inBodyTransform);
 
 
+	// Gravity override
+	bool						mIsGravityOverridden = false;				///< If the gravity is currently overridden
+	Vec3						mGravityOverride = Vec3::sZero();			///< Gravity override value, replaces PhysicsSystem::GetGravity() when mIsGravityOverridden is true
+
 	// Simulation information
 	// Simulation information
 	Body *						mBody;										///< Body of the vehicle
 	Body *						mBody;										///< Body of the vehicle
 	Vec3						mForward;									///< Local space forward vector for the vehicle
 	Vec3						mForward;									///< Local space forward vector for the vehicle

+ 19 - 1
Samples/Tests/Vehicle/MotorcycleTest.cpp

@@ -5,8 +5,11 @@
 #include <TestFramework.h>
 #include <TestFramework.h>
 
 
 #include <Tests/Vehicle/MotorcycleTest.h>
 #include <Tests/Vehicle/MotorcycleTest.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
 #include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
+#include <Jolt/Physics/Collision/ShapeCast.h>
+#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
 #include <Jolt/Physics/Vehicle/MotorcycleController.h>
 #include <Jolt/Physics/Vehicle/MotorcycleController.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Application/DebugUI.h>
 #include <Application/DebugUI.h>
@@ -198,6 +201,21 @@ void MotorcycleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	controller->SetDriverInput(mForward, mRight, mBrake, false);
 	controller->SetDriverInput(mForward, mRight, mBrake, false);
 	controller->EnableLeanController(sEnableLeanController);
 	controller->EnableLeanController(sEnableLeanController);
 
 
+	if (sOverrideGravity)
+	{
+		// When overriding gravity is requested, we cast a sphere downwards (opposite to the previous up position) and use the contact normal as the new gravity direction
+		SphereShape sphere(0.5f);
+		sphere.SetEmbedded();
+		RShapeCast shape_cast(&sphere, Vec3::sReplicate(1.0f), RMat44::sTranslation(mMotorcycleBody->GetPosition()), -3.0f * mVehicleConstraint->GetWorldUp());
+		ShapeCastSettings settings;
+		ClosestHitCollisionCollector<CastShapeCollector> collector;
+		mPhysicsSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, mMotorcycleBody->GetPosition(), collector, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING));
+		if (collector.HadHit())
+			mVehicleConstraint->OverrideGravity(9.81f * collector.mHit.mPenetrationAxis.Normalized());
+		else
+			mVehicleConstraint->ResetGravityOverride();
+	}
+
 	// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
 	// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
 	for (uint w = 0; w < 2; ++w)
 	for (uint w = 0; w < 2; ++w)
 	{
 	{
@@ -207,7 +225,6 @@ void MotorcycleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	}
 	}
 }
 }
 
 
-
 void MotorcycleTest::SaveInputState(StateRecorder &inStream) const
 void MotorcycleTest::SaveInputState(StateRecorder &inStream) const
 {
 {
 	inStream.Write(mForward);
 	inStream.Write(mForward);
@@ -254,5 +271,6 @@ void MotorcycleTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
 	inUI->CreateCheckBox(inSubMenu, "Override Front Suspension Force Point", sOverrideFrontSuspensionForcePoint, [](UICheckBox::EState inState) { sOverrideFrontSuspensionForcePoint = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Override Front Suspension Force Point", sOverrideFrontSuspensionForcePoint, [](UICheckBox::EState inState) { sOverrideFrontSuspensionForcePoint = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Override Rear Suspension Force Point", sOverrideRearSuspensionForcePoint, [](UICheckBox::EState inState) { sOverrideRearSuspensionForcePoint = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Override Rear Suspension Force Point", sOverrideRearSuspensionForcePoint, [](UICheckBox::EState inState) { sOverrideRearSuspensionForcePoint = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enable Lean Controller", sEnableLeanController, [](UICheckBox::EState inState) { sEnableLeanController = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enable Lean Controller", sEnableLeanController, [](UICheckBox::EState inState) { sEnableLeanController = inState == UICheckBox::STATE_CHECKED; });
+	inUI->CreateCheckBox(inSubMenu, "Override Gravity", sOverrideGravity, [](UICheckBox::EState inState) { sOverrideGravity = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateTextButton(inSubMenu, "Accept", [this]() { RestartTest(); });
 	inUI->CreateTextButton(inSubMenu, "Accept", [this]() { RestartTest(); });
 }
 }

+ 1 - 0
Samples/Tests/Vehicle/MotorcycleTest.h

@@ -35,6 +35,7 @@ private:
 	static inline bool			sOverrideFrontSuspensionForcePoint = false;	///< If true, the front suspension force point is overridden
 	static inline bool			sOverrideFrontSuspensionForcePoint = false;	///< If true, the front suspension force point is overridden
 	static inline bool			sOverrideRearSuspensionForcePoint = false;	///< If true, the rear suspension force point is overridden
 	static inline bool			sOverrideRearSuspensionForcePoint = false;	///< If true, the rear suspension force point is overridden
 	static inline bool			sEnableLeanController = true;				///< If true, the lean controller is enabled
 	static inline bool			sEnableLeanController = true;				///< If true, the lean controller is enabled
+	static inline bool			sOverrideGravity = false;					///< If true, gravity is overridden to always oppose the ground normal
 
 
 	Body *						mMotorcycleBody;							///< The vehicle
 	Body *						mMotorcycleBody;							///< The vehicle
 	Ref<VehicleConstraint>		mVehicleConstraint;							///< The vehicle constraint
 	Ref<VehicleConstraint>		mVehicleConstraint;							///< The vehicle constraint

+ 19 - 0
Samples/Tests/Vehicle/VehicleConstraintTest.cpp

@@ -6,7 +6,10 @@
 
 
 #include <Tests/Vehicle/VehicleConstraintTest.h>
 #include <Tests/Vehicle/VehicleConstraintTest.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
 #include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
+#include <Jolt/Physics/Collision/ShapeCast.h>
+#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
 #include <Jolt/Physics/Vehicle/WheeledVehicleController.h>
 #include <Jolt/Physics/Vehicle/WheeledVehicleController.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Application/DebugUI.h>
 #include <Application/DebugUI.h>
@@ -241,6 +244,21 @@ void VehicleConstraintTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	// Set the collision tester
 	// Set the collision tester
 	mVehicleConstraint->SetVehicleCollisionTester(mTesters[sCollisionMode]);
 	mVehicleConstraint->SetVehicleCollisionTester(mTesters[sCollisionMode]);
 
 
+	if (sOverrideGravity)
+	{
+		// When overriding gravity is requested, we cast a sphere downwards (opposite to the previous up position) and use the contact normal as the new gravity direction
+		SphereShape sphere(0.5f);
+		sphere.SetEmbedded();
+		RShapeCast shape_cast(&sphere, Vec3::sReplicate(1.0f), RMat44::sTranslation(mCarBody->GetPosition()), -3.0f * mVehicleConstraint->GetWorldUp());
+		ShapeCastSettings settings;
+		ClosestHitCollisionCollector<CastShapeCollector> collector;
+		mPhysicsSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, mCarBody->GetPosition(), collector, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING));
+		if (collector.HadHit())
+			mVehicleConstraint->OverrideGravity(9.81f * collector.mHit.mPenetrationAxis.Normalized());
+		else
+			mVehicleConstraint->ResetGravityOverride();
+	}
+
 	// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
 	// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
 	for (uint w = 0; w < 4; ++w)
 	for (uint w = 0; w < 4; ++w)
 	{
 	{
@@ -302,6 +320,7 @@ void VehicleConstraintTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMe
 	inUI->CreateCheckBox(inSubMenu, "4 Wheel Drive", sFourWheelDrive, [](UICheckBox::EState inState) { sFourWheelDrive = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "4 Wheel Drive", sFourWheelDrive, [](UICheckBox::EState inState) { sFourWheelDrive = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Anti Rollbars", sAntiRollbar, [](UICheckBox::EState inState) { sAntiRollbar = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Anti Rollbars", sAntiRollbar, [](UICheckBox::EState inState) { sAntiRollbar = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Limited Slip Differentials", sLimitedSlipDifferentials, [](UICheckBox::EState inState) { sLimitedSlipDifferentials = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Limited Slip Differentials", sLimitedSlipDifferentials, [](UICheckBox::EState inState) { sLimitedSlipDifferentials = inState == UICheckBox::STATE_CHECKED; });
+	inUI->CreateCheckBox(inSubMenu, "Override Gravity", sOverrideGravity, [](UICheckBox::EState inState) { sOverrideGravity = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateSlider(inSubMenu, "Max Engine Torque", float(sMaxEngineTorque), 100.0f, 2000.0f, 10.0f, [](float inValue) { sMaxEngineTorque = inValue; });
 	inUI->CreateSlider(inSubMenu, "Max Engine Torque", float(sMaxEngineTorque), 100.0f, 2000.0f, 10.0f, [](float inValue) { sMaxEngineTorque = inValue; });
 	inUI->CreateSlider(inSubMenu, "Clutch Strength", float(sClutchStrength), 1.0f, 40.0f, 1.0f, [](float inValue) { sClutchStrength = inValue; });
 	inUI->CreateSlider(inSubMenu, "Clutch Strength", float(sClutchStrength), 1.0f, 40.0f, 1.0f, [](float inValue) { sClutchStrength = inValue; });
 	inUI->CreateSlider(inSubMenu, "Front Caster Angle", RadiansToDegrees(sFrontCasterAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontCasterAngle = DegreesToRadians(inValue); });
 	inUI->CreateSlider(inSubMenu, "Front Caster Angle", RadiansToDegrees(sFrontCasterAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontCasterAngle = DegreesToRadians(inValue); });

+ 1 - 0
Samples/Tests/Vehicle/VehicleConstraintTest.h

@@ -38,6 +38,7 @@ private:
 	static inline bool			sFourWheelDrive = false;
 	static inline bool			sFourWheelDrive = false;
 	static inline bool			sAntiRollbar = true;
 	static inline bool			sAntiRollbar = true;
 	static inline bool			sLimitedSlipDifferentials = true;
 	static inline bool			sLimitedSlipDifferentials = true;
+	static inline bool			sOverrideGravity = false;					///< If true, gravity is overridden to always oppose the ground normal
 	static inline float			sMaxEngineTorque = 500.0f;
 	static inline float			sMaxEngineTorque = 500.0f;
 	static inline float			sClutchStrength = 10.0f;
 	static inline float			sClutchStrength = 10.0f;
 	static inline float			sFrontCasterAngle = 0.0f;
 	static inline float			sFrontCasterAngle = 0.0f;

+ 46 - 0
Samples/Tests/Vehicle/VehicleTest.cpp

@@ -8,6 +8,7 @@
 #include <Jolt/Physics/Constraints/DistanceConstraint.h>
 #include <Jolt/Physics/Constraints/DistanceConstraint.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
 #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
 #include <Jolt/Physics/Collision/GroupFilterTable.h>
 #include <Jolt/Physics/Collision/GroupFilterTable.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Jolt/Physics/PhysicsScene.h>
 #include <Jolt/Physics/PhysicsScene.h>
@@ -30,6 +31,7 @@ const char *VehicleTest::sScenes[] =
 	"Step",
 	"Step",
 	"Dynamic Step",
 	"Dynamic Step",
 	"Playground",
 	"Playground",
+	"Loop",
 #ifdef JPH_OBJECT_STREAM
 #ifdef JPH_OBJECT_STREAM
 	"Terrain1",
 	"Terrain1",
 #endif // JPH_OBJECT_STREAM
 #endif // JPH_OBJECT_STREAM
@@ -113,6 +115,50 @@ void VehicleTest::Initialize()
 
 
 		CreateRubble();
 		CreateRubble();
 	}
 	}
+	else if (strcmp(sSceneName, "Loop") == 0)
+	{
+		CreateFloor();
+
+		TriangleList triangles;
+		const int cNumSegments = 100;
+		const float cLoopWidth = 20.0f;
+		const float cLoopRadius = 20.0f;
+		const float cLoopThickness = 0.5f;
+		Vec3 prev_center = Vec3::sZero();
+		Vec3 prev_center_bottom = Vec3::sZero();
+		for (int i = 0; i < cNumSegments; ++i)
+		{
+			float angle = i * 2.0f * JPH_PI / (cNumSegments - 1);
+			Vec3 radial(0, -Cos(angle), Sin(angle));
+			Vec3 center = Vec3(-i * cLoopWidth / (cNumSegments - 1), cLoopRadius, cLoopRadius) + cLoopRadius * radial;
+			Vec3 half_width(0.5f * cLoopWidth, 0, 0);
+			Vec3 center_bottom = center + cLoopThickness * radial;
+			if (i > 0)
+			{
+				// Top surface
+				triangles.push_back(Triangle(prev_center + half_width, prev_center - half_width, center - half_width));
+				triangles.push_back(Triangle(prev_center + half_width, center - half_width, center + half_width));
+
+				// Bottom surface
+				triangles.push_back(Triangle(prev_center_bottom + half_width, center_bottom - half_width, prev_center_bottom - half_width));
+				triangles.push_back(Triangle(prev_center_bottom + half_width, center_bottom + half_width, center_bottom - half_width));
+
+				// Sides
+				triangles.push_back(Triangle(prev_center + half_width, center + half_width, prev_center_bottom + half_width));
+				triangles.push_back(Triangle(prev_center_bottom + half_width, center + half_width, center_bottom + half_width));
+				triangles.push_back(Triangle(prev_center - half_width, prev_center_bottom - half_width, center - half_width));
+				triangles.push_back(Triangle(prev_center_bottom - half_width, center_bottom - half_width, center - half_width));
+			}
+			prev_center = center;
+			prev_center_bottom = center_bottom;
+		}
+		MeshShapeSettings mesh(triangles);
+		mesh.SetEmbedded();
+
+		Body &loop = *mBodyInterface->CreateBody(BodyCreationSettings(&mesh, RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+		loop.SetFriction(1.0f);
+		mBodyInterface->AddBody(loop.GetID(), EActivation::Activate);
+	}
 #ifdef JPH_OBJECT_STREAM
 #ifdef JPH_OBJECT_STREAM
 	else
 	else
 	{
 	{