Pārlūkot izejas kodu

Character tweaks (#218)

* Treat surfaces we're moving away from as non-colliding for determining supporting contact
* Made min displacement delta time dependent, otherwise 120 Hz updates will cause the character to think it's on the ground instead of sliding
* Moved up vector to CharacterBase and added IsSlopeTooSteep helper function
* Added OnContactSolve callback that allows code to modify the sliding behavior of the virtual character
* Added StickToFloor function that allows moving character down if it is slightly above the floor
Jorrit Rouwe 2 gadi atpakaļ
vecāks
revīzija
efb0012221

+ 9 - 10
Jolt/Physics/Character/Character.cpp

@@ -124,14 +124,14 @@ void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 	{
 	public:
 		// Constructor
-		explicit			MyCollector(Vec3Arg inGravity) : mGravity(inGravity) { }
+		explicit			MyCollector(Vec3Arg inUp) : mUp(inUp) { }
 
 		// See: CollectorType::AddHit
 		virtual void		AddHit(const CollideShapeResult &inResult) override
 		{
 			Vec3 normal = -inResult.mPenetrationAxis.Normalized();
-			float dot = normal.Dot(mGravity);
-			if (dot < mBestDot) // Find the hit that is most opposite to the gravity
+			float dot = normal.Dot(mUp);
+			if (dot > mBestDot) // Find the hit that is most aligned with the up vector
 			{
 				mGroundBodyID = inResult.mBodyID2;
 				mGroundBodySubShapeID = inResult.mSubShapeID2;
@@ -147,12 +147,12 @@ void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 		Vec3				mGroundNormal = Vec3::sZero();
 
 	private:
-		float				mBestDot = FLT_MAX;
-		Vec3				mGravity;
+		float				mBestDot = -FLT_MAX;
+		Vec3				mUp;
 	};
 
 	// Collide shape
-	MyCollector collector(mSystem->GetGravity());
+	MyCollector collector(mUp);
 	CheckCollision(mShape, inMaxSeparationDistance, collector, inLockBodies);
 
 	// Copy results
@@ -168,11 +168,10 @@ void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 		const Body &body = lock.GetBody();
 
 		// Update ground state
-		Vec3 up = -mSystem->GetGravity().Normalized();
-		if (mGroundNormal.Dot(up) > mCosMaxSlopeAngle)
-			mGroundState = EGroundState::OnGround;
-		else
+		if (IsSlopeTooSteep(mGroundNormal))
 			mGroundState = EGroundState::Sliding;
+		else
+			mGroundState = EGroundState::OnGround;
 
 		// Copy other body properties
 		mGroundMaterial = body.GetShape()->GetMaterial(mGroundBodySubShapeID);

+ 2 - 1
Jolt/Physics/Character/CharacterBase.cpp

@@ -10,7 +10,8 @@ JPH_NAMESPACE_BEGIN
 
 CharacterBase::CharacterBase(const CharacterBaseSettings *inSettings, PhysicsSystem *inSystem) :
 	mSystem(inSystem),
-	mShape(inSettings->mShape)
+	mShape(inSettings->mShape),
+	mUp(inSettings->mUp)
 {
 	// Initialize max slope angle
 	SetMaxSlopeAngle(inSettings->mMaxSlopeAngle);

+ 22 - 0
Jolt/Physics/Character/CharacterBase.h

@@ -24,6 +24,9 @@ public:
 	/// Virtual destructor
 	virtual								~CharacterBaseSettings() = default;
 
+	/// Vector indicating the up direction of the character
+	Vec3								mUp = Vec3::sAxisY();
+
 	/// Maximum angle of slope that character can still walk on (radians).
 	float								mMaxSlopeAngle = DegreesToRadians(50.0f);
 
@@ -46,6 +49,19 @@ public:
 
 	/// Set the maximum angle of slope that character can still walk on (radians)
 	void								SetMaxSlopeAngle(float inMaxSlopeAngle)					{ mCosMaxSlopeAngle = Cos(inMaxSlopeAngle); }
+	float								GetCosMaxSlopeAngle() const								{ return mCosMaxSlopeAngle; }
+
+	/// Set the up vector for the character
+	void								SetUp(Vec3Arg inUp)										{ mUp = inUp; }
+	Vec3								GetUp() const											{ return mUp; }
+
+	/// Check if the normal of the ground surface is too steep to walk on
+	bool								IsSlopeTooSteep(Vec3Arg inNormal) const
+	{
+		// If cos max slope angle is close to one the system is turned off,
+		// otherwise check the angle between the up and normal vector
+		return mCosMaxSlopeAngle < cNoMaxSlopeAngle && inNormal.Dot(mUp) < mCosMaxSlopeAngle;
+	}
 
 	/// Get the current shape that the character is using.
 	const Shape *						GetShape() const										{ return mShape; }
@@ -94,6 +110,12 @@ protected:
 	// The shape that the body currently has
 	RefConst<Shape>						mShape;
 
+	// The character's world space up axis
+	Vec3								mUp;
+
+	// Beyond this value there is no max slope
+	static constexpr float				cNoMaxSlopeAngle = 0.9999f;
+
 	// Cosine of the maximum angle of slope that character can still walk on
 	float								mCosMaxSlopeAngle;
 

+ 51 - 21
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -17,7 +17,6 @@ JPH_NAMESPACE_BEGIN
 
 CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, PhysicsSystem *inSystem) :
 	CharacterBase(inSettings, inSystem),
-	mUp(inSettings->mUp),
 	mPredictiveContactDistance(inSettings->mPredictiveContactDistance),
 	mMaxCollisionIterations(inSettings->mMaxCollisionIterations),
 	mMaxConstraintIterations(inSettings->mMaxConstraintIterations),
@@ -243,10 +242,11 @@ void CharacterVirtual::DetermineConstraints(TempContactList &inContacts, Constra
 		constraint.mPlane = Plane(c.mNormal, c.mDistance);
 
 		// Next check if the angle is too steep and if it is add an additional constraint that holds the character back
-		if (mCosMaxSlopeAngle < 0.999f) // If cos(slope angle) is close to 1 then there's no limit
+		if (IsSlopeTooSteep(c.mNormal))
 		{
+			// Only take planes that point up
 			float dot = c.mNormal.Dot(mUp);
-			if (dot > 0.0f && dot < mCosMaxSlopeAngle)
+			if (dot > 0.0f)
 			{
 				// Make horizontal normal
 				Vec3 normal = (c.mNormal - dot * mUp).Normalized();
@@ -515,13 +515,15 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, f
 			Vec3 other_perpendicular_velocity = other_constraint->mLinearVelocity - other_constraint->mLinearVelocity.Dot(slide_dir) * slide_dir;
 
 			// Add all components together
-			velocity = velocity_in_slide_dir + perpendicular_velocity + other_perpendicular_velocity;
+			new_velocity = velocity_in_slide_dir + perpendicular_velocity + other_perpendicular_velocity;
 		}			
-		else
-		{
-			// Update the velocity
-			velocity = new_velocity;
-		}
+
+		// Allow application to modify calculated velocity
+		if (mListener != nullptr)
+			mListener->OnContactSolve(this, constraint->mContact->mBodyB, constraint->mContact->mSubShapeIDB, constraint->mContact->mPosition, constraint->mContact->mNormal, constraint->mContact->mLinearVelocity, constraint->mContact->mMaterial, velocity, new_velocity);
+
+		// Update the velocity
+		velocity = new_velocity;
 
 		// Add the contact to the list so that next iteration we can avoid violating it again
 		previous_contacts[num_previous_contacts] = constraint;
@@ -533,13 +535,14 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, f
 	}
 }
 
-void CharacterVirtual::UpdateSupportingContact(TempAllocator &inAllocator)
+void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck, TempAllocator &inAllocator)
 {
-	// Flag contacts as having a collision if they're close enough.
+	// Flag contacts as having a collision if they're close enough but ignore contacts we're moving away from.
 	// Note that if we did MoveShape before we want to preserve any contacts that it marked as colliding
 	for (Contact &c : mActiveContacts)
 		if (!c.mWasDiscarded)
-			c.mHadCollision |= c.mDistance < mCollisionTolerance;
+			c.mHadCollision |= c.mDistance < mCollisionTolerance
+								&& (inSkipContactVelocityCheck || c.mNormal.Dot(mLinearVelocity - c.mLinearVelocity) <= 0.0f);
 
 	// Determine if we're supported or not
 	int num_supported = 0;
@@ -563,7 +566,7 @@ void CharacterVirtual::UpdateSupportingContact(TempAllocator &inAllocator)
 			}
 
 			// Check if this is a sliding or supported contact
-			bool is_supported = cos_angle >= mCosMaxSlopeAngle;
+			bool is_supported = mCosMaxSlopeAngle > cNoMaxSlopeAngle || cos_angle >= mCosMaxSlopeAngle;
 			if (is_supported)
 				num_supported++;
 			else
@@ -678,8 +681,8 @@ void CharacterVirtual::UpdateSupportingContact(TempAllocator &inAllocator)
 		SolveConstraints(-mUp, mSystem->GetGravity(), 1.0f, 1.0f, constraints, ignored_contacts, time_simulated, displacement, inAllocator);
 
 		// If we're blocked then we're supported, otherwise we're sliding
-		constexpr float cMinRequiredDisplacementSquared = Square(0.01f);
-		if (time_simulated < 0.001f || displacement.LengthSq() < cMinRequiredDisplacementSquared)
+		float min_required_displacement_sq = Square(0.6f * mLastDeltaTime);
+		if (time_simulated < 0.001f || displacement.LengthSq() < min_required_displacement_sq)
 			mGroundState = EGroundState::OnGround;
 		else
 			mGroundState = EGroundState::Sliding;
@@ -695,7 +698,7 @@ void CharacterVirtual::StoreActiveContacts(const TempContactList &inContacts, Te
 {
 	mActiveContacts.assign(inContacts.begin(), inContacts.end());
 
-	UpdateSupportingContact(inAllocator);
+	UpdateSupportingContact(true, inAllocator);
 }
 
 void CharacterVirtual::MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const
@@ -778,7 +781,7 @@ void CharacterVirtual::Update(float inDeltaTime, Vec3Arg inGravity, const BroadP
 	MoveShape(mPosition, mLinearVelocity, inGravity, inDeltaTime, &mActiveContacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
 
 	// Determine the object that we're standing on
-	UpdateSupportingContact(inAllocator);
+	UpdateSupportingContact(false, inAllocator);
 }
 
 void CharacterVirtual::RefreshContacts(const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator)
@@ -835,7 +838,7 @@ bool CharacterVirtual::CanWalkStairs() const
 	for (const Contact &c : mActiveContacts)
 		if (c.mHadCollision
 			&& c.mNormal.Dot(horizontal_velocity - c.mLinearVelocity) < 0.0f // Pushing into the contact
-			&& c.mNormal.Dot(mUp) < mCosMaxSlopeAngle) // Slope too steep
+			&& IsSlopeTooSteep(c.mNormal)) // Slope too steep
 			return true;
 
 	return false;
@@ -893,8 +896,7 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 #endif // JPH_DEBUG_RENDERER
 
 	// Test for floor that will support the character
-	if (mCosMaxSlopeAngle < 0.999f // If cos(slope angle) is close to 1 then there's no limit
-		&& contact.mNormal.Dot(mUp) < mCosMaxSlopeAngle) // Check slope angle
+	if (IsSlopeTooSteep(contact.mNormal))
 	{
 		// If no test position was provided, we cancel the stair walk
 		if (inStepForwardTest.IsNearZero())
@@ -920,7 +922,7 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 		}
 	#endif // JPH_DEBUG_RENDERER
 
-		if (test_contact.mNormal.Dot(mUp) < mCosMaxSlopeAngle)
+		if (IsSlopeTooSteep(test_contact.mNormal))
 			return false;
 	}
 
@@ -934,6 +936,34 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 	return true;
 }
 
+bool CharacterVirtual::StickToFloor(Vec3Arg inStepDown, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator)
+{
+	JPH_ASSERT(GetGroundState() == EGroundState::InAir, "Makes no sense to call this if we're not in air");
+
+	// Try to find the floor
+	Contact contact;
+	IgnoredContactList dummy_ignored_contacts(inAllocator);
+	if (!GetFirstContactForSweep(mPosition, inStepDown, contact, dummy_ignored_contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator))
+		return false; // If no floor found, don't update our position
+
+	// Calculate new position
+	Vec3 new_position = mPosition + contact.mFraction * inStepDown;
+
+#ifdef JPH_DEBUG_RENDERER
+	// Draw sweep down
+	if (sDrawStickToFloor)
+	{
+		DebugRenderer::sInstance->DrawArrow(mPosition, new_position, Color::sOrange, 0.01f);
+		mShape->Draw(DebugRenderer::sInstance, GetCenterOfMassTransform(new_position, mRotation, mShape), Vec3::sReplicate(1.0f), Color::sOrange, false, true);
+	}
+#endif // JPH_DEBUG_RENDERER
+
+	// Move the character to the new location
+	SetPosition(new_position);
+	RefreshContacts(inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
+	return true;
+}
+
 void CharacterVirtual::SaveState(StateRecorder &inStream) const
 {
 	CharacterBase::SaveState(inStream);

+ 26 - 8
Jolt/Physics/Character/CharacterVirtual.h

@@ -20,9 +20,6 @@ class CharacterVirtualSettings : public CharacterBaseSettings
 public:
 	JPH_OVERRIDE_NEW_DELETE
 
-	/// Vector indicating the up direction of the character
-	Vec3								mUp = Vec3::sAxisY();
-
 	/// Character mass (kg). Used to push down objects with gravity when the character is standing on top.
 	float								mMass = 70.0f;
 
@@ -62,6 +59,18 @@ public:
 
 	/// 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, Vec3Arg 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).
+	/// @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 inContactVelocity World space velocity of contact point (e.g. for a moving platform)
+	/// @param inContactMaterial Material of contact point
+	/// @param inCharacterVelocity World space velocity of the character prior to hitting this contact
+	/// @param ioNewCharacterVelocity Contains the calculated world space velocity of the character after hitting this contact, this velocity slides along the surface of the contact. Can be modified by the listener to provide an alternative velocity.
+	virtual void						OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, Vec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity) { /* Default do nothing */ }
 };
 
 /// Runtime character object.
@@ -147,6 +156,17 @@ public:
 	/// @return true if the stair walk was successful
 	bool								WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg inStepUp, Vec3Arg inStepForward, Vec3Arg inStepForwardTest, Vec3Arg inStepDownExtra, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator);
 
