Browse Source

Created CharacterContactListener::OnAdjustBodyVelocity callback (#408)

This allows overriding the velocity of a body that the character interacts with to implement e.g. a space ship with inertial dampeners or a conveyor belt.
Jorrit Rouwe 2 năm trước cách đây
mục cha
commit
3d99c876fd

+ 33 - 7
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -38,11 +38,34 @@ CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, R
 	SetMass(inSettings->mMass);
 	SetMass(inSettings->mMass);
 }
 }
 
 
+void CharacterVirtual::GetAdjustedBodyVelocity(const Body& inBody, Vec3 &outLinearVelocity, Vec3 &outAngularVelocity) const
+{
+	// Get real velocity of body
+	if (!inBody.IsStatic())
+	{
+		const MotionProperties *mp = inBody.GetMotionPropertiesUnchecked();
+		outLinearVelocity = mp->GetLinearVelocity();
+		outAngularVelocity = mp->GetAngularVelocity();
+	}
+	else
+	{
+		outLinearVelocity = outAngularVelocity = Vec3::sZero();
+	}
+
+	// Allow application to override
+	if (mListener != nullptr)
+		mListener->OnAdjustBodyVelocity(this, inBody, outLinearVelocity, outAngularVelocity);
+}
+
 template <class taCollector>
 template <class taCollector>
-void CharacterVirtual::sFillContactProperties(Contact &outContact, const Body &inBody, Vec3Arg inUp, RVec3Arg inBaseOffset, const taCollector &inCollector, const CollideShapeResult &inResult)
+void CharacterVirtual::sFillContactProperties(const CharacterVirtual *inCharacter, Contact &outContact, const Body &inBody, Vec3Arg inUp, RVec3Arg inBaseOffset, const taCollector &inCollector, const CollideShapeResult &inResult)
 {
 {
+	// Get adjusted body velocity
+	Vec3 linear_velocity, angular_velocity;
+	inCharacter->GetAdjustedBodyVelocity(inBody, linear_velocity, angular_velocity);
+
 	outContact.mPosition = inBaseOffset + inResult.mContactPointOn2;
 	outContact.mPosition = inBaseOffset + inResult.mContactPointOn2;
-	outContact.mLinearVelocity = inBody.GetPointVelocity(outContact.mPosition);
+	outContact.mLinearVelocity = linear_velocity + angular_velocity.Cross(Vec3(outContact.mPosition - inBody.GetCenterOfMassPosition())); // Calculate point velocity
 	outContact.mContactNormal = -inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero());
 	outContact.mContactNormal = -inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero());
 	outContact.mSurfaceNormal = inCollector.GetContext()->GetWorldSpaceSurfaceNormal(inResult.mSubShapeID2, outContact.mPosition);
 	outContact.mSurfaceNormal = inCollector.GetContext()->GetWorldSpaceSurfaceNormal(inResult.mSubShapeID2, outContact.mPosition);
 	if (outContact.mContactNormal.Dot(outContact.mSurfaceNormal) < 0.0f)
 	if (outContact.mContactNormal.Dot(outContact.mSurfaceNormal) < 0.0f)
@@ -120,7 +143,7 @@ void CharacterVirtual::ContactCollector::AddHit(const CollideShapeResult &inResu
 
 
 		mContacts.emplace_back();
 		mContacts.emplace_back();
 		Contact &contact = mContacts.back();
 		Contact &contact = mContacts.back();
-		sFillContactProperties(contact, body, mUp, mBaseOffset, *this, inResult);
+		sFillContactProperties(mCharacter, contact, body, mUp, mBaseOffset, *this, inResult);
 		contact.mFraction = 0.0f;
 		contact.mFraction = 0.0f;
 	}
 	}
 }
 }
