Prechádzať zdrojové kódy

Character improvements (#273)

* Added supporting volume for character, contacts that fall outside will return the state NotSupported and will no longer prevent the character from falling
* Improved fraction correction in GetFirstContactForSweep so that it uses the actual face of the contact instead of estimating it with an infinite plane. Before the character would jump up a lot during stair walk with a frequency of 300Hz.
* Allow walk stairs to slide during horizontal movement. This fixes a bug where a wall next to a stairs would prevent the player from doing stair walking
* Ensure that we prefer returning a supporting contact even if the normal of a non-supporting contact is pointing up more
* Update character test level with stairs and walls next to stairs
* Set max update frequency for sample app to 300 Hz
* Fixed issue where character would not walk stairs well at 300 Hz by doing stick to floor before walk stairs. Sliding up a convex shape (along the rounded side) could cause the character to lose ground for a moment which would then prevent stair walking.
Jorrit Rouwe 3 rokov pred
rodič
commit
f7527f3282

+ 19 - 2
Jolt/Physics/Character/Character.cpp

@@ -119,6 +119,20 @@ void Character::CheckCollision(const Shape *inShape, float inMaxSeparationDistan
 
 void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 {
+	// Get character position, rotation and velocity
+	Vec3 char_pos;
+	Quat char_rot;
+	Vec3 char_vel;
+	{
+		BodyLockRead lock(sGetBodyLockInterface(mSystem, inLockBodies), mBodyID);
+		if (!lock.Succeeded())
+			return;
+		const Body &body = lock.GetBody();
+		char_pos = body.GetPosition();
+		char_rot = body.GetRotation();
+		char_vel = body.GetLinearVelocity();
+	}
+
 	// Collector that finds the hit with the normal that is the most 'up'
 	class MyCollector : public CollideShapeCollector
 	{
@@ -153,7 +167,7 @@ void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 
 	// Collide shape
 	MyCollector collector(mUp);
-	CheckCollision(mShape, inMaxSeparationDistance, collector, inLockBodies);
+	CheckCollision(char_pos, char_rot, char_vel, inMaxSeparationDistance, mShape, collector, inLockBodies);
 
 	// Copy results
 	mGroundBodyID = collector.mGroundBodyID;
@@ -168,7 +182,10 @@ void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 		const Body &body = lock.GetBody();
 
 		// Update ground state
-		if (IsSlopeTooSteep(mGroundNormal))
+		Mat44 inv_transform = Mat44::sInverseRotationTranslation(char_rot, char_pos);
+		if (mSupportingVolume.SignedDistance(inv_transform * mGroundPosition) > 0.0f)
+			mGroundState = EGroundState::NotSupported;
+		else if (IsSlopeTooSteep(mGroundNormal))
 			mGroundState = EGroundState::OnSteepGround;
 		else
 			mGroundState = EGroundState::OnGround;

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

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

+ 13 - 1
Jolt/Physics/Character/CharacterBase.h

@@ -27,6 +27,11 @@ public:
 	/// Vector indicating the up direction of the character
 	Vec3								mUp = Vec3::sAxisY();
 
+	/// Plane, defined in local space relative to the character. Every contact behind this plane can support the
+	/// character, every contact in front of this plane is treated as only colliding with the player.
+	/// Default: Accept any contact.
+	Plane								mSupportingVolume { Vec3::sAxisY(), -1.0e10f };
+
 	/// Maximum angle of slope that character can still walk on (radians).
 	float								mMaxSlopeAngle = DegreesToRadians(50.0f);
 
@@ -70,7 +75,8 @@ public:
 	{
 		OnGround,						///< Character is on the ground and can move freely.
 		OnSteepGround,					///< Character is on a slope that is too steep and can't climb up any further. The caller should start applying downward velocity if sliding from the slope is desired.
-		InAir,							///< Character is in the air.
+		NotSupported,					///< Character is touching an object, but is not supported by it and should fall. The GetGroundXXX functions will return information about the touched object.
+		InAir,							///< Character is in the air and is not touching anything.
 	};
 
 	///@name Properties of the ground this character is standing on
@@ -78,6 +84,9 @@ public:
 	/// Current ground state
 	EGroundState						GetGroundState() const									{ return mGroundState; }
 
+	/// Returns true if the player is supported by normal or steep ground
+	bool								IsSupported() const										{ return mGroundState == EGroundState::OnGround || mGroundState == EGroundState::OnSteepGround; }
+
 	/// Get the contact point with the ground
 	Vec3 								GetGroundPosition() const								{ return mGroundPosition; }
 
@@ -113,6 +122,9 @@ protected:
 	// The character's world space up axis
 	Vec3								mUp;
 
+	// Every contact behind this plane can support the character
+	Plane								mSupportingVolume;
+
 	// Beyond this value there is no max slope
 	static constexpr float				cNoMaxSlopeAngle = 0.9999f;
 

+ 118 - 43
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -8,7 +8,10 @@
 #include <Jolt/Physics/PhysicsSystem.h>
 #include <Jolt/Physics/Collision/ShapeCast.h>
 #include <Jolt/Physics/Collision/CollideShape.h>
+#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Jolt/Core/QuickSort.h>
+#include <Jolt/Geometry/ConvexSupport.h>
+#include <Jolt/Geometry/GJKClosestPoint.h>
 #ifdef JPH_DEBUG_RENDERER
 	#include <Jolt/Renderer/DebugRenderer.h>
 #endif // JPH_DEBUG_RENDERER
@@ -175,10 +178,37 @@ bool CharacterVirtual::ValidateContact(const Contact &inContact) const
 	return mListener->OnContactValidate(this, inContact.mBodyB, inContact.mSubShapeIDB);
 }
 
+template <class T>
+inline static bool sCorrectFractionForCharacterPadding(const Shape *inShape, Mat44Arg inStart, Vec3Arg inDisplacement, const T &inPolygon, float &ioFraction)
+{
+	if (inShape->GetType() == EShapeType::Convex)
+	{
+		// Get the support function for the shape we're casting
+		const ConvexShape *convex_shape = static_cast<const ConvexShape *>(inShape);
+		ConvexShape::SupportBuffer buffer;
+		const ConvexShape::Support *support = convex_shape->GetSupportFunction(ConvexShape::ESupportMode::IncludeConvexRadius, buffer, Vec3::sReplicate(1.0f));
+
+		// Cast the shape against the polygon
+		GJKClosestPoint gjk;
+		return gjk.CastShape(inStart, inDisplacement, cDefaultCollisionTolerance, *support, inPolygon, ioFraction);
+	}
+	else if (inShape->GetSubType() == EShapeSubType::RotatedTranslated)
+	{
+		const RotatedTranslatedShape *rt_shape = static_cast<const RotatedTranslatedShape *>(inShape);
+		return sCorrectFractionForCharacterPadding(rt_shape->GetInnerShape(), inStart * Mat44::sRotation(rt_shape->GetRotation()), inDisplacement, inPolygon, ioFraction);
+	}
+	else
+	{
+		JPH_ASSERT(false, "Not supported yet!");
+		return false;
+	}
+}
+
 bool CharacterVirtual::GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDisplacement, Contact &outContact, const IgnoredContactList &inIgnoredContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const
 {
 	// Too small distance -> skip checking
-	if (inDisplacement.LengthSq() < 1.0e-8f)
+	float displacement_len_sq = inDisplacement.LengthSq();
+	if (displacement_len_sq < 1.0e-8f)
 		return false;
 
 	// Calculate start transform
@@ -217,15 +247,28 @@ bool CharacterVirtual::GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDis
 	if (!valid_contact)
 		return false;
 
-	// Correct fraction for the padding that we want to keep from geometry
-	// We want to maintain distance of mCharacterPadding (p) along plane normal outContact.mContactNormal (n) to the capsule by moving back along inDisplacement (d) by amount d'
-	// cos(angle between d and -n) = -n dot d / |d| = p / d'
-	// <=> d' = -p |d| / n dot d
-	// The new fraction of collision is then:
-	// f' = f - d' / |d| = f + p / n dot d
-	float dot = outContact.mContactNormal.Dot(inDisplacement);
-	if (dot < 0.0f) // We should not divide by zero and we should only update the fraction if normal is pointing towards displacement
-		outContact.mFraction = max(0.0f, outContact.mFraction + mCharacterPadding / dot);
+	// Fetch the face we're colliding with
+	TransformedShape ts = mSystem->GetBodyInterface().GetTransformedShape(outContact.mBodyB);
+	Shape::SupportingFace face;
+	ts.GetSupportingFace(outContact.mSubShapeIDB, -outContact.mContactNormal, face);
+
+	bool corrected = false;
+	if (face.size() >= 2)
+	{
+		// Inflate the colliding face by the character padding
+		PolygonConvexSupport polygon(face);
+		AddConvexRadius add_cvx(polygon, mCharacterPadding);
+
+		// Correct fraction to hit this inflated face instead of the inner shape
+		corrected = sCorrectFractionForCharacterPadding(mShape, start, inDisplacement, add_cvx, outContact.mFraction);
+	}
+	if (!corrected)
+	{
+		// When there's only a single contact point or when we were unable to correct the fraction,
+		// we can just move the fraction back so that the character and its padding don't hit the contact point anymore
+		outContact.mFraction = max(0.0f, outContact.mFraction - mCharacterPadding / sqrt(displacement_len_sq));
+	}
+
 	return true;
 }
 
@@ -584,6 +627,9 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 			c.mHadCollision |= c.mDistance < mCollisionTolerance
 								&& (inSkipContactVelocityCheck || c.mSurfaceNormal.Dot(mLinearVelocity - c.mLinearVelocity) <= 0.0f);
 
+	// Calculate transform that takes us to character local space
+	Mat44 inv_transform = Mat44::sInverseRotationTranslation(mRotation, mPosition);
+
 	// Determine if we're supported or not
 	int num_supported = 0;
 	int num_sliding = 0;
@@ -592,13 +638,26 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 	Vec3 avg_velocity = Vec3::sZero();
 	const Contact *supporting_contact = nullptr;
 	float max_cos_angle = -FLT_MAX;
+	const Contact *deepest_contact = nullptr;
+	float smallest_distance = FLT_MAX;
 	for (const Contact &c : mActiveContacts)
 		if (c.mHadCollision)
 		{
 			// Calculate the angle between the plane normal and the up direction
 			float cos_angle = c.mSurfaceNormal.Dot(mUp);
 
-			// Find the contact with the normal that is pointing most upwards and store it in mSupportingContact
+			// Find the deepest contact
+			if (c.mDistance < smallest_distance)
+			{
+				deepest_contact = &c;
+				smallest_distance = c.mDistance;
+			}
+
+			// If this contact is in front of our plane, we cannot be supported by it
+			if (mSupportingVolume.SignedDistance(inv_transform * c.mPosition) > 0.0f)
+				continue;
+
+			// Find the contact with the normal that is pointing most upwards and store it
 			if (max_cos_angle < cos_angle)
 			{
 				supporting_contact = &c;
@@ -667,16 +726,19 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 			}
 		}
 
+	// Take either the most supporting contact or the deepest contact
+	const Contact *best_contact = supporting_contact != nullptr? supporting_contact : deepest_contact;
+
 	// Calculate average normal and velocity
 	if (num_avg_normal >= 1)
 	{
 		mGroundNormal = avg_normal.Normalized();
 		mGroundVelocity = avg_velocity / float(num_avg_normal);
 	}
-	else if (supporting_contact != nullptr)
+	else if (best_contact != nullptr)
 	{
-		mGroundNormal = supporting_contact->mSurfaceNormal;
-		mGroundVelocity = supporting_contact->mLinearVelocity;
+		mGroundNormal = best_contact->mSurfaceNormal;
+		mGroundVelocity = best_contact->mLinearVelocity;
 	}
 	else
 	{
@@ -684,14 +746,14 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 		mGroundVelocity = Vec3::sZero();
 	}
 
-	// Copy supporting contact properties
-	if (supporting_contact != nullptr)
+	// Copy contact properties
+	if (best_contact != nullptr)
 	{
-		mGroundBodyID = supporting_contact->mBodyB;
-		mGroundBodySubShapeID = supporting_contact->mSubShapeIDB;
-		mGroundPosition = supporting_contact->mPosition;
-		mGroundMaterial = supporting_contact->mMaterial;
-		mGroundUserData = supporting_contact->mUserData;
+		mGroundBodyID = best_contact->mBodyB;
+		mGroundBodySubShapeID = best_contact->mSubShapeIDB;
+		mGroundPosition = best_contact->mPosition;
+		mGroundMaterial = best_contact->mMaterial;
+		mGroundUserData = best_contact->mUserData;
 	}
 	else
 	{
@@ -734,8 +796,8 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 	}
 	else
 	{
-		// Not in contact with anything
-		mGroundState = EGroundState::InAir;
+		// Not supported by anything
+		mGroundState = best_contact != nullptr? EGroundState::NotSupported : EGroundState::InAir;
 	}
 }
 
@@ -876,7 +938,7 @@ void CharacterVirtual::MoveToContact(Vec3Arg inPosition, const Contact &inContac
 	GetContactsAtPosition(mPosition, mLinearVelocity.NormalizedOr(Vec3::sZero()), mShape, contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
 
 	// Ensure that we mark inContact as colliding
-	JPH_IF_ENABLE_ASSERTS(bool found_contact = false;)
+	bool found_contact = false;
 	for (Contact &c : contacts)
 		if (c.mBodyB == inContact.mBodyB
 			&& c.mSubShapeIDB == inContact.mSubShapeIDB)
@@ -884,7 +946,13 @@ void CharacterVirtual::MoveToContact(Vec3Arg inPosition, const Contact &inContac
 			c.mHadCollision = true;
 			JPH_IF_ENABLE_ASSERTS(found_contact = true;)
 		}
-	JPH_ASSERT(found_contact);
+	if (!found_contact)
+	{
+		contacts.push_back(inContact);
+
+		Contact &copy = contacts.back();
+		copy.mHadCollision = true;
+	}
 
 	StoreActiveContacts(contacts, inAllocator);
 	JPH_ASSERT(mGroundState != EGroundState::InAir);
@@ -925,6 +993,10 @@ bool CharacterVirtual::SetShape(const Shape *inShape, float inMaxPenetrationDept
 
 bool CharacterVirtual::CanWalkStairs(Vec3Arg inLinearVelocity) const
 {
+	// We can only walk stairs if we're supported
+	if (!IsSupported())
+		return false;
+
 	// Check if there's enough horizontal velocity to trigger a stair walk
 	Vec3 horizontal_velocity = inLinearVelocity - inLinearVelocity.Dot(mUp) * mUp;
 	if (horizontal_velocity.IsNearZero(1.0e-6f))
@@ -965,7 +1037,8 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inStepUp, Vec3Arg i
 	// Horizontal movement
 	Vec3 new_position = up_position;
 	MoveShape(new_position, inStepForward / inDeltaTime, inDeltaTime, nullptr, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
-	if (new_position.IsClose(up_position, 1.0e-8f))
+	float horizontal_movement_sq = (new_position - up_position).LengthSq();
+	if (horizontal_movement_sq < 1.0e-8f)
 		return false; // No movement, cancel
 
 #ifdef JPH_DEBUG_RENDERER
@@ -1002,28 +1075,28 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inStepUp, Vec3Arg i
 		// Delta time may be very small, so it may be that we hit the edge of a step and the normal is too horizontal.
 		// In order to judge if the floor is flat further along the sweep, we test again for a floor at inStepForwardTest
 		// and check if the normal is valid there.
-		Vec3 test_position = up_position + inStepForwardTest;
+		Vec3 test_position = up_position;
+		MoveShape(test_position, inStepForwardTest / inDeltaTime, inDeltaTime, nullptr, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
+		float test_horizontal_movement_sq = (test_position - up_position).LengthSq();
+		if (test_horizontal_movement_sq <= horizontal_movement_sq + 1.0e-8f)
+			return false; // We didn't move any further than in the previous test
+
+	#ifdef JPH_DEBUG_RENDERER
+		// Draw 2nd sweep horizontal
+		if (sDrawWalkStairs)
+			DebugRenderer::sInstance->DrawArrow(up_position, test_position, Color::sCyan, 0.01f);
+	#endif // JPH_DEBUG_RENDERER
 
-		// First sweep forward to the test position
+		// Then sweep down
 		Contact test_contact;
-		if (!GetFirstContactForSweep(up_position, inStepForwardTest, test_contact, dummy_ignored_contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator))
-		{
-			// Then sweep down
-			if (!GetFirstContactForSweep(test_position, down, test_contact, dummy_ignored_contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator))
-				return false;
-		}
-		else
-		{
-			// If we didn't move down, set the 'down' fraction to zero
-			test_contact.mFraction = 0.0f;
-		}
+		if (!GetFirstContactForSweep(test_position, down, test_contact, dummy_ignored_contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator))
+			return false;
 
 	#ifdef JPH_DEBUG_RENDERER
-		// Draw 2nd sweep forward and down
+		// Draw 2nd sweep down
 		if (sDrawWalkStairs)
 		{
 			Vec3 debug_pos = test_position + test_contact.mFraction * down; 
-			DebugRenderer::sInstance->DrawArrow(up_position, test_position, Color::sCyan, 0.01f);
 			DebugRenderer::sInstance->DrawArrow(test_position, debug_pos, Color::sCyan, 0.01f);
 			DebugRenderer::sInstance->DrawArrow(test_contact.mPosition, test_contact.mPosition + test_contact.mSurfaceNormal, Color::sCyan, 0.01f);
 			mShape->Draw(DebugRenderer::sInstance, GetCenterOfMassTransform(debug_pos, mRotation, mShape), Vec3::sReplicate(1.0f), Color::sCyan, false, true);
@@ -1040,13 +1113,15 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inStepUp, Vec3Arg i
 
 	// Move the character to the new location
 	MoveToContact(new_position, contact, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
+
+	// Override ground state to 'on ground', it is possible that the contact normal is too steep, but in this case the inStepForwardTest has found a contact normal that is not too steep
+	mGroundState = EGroundState::OnGround;
+
 	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);

+ 1 - 1
Samples/SamplesApp.cpp

@@ -373,7 +373,7 @@ SamplesApp::SamplesApp()
 			UIElement *phys_settings = mDebugUI->CreateMenu();
 			mDebugUI->CreateSlider(phys_settings, "Max Concurrent Jobs", float(mMaxConcurrentJobs), 1, float(thread::hardware_concurrency()), 1, [this](float inValue) { mMaxConcurrentJobs = (int)inValue; });
 			mDebugUI->CreateSlider(phys_settings, "Gravity (m/s^2)", -mPhysicsSystem->GetGravity().GetY(), 0.0f, 20.0f, 1.0f, [this](float inValue) { mPhysicsSystem->SetGravity(Vec3(0, -inValue, 0)); });
-			mDebugUI->CreateSlider(phys_settings, "Update Frequency (Hz)", mUpdateFrequency, 7.5f, 120.0f, 2.5f, [this](float inValue) { mUpdateFrequency = inValue; });
+			mDebugUI->CreateSlider(phys_settings, "Update Frequency (Hz)", mUpdateFrequency, 7.5f, 300.0f, 2.5f, [this](float inValue) { mUpdateFrequency = inValue; });
 			mDebugUI->CreateSlider(phys_settings, "Num Collision Steps", float(mCollisionSteps), 1.0f, 4.0f, 1.0f, [this](float inValue) { mCollisionSteps = int(inValue); });
 			mDebugUI->CreateSlider(phys_settings, "Num Integration Sub Steps", float(mIntegrationSubSteps), 1.0f, 4.0f, 1.0f, [this](float inValue) { mIntegrationSubSteps = int(inValue); });
 			mDebugUI->CreateSlider(phys_settings, "Num Velocity Steps", float(mPhysicsSettings.mNumVelocitySteps), 0, 30, 1, [this](float inValue) { mPhysicsSettings.mNumVelocitySteps = int(round(inValue)); mPhysicsSystem->SetPhysicsSettings(mPhysicsSettings); });

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

@@ -56,10 +56,12 @@ static const float cLargeBumpWidth = 0.1f;
 static const float cLargeBumpDelta = 2.0f;
 static const Vec3 cStairsPosition = Vec3(-15.0f, 0, 2.5f);
 static const float cStairsStepHeight = 0.3f;
+static const Vec3 cMeshStairsPosition = Vec3(-20.0f, 0, 2.5f);
 static const Vec3 cNoStairsPosition = Vec3(-15.0f, 0, 10.0f);
 static const float cNoStairsStepHeight = 0.3f;
 static const float cNoStairsStepDelta = 0.05f;
-static const Vec3 cMeshWallPosition = Vec3(-20.0f, 0, -27.0f);
+static const Vec3 cMeshNoStairsPosition = Vec3(-20.0f, 0, 10.0f);
+static const Vec3 cMeshWallPosition = Vec3(-25.0f, 0, -27.0f);
 static const float cMeshWallHeight = 3.0f;
 static const float cMeshWallWidth = 2.0f;
 static const float cMeshWallStepStart = 0.5f;
@@ -247,6 +249,55 @@ void CharacterBaseTest::Initialize()
 			}
 		}
 
+		// A wall beside the stairs
+		mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 2.0f, 5.0f * cStairsStepHeight)), cStairsPosition + Vec3(-2.5f, 2.0f, 5.0f * cStairsStepHeight), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+		// Create stairs from triangles
+		{
+			TriangleList triangles;
+
+			float rear_z = 10 * cStairsStepHeight;
+
+			for (int i = 0; i < 10; ++i)
+			{
+				// Start of step
+				Vec3 base(0, cStairsStepHeight * i, cStairsStepHeight * i);
+
+				// Left side
+				Vec3 b1 = base + Vec3(2.0f, 0, 0);
+				Vec3 s1 = b1 + Vec3(0, cStairsStepHeight, 0);
+				Vec3 p1 = s1 + Vec3(0, 0, cStairsStepHeight);
+
+				// Right side
+				Vec3 width(-4.0f, 0, 0);
+				Vec3 b2 = b1 + width;
+				Vec3 s2 = s1 + width;
+				Vec3 p2 = p1 + width;
+
+				triangles.push_back(Triangle(s1, b1, s2));
+				triangles.push_back(Triangle(b1, b2, s2));
+				triangles.push_back(Triangle(s1, p2, p1));
+				triangles.push_back(Triangle(s1, s2, p2));
+
+				// Side of stairs
+				Vec3 rb2 = b2; rb2.SetZ(rear_z);
+				Vec3 rs2 = s2; rs2.SetZ(rear_z);
+
+				triangles.push_back(Triangle(s2, b2, rs2));
+				triangles.push_back(Triangle(rs2, b2, rb2));
+
+				p1 = p2;
+			}
+
+			MeshShapeSettings mesh(triangles);
+			mesh.SetEmbedded();
+			BodyCreationSettings mesh_stairs(&mesh, cMeshStairsPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+			mBodyInterface->CreateAndAddBody(mesh_stairs, EActivation::DontActivate);
+		}
+
+		// A wall to the side and behind the stairs
+		mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 2.0f, 0.25f)), cStairsPosition + Vec3(-7.5f, 2.0f, 10.0f * cStairsStepHeight + 0.25f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
 		// Create stairs with too little space between the steps
 		{
 			BodyCreationSettings step(new BoxShape(Vec3(2.0f, 0.5f * cNoStairsStepHeight, 0.5f * cNoStairsStepHeight)), Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
@@ -257,6 +308,39 @@ void CharacterBaseTest::Initialize()
 			}
 		}
 
+		// Create stairs with too little space between the steps consisting of triangles
+		{
+			TriangleList triangles;
+
+			for (int i = 0; i < 10; ++i)
+			{
+				// Start of step
+				Vec3 base(0, cStairsStepHeight * i, cNoStairsStepDelta * i);
+
+				// Left side
+				Vec3 b1 = base - Vec3(2.0f, 0, 0);
+				Vec3 s1 = b1 + Vec3(0, cStairsStepHeight, 0);
+				Vec3 p1 = s1 + Vec3(0, 0, cNoStairsStepDelta);
+
+				// Right side
+				Vec3 width(4.0f, 0, 0);
+				Vec3 b2 = b1 + width;
+				Vec3 s2 = s1 + width;
+				Vec3 p2 = p1 + width;
+
+				triangles.push_back(Triangle(s1, s2, b1));
+				triangles.push_back(Triangle(b1, s2, b2));
+				triangles.push_back(Triangle(s1, p1, p2));
+				triangles.push_back(Triangle(s1, p2, s2));
+				p1 = p2;
+			}
+
+			MeshShapeSettings mesh(triangles);
+			mesh.SetEmbedded();
+			BodyCreationSettings mesh_stairs(&mesh, cMeshNoStairsPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+			mBodyInterface->CreateAndAddBody(mesh_stairs, EActivation::DontActivate);
+		}
+
 		// Create mesh with walls at varying angles
 		{
 			TriangleList triangles;
@@ -403,6 +487,9 @@ void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, Mat
 		color = Color::sGreen;
 		break;
 	case CharacterBase::EGroundState::OnSteepGround:
+		color = Color::sYellow;
+		break;
+	case CharacterBase::EGroundState::NotSupported:
 		color = Color::sOrange;
 		break;
 	case CharacterBase::EGroundState::InAir:

+ 4 - 2
Samples/Tests/Character/CharacterTest.cpp

@@ -29,6 +29,7 @@ void CharacterTest::Initialize()
 	settings->mLayer = Layers::MOVING;
 	settings->mShape = mStandingShape;
 	settings->mFriction = 0.5f;
+	settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
 	mCharacter = new Character(settings, Vec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
 	mCharacter->AddToPhysicsSystem(EActivation::Activate);
 }
@@ -69,9 +70,10 @@ void CharacterTest::RestoreState(StateRecorder &inStream)
 
 void CharacterTest::HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime)
 {
-	// Cancel movement in opposite direction of normal when sliding
+	// Cancel movement in opposite direction of normal when touching something we can't walk up
 	Character::EGroundState ground_state = mCharacter->GetGroundState();
-	if (ground_state == Character::EGroundState::OnSteepGround)
+	if (ground_state == Character::EGroundState::OnSteepGround
+		|| ground_state == Character::EGroundState::NotSupported)
 	{
 		Vec3 normal = mCharacter->GetGroundNormal();
 		normal.SetY(0.0f);

+ 15 - 14
Samples/Tests/Character/CharacterVirtualTest.cpp

@@ -31,6 +31,7 @@ void CharacterVirtualTest::Initialize()
 	settings->mCharacterPadding = sCharacterPadding;
 	settings->mPenetrationRecoverySpeed = sPenetrationRecoverySpeed;
 	settings->mPredictiveContactDistance = sPredictiveContactDistance;
+	settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
 	mCharacter = new CharacterVirtual(settings, Vec3::sZero(), Quat::sIdentity(), mPhysicsSystem);
 	mCharacter->SetListener(this);
 }
@@ -56,15 +57,24 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	Vec3 old_position = mCharacter->GetPosition();
 
 	// Track that on ground before the update
-	bool ground_to_air = mCharacter->GetGroundState() != CharacterBase::EGroundState::InAir;
+	bool ground_to_air = mCharacter->IsSupported();
 
 	// 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)
+	if (mCharacter->IsSupported())
 		ground_to_air = false;
 
+	// If we're in air for the first frame and the user has enabled stick to floor
+	if (sEnableStickToFloor && ground_to_air)
+	{
+		// If we're not moving up, stick to the floor
+		float velocity = (mCharacter->GetPosition().GetY() - old_position.GetY()) / inParams.mDeltaTime;
+		if (velocity <= 1.0e-6f)
+			mCharacter->StickToFloor(Vec3(0, -0.5f, 0), mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, *mTempAllocator);
+	}
+
 	// Allow user to turn off walk stairs algorithm
 	if (sEnableWalkStairs)
 	{
@@ -87,9 +97,6 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 			if (achieved_horizontal_step_len + 1.0e-4f < desired_horizontal_step_len
 				&& mCharacter->CanWalkStairs(mDesiredVelocity))
 			{
-				// CanWalkStairs should not have returned true if we are in air
-				JPH_ASSERT(!ground_to_air);
-
 				// Calculate how much we should step forward
 				// Note that we clamp the step forward to a minimum distance. This is done because at very high frame rates the delta time
 				// may be very small, causing a very small step forward. If the step becomes small enough, we may not move far enough
@@ -109,13 +116,6 @@ 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);
 
@@ -132,10 +132,11 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 	// True if the player intended to move
 	mAllowSliding = !inMovementDirection.IsNearZero();
 
-	// Cancel movement in opposite direction of normal when sliding
+	// Cancel movement in opposite direction of normal when touching something we can't walk up
 	CharacterVirtual::EGroundState ground_state = mCharacter->GetGroundState();
 	Vec3 desired_velocity = mDesiredVelocity;
-	if (ground_state == CharacterVirtual::EGroundState::OnSteepGround)
+	if (ground_state == CharacterVirtual::EGroundState::OnSteepGround
+		|| ground_state == CharacterVirtual::EGroundState::NotSupported)
 	{
 		Vec3 normal = mCharacter->GetGroundNormal();
 		normal.SetY(0.0f);