+	/// This function can be used to artificially keep the character to the floor. Normally when a character is on a small step and starts moving horizontally, the character will
+	/// lose contact with the floor because the initial vertical velocity is zero while the horizontal velocity is quite high. To prevent the character from losing contact with the floor,
+	/// we do an additional collision check downwards and if we find the floor within a certain distance, we project the character onto the floor.
+	/// @param inStepDown Max amount to project the character downwards (if no floor is found within this distance, the function will return false)
+	/// @param inBroadPhaseLayerFilter Filter that is used to check if the character collides with something in the broadphase.
+	/// @param inObjectLayerFilter Filter that is used to check if a character collides with a layer.
+	/// @param inBodyFilter Filter that is used to check if a character collides with a body.
+	/// @param inAllocator An allocator for temporary allocations. All memory will be freed by the time this function returns.
+	/// @return True if the character was successfully projected onto the floor.
+	bool								StickToFloor(Vec3Arg inStepDown, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator);
+
 	/// This function can be used after a character has teleported to determine the new contacts with the world.
 	void								RefreshContacts(const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator);
 
@@ -179,6 +199,7 @@ public:
 #ifdef JPH_DEBUG_RENDERER
 	static inline bool					sDrawConstraints = false;								///< Draw the current state of the constraints for iteration 0 when creating them
 	static inline bool					sDrawWalkStairs = false;								///< Draw the state of the walk stairs algorithm
