Prechádzať zdrojové kódy

CharacterVirtual: Added missing delta time term in DetermineConstraints (#532)

* This was causing moving objects to push out characters at a lower speed than specified in the documentation. Note that this changes behavior, lower mPenetrationRecoverySpeed if a behavior change is undesired.
* Added vertically moving platform to ObstacleCourse test level instantly changes velocity
* Added InitiallyIntersecting to character test level that shows character resolving collisions
* Added option to disable simulated inertia for the virtual character
* Added unit test for initially intersecting character
Jorrit Rouwe 2 rokov pred
rodič
commit
8dd93317d6

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

@@ -377,7 +377,7 @@ bool CharacterVirtual::GetFirstContactForSweep(RVec3Arg inPosition, Vec3Arg inDi
 	return true;
 }
 
-void CharacterVirtual::DetermineConstraints(TempContactList &inContacts, ConstraintList &outConstraints) const
+void CharacterVirtual::DetermineConstraints(TempContactList &inContacts, float inDeltaTime, ConstraintList &outConstraints) const
 {
 	for (Contact &c : inContacts)
 	{
@@ -385,7 +385,7 @@ void CharacterVirtual::DetermineConstraints(TempContactList &inContacts, Constra
 
 		// Penetrating contact: Add a contact velocity that pushes the character out at the desired speed
 		if (c.mDistance < 0.0f)
-			contact_velocity -= c.mContactNormal * c.mDistance * mPenetrationRecoverySpeed;
+			contact_velocity -= c.mContactNormal * c.mDistance * mPenetrationRecoverySpeed / inDeltaTime;
 
 		// Convert to a constraint
 		outConstraints.emplace_back();
@@ -869,7 +869,7 @@ void CharacterVirtual::UpdateSupportingContact(bool inSkipContactVelocityCheck,
 		TempContactList contacts(mActiveContacts.begin(), mActiveContacts.end(), inAllocator);
 		ConstraintList constraints(inAllocator);
 		constraints.reserve(contacts.size() * 2);
-		DetermineConstraints(contacts, constraints);
+		DetermineConstraints(contacts, mLastDeltaTime, constraints);
 
 		// Solve the displacement using these constraints, this is used to check if we didn't move at all because we are supported
 		Vec3 displacement;
@@ -923,7 +923,7 @@ void CharacterVirtual::MoveShape(RVec3 &ioPosition, Vec3Arg inVelocity, float in
 		// Convert contacts into constraints
 		ConstraintList constraints(inAllocator);
 		constraints.reserve(contacts.size() * 2);
-		DetermineConstraints(contacts, constraints);
+		DetermineConstraints(contacts, inDeltaTime, constraints);
 
 #ifdef JPH_DEBUG_RENDERER
 		bool draw_constraints = inDrawConstraints && iteration == 0;

+ 1 - 1
Jolt/Physics/Character/CharacterVirtual.h

@@ -393,7 +393,7 @@ private:
 	void								RemoveConflictingContacts(TempContactList &ioContacts, IgnoredContactList &outIgnoredContacts) const;
 
 	// Convert contacts into constraints. The character is assumed to start at the origin and the constraints are planes around the origin that confine the movement of the character.
-	void								DetermineConstraints(TempContactList &inContacts, ConstraintList &outConstraints) const;
+	void								DetermineConstraints(TempContactList &inContacts, float inDeltaTime, ConstraintList &outConstraints) const;
 
 	// 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, float inDeltaTime, float inTimeRemaining, ConstraintList &ioConstraints, IgnoredContactList &ioIgnoredContacts, float &outTimeSimulated, Vec3 &outDisplacement, TempAllocator &inAllocator

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

@@ -30,6 +30,7 @@ const char *CharacterBaseTest::sScenes[] =
 	"PerlinMesh",
 	"PerlinHeightField",
 	"ObstacleCourse",
+	"InitiallyIntersecting",
 	"Terrain1",
 	"Terrain2",
 };
@@ -41,10 +42,12 @@ static const RVec3 cRotatingPosition(-5, 0.15f, 15);
 static const Quat cRotatingOrientation = Quat::sIdentity();
 static const RVec3 cRotatingWallPosition(5, 1.0f, 25.0f);
 static const Quat cRotatingWallOrientation = Quat::sIdentity();
-static const RVec3 cRotatingAndTranslatingPosition(-5, 0.15f, 27.5f);
+static const RVec3 cRotatingAndTranslatingPosition(-10, 0.15f, 27.5f);
 static const Quat cRotatingAndTranslatingOrientation = Quat::sIdentity();
-static const RVec3 cVerticallyMovingPosition(0, 2.0f, 15);
-static const Quat cVerticallyMovingOrientation = Quat::sIdentity();
+static const RVec3 cSmoothVerticallyMovingPosition(0, 2.0f, 15);
+static const Quat cSmoothVerticallyMovingOrientation = Quat::sIdentity();
+static const RVec3 cReversingVerticallyMovingPosition(0, 0.15f, 25);
+static const Quat cReversingVerticallyMovingOrientation = Quat::sIdentity();
 static const RVec3 cHorizontallyMovingPosition(5, 1, 15);
 static const Quat cHorizontallyMovingOrientation = Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI);
 static const RVec3 cConveyorBeltPosition(-10, 0.15f, 15);
@@ -90,6 +93,21 @@ void CharacterBaseTest::Initialize()
 		// Default terrain
 		CreateHeightFieldTerrain();
 	}
+	else if (strcmp(sSceneName, "InitiallyIntersecting") == 0)
+	{
+		CreateFloor();
+
+		// Create a grid of boxes that are initially intersecting with the character
+		RefConst<Shape> box = new BoxShape(Vec3(0.1f, 0.1f, 0.1f));
+		BodyCreationSettings settings(box, RVec3(0, 0.5f, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+		for (int x = 0; x < 4; ++x)
+			for (int y = 0; y <= 10; ++y)
+				for (int z = 0; z <= 10; ++z)
+				{
+					settings.mPosition = RVec3(-0.5f + 0.1f * x, 0.1f + 0.1f * y, -0.5f + 0.1f * z);
+					mBodyInterface->CreateAndAddBody(settings, EActivation::DontActivate);
+				}
+	}
 	else if (strcmp(sSceneName, "ObstacleCourse") == 0)
 	{
 		// Default terrain
@@ -116,7 +134,8 @@ void CharacterBaseTest::Initialize()
 			mRotatingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cRotatingPosition, cRotatingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
 			mRotatingWallBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(3.0f, 1, 0.15f)), cRotatingWallPosition, cRotatingWallOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
 			mRotatingAndTranslatingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cRotatingAndTranslatingPosition, cRotatingAndTranslatingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
-			mVerticallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cVerticallyMovingPosition, cVerticallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
+			mSmoothVerticallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cSmoothVerticallyMovingPosition, cSmoothVerticallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
+			mReversingVerticallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cReversingVerticallyMovingPosition, cReversingVerticallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
 			mHorizontallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cHorizontallyMovingPosition, cHorizontallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
 		}
 
@@ -540,8 +559,17 @@ void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		mBodyInterface->MoveKinematic(mRotatingAndTranslatingBody, cRotatingAndTranslatingPosition + 5.0f * Vec3(Sin(JPH_PI * mTime), 0, Cos(JPH_PI * mTime)), Quat::sRotation(Vec3::sAxisY(), JPH_PI * Sin(mTime)), inParams.mDeltaTime);
 	if (!mHorizontallyMovingBody.IsInvalid())
 		mBodyInterface->MoveKinematic(mHorizontallyMovingBody, cHorizontallyMovingPosition + Vec3(3.0f * Sin(mTime), 0, 0), cHorizontallyMovingOrientation, inParams.mDeltaTime);
-	if (!mVerticallyMovingBody.IsInvalid())
-		mBodyInterface->MoveKinematic(mVerticallyMovingBody, cVerticallyMovingPosition + Vec3(0, 1.75f * Sin(mTime), 0), cVerticallyMovingOrientation, inParams.mDeltaTime);
+	if (!mSmoothVerticallyMovingBody.IsInvalid())
+		mBodyInterface->MoveKinematic(mSmoothVerticallyMovingBody, cSmoothVerticallyMovingPosition + Vec3(0, 1.75f * Sin(mTime), 0), cSmoothVerticallyMovingOrientation, inParams.mDeltaTime);
+	if (!mReversingVerticallyMovingBody.IsInvalid())
+	{
+		RVec3 pos = mBodyInterface->GetPosition(mReversingVerticallyMovingBody);
+		if (pos.GetY() < cReversingVerticallyMovingPosition.GetY())
+			mReversingVerticallyMovingVelocity = 1.0f;
+		else if (pos.GetY() > cReversingVerticallyMovingPosition.GetY() + 5.0f)
+			mReversingVerticallyMovingVelocity = -1.0f;
+		mBodyInterface->MoveKinematic(mReversingVerticallyMovingBody, pos + Vec3(0, mReversingVerticallyMovingVelocity * 3.0f * inParams.mDeltaTime, 0), cReversingVerticallyMovingOrientation, inParams.mDeltaTime);
+	}
 
 	// Reset ramp blocks
 	mRampBlocksTimeLeft -= inParams.mDeltaTime;
@@ -574,6 +602,7 @@ void CharacterBaseTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
 		inUI->CreateCheckBox(movement_settings, "Control Movement During Jump", sControlMovementDuringJump, [](UICheckBox::EState inState) { sControlMovementDuringJump = inState == UICheckBox::STATE_CHECKED; });
 		inUI->CreateSlider(movement_settings, "Character Speed", sCharacterSpeed, 0.1f, 10.0f, 0.1f, [](float inValue) { sCharacterSpeed = inValue; });
 		inUI->CreateSlider(movement_settings, "Character Jump Speed", sJumpSpeed, 0.1f, 10.0f, 0.1f, [](float inValue) { sJumpSpeed = inValue; });
+		AddCharacterMovementSettings(inUI, movement_settings);
 		inUI->ShowMenu(movement_settings);
 	});
 

+ 6 - 1
Samples/Tests/Character/CharacterBaseTest.h

@@ -46,6 +46,9 @@ protected:
 	// Draw the character state
 	void					DrawCharacterState(const CharacterBase *inCharacter, RMat44Arg inCharacterTransform, Vec3Arg inCharacterVelocity);
 
+	// Add character movement settings
+	virtual void			AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu) { /* Nothing by default */ }
+
 	// Add test configuration settings
 	virtual void			AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu) { /* Nothing by default */ }
 
