Преглед изворни кода

Fixed an issue where a character could get stuck (#1876)

If the character was teleported inside an area surrounded by slopes that are steeper than mMaxSlopeAngle, the code to stop the constraint solver from ping ponging between two planes didn't work properly.
Jorrit Rouwe пре 2 недеља
родитељ
комит
83eea75749

+ 1 - 1
.github/workflows/determinism_check.yml

@@ -4,7 +4,7 @@ env:
   CONVEX_VS_MESH_HASH: '0xa7348cad585544bf'
   RAGDOLL_HASH: '0xc392d8f867b0be5b'
   PYRAMID_HASH: '0xafd93b295e75e3f6'
-  CHARACTER_VIRTUAL_HASH: '0x19c55223035a8f1a'
+  CHARACTER_VIRTUAL_HASH: '0x5bb957ed934a01f3'
   EMSCRIPTEN_VERSION: 4.0.22
   NODE_VERSION: 24.x
   UBUNTU_CLANG_VERSION: clang++-18

+ 1 - 0
Docs/ReleaseNotes.md

@@ -20,6 +20,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 
 * Made it possible to make a class outside the JPH namespace serializable.
 * `VehicleConstraint`s are automatically disabled when the vehicle body is not in the `PhysicsSystem`.
+* Fixed an issue where a character could get stuck. If the character was teleported inside an area surrounded by slopes that are steeper than mMaxSlopeAngle, the code to stop the constraint solver from ping ponging between two planes didn't work properly.
 
 ## v5.5.0
 

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

@@ -925,7 +925,7 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, float inDeltaTime, f
 
 		// Find the normal of the previous contact that we will violate the most if we move in this new direction
 		float highest_penetration = 0.0f;
-		Constraint *other_constraint = nullptr;
+		const Constraint *other_constraint = nullptr;
 		for (Constraint **c = previous_contacts.data(); c < previous_contacts.data() + num_previous_contacts; ++c)
 			if (*c != constraint)
 			{
@@ -942,6 +942,12 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, float inDeltaTime, f
 						other_constraint = *c;
 					}
 				}
+
+				// Cancel the constraint velocity in the other constraint plane's direction so that we won't try to apply it again and keep ping ponging between planes
+				constraint->mLinearVelocity -= min(0.0f, constraint->mLinearVelocity.Dot(other_normal)) * other_normal;
+
+				// Cancel the other constraints velocity in this constraint plane's direction so that we won't try to apply it again and keep ping ponging between planes
+				(*c)->mLinearVelocity -= min(0.0f, (*c)->mLinearVelocity.Dot(plane_normal)) * plane_normal;
 			}
 
 		// Check if we found a 2nd constraint
@@ -952,12 +958,6 @@ void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, float inDeltaTime, f
 			Vec3 slide_dir = plane_normal.Cross(other_normal).Normalized();
 			Vec3 velocity_in_slide_dir = new_velocity.Dot(slide_dir) * slide_dir;
 
-			// Cancel the constraint velocity in the other constraint plane's direction so that we won't try to apply it again and keep ping ponging between planes
-			constraint->mLinearVelocity -= min(0.0f, constraint->mLinearVelocity.Dot(other_normal)) * other_normal;
-
-			// Cancel the other constraints velocity in this constraint plane's direction so that we won't try to apply it again and keep ping ponging between planes
-			other_constraint->mLinearVelocity -= min(0.0f, other_constraint->mLinearVelocity.Dot(plane_normal)) * plane_normal;
-
 			// Calculate the velocity of this constraint perpendicular to the slide direction
 			Vec3 perpendicular_velocity = constraint->mLinearVelocity - constraint->mLinearVelocity.Dot(slide_dir) * slide_dir;
 

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

@@ -34,6 +34,7 @@ const char *CharacterBaseTest::sScenes[] =
 	"PerlinHeightField",
 	"ObstacleCourse",
 	"InitiallyIntersecting",