+	static inline bool					sDrawStickToFloor = false;								///< Draw the state of the stick to floor algorithm
 #endif
 
 private:
@@ -283,11 +304,11 @@ private:
 	// Does a swept test of the shape from inPosition with displacement inDisplacement, returns true if there was a collision
 	bool								GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDisplacement, Contact &outContact, const IgnoredContactList &inIgnoredContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const;
 
-	// Store contacts so that CheckSupport and GetStandingPhysicsInstance etc. can return information
+	// Store contacts so that we have proper ground information
 	void								StoreActiveContacts(const TempContactList &inContacts, TempAllocator &inAllocator);
 
 	// This function will determine which contacts are touching the character and will calculate the one that is supporting us
-	void								UpdateSupportingContact(TempAllocator &inAllocator);
+	void								UpdateSupportingContact(bool inSkipContactVelocityCheck, TempAllocator &inAllocator);
 
 	// This function returns the actual center of mass of the shape, not corrected for the character padding
 	inline Mat44						GetCenterOfMassTransform(Vec3Arg inPosition, QuatArg inRotation, const Shape *inShape) const
@@ -298,9 +319,6 @@ private:
 	// Our main listener for contacts
 	CharacterContactListener *			mListener = nullptr;
 
-	// The character's world space up axis
-	Vec3								mUp;
-
 	// Movement settings
 	float								mPredictiveContactDistance;								// How far to scan outside of the shape for predictive contacts
 	uint								mMaxCollisionIterations;								// Max amount of collision loops