@@ -99,6 +102,8 @@ private:
 	BodyID					mRotatingBody;
 	BodyID					mRotatingWallBody;
 	BodyID					mRotatingAndTranslatingBody;
-	BodyID					mVerticallyMovingBody;
+	BodyID					mSmoothVerticallyMovingBody;
+	BodyID					mReversingVerticallyMovingBody;
+	float					mReversingVerticallyMovingVelocity = 1.0f;
 	BodyID					mHorizontallyMovingBody;
 };

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

@@ -40,6 +40,7 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 
 	// Draw character pre update (the sim is also drawn pre update)
 	RMat44 com = mCharacter->GetCenterOfMassTransform();
+	RMat44 world_transform = mCharacter->GetWorldTransform();
 #ifdef JPH_DEBUG_RENDERER
 	mCharacter->GetShape()->Draw(mDebugRenderer, com, Vec3::sReplicate(1.0f), Color::sGreen, false, true);
 #endif // JPH_DEBUG_RENDERER
@@ -82,7 +83,7 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	Vec3 velocity = Vec3(new_position - old_position) / inParams.mDeltaTime;
 
 	// Draw state of character
-	DrawCharacterState(mCharacter, mCharacter->GetWorldTransform(), velocity);
+	DrawCharacterState(mCharacter, world_transform, velocity);
 
 	// Draw labels on ramp blocks
 	for (size_t i = 0; i < mRampBlocks.size(); ++i)