+	"InitiallyIntersecting2",
 #ifdef JPH_OBJECT_STREAM
 	"Terrain1",
 	"Terrain2",
@@ -172,6 +173,17 @@ void CharacterBaseTest::Initialize()
 					mBodyInterface->CreateAndAddBody(settings, EActivation::DontActivate);
 				}
 	}
+	else if (strcmp(sSceneName, "InitiallyIntersecting2") == 0)
+	{
+		CreateFloor();
+
+		// Create two very steep sloped floors that are initially intersecting with the character
+		RefConst<Shape> box = new BoxShape(Vec3(1, 0.1f, 1));
+		BodyCreationSettings settings(box, RVec3(0, 0, 0), Quat::sRotation(Vec3::sAxisZ(), DegreesToRadians(75.0f)), EMotionType::Static, Layers::NON_MOVING);
+		mBodyInterface->CreateAndAddBody(settings, EActivation::DontActivate);
+		settings.mRotation = Quat::sRotation(Vec3::sAxisZ(), DegreesToRadians(-75.0f));
+		mBodyInterface->CreateAndAddBody(settings, EActivation::DontActivate);
+	}
 	else if (strcmp(sSceneName, "ObstacleCourse") == 0)
 	{
 		// Default terrain

+ 47 - 0
UnitTests/Physics/CharacterVirtualTests.cpp

@@ -777,6 +777,53 @@ TEST_SUITE("CharacterVirtualTests")
 		}
 	}
 
+	TEST_CASE("TestInitiallyIntersecting2")
+	{
+		PhysicsTestContext c;
+		c.CreateFloor();
+
+		// Create box that is intersecting with the character
+		c.CreateBox(RVec3(-0.5f, 0.5f, 0), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3::sReplicate(0.5f));
+
+		// Create two very steep sloped floors that are initially intersecting with the character
+		constexpr float cSize = 1.0f;
+		constexpr float cThickness = 0.1f;
+		constexpr float cSlopeAngle = DegreesToRadians(75.0f);
+		RefConst<Shape> box = new BoxShape(Vec3(cSize, cThickness, cSize));
+		BodyCreationSettings settings(box, RVec3(0, 0, 0), Quat::sRotation(Vec3::sAxisZ(), cSlopeAngle), EMotionType::Static, Layers::NON_MOVING);
+		c.GetBodyInterface().CreateAndAddBody(settings, EActivation::DontActivate);
+		settings.mRotation = Quat::sRotation(Vec3::sAxisZ(), -cSlopeAngle);
+		c.GetBodyInterface().CreateAndAddBody(settings, EActivation::DontActivate);
+
+		// Create character
+		Character character(c);
+		character.Create();
+		CHECK_APPROX_EQUAL(character.GetPosition(), RVec3::sZero());
+
+		// Allow the character to step and see that it gets driven upwards
+		Real y = 0;
+		for (int step = 0; step < 10; ++step)
+		{
+			character.Step();
+			Real new_y = character.GetPosition().GetY();
+			CHECK(new_y >= y);
+			y = new_y;
+		}
+		CHECK(abs(character.GetPosition().GetX()) < 1.0e-3_r);
+		CHECK(y > 0.5_r);
+		CHECK(abs(character.GetPosition().GetZ()) < 1.0e-3_r);
+
+		// When moving along the to sloped floors, check that we are not stuck and fall back on the floor when we've passed them
+		character.mHorizontalSpeed = 2.0f * Vec3::sAxisX();
+		constexpr int cNumSteps = 60; // 1 second
+		RVec3 starting_position = character.GetPosition();
+		for (int step = 0; step < cNumSteps; ++step)
+			character.Step();
+		RVec3 expected_position = starting_position + character.mHorizontalSpeed * cNumSteps * c.GetDeltaTime();
+		expected_position.SetY(0);
+		CHECK_APPROX_EQUAL(character.GetPosition(), expected_position, 1e-3f);
+	}
+
 	TEST_CASE("TestCharacterVsCharacter")
 	{
 		PhysicsTestContext c;