Browse Source

Character improvements (#231)

- Use surface instead of contact normal to check if slope is too steep
- Reduce amount of jittering by keeping track of last velocity applied and stopping sliding when velocity reverses
- Fixed bug in stair walk: 2nd sweep wasn't doing a horizontal sweep first which could cause it to end up in the middle of something at which point the sweep down would completely ignore this (because initial overlaps are ignored)
- Drawing contact point more subtle
- Draw constraints only in the first iteration (else they will start to overlap and become unreadable)
Jorrit Rouwe 2 years ago
parent
commit
298cd5a026

+ 78 - 47
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -34,11 +34,16 @@ CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, V
 }
 }
 
 
 template <class taCollector>
 template <class taCollector>
-void CharacterVirtual::sFillContactProperties(Contact &outContact, const Body &inBody, const taCollector &inCollector, const CollideShapeResult &inResult)
+void CharacterVirtual::sFillContactProperties(Contact &outContact, const Body &inBody, Vec3Arg inUp, const taCollector &inCollector, const CollideShapeResult &inResult)
 {
 {
 	outContact.mPosition = inResult.mContactPointOn2;	
 	outContact.mPosition = inResult.mContactPointOn2;	
 	outContact.mLinearVelocity = inBody.GetPointVelocity(inResult.mContactPointOn2);
 	outContact.mLinearVelocity = inBody.GetPointVelocity(inResult.mContactPointOn2);
-	outContact.mNormal = -inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero());
+	outContact.mContactNormal = -inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero());
+	outContact.mSurfaceNormal = inCollector.GetContext()->GetWorldSpaceSurfaceNormal(inResult.mSubShapeID2, inResult.mContactPointOn2);
+	if (outContact.mContactNormal.Dot(outContact.mSurfaceNormal) < 0.0f)
+		outContact.mSurfaceNormal = -outContact.mSurfaceNormal; // Flip surface normal if we're hitting a back face
+	if (outContact.mContactNormal.Dot(inUp) > outContact.mSurfaceNormal.Dot(inUp))
+		outContact.mSurfaceNormal = outContact.mContactNormal; // Replace surface normal with contact normal if the contact normal is pointing more upwards
 	outContact.mDistance = -inResult.mPenetrationDepth;
 	outContact.mDistance = -inResult.mPenetrationDepth;
 	outContact.mBodyB = inResult.mBodyID2;
 	outContact.mBodyB = inResult.mBodyID2;
 	outContact.mSubShapeIDB = inResult.mSubShapeID2;
 	outContact.mSubShapeIDB = inResult.mSubShapeID2;
@@ -56,7 +61,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, *this, inResult);
+		sFillContactProperties(contact, body, mUp, *this, inResult);
 		contact.mFraction = 0.0f;
 		contact.mFraction = 0.0f;
 
 
 		// Protection from excess of contact points
 		// Protection from excess of contact points