@@ -95,7 +96,7 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 	if (player_controls_horizontal_velocity)
 	{
 		// Smooth the player input
-		mDesiredVelocity = 0.25f * inMovementDirection * sCharacterSpeed + 0.75f * mDesiredVelocity;
+		mDesiredVelocity = sEnableCharacterInertia? 0.25f * inMovementDirection * sCharacterSpeed + 0.75f * mDesiredVelocity : inMovementDirection * sCharacterSpeed;
 
 		// True if the player intended to move
 		mAllowSliding = !inMovementDirection.IsNearZero();
@@ -119,14 +120,17 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 	Vec3 current_vertical_velocity = mCharacter->GetLinearVelocity().Dot(mCharacter->GetUp()) * mCharacter->GetUp();
 	Vec3 ground_velocity = mCharacter->GetGroundVelocity();
 	Vec3 new_velocity;
-	if (mCharacter->GetGroundState() == CharacterVirtual::EGroundState::OnGround // If on ground
-		&& (current_vertical_velocity.GetY() - ground_velocity.GetY()) < 0.1f) // And not moving away from ground
+	bool moving_towards_ground = (current_vertical_velocity.GetY() - ground_velocity.GetY()) < 0.1f;
+	if (mCharacter->GetGroundState() == CharacterVirtual::EGroundState::OnGround	// If on ground
+		&& (sEnableCharacterInertia?
+			moving_towards_ground													// Inertia enabled: And not moving away from ground
+			: !mCharacter->IsSlopeTooSteep(mCharacter->GetGroundNormal())))			// Inertia disabled: And not on a slope that is too steep
 	{
 		// Assume velocity of ground when on ground
 		new_velocity = ground_velocity;
 
 		// Jump
-		if (inJump)
+		if (inJump && moving_towards_ground)
 			new_velocity += sJumpSpeed * mCharacter->GetUp();
 	}
 	else
@@ -155,6 +159,11 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 		mCharacter->SetShape(mCharacter->GetShape() == mStandingShape? mCrouchingShape : mStandingShape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop, mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, { }, *mTempAllocator);
 }
 