+ 1 - 0
Samples/SamplesApp.cpp

@@ -431,6 +431,7 @@ SamplesApp::SamplesApp()
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Submerged Volumes", Shape::sDrawSubmergedVolumes, [](UICheckBox::EState inState) { Shape::sDrawSubmergedVolumes = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Character Virtual Constraints", CharacterVirtual::sDrawConstraints, [](UICheckBox::EState inState) { CharacterVirtual::sDrawConstraints = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Character Virtual Walk Stairs", CharacterVirtual::sDrawWalkStairs, [](UICheckBox::EState inState) { CharacterVirtual::sDrawWalkStairs = inState == UICheckBox::STATE_CHECKED; });
+			mDebugUI->CreateCheckBox(drawing_options, "Draw Character Virtual Stick To Floor", CharacterVirtual::sDrawStickToFloor, [](UICheckBox::EState inState) { CharacterVirtual::sDrawStickToFloor = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->ShowMenu(drawing_options);
 		});
 	#endif // JPH_DEBUG_RENDERER

+ 3 - 1
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -313,5 +313,7 @@ void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, Mat
 
 	// Draw text info
 	const PhysicsMaterial *ground_material = inCharacter->GetGroundMaterial();
-	mDebugRenderer->DrawText3D(inCharacterTransform.GetTranslation(), StringFormat("Mat: %s\nVel: %.1f m/s", ground_material->GetDebugName(), (double)inCharacterVelocity.Length()), color, 0.25f);
+	Vec3 horizontal_velocity = inCharacterVelocity;
+	horizontal_velocity.SetY(0);
+	mDebugRenderer->DrawText3D(inCharacterTransform.GetTranslation(), StringFormat("Mat: %s\nHorizontal Vel: %.1f m/s\nVertical Vel: %.1f m/s", ground_material->GetDebugName(), (double)horizontal_velocity.Length(), (double)inCharacterVelocity.GetY()), color, 0.25f);
 }

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