@@ -82,7 +87,7 @@ void CharacterVirtual::ContactCastCollector::AddHit(const ShapeCastResult &inRes
 
 
 			mContacts.emplace_back();
 			mContacts.emplace_back();
 			Contact &contact = mContacts.back();
 			Contact &contact = mContacts.back();
-			sFillContactProperties(contact, body, *this, inResult);
+			sFillContactProperties(contact, body, mUp, *this, inResult);
 			contact.mFraction = inResult.mFraction;
 			contact.mFraction = inResult.mFraction;
 
 
 			// Protection from excess of contact points
 			// Protection from excess of contact points
@@ -114,7 +119,7 @@ void CharacterVirtual::GetContactsAtPosition(Vec3Arg inPosition, Vec3Arg inMovem
 	outContacts.clear();
 	outContacts.clear();
 
 
 	// Collide shape
 	// Collide shape
-	ContactCollector collector(mSystem, mMaxNumHits, outContacts);
+	ContactCollector collector(mSystem, mMaxNumHits, mUp, outContacts);
 	CheckCollision(inPosition, mRotation, inMovementDirection, mPredictiveContactDistance, inShape, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
 	CheckCollision(inPosition, mRotation, inMovementDirection, mPredictiveContactDistance, inShape, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
 
 
 	// Reduce distance to contact by padding to ensure we stay away from the object by a little margin
 	// Reduce distance to contact by padding to ensure we stay away from the object by a little margin
@@ -139,7 +144,7 @@ void CharacterVirtual::RemoveConflictingContacts(TempContactList &ioContacts, Ig
 				Contact &contact2 = ioContacts[c2];
 				Contact &contact2 = ioContacts[c2];
 				if (contact1.mBodyB == contact2.mBodyB // Only same body
 				if (contact1.mBodyB == contact2.mBodyB // Only same body
 					&& contact2.mDistance <= -cMinRequiredPenetration // Only for penetrations
 					&& contact2.mDistance <= -cMinRequiredPenetration // Only for penetrations
-					&& contact1.mNormal.Dot(contact2.mNormal) < 0.0f) // Only opposing normals
+					&& contact1.mContactNormal.Dot(contact2.mContactNormal) < 0.0f) // Only opposing normals
 				{
 				{
 					// Discard contacts with the least amount of penetration
 					// Discard contacts with the least amount of penetration
 					if (contact1.mDistance < contact2.mDistance)
 					if (contact1.mDistance < contact2.mDistance)
@@ -190,7 +195,7 @@ bool CharacterVirtual::GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDis
 	// Cast shape
 	// Cast shape
 	TempContactList contacts(inAllocator);
 	TempContactList contacts(inAllocator);
 	contacts.reserve(mMaxNumHits);
 	contacts.reserve(mMaxNumHits);
-	ContactCastCollector collector(mSystem, inDisplacement, mMaxNumHits, inIgnoredContacts, contacts);
+	ContactCastCollector collector(mSystem, inDisplacement, mMaxNumHits, mUp, inIgnoredContacts, contacts);
 	ShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
 	ShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
 	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
 	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
 	if (contacts.empty())
 	if (contacts.empty())
@@ -202,7 +207,7 @@ bool CharacterVirtual::GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDis
 	// Check the first contact that will make us penetrate more than the allowed tolerance
 	// Check the first contact that will make us penetrate more than the allowed tolerance
 	bool valid_contact = false;
 	bool valid_contact = false;
 	for (const Contact &c : contacts)
 	for (const Contact &c : contacts)
-		if (c.mDistance + c.mNormal.Dot(inDisplacement) < -mCollisionTolerance
+		if (c.mDistance + c.mContactNormal.Dot(inDisplacement) < -mCollisionTolerance
 			&& ValidateContact(c))
 			&& ValidateContact(c))
 		{
 		{
 			outContact = c;
 			outContact = c;
@@ -218,7 +223,7 @@ bool CharacterVirtual::GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDis
 	// <=> d' = -p |d| / n dot d
 	// <=> d' = -p |d| / n dot d
 	// The new fraction of collision is then:
 	// The new fraction of collision is then:
 	// f' = f - d' / |d| = f + p / n dot d
 	// f' = f - d' / |d| = f + p / n dot d
-	float dot = outContact.mNormal.Dot(inDisplacement);
+	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
 	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);
 		outContact.mFraction = max(0.0f, outContact.mFraction + mCharacterPadding / dot);
 	return true;
 	return true;
@@ -232,31 +237,31 @@ void CharacterVirtual::DetermineConstraints(TempContactList &inContacts, Constra
 
 
 		// Penetrating contact: Add a contact velocity that pushes the character out at the desired speed
 		// Penetrating contact: Add a contact velocity that pushes the character out at the desired speed
 		if (c.mDistance < 0.0f)
 		if (c.mDistance < 0.0f)
-			contact_velocity -= c.mNormal * c.mDistance * mPenetrationRecoverySpeed;
+			contact_velocity -= c.mContactNormal * c.mDistance * mPenetrationRecoverySpeed;
 
 
 		// Convert to a constraint
 		// Convert to a constraint
 		outConstraints.emplace_back();
 		outConstraints.emplace_back();
 		Constraint &constraint = outConstraints.back();
 		Constraint &constraint = outConstraints.back();
 		constraint.mContact = &c;
 		constraint.mContact = &c;
 		constraint.mLinearVelocity = contact_velocity;
 		constraint.mLinearVelocity = contact_velocity;
-		constraint.mPlane = Plane(c.mNormal, c.mDistance);
+		constraint.mPlane = Plane(c.mContactNormal, c.mDistance);
 
 
 		// Next check if the angle is too steep and if it is add an additional constraint that holds the character back
 		// Next check if the angle is too steep and if it is add an additional constraint that holds the character back
-		if (IsSlopeTooSteep(c.mNormal))
+		if (IsSlopeTooSteep(c.mSurfaceNormal))
 		{
 		{
 			// Only take planes that point up
 			// Only take planes that point up
-			float dot = c.mNormal.Dot(mUp);
+			float dot = c.mSurfaceNormal.Dot(mUp);
 			if (dot > 0.0f)
 			if (dot > 0.0f)
 			{
 			{
 				// Make horizontal normal
 				// Make horizontal normal
-				Vec3 normal = (c.mNormal - dot * mUp).Normalized();
+				Vec3 normal = (c.mSurfaceNormal - dot * mUp).Normalized();
 
 
 				// Create a secondary constraint that blocks horizontal movement
 				// Create a secondary constraint that blocks horizontal movement
 				outConstraints.emplace_back();
 				outConstraints.emplace_back();
 				Constraint &vertical_constraint = outConstraints.back();
 				Constraint &vertical_constraint = outConstraints.back();
 				vertical_constraint.mContact = &c;
 				vertical_constraint.mContact = &c;
 				vertical_constraint.mLinearVelocity = contact_velocity.Dot(normal) * normal; // Project the contact velocity on the new normal so that both planes push at an equal rate
 				vertical_constraint.mLinearVelocity = contact_velocity.Dot(normal) * normal; // Project the contact velocity on the new normal so that both planes push at an equal rate
-				vertical_constraint.mPlane = Plane(normal, c.mDistance / normal.Dot(c.mNormal)); // Calculate the distance we have to travel horizontally to hit the contact plane
+				vertical_constraint.mPlane = Plane(normal, c.mDistance / normal.Dot(c.mSurfaceNormal)); // Calculate the distance we have to travel horizontally to hit the contact plane
 			}
 			}
 		}
 		}
 	}
 	}
@@ -273,7 +278,7 @@ bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstrain
 	// Send contact added event
 	// Send contact added event
 	CharacterContactSettings settings;
 	CharacterContactSettings settings;
 	if (mListener != nullptr)
 	if (mListener != nullptr)
-		mListener->OnContactAdded(this, contact.mBodyB, contact.mSubShapeIDB, contact.mPosition, -contact.mNormal, settings);
+		mListener->OnContactAdded(this, contact.mBodyB, contact.mSubShapeIDB, contact.mPosition, -contact.mContactNormal, settings);
 	contact.mCanPushCharacter = settings.mCanPushCharacter;
 	contact.mCanPushCharacter = settings.mCanPushCharacter;
 
 
 	// If body B cannot receive an impulse, we're done
 	// If body B cannot receive an impulse, we're done
@@ -290,7 +295,7 @@ bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstrain
 	constexpr float cDamping = 0.9f;
 	constexpr float cDamping = 0.9f;
 	constexpr float cPenetrationResolution = 0.4f;
 	constexpr float cPenetrationResolution = 0.4f;
 	Vec3 relative_velocity = inVelocity - contact.mLinearVelocity;
 	Vec3 relative_velocity = inVelocity - contact.mLinearVelocity;
-	float projected_velocity = relative_velocity.Dot(contact.mNormal);
+	float projected_velocity = relative_velocity.Dot(contact.mContactNormal);
 	float delta_velocity = -projected_velocity * cDamping - min(contact.mDistance, 0.0f) * cPenetrationResolution / inDeltaTime;
 	float delta_velocity = -projected_velocity * cDamping - min(contact.mDistance, 0.0f) * cPenetrationResolution / inDeltaTime;
 
 
 	// Don't apply impulses if we're separating
 	// Don't apply impulses if we're separating
@@ -304,7 +309,7 @@ bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstrain
 	float inverse_mass = motion_properties->GetInverseMass();
 	float inverse_mass = motion_properties->GetInverseMass();
 
 
 	// Calculate the inverse of the mass of body B as seen at the contact point in the direction of the contact normal
 	// Calculate the inverse of the mass of body B as seen at the contact point in the direction of the contact normal
-	Vec3 jacobian = (contact.mPosition - center_of_mass).Cross(contact.mNormal);
+	Vec3 jacobian = (contact.mPosition - center_of_mass).Cross(contact.mContactNormal);
 	float inv_effective_mass = inverse_inertia.Multiply3x3(jacobian).Dot(jacobian) + inverse_mass;
 	float inv_effective_mass = inverse_inertia.Multiply3x3(jacobian).Dot(jacobian) + inverse_mass;
 
 
 	// Impulse P = M dv
 	// Impulse P = M dv
@@ -315,10 +320,10 @@ bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstrain
 	impulse = min(impulse, max_impulse);
 	impulse = min(impulse, max_impulse);
 
 
 	// Calculate the world space impulse to apply
 	// Calculate the world space impulse to apply
-	Vec3 world_impulse = -impulse * contact.mNormal;
+	Vec3 world_impulse = -impulse * contact.mContactNormal;
 
 
 	// Add the impulse due to gravity working on the player: P = F dt = M g dt
 	// Add the impulse due to gravity working on the player: P = F dt = M g dt
-	float normal_dot_gravity = contact.mNormal.Dot(inGravity);
+	float normal_dot_gravity = contact.mContactNormal.Dot(inGravity);
 	if (normal_dot_gravity < 0.0f)
 	if (normal_dot_gravity < 0.0f)
 		world_impulse -= (mMass * normal_dot_gravity / inGravity.Length() * inDeltaTime) * inGravity;
 		world_impulse -= (mMass * normal_dot_gravity / inGravity.Length() * inDeltaTime) * inGravity;
 
 
@@ -350,6 +355,9 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, f
 	// This is the velocity we use for the displacement, if we hit something it will be shortened
 	// This is the velocity we use for the displacement, if we hit something it will be shortened
 	Vec3 velocity = inVelocity;
 	Vec3 velocity = inVelocity;
 
 
+	// Keep track of the last velocity that was applied to the character so that we can detect when the velocity reverses
+	Vec3 last_velocity = inVelocity;
+
 	// Start with no displacement
 	// Start with no displacement
 	outDisplacement = Vec3::sZero();
 	outDisplacement = Vec3::sZero();
 	outTimeSimulated = 0.0f;
 	outTimeSimulated = 0.0f;
@@ -524,7 +532,7 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, f
 
 
 		// Allow application to modify calculated velocity
 		// Allow application to modify calculated velocity
 		if (mListener != nullptr)
 		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);
+			mListener->OnContactSolve(this, constraint->mContact->mBodyB, constraint->mContact->mSubShapeIDB, constraint->mContact->mPosition, constraint->mContact->mContactNormal, constraint->mContact->mLinearVelocity, constraint->mContact->mMaterial, velocity, new_velocity);
 
 
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
 		if (inDrawConstraints)
 		if (inDrawConstraints)
@@ -557,6 +565,12 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, f
 		// If there's not enough velocity left, bail
 		// If there's not enough velocity left, bail
 		if (velocity.LengthSq() < 1.0e-8f)
 		if (velocity.LengthSq() < 1.0e-8f)
 			return;
 			return;
+
+		// If the constraint has velocity we accept the new velocity, otherwise check that we didn't reverse velocity
+		if (!constraint->mLinearVelocity.IsNearZero(1.0e-8f))
+			last_velocity = constraint->mLinearVelocity;
+		else if (velocity.Dot(last_velocity) < 0.0f)
+			return;
 	}
 	}
 }
 }
 
 
@@ -567,7 +581,7 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 	for (Contact &c : mActiveContacts)
 	for (Contact &c : mActiveContacts)
 		if (!c.mWasDiscarded)
 		if (!c.mWasDiscarded)
 			c.mHadCollision |= c.mDistance < mCollisionTolerance
 			c.mHadCollision |= c.mDistance < mCollisionTolerance
-								&& (inSkipContactVelocityCheck || c.mNormal.Dot(mLinearVelocity - c.mLinearVelocity) <= 0.0f);
+								&& (inSkipContactVelocityCheck || c.mSurfaceNormal.Dot(mLinearVelocity - c.mLinearVelocity) <= 0.0f);
 
 
 	// Determine if we're supported or not
 	// Determine if we're supported or not
 	int num_supported = 0;
 	int num_supported = 0;
@@ -581,7 +595,7 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 		if (c.mHadCollision)
 		if (c.mHadCollision)
 		{
 		{
 			// Calculate the angle between the plane normal and the up direction
 			// Calculate the angle between the plane normal and the up direction
-			float cos_angle = c.mNormal.Dot(mUp);
+			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 contact with the normal that is pointing most upwards and store it in mSupportingContact
 			if (max_cos_angle < cos_angle)
 			if (max_cos_angle < cos_angle)
@@ -600,7 +614,7 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 			// If the angle between the two is less than 85 degrees we also use it to calculate the average normal
 			// If the angle between the two is less than 85 degrees we also use it to calculate the average normal
 			if (cos_angle >= 0.08f)
 			if (cos_angle >= 0.08f)
 			{
 			{
-				avg_normal += c.mNormal;
+				avg_normal += c.mSurfaceNormal;
 				num_avg_normal++;
 				num_avg_normal++;
 
 
 				// For static or dynamic objects or for contacts that don't support us just take the contact velocity
 				// For static or dynamic objects or for contacts that don't support us just take the contact velocity
@@ -703,11 +717,7 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 		float time_simulated;
 		float time_simulated;
 		IgnoredContactList ignored_contacts(inAllocator);
 		IgnoredContactList ignored_contacts(inAllocator);
 		ignored_contacts.reserve(contacts.size());
 		ignored_contacts.reserve(contacts.size());
-		SolveConstraints(-mUp, mSystem->GetGravity(), 1.0f, 1.0f, constraints, ignored_contacts, time_simulated, displacement, inAllocator
-		#ifdef JPH_DEBUG_RENDERER
-			, false
-		#endif // JPH_DEBUG_RENDERER
-			);
+		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
 		// If we're blocked then we're supported, otherwise we're sliding
 		float min_required_displacement_sq = Square(0.6f * mLastDeltaTime);
 		float min_required_displacement_sq = Square(0.6f * mLastDeltaTime);
@@ -730,7 +740,11 @@ void CharacterVirtual::StoreActiveContacts(const TempContactList &inContacts, Te
 	UpdateSupportingContact(true, 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
+void CharacterVirtual::MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator
+#ifdef JPH_DEBUG_RENDERER
+	, bool inDrawConstraints
+#endif // JPH_DEBUG_RENDERER
+	) const
 {
 {
 	Vec3 movement_direction = inVelocity.NormalizedOr(Vec3::sZero());
 	Vec3 movement_direction = inVelocity.NormalizedOr(Vec3::sZero());
 
 
@@ -753,7 +767,8 @@ void CharacterVirtual::MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg i
 		DetermineConstraints(contacts, constraints);
 		DetermineConstraints(contacts, constraints);
 
 
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
-		if (sDrawConstraints && iteration == 0)
+		bool draw_constraints = inDrawConstraints && iteration == 0;
+		if (draw_constraints)
 		{
 		{
 			for (const Constraint &c : constraints)
 			for (const Constraint &c : constraints)
 			{
 			{
@@ -766,6 +781,7 @@ void CharacterVirtual::MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg i
 
 
 				// Draw plane around the player position indicating the space that we can move
 				// Draw plane around the player position indicating the space that we can move
 				DebugRenderer::sInstance->DrawPlane(mPosition + dist_to_plane, c.mPlane.GetNormal(), Color::sCyan, 1.0f);
 				DebugRenderer::sInstance->DrawPlane(mPosition + dist_to_plane, c.mPlane.GetNormal(), Color::sCyan, 1.0f);
+				DebugRenderer::sInstance->DrawArrow(mPosition + dist_to_plane, mPosition + dist_to_plane + c.mContact->mSurfaceNormal, Color::sRed, 0.05f);
 			}
 			}
 		}
 		}
 #endif // JPH_DEBUG_RENDERER
 #endif // JPH_DEBUG_RENDERER
@@ -775,7 +791,7 @@ void CharacterVirtual::MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg i
 		float time_simulated;
 		float time_simulated;
 		SolveConstraints(inVelocity, inGravity, inDeltaTime, time_remaining, constraints, ignored_contacts, time_simulated, displacement, inAllocator
 		SolveConstraints(inVelocity, inGravity, inDeltaTime, time_remaining, constraints, ignored_contacts, time_simulated, displacement, inAllocator
 		#ifdef JPH_DEBUG_RENDERER
 		#ifdef JPH_DEBUG_RENDERER
-			, sDrawConstraints
+			, draw_constraints
 		#endif // JPH_DEBUG_RENDERER
 		#endif // JPH_DEBUG_RENDERER
 			);
 			);
 
 
@@ -811,7 +827,11 @@ void CharacterVirtual::Update(float inDeltaTime, Vec3Arg inGravity, const BroadP
 	mLastDeltaTime = inDeltaTime;
 	mLastDeltaTime = inDeltaTime;
 
 
 	// Slide the shape through the world
 	// Slide the shape through the world
-	MoveShape(mPosition, mLinearVelocity, inGravity, inDeltaTime, &mActiveContacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
+	MoveShape(mPosition, mLinearVelocity, inGravity, inDeltaTime, &mActiveContacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator
+	#ifdef JPH_DEBUG_RENDERER
+		, sDrawConstraints
+	#endif // JPH_DEBUG_RENDERER
+		);
 
 
 	// Determine the object that we're standing on
 	// Determine the object that we're standing on
 	UpdateSupportingContact(false, inAllocator);
 	UpdateSupportingContact(false, inAllocator);
@@ -870,8 +890,8 @@ bool CharacterVirtual::CanWalkStairs(Vec3Arg inLinearVelocity) const
 	// Check contacts for steep slopes
 	// Check contacts for steep slopes
 	for (const Contact &c : mActiveContacts)
 	for (const Contact &c : mActiveContacts)
 		if (c.mHadCollision
 		if (c.mHadCollision
-			&& c.mNormal.Dot(horizontal_velocity - c.mLinearVelocity) < 0.0f // Pushing into the contact
-			&& IsSlopeTooSteep(c.mNormal)) // Slope too steep
+			&& c.mSurfaceNormal.Dot(horizontal_velocity - c.mLinearVelocity) < 0.0f // Pushing into the contact
+			&& IsSlopeTooSteep(c.mSurfaceNormal)) // Slope too steep
 			return true;
 			return true;
 
 
 	return false;
 	return false;
@@ -896,7 +916,7 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
 	// Draw sweep up
 	// Draw sweep up
 	if (sDrawWalkStairs)
 	if (sDrawWalkStairs)
-		DebugRenderer::sInstance->DrawArrow(mPosition, up_position, Color::sGrey, 0.01f);
+		DebugRenderer::sInstance->DrawArrow(mPosition, up_position, Color::sWhite, 0.01f);
 #endif // JPH_DEBUG_RENDERER
 #endif // JPH_DEBUG_RENDERER
 
 
 	// Horizontal movement
 	// Horizontal movement
@@ -908,7 +928,7 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
 	// Draw horizontal sweep
 	// Draw horizontal sweep
 	if (sDrawWalkStairs)
 	if (sDrawWalkStairs)
-		DebugRenderer::sInstance->DrawArrow(up_position, new_position, Color::sGrey, 0.01f);
+		DebugRenderer::sInstance->DrawArrow(up_position, new_position, Color::sWhite, 0.01f);
 #endif // JPH_DEBUG_RENDERER
 #endif // JPH_DEBUG_RENDERER
 
 
 	// Move down towards the floor.
 	// Move down towards the floor.
@@ -923,14 +943,14 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 	if (sDrawWalkStairs)
 	if (sDrawWalkStairs)
 	{
 	{
 		Vec3 debug_pos = new_position + contact.mFraction * down; 
 		Vec3 debug_pos = new_position + contact.mFraction * down; 
-		DebugRenderer::sInstance->DrawArrow(new_position, debug_pos, Color::sYellow, 0.01f);
-		DebugRenderer::sInstance->DrawArrow(contact.mPosition, contact.mPosition + contact.mNormal, Color::sYellow, 0.01f);
-		mShape->Draw(DebugRenderer::sInstance, GetCenterOfMassTransform(debug_pos, mRotation, mShape), Vec3::sReplicate(1.0f), Color::sYellow, false, true);
+		DebugRenderer::sInstance->DrawArrow(new_position, debug_pos, Color::sWhite, 0.01f);
+		DebugRenderer::sInstance->DrawArrow(contact.mPosition, contact.mPosition + contact.mSurfaceNormal, Color::sWhite, 0.01f);
+		mShape->Draw(DebugRenderer::sInstance, GetCenterOfMassTransform(debug_pos, mRotation, mShape), Vec3::sReplicate(1.0f), Color::sWhite, false, true);
 	}
 	}
 #endif // JPH_DEBUG_RENDERER
 #endif // JPH_DEBUG_RENDERER
 
 
 	// Test for floor that will support the character
 	// Test for floor that will support the character
-	if (IsSlopeTooSteep(contact.mNormal))
+	if (IsSlopeTooSteep(contact.mSurfaceNormal))
 	{
 	{
 		// If no test position was provided, we cancel the stair walk
 		// If no test position was provided, we cancel the stair walk
 		if (inStepForwardTest.IsNearZero())
 		if (inStepForwardTest.IsNearZero())
@@ -940,23 +960,34 @@ bool CharacterVirtual::WalkStairs(float inDeltaTime, Vec3Arg inGravity, Vec3Arg
 		// In order to judge if the floor is flat further along the sweep, we test again for a floor at inStepForwardTest
 		// 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.
 		// and check if the normal is valid there.
 		Vec3 test_position = up_position + inStepForwardTest;
 		Vec3 test_position = up_position + inStepForwardTest;
+
+		// First sweep forward to the test position
 		Contact test_contact;
 		Contact test_contact;
-		bool hit = GetFirstContactForSweep(test_position, down, test_contact, dummy_ignored_contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
-		if (!hit)
-			return false;
+		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;
+		}
 
 
 	#ifdef JPH_DEBUG_RENDERER
 	#ifdef JPH_DEBUG_RENDERER
-		// Draw 2nd sweep down
+		// Draw 2nd sweep forward and down
 		if (sDrawWalkStairs)
 		if (sDrawWalkStairs)
 		{
 		{
 			Vec3 debug_pos = test_position + test_contact.mFraction * down; 
 			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_position, debug_pos, Color::sCyan, 0.01f);
-			DebugRenderer::sInstance->DrawArrow(test_contact.mPosition, test_contact.mPosition + test_contact.mNormal, 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);
 			mShape->Draw(DebugRenderer::sInstance, GetCenterOfMassTransform(debug_pos, mRotation, mShape), Vec3::sReplicate(1.0f), Color::sCyan, false, true);
 		}
 		}
 	#endif // JPH_DEBUG_RENDERER
 	#endif // JPH_DEBUG_RENDERER
 
 
-		if (IsSlopeTooSteep(test_contact.mNormal))
+		if (IsSlopeTooSteep(test_contact.mSurfaceNormal))
 			return false;
 			return false;
 	}
 	}
 
 

+ 14 - 7
Jolt/Physics/Character/CharacterVirtual.h

@@ -209,7 +209,8 @@ private:
 	{
 	{
 		Vec3							mPosition;												///< Position where the character makes contact
 		Vec3							mPosition;												///< Position where the character makes contact
 		Vec3							mLinearVelocity;										///< Velocity of the contact point
 		Vec3							mLinearVelocity;										///< Velocity of the contact point
-		Vec3							mNormal;												///< Contact normal, pointing towards the character
+		Vec3							mContactNormal;											///< Contact normal, pointing towards the character
+		Vec3							mSurfaceNormal;											///< Surface normal of the contact
 		float							mDistance;												///< Distance to the contact <= 0 means that it is an actual contact, > 0 means predictive
 		float							mDistance;												///< Distance to the contact <= 0 means that it is an actual contact, > 0 means predictive
 		float							mFraction;												///< Fraction along the path where this contact takes place
 		float							mFraction;												///< Fraction along the path where this contact takes place
 		BodyID							mBodyB;													///< ID of body we're colliding with
 		BodyID							mBodyB;													///< ID of body we're colliding with
@@ -253,10 +254,11 @@ private:
 	class ContactCollector : public CollideShapeCollector
 	class ContactCollector : public CollideShapeCollector
 	{
 	{
 	public:
 	public:
-										ContactCollector(PhysicsSystem *inSystem, uint inMaxHits, TempContactList &outContacts) : mSystem(inSystem), mContacts(outContacts), mMaxHits(inMaxHits) { }
+										ContactCollector(PhysicsSystem *inSystem, uint inMaxHits, Vec3Arg inUp, TempContactList &outContacts) : mUp(inUp), mSystem(inSystem), mContacts(outContacts), mMaxHits(inMaxHits) { }
 
 
 		virtual void					AddHit(const CollideShapeResult &inResult) override;
 		virtual void					AddHit(const CollideShapeResult &inResult) override;
 
 
+		Vec3							mUp;
 		PhysicsSystem *					mSystem;
 		PhysicsSystem *					mSystem;
 		TempContactList &				mContacts;
 		TempContactList &				mContacts;
 		uint							mMaxHits;
 		uint							mMaxHits;
@@ -266,12 +268,13 @@ private:
 	class ContactCastCollector : public CastShapeCollector
 	class ContactCastCollector : public CastShapeCollector
 	{
 	{
 	public:
 	public:
-										ContactCastCollector(PhysicsSystem *inSystem, Vec3Arg inDisplacement, uint inMaxHits, const IgnoredContactList &inIgnoredContacts, TempContactList &outContacts) : mSystem(inSystem), mDisplacement(inDisplacement), mIgnoredContacts(inIgnoredContacts), mContacts(outContacts), mMaxHits(inMaxHits) { }
+										ContactCastCollector(PhysicsSystem *inSystem, Vec3Arg inDisplacement, uint inMaxHits, Vec3Arg inUp, const IgnoredContactList &inIgnoredContacts, TempContactList &outContacts) : mDisplacement(inDisplacement), mUp(inUp), mSystem(inSystem), mIgnoredContacts(inIgnoredContacts), mContacts(outContacts), mMaxHits(inMaxHits) { }
 
 
 		virtual void					AddHit(const ShapeCastResult &inResult) override;
 		virtual void					AddHit(const ShapeCastResult &inResult) override;
 
 
-		PhysicsSystem *					mSystem;
 		Vec3							mDisplacement;
 		Vec3							mDisplacement;
+		Vec3							mUp;
+		PhysicsSystem *					mSystem;
 		const IgnoredContactList &		mIgnoredContacts;
 		const IgnoredContactList &		mIgnoredContacts;
 		TempContactList &				mContacts;
 		TempContactList &				mContacts;
 		uint							mMaxHits;
 		uint							mMaxHits;
@@ -279,10 +282,14 @@ 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, const taCollector &inCollector, const CollideShapeResult &inResult);
+	inline static void					sFillContactProperties(Contact &outContact, const Body &inBody, Vec3Arg inUp, 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(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const;
+	void								MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator
+	#ifdef JPH_DEBUG_RENDERER
+		, bool inDrawConstraints = false
+	#endif // JPH_DEBUG_RENDERER
+		) const;
 
 
 	// Ask the callback if inContact is a valid contact point
 	// Ask the callback if inContact is a valid contact point
 	bool								ValidateContact(const Contact &inContact) const;
 	bool								ValidateContact(const Contact &inContact) const;
@@ -299,7 +306,7 @@ private:
 	// Use the constraints to solve the displacement of the character. This will slide the character on the planes around the origin for as far as possible.
 	// Use the constraints to solve the displacement of the character. This will slide the character on the planes around the origin for as far as possible.
 	void								SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, float inTimeRemaining, ConstraintList &ioConstraints, IgnoredContactList &ioIgnoredContacts, float &outTimeSimulated, Vec3 &outDisplacement, TempAllocator &inAllocator
 	void								SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, float inTimeRemaining, ConstraintList &ioConstraints, IgnoredContactList &ioIgnoredContacts, float &outTimeSimulated, Vec3 &outDisplacement, TempAllocator &inAllocator
 	#ifdef JPH_DEBUG_RENDERER
 	#ifdef JPH_DEBUG_RENDERER
-		, bool inDrawConstraints
+		, bool inDrawConstraints = false
 	#endif // JPH_DEBUG_RENDERER
 	#endif // JPH_DEBUG_RENDERER
 		) const;
 		) const;
 
 

+ 2 - 2
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -286,7 +286,7 @@ void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, Mat
 {
 {
 	// Draw current location
 	// Draw current location
 	// Drawing prior to update since the physics system state is also that prior to the simulation step (so that all detected collisions etc. make sense)
 	// Drawing prior to update since the physics system state is also that prior to the simulation step (so that all detected collisions etc. make sense)
-	mDebugRenderer->DrawCoordinateSystem(inCharacterTransform);
+	mDebugRenderer->DrawCoordinateSystem(inCharacterTransform, 0.1f);
 
 
 	// Determine color
 	// Determine color
 	CharacterBase::EGroundState ground_state = inCharacter->GetGroundState();
 	CharacterBase::EGroundState ground_state = inCharacter->GetGroundState();
@@ -313,7 +313,7 @@ void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, Mat
 		Vec3 ground_velocity = inCharacter->GetGroundVelocity();
 		Vec3 ground_velocity = inCharacter->GetGroundVelocity();
 
 
 		// Draw ground position
 		// Draw ground position
-		mDebugRenderer->DrawWireSphere(ground_position, 0.1f, Color::sRed);
+		mDebugRenderer->DrawMarker(ground_position, Color::sRed, 0.1f);
 		mDebugRenderer->DrawArrow(ground_position, ground_position + 2.0f * ground_normal, Color::sGreen, 0.1f);
 		mDebugRenderer->DrawArrow(ground_position, ground_position + 2.0f * ground_normal, Color::sGreen, 0.1f);
 
 
 		// Draw ground velocity
 		// Draw ground velocity