@@ -147,7 +170,7 @@ void CharacterVirtual::ContactCastCollector::AddHit(const ShapeCastResult &inRes
 				return;
 				return;
 
 
 			// Convert the hit result into a contact
 			// Convert the hit result into a contact
-			sFillContactProperties(contact, lock.GetBody(), mUp, mBaseOffset, *this, inResult);
+			sFillContactProperties(mCharacter, contact, lock.GetBody(), mUp, mBaseOffset, *this, inResult);
 		}
 		}
 			
 			
 		contact.mFraction = inResult.mFraction;
 		contact.mFraction = inResult.mFraction;
@@ -184,7 +207,7 @@ void CharacterVirtual::GetContactsAtPosition(RVec3Arg inPosition, Vec3Arg inMove
 	outContacts.clear();
 	outContacts.clear();
 
 
 	// Collide shape
 	// Collide shape
-	ContactCollector collector(mSystem, mMaxNumHits, mHitReductionCosMaxAngle, mUp, mPosition, outContacts);
+	ContactCollector collector(mSystem, this, mMaxNumHits, mHitReductionCosMaxAngle, mUp, mPosition, outContacts);
 	CheckCollision(inPosition, mRotation, inMovementDirection, mPredictiveContactDistance, inShape, mPosition, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
 	CheckCollision(inPosition, mRotation, inMovementDirection, mPredictiveContactDistance, inShape, mPosition, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
 
 
 	// Flag if we exceeded the max number of hits
 	// Flag if we exceeded the max number of hits
@@ -755,10 +778,13 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 						{
 						{
 							const Body &body = lock.GetBody();
 							const Body &body = lock.GetBody();
 
 
+							// Get adjusted body velocity
+							Vec3 linear_velocity;
+							GetAdjustedBodyVelocity(body, linear_velocity, angular_velocity);
+							
 							// Add the linear velocity to the average velocity
 							// Add the linear velocity to the average velocity
-							avg_velocity += body.GetLinearVelocity();
+							avg_velocity += linear_velocity;
 
 
-							angular_velocity = body.GetAngularVelocity();
 							com = body.GetCenterOfMassPosition();
 							com = body.GetCenterOfMassPosition();
 						}
 						}
 						else
 						else

+ 16 - 2
Jolt/Physics/Character/CharacterVirtual.h

@@ -56,10 +56,20 @@ public:
 	/// Destructor
 	/// Destructor
 	virtual								~CharacterContactListener() = default;
 	virtual								~CharacterContactListener() = default;
 
 
+	/// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship.
+	/// Note that inBody2 is locked during the callback so you can read its properties freely.
+	virtual void						OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity) { /* Do nothing, the linear and angular velocity are already filled in */ }
+
 	/// Checks if a character can collide with specified body. Return true if the contact is valid.
 	/// Checks if a character can collide with specified body. Return true if the contact is valid.
 	virtual bool						OnContactValidate(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) { return true; }
 	virtual bool						OnContactValidate(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) { return true; }
 
 
 	/// Called whenever the character collides with a body. Returns true if the contact can push the character.
 	/// Called whenever the character collides with a body. Returns true if the contact can push the character.
+	/// @param inCharacter Character that is being solved
+	/// @param inBodyID2 Body ID of body that is being hit
+	/// @param inSubShapeID2 Sub shape ID of shape that is being hit
+	/// @param inContactPosition World space contact position
+	/// @param inContactNormal World space contact normal
+	/// @param ioSettings Settings returned by the contact callback to indicate how the character should behave
 	virtual void						OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) { /* Default do nothing */ }
 	virtual void						OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) { /* Default do nothing */ }
 
 
 	/// Called whenever a contact is being used by the solver. Allows the listener to override the resulting character velocity (e.g. by preventing sliding along certain surfaces).
 	/// Called whenever a contact is being used by the solver. Allows the listener to override the resulting character velocity (e.g. by preventing sliding along certain surfaces).
@@ -325,13 +335,14 @@ private:
 	class ContactCollector : public CollideShapeCollector
 	class ContactCollector : public CollideShapeCollector
 	{
 	{
 	public:
 	public:
-										ContactCollector(PhysicsSystem *inSystem, uint inMaxHits, float inHitReductionCosMaxAngle, Vec3Arg inUp, RVec3Arg inBaseOffset, TempContactList &outContacts) : mBaseOffset(inBaseOffset), mUp(inUp), mSystem(inSystem), mContacts(outContacts), mMaxHits(inMaxHits), mHitReductionCosMaxAngle(inHitReductionCosMaxAngle) { }
+										ContactCollector(PhysicsSystem *inSystem, const CharacterVirtual *inCharacter, uint inMaxHits, float inHitReductionCosMaxAngle, Vec3Arg inUp, RVec3Arg inBaseOffset, TempContactList &outContacts) : mBaseOffset(inBaseOffset), mUp(inUp), mSystem(inSystem), mCharacter(inCharacter), mContacts(outContacts), mMaxHits(inMaxHits), mHitReductionCosMaxAngle(inHitReductionCosMaxAngle) { }
 
 
 		virtual void					AddHit(const CollideShapeResult &inResult) override;
 		virtual void					AddHit(const CollideShapeResult &inResult) override;
 
 
 		RVec3							mBaseOffset;
 		RVec3							mBaseOffset;
 		Vec3							mUp;
 		Vec3							mUp;
 		PhysicsSystem *					mSystem;
 		PhysicsSystem *					mSystem;
+		const CharacterVirtual *		mCharacter;
 		TempContactList &				mContacts;
 		TempContactList &				mContacts;
 		uint							mMaxHits;
 		uint							mMaxHits;
 		float							mHitReductionCosMaxAngle;
 		float							mHitReductionCosMaxAngle;
@@ -357,7 +368,7 @@ private:
 
 
 	// Helper function to convert a Jolt collision result into a contact
 	// Helper function to convert a Jolt collision result into a contact
 	template <class taCollector>
 	template <class taCollector>
-	inline static void					sFillContactProperties(Contact &outContact, const Body &inBody, Vec3Arg inUp, RVec3Arg inBaseOffset, const taCollector &inCollector, const CollideShapeResult &inResult);
+	inline static void					sFillContactProperties(const CharacterVirtual *inCharacter, Contact &outContact, const Body &inBody, Vec3Arg inUp, RVec3Arg inBaseOffset, const taCollector &inCollector, const CollideShapeResult &inResult);
 
 
 	// Move the shape from ioPosition and try to displace it by inVelocity * inDeltaTime, this will try to slide the shape along the world geometry
 	// Move the shape from ioPosition and try to displace it by inVelocity * inDeltaTime, this will try to slide the shape along the world geometry
 	void								MoveShape(RVec3 &ioPosition, Vec3Arg inVelocity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator
 	void								MoveShape(RVec3 &ioPosition, Vec3Arg inVelocity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator
@@ -385,6 +396,9 @@ private:
 	#endif // JPH_DEBUG_RENDERER
 	#endif // JPH_DEBUG_RENDERER
 		) const;
 		) const;
 
 
+	// Get the velocity of a body adjusted by the contact listener
+	void								GetAdjustedBodyVelocity(const Body& inBody, Vec3 &outLinearVelocity, Vec3 &outAngularVelocity) const;
+
 	// Handle contact with physics object that we're colliding against
 	// Handle contact with physics object that we're colliding against
 	bool								HandleContact(Vec3Arg inVelocity, Constraint &ioConstraint, float inDeltaTime) const;
 	bool								HandleContact(Vec3Arg inVelocity, Constraint &ioConstraint, float inDeltaTime) const;
 
 

+ 2 - 0
Samples/Samples.cmake

@@ -19,6 +19,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Character/CharacterTest.h
 	${SAMPLES_ROOT}/Tests/Character/CharacterTest.h
 	${SAMPLES_ROOT}/Tests/Character/CharacterVirtualTest.cpp
 	${SAMPLES_ROOT}/Tests/Character/CharacterVirtualTest.cpp
 	${SAMPLES_ROOT}/Tests/Character/CharacterVirtualTest.h
 	${SAMPLES_ROOT}/Tests/Character/CharacterVirtualTest.h
+	${SAMPLES_ROOT}/Tests/Character/CharacterSpaceShipTest.cpp
+	${SAMPLES_ROOT}/Tests/Character/CharacterSpaceShipTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.cpp
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.cpp
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConstraintSingularityTest.cpp
 	${SAMPLES_ROOT}/Tests/Constraints/ConstraintSingularityTest.cpp

+ 2 - 0
Samples/SamplesApp.cpp

@@ -250,11 +250,13 @@ static TestNameAndRTTI sRigTests[] =
 
 
 JPH_DECLARE_RTTI_FOR_FACTORY(CharacterTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(CharacterTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(CharacterVirtualTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(CharacterVirtualTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(CharacterSpaceShipTest)
 
 
 static TestNameAndRTTI sCharacterTests[] =
 static TestNameAndRTTI sCharacterTests[] =
 {
 {
 	{ "Character",							JPH_RTTI(CharacterTest) },
 	{ "Character",							JPH_RTTI(CharacterTest) },
 	{ "Character Virtual",					JPH_RTTI(CharacterVirtualTest) },
 	{ "Character Virtual",					JPH_RTTI(CharacterVirtualTest) },
+	{ "Character Virtual vs Space Ship",	JPH_RTTI(CharacterSpaceShipTest) },
 };
 };
 
 
 JPH_DECLARE_RTTI_FOR_FACTORY(WaterShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(WaterShapeTest)

+ 6 - 0
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -42,6 +42,7 @@ static const RVec3 cVerticallyMovingPosition(0, 2.0f, 15);
 static const Quat cVerticallyMovingOrientation = Quat::sIdentity();
 static const Quat cVerticallyMovingOrientation = Quat::sIdentity();
 static const RVec3 cHorizontallyMovingPosition(5, 1, 15);
 static const RVec3 cHorizontallyMovingPosition(5, 1, 15);
 static const Quat cHorizontallyMovingOrientation = Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI);
 static const Quat cHorizontallyMovingOrientation = Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI);
+static const RVec3 cConveyorBeltPosition(-10, 0.15f, 15);
 static const RVec3 cRampPosition(15, 2.2f, 15);
 static const RVec3 cRampPosition(15, 2.2f, 15);
 static const Quat cRampOrientation = Quat::sRotation(Vec3::sAxisX(), -0.25f * JPH_PI);
 static const Quat cRampOrientation = Quat::sRotation(Vec3::sAxisX(), -0.25f * JPH_PI);
 static const RVec3 cRampBlocksStart = cRampPosition + Vec3(-3.0f, 3.0f, 1.5f);
 static const RVec3 cRampBlocksStart = cRampPosition + Vec3(-3.0f, 3.0f, 1.5f);
@@ -111,6 +112,11 @@ void CharacterBaseTest::Initialize()
 			mHorizontallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cHorizontallyMovingPosition, cHorizontallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
 			mHorizontallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cHorizontallyMovingPosition, cHorizontallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
 		}
 		}
 
 
+		{
+			// Conveyor belt (only works with virtual character)
+			mConveyorBeltBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(1, 0.15f, 3.0f)), cConveyorBeltPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::Activate);
+		}
+
 		{
 		{
 			// A rolling sphere towards the player
 			// A rolling sphere towards the player
 			BodyCreationSettings bcs(new SphereShape(0.2f), RVec3(0.0f, 0.2f, -1.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
 			BodyCreationSettings bcs(new SphereShape(0.2f), RVec3(0.0f, 0.2f, -1.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);

+ 3 - 0
Samples/Tests/Character/CharacterBaseTest.h

@@ -64,6 +64,9 @@ protected:
 	Array<BodyID>			mRampBlocks;
 	Array<BodyID>			mRampBlocks;
 	float					mRampBlocksTimeLeft = 0.0f;
 	float					mRampBlocksTimeLeft = 0.0f;
 
 
+	// Conveyor belt body
+	BodyID					mConveyorBeltBody;
+
 private:
 private:
 	// Shape types
 	// Shape types
 	enum class EType
 	enum class EType

+ 186 - 0
Samples/Tests/Character/CharacterSpaceShipTest.cpp

@@ -0,0 +1,186 @@
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Character/CharacterSpaceShipTest.h>
+#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
+#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
+#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
+#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(CharacterSpaceShipTest)
+{
+	JPH_ADD_BASE_CLASS(CharacterSpaceShipTest, Test)
+}
+
+void CharacterSpaceShipTest::Initialize()
+{
+	// Dimensions of our space ship
+	constexpr float cSpaceShipHeight = 2.0f;
+	constexpr float cSpaceShipRingHeight = 0.2f;
+	constexpr float cSpaceShipRadius = 100.0f;
+	const RVec3 cShipInitialPosition(-25, 15, 0);
+
+	// Create floor for reference
+	CreateFloor();
+
+	// Create 'player' character
+	Ref<CharacterVirtualSettings> settings = new CharacterVirtualSettings();
+	settings->mShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
+	settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
+	mCharacter = new CharacterVirtual(settings, cShipInitialPosition + Vec3(0, cSpaceShipHeight, 0), Quat::sIdentity(), mPhysicsSystem);
+	mCharacter->SetListener(this);
+
+	// Create the space ship
+	StaticCompoundShapeSettings compound;
+	compound.SetEmbedded();
+	for (float h = cSpaceShipRingHeight; h < cSpaceShipHeight; h += cSpaceShipRingHeight)
+		compound.AddShape(Vec3::sZero(), Quat::sIdentity(), new CylinderShape(h, sqrt(Square(cSpaceShipRadius) - Square(cSpaceShipRadius - cSpaceShipHeight - cSpaceShipRingHeight + h))));
+	mSpaceShip = mBodyInterface->CreateAndAddBody(BodyCreationSettings(&compound, cShipInitialPosition, Quat::sIdentity(), EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
+	mSpaceShipPrevTransform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
+}
+
+void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	// Update scene time
+	mTime += inParams.mDeltaTime;
+
+	// Update the character so it stays relative to the space ship
+	RMat44 new_space_ship_transform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
+	mCharacter->SetPosition(new_space_ship_transform * mSpaceShipPrevTransform.Inversed() * mCharacter->GetPosition());
+
+	// Update the character rotation and its up vector to match the new up vector of the ship
+	mCharacter->SetUp(new_space_ship_transform.GetAxisY());
+	mCharacter->SetRotation(new_space_ship_transform.GetRotation().GetQuaternion());
+
+	// Draw character pre update (the sim is also drawn pre update)
+	// Note that we have first updated the position so that it matches the new position of the ship
+#ifdef JPH_DEBUG_RENDERER
+	mCharacter->GetShape()->Draw(mDebugRenderer, mCharacter->GetCenterOfMassTransform(), Vec3::sReplicate(1.0f), Color::sGreen, false, true);
+#endif // JPH_DEBUG_RENDERER
+
+	// Determine controller input
+	Vec3 control_input = Vec3::sZero();
+	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		control_input.SetZ(-1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	control_input.SetZ(1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		control_input.SetX(1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		control_input.SetX(-1);
+	if (control_input != Vec3::sZero())
+		control_input = control_input.Normalized();
+
+	// Calculate the desired velocity in local space to the ship based on the camera forward
+	Vec3 cam_fwd = new_space_ship_transform.GetRotation().Multiply3x3Transposed(inParams.mCameraState.mForward);
+	cam_fwd.SetY(0.0f);
+	cam_fwd = cam_fwd.NormalizedOr(Vec3::sAxisX());
+	Quat rotation = Quat::sFromTo(Vec3::sAxisX(), cam_fwd);
+	control_input = rotation * control_input;
+
+	// Smooth the player input in local space to the ship
+	mDesiredVelocity = 0.25f * control_input * cCharacterSpeed + 0.75f * mDesiredVelocity;
+		
+	// Check jump
+	bool jump = false;
+	for (int key = inParams.mKeyboard->GetFirstKey(); key != 0; key = inParams.mKeyboard->GetNextKey())
+	{
+		if (key == DIK_RCONTROL)
+			jump = true;
+	}
+
+	// Determine new character velocity
+	Vec3 current_vertical_velocity = mCharacter->GetLinearVelocity().Dot(mSpaceShipPrevTransform.GetAxisY()) * mCharacter->GetUp();
+	Vec3 ground_velocity = mCharacter->GetGroundVelocity();
+	Vec3 new_velocity;
+	if (mCharacter->GetGroundState() == CharacterVirtual::EGroundState::OnGround // If on ground
+		&& (current_vertical_velocity.GetY() - ground_velocity.GetY()) < 0.1f) // And not moving away from ground
+	{
+		// Assume velocity of ground when on ground
+		new_velocity = ground_velocity;
+
+		// Jump
+		if (jump)
+			new_velocity += cJumpSpeed * mCharacter->GetUp();
+	}
+	else
+		new_velocity = current_vertical_velocity;
+
+	// Gravity always acts relative to the ship
+	Vec3 gravity = new_space_ship_transform.Multiply3x3(mPhysicsSystem->GetGravity());
+	new_velocity += gravity * inParams.mDeltaTime;
+
+	// Transform player input to world space
+	new_velocity += new_space_ship_transform.Multiply3x3(mDesiredVelocity);
+	
+	// Update character velocity
+	mCharacter->SetLinearVelocity(new_velocity);
+
+	// Update the character position
+	CharacterVirtual::ExtendedUpdateSettings update_settings;
+	mCharacter->ExtendedUpdate(inParams.mDeltaTime,
+		gravity,
+		update_settings,
+		mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
+		mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING),
+		{ },
+		{ },
+		*mTempAllocator);
+
+	// Update previous transform
+	mSpaceShipPrevTransform = new_space_ship_transform;
+
+	// Calculate new velocity
+	UpdateShipVelocity();
+}
+
+void CharacterSpaceShipTest::UpdateShipVelocity()
+{
+	// Make it a rocky ride...
+	mSpaceShipLinearVelocity = Vec3(Sin(mTime), 0, Cos(mTime)) * 50.0f;
+	mSpaceShipAngularVelocity = Vec3(Sin(2.0f * mTime), 1, Cos(2.0f * mTime)) * 0.5f;
+
+	mBodyInterface->SetLinearAndAngularVelocity(mSpaceShip, mSpaceShipLinearVelocity, mSpaceShipAngularVelocity);
+}
+
+void CharacterSpaceShipTest::GetInitialCamera(CameraState& ioState) const
+{
+	// This will become the local space offset, look down the x axis and slightly down
+	ioState.mPos = RVec3::sZero();
+	ioState.mForward = Vec3(10.0f, -2.0f, 0).Normalized();
+}
+
+RMat44 CharacterSpaceShipTest::GetCameraPivot(float inCameraHeading, float inCameraPitch) const 
+{
+	// Pivot is center of character + distance behind based on the heading and pitch of the camera
+	Vec3 fwd = Vec3(Cos(inCameraPitch) * Cos(inCameraHeading), Sin(inCameraPitch), Cos(inCameraPitch) * Sin(inCameraHeading));
+	return RMat44::sTranslation(mCharacter->GetPosition() + Vec3(0, cCharacterHeightStanding + cCharacterRadiusStanding, 0) - 5.0f * fwd);
+}
+
+void CharacterSpaceShipTest::SaveState(StateRecorder &inStream) const
+{
+	mCharacter->SaveState(inStream);
+
+	inStream.Write(mTime);
+	inStream.Write(mDesiredVelocity);
+	inStream.Write(mSpaceShipPrevTransform);
+}
+
+void CharacterSpaceShipTest::RestoreState(StateRecorder &inStream)
+{
+	mCharacter->RestoreState(inStream);
+
+	inStream.Read(mTime);
+	inStream.Read(mDesiredVelocity);
+	inStream.Read(mSpaceShipPrevTransform);
+
+	// Calculate new velocity
+	UpdateShipVelocity();
+}
+
+void CharacterSpaceShipTest::OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity)
+{
+	// Cancel out velocity of space ship, we move relative to this which means we don't feel any of the acceleration of the ship (= engage inertial dampeners!)
+	ioLinearVelocity -= mSpaceShipLinearVelocity;
+	ioAngularVelocity -= mSpaceShipAngularVelocity;
+}

+ 64 - 0
Samples/Tests/Character/CharacterSpaceShipTest.h

@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+#include <Jolt/Physics/Character/CharacterVirtual.h>
+
+// A test that demonstrates how a character may walk around a fast moving/accelerating sci-fi space ship that is equipped with inertial dampeners.
+// Note that this is 'game physics' and not real physics, inertial dampeners only exist in the movies.
+// You can walk off the ship and remain attached to the ship. A proper implementation would detect this and detach the character.
+class CharacterSpaceShipTest : public Test, public CharacterContactListener
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(CharacterSpaceShipTest)
+
+	// Initialize the test
+	virtual void			Initialize() override;
+
+	// Update the test, called before the physics update
+	virtual void			PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+
+	// Override to specify the initial camera state (local to GetCameraPivot)
+	virtual void			GetInitialCamera(CameraState &ioState) const override;
+
+	// Override to specify a camera pivot point and orientation (world space)
+	virtual RMat44			GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
+
+	// Saving / restoring state for replay
+	virtual void			SaveState(StateRecorder &inStream) const override;
+	virtual void			RestoreState(StateRecorder &inStream) override;
+
+private:
+	// Calculate new ship velocity
+	void					UpdateShipVelocity();
+
+	/// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship.
+	virtual void			OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity) override;
+
+	// Character size
+	static constexpr float	cCharacterHeightStanding = 1.35f;
+	static constexpr float	cCharacterRadiusStanding = 0.3f;
+	static constexpr float	cCharacterSpeed = 6.0f;
+	static constexpr float	cJumpSpeed = 4.0f;
+
+	// The 'player' character
+	Ref<CharacterVirtual>	mCharacter;
+
+	// The space ship
+	BodyID					mSpaceShip;
+
+	// Previous frame space ship transform
+	RMat44					mSpaceShipPrevTransform;
+
+	// Space ship velocity
+	Vec3					mSpaceShipLinearVelocity;
+	Vec3					mSpaceShipAngularVelocity;
+	
+	// Global time
+	float					mTime = 0.0f;
+
+	// Smoothed value of the player input
+	Vec3					mDesiredVelocity = Vec3::sZero();
+};

+ 7 - 0
Samples/Tests/Character/CharacterVirtualTest.cpp

@@ -171,6 +171,13 @@ void CharacterVirtualTest::RestoreState(StateRecorder &inStream)
 	inStream.Read(mDesiredVelocity);
 	inStream.Read(mDesiredVelocity);
 }
 }
 
 
+void CharacterVirtualTest::OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity)
+{
+	// Apply artificial velocity to the character when standing on the conveyor belt
+	if (inBody2.GetID() == mConveyorBeltBody)
+		ioLinearVelocity += Vec3(0, 0, 2);
+}
+
 void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
 void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
 {
 {
 	// Dynamic boxes on the ramp go through all permutations
 	// Dynamic boxes on the ramp go through all permutations

+ 3 - 0
Samples/Tests/Character/CharacterVirtualTest.h

@@ -22,6 +22,9 @@ public:
 	virtual void			SaveState(StateRecorder &inStream) const override;
 	virtual void			SaveState(StateRecorder &inStream) const override;
 	virtual void			RestoreState(StateRecorder &inStream) override;
 	virtual void			RestoreState(StateRecorder &inStream) override;
 
 
+	/// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship.
+	virtual void			OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity) override;
+
 	// Called whenever the character collides with a body. Returns true if the contact can push the character.
 	// Called whenever the character collides with a body. Returns true if the contact can push the character.
 	virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
 	virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;