@@ -54,9 +54,16 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	// Remember old position
 	Vec3 old_position = mCharacter->GetPosition();
 
+	// Track that on ground before the update
+	bool ground_to_air = mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround;
+
 	// Update the character position (instant, do not have to wait for physics update)
 	mCharacter->Update(inParams.mDeltaTime, mPhysicsSystem->GetGravity(), mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, *mTempAllocator);
 
+	// ... and that we got into air after
+	if (mCharacter->GetGroundState() != CharacterBase::EGroundState::InAir)
+		ground_to_air = false;
+
 	// Allow user to turn off walk stairs algorithm
 	if (sEnableWalkStairs)
 	{
@@ -74,6 +81,9 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		if (achieved_horizontal_step_len + 1.0e-4f < desired_horizontal_step_len
 			&& mCharacter->CanWalkStairs())
 		{
+			// CanWalkStairs should not have returned true if we are in air
+			JPH_ASSERT(!ground_to_air);
+
 			// Calculate how much we should step forward
 			Vec3 step_forward_normalized = desired_horizontal_step / desired_horizontal_step_len;
 			Vec3 step_forward = step_forward_normalized * (desired_horizontal_step_len - achieved_horizontal_step_len);
@@ -89,6 +99,13 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	Vec3 new_position = mCharacter->GetPosition();
 	Vec3 velocity = (new_position - old_position) / inParams.mDeltaTime;
 
+	if (sEnableStickToFloor)
+	{
+		// If we're in air for the first frame and we're not moving up, stick to the floor
+		if (ground_to_air && velocity.GetY() <= 1.0e-6f)
+			mCharacter->StickToFloor(Vec3(0, -0.5f, 0), mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, *mTempAllocator);
+	}
+
 	// Draw state of character
 	DrawCharacterState(mCharacter, mCharacter->GetWorldTransform(), velocity);
 
@@ -113,6 +130,9 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 	// Smooth the player input
 	mSmoothMovementDirection = 0.25f * inMovementDirection + 0.75f * mSmoothMovementDirection;
 
+	// True if the player intended to move
+	mAllowSliding = !inMovementDirection.IsNearZero();
+
 	Vec3 current_vertical_velocity = Vec3(0, mCharacter->GetLinearVelocity().GetY(), 0);
 
 	Vec3 ground_velocity = mCharacter->GetGroundVelocity();
@@ -158,6 +178,7 @@ void CharacterVirtualTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMen
 		inUI->CreateSlider(configuration_settings, "Penetration Recovery Speed", sPenetrationRecoverySpeed, 0.0f, 1.0f, 0.05f, [](float inValue) { sPenetrationRecoverySpeed = inValue; });
 		inUI->CreateSlider(configuration_settings, "Predictive Contact Distance", sPredictiveContactDistance, 0.01f, 1.0f, 0.01f, [](float inValue) { sPredictiveContactDistance = inValue; });
 		inUI->CreateCheckBox(configuration_settings, "Enable Walk Stairs", sEnableWalkStairs, [](UICheckBox::EState inState) { sEnableWalkStairs = inState == UICheckBox::STATE_CHECKED; });
+		inUI->CreateCheckBox(configuration_settings, "Enable Stick To Floor", sEnableStickToFloor, [](UICheckBox::EState inState) { sEnableStickToFloor = inState == UICheckBox::STATE_CHECKED; });
 		inUI->CreateTextButton(configuration_settings, "Accept Changes", [=]() { RestartTest(); });
 		inUI->ShowMenu(configuration_settings);
 	});
@@ -198,4 +219,15 @@ void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, c
 		ioSettings.mCanPushCharacter = (index & 1) != 0;
 		ioSettings.mCanReceiveImpulses = (index & 2) != 0;
 	}
+
+	// If we encounter an object that can push us, enable sliding
+	if (ioSettings.mCanPushCharacter)
+		mAllowSliding = true;
+}
+
+void CharacterVirtualTest::OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, Vec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity)
+{
+	// Don't allow the player to slide down static not-too-steep surfaces when not actively moving and when not on a moving platform
+	if (!mAllowSliding && inContactVelocity.IsNearZero() && !inCharacter->IsSlopeTooSteep(inContactNormal))
+		ioNewCharacterVelocity = Vec3::sZero();
 }

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

@@ -28,6 +28,9 @@ public:
 	// 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, Vec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
 
+	// Called whenever the character movement is solved and a constraint is hit. Allows the listener to override the resulting character velocity (e.g. by preventing sliding along certain surfaces).
+	virtual void			OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, Vec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity) override;
+
 protected:
 	// Get position of the character
 	virtual Vec3			GetCharacterPosition() const override				{ return mCharacter->GetPosition(); }
@@ -43,10 +46,14 @@ private:
 	static inline float		sPenetrationRecoverySpeed = 1.0f;
 	static inline float		sPredictiveContactDistance = 0.1f;
 	static inline bool		sEnableWalkStairs = true;
+	static inline bool		sEnableStickToFloor = true;
 
 	// The 'player' character
 	Ref<CharacterVirtual>	mCharacter;
 
 	// Smoothed value of the player input
 	Vec3					mSmoothMovementDirection = Vec3::sZero();
+
+	// True when the player is pressing movement controls
+	bool					mAllowSliding = false;
 };