+void CharacterVirtualTest::AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu)
+{
+	inUI->CreateCheckBox(inSubMenu, "Enable Character Inertia", sEnableCharacterInertia, [](UICheckBox::EState inState) { sEnableCharacterInertia = inState == UICheckBox::STATE_CHECKED; });
+}
+
 void CharacterVirtualTest::AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu)
 {
 	inUI->CreateComboBox(inSubMenu, "Back Face Mode", { "Ignore", "Collide" }, (int)sBackFaceMode, [=](int inItem) { sBackFaceMode = (EBackFaceMode)inItem; });

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

@@ -39,11 +39,17 @@ protected:
 	// Handle user input to the character
 	virtual void			HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime) override;
 
+	// Add character movement settings
+	virtual void			AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu) override;
+
 	// Add test configuration settings
 	virtual void			AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu) override;
 
 private:
-	// Test settings
+	// Character movement settings
+	static inline bool		sEnableCharacterInertia = true;
+
+	// Test configuration settings
 	static inline EBackFaceMode sBackFaceMode = EBackFaceMode::CollideWithBackFaces;
 	static inline float		sUpRotationX = 0;
 	static inline float		sUpRotationZ = 0;	

+ 35 - 2
UnitTests/Physics/CharacterVirtualTests.cpp

@@ -198,7 +198,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// After 1 step we should be on the slope
 			character.Step();
 			CHECK(character.mCharacter->GetGroundState() == expected_ground_state);
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), position_after_1_step);
+			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), position_after_1_step, 2.0e-6f);
 
 			// Cancel any velocity to make the calculation below easier (otherwise we have to take gravity for 1 time step into account)
 			character.mCharacter->SetLinearVelocity(Vec3::sZero());
@@ -366,7 +366,7 @@ TEST_SUITE("CharacterVirtualTests")
 			// We should have gotten stuck at the start of the stairs (can't move up)
 			CHECK(character.mCharacter->GetGroundState() == CharacterBase::EGroundState::OnGround);
 			float radius_and_padding = character.mRadiusStanding + character.mCharacterSettings.mCharacterPadding;
-			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(0, 0, -radius_and_padding), 1.0e-2f);
+			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(0, 0, -radius_and_padding), 1.1e-2f);
 
 			// Enable stair walking
 			character.mUpdateSettings.mWalkStairsStepUp = Vec3(0, 0.4f, 0);
@@ -644,4 +644,37 @@ TEST_SUITE("CharacterVirtualTests")
 			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(5.0f, 0, 0), 1.0e-2f);
 		}
 	}
+
+	TEST_CASE("TestInitiallyIntersecting")
+	{
+		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));
+
+		// Try various penetration recovery values
+		for (float penetration_recovery : { 0.0f, 0.5f, 0.75f, 1.0f })
+		{
+			// Create character
+			Character character(c);
+			character.mCharacterSettings.mPenetrationRecoverySpeed = penetration_recovery;
+			character.Create();
+			CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3::sZero());
+
+			// Total radius of character
+			float radius_and_padding = character.mRadiusStanding + character.mCharacterSettings.mCharacterPadding;
+
+			float x = 0.0f;
+			for (int step = 0; step < 3; ++step)
+			{
+				// Calculate expected position
+				x += penetration_recovery * (radius_and_padding - x);
+
+				// Step character and check that it matches expected recovery
+				character.Step();
+				CHECK_APPROX_EQUAL(character.mCharacter->GetPosition(), RVec3(x, 0, 0));
+			}
+		}
+	}
 }