Browse Source

Added support for compound shapes as character shape in CharacterVirtual (#1745)

* Fixed colliding a compound shape vs a regular shape ignoring CollideShapeSettings::mMaxSeparationDistance. This potentially led to missed collisions.

See: jrouwe/JoltPhysics.js#273
Jorrit Rouwe 1 week ago
parent
commit
475e564ae8

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

@@ -2,7 +2,7 @@ name: Determinism Check
 
 env:
   CONVEX_VS_MESH_HASH: '0x4e5ff3fefc2a35fb'
-  RAGDOLL_HASH: '0x5c61578b3cfa4cd0'
+  RAGDOLL_HASH: '0x932476a7e96702dc'
   PYRAMID_HASH: '0xafd93b295e75e3f6'
   CHARACTER_VIRTUAL_HASH: '0x19c55223035a8f1a'
   EMSCRIPTEN_VERSION: 4.0.2

+ 4 - 2
Docs/ReleaseNotes.md

@@ -11,6 +11,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added `JPH_USE_EXTERNAL_PROFILE` cmake option that allows overriding the behavior of the profile macros.
 * Added `SoftBodyCreationSettings::mFacesDoubleSided` which treats the faces of the soft body as double sided. This can be used to make e.g. flags double sided.
 * Added functions to get the `CharacterSettings` from a `Character` and `CharacterVirtualSettings` from a `CharacterVirtual`.
+* Added support for compound shapes as character shape in `CharacterVirtual`.
 * Added `BodyInterface::SetIsSensor`/`IsSensor` functions.
 
 ### Bug Fixes
@@ -30,8 +31,9 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added an epsilon to the `CastRay` / `CastShape` early out condition to avoid dividing by a very small number and overflowing to INF. This can cause a float overflow exception.
 * Fixed Samples requiring Vulkan extension `VK_EXT_device_address_binding_report` without checking if it is available.
 * Fixed Vulkan warning in Samples: VkSemaphore is being signaled by VkQueue but it may still be in use by VkSwapchainKHR.
-* Fixed incorrect RTTI definition of MotorcycleControllerSettings which led to the members of WheeledVehicleControllerSettings not being serialized.
-* Implemented missing VehicleConstraint::GetConstraintSettings function.
+* Fixed incorrect RTTI definition of `MotorcycleControllerSettings` which led to the members of `WheeledVehicleControllerSettings` not being serialized.
+* Implemented missing `VehicleConstraint::GetConstraintSettings` function.
+* Fixed colliding a compound shape vs a regular shape ignoring `CollideShapeSettings::mMaxSeparationDistance`. This potentially led to missed collisions.
 
 ## v5.3.0
 

+ 9 - 0
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -12,6 +12,7 @@
 #include <Jolt/Physics/Collision/CollideShape.h>
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
+#include <Jolt/Physics/Collision/Shape/CompoundShape.h>
 #include <Jolt/Physics/Collision/CollisionDispatch.h>
 #include <Jolt/Core/QuickSort.h>
 #include <Jolt/Core/ScopeExit.h>
@@ -542,6 +543,14 @@ inline static bool sCorrectFractionForCharacterPadding(const Shape *inShape, Mat
 		const ScaledShape *scaled_shape = static_cast<const ScaledShape *>(inShape);
 		return sCorrectFractionForCharacterPadding(scaled_shape->GetInnerShape(), inStart, inDisplacement, inScale * scaled_shape->GetScale(), inPolygon, ioFraction);
 	}
+	else if (inShape->GetType() == EShapeType::Compound)
+	{
+		const CompoundShape *compound = static_cast<const CompoundShape *>(inShape);
+		bool return_value = false;
+		for (const CompoundShape::SubShape &sub_shape : compound->GetSubShapes())
+			return_value |= sCorrectFractionForCharacterPadding(sub_shape.mShape, inStart * sub_shape.GetLocalTransformNoScale(inScale), inDisplacement, sub_shape.TransformScale(inScale), inPolygon, ioFraction);
+		return return_value;
+	}
 	else
 	{
 		JPH_ASSERT(false, "Not supported yet!");

+ 1 - 0
Jolt/Physics/Collision/Shape/CompoundShapeVisitors.h

@@ -302,6 +302,7 @@ struct CompoundShape::CollideCompoundVsShapeVisitor
 
 		// Convert bounding box of 2 into space of 1
 		mBoundsOf2InSpaceOf1 = inShape2->GetLocalBounds().Scaled(inScale2).Transformed(transform2_to_1);
+		mBoundsOf2InSpaceOf1.ExpandBy(Vec3::sReplicate(inCollideShapeSettings.mMaxSeparationDistance));
 	}
 
 	/// Returns true when collision detection should abort because it's not possible to find a better hit

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

@@ -11,6 +11,7 @@
 #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/MeshShape.h>
 #include <Jolt/Physics/Constraints/HingeConstraint.h>
 #include <Jolt/Core/StringTools.h>
@@ -119,6 +120,30 @@ void CharacterBaseTest::Initialize()
 		mInnerStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding))).Create().Get();
 		mInnerCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching))).Create().Get();
 		break;
+
+	case EType::Compound:
+		{
+			StaticCompoundShapeSettings standing_compound;
+			standing_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding));
+			standing_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)));
+			mStandingShape = standing_compound.Create().Get();
+
+			StaticCompoundShapeSettings crouching_compound;
+			crouching_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching));
+			crouching_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)));
+			mCrouchingShape = crouching_compound.Create().Get();
+
+			StaticCompoundShapeSettings inner_standing_compound;
+			inner_standing_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightStanding, cInnerShapeFraction * cCharacterRadiusStanding));
+			inner_standing_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)));
+			mInnerStandingShape = inner_standing_compound.Create().Get();
+
+			StaticCompoundShapeSettings inner_crouching_compound;
+			inner_crouching_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightCrouching, cInnerShapeFraction * cCharacterRadiusCrouching));
+			inner_crouching_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)));
+			mInnerCrouchingShape = inner_crouching_compound.Create().Get();
+		}
+		break;
 	}
 
 	if (strcmp(sSceneName, "PerlinMesh") == 0)
@@ -638,11 +663,8 @@ void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	for (CharacterVirtual *character : { mAnimatedCharacterVirtual, mAnimatedCharacterVirtualWithInnerBody })
 		if (character != nullptr)
 		{
-		#ifdef JPH_DEBUG_RENDERER
-			character->GetShape()->Draw(mDebugRenderer, character->GetCenterOfMassTransform(), Vec3::sOne(), Color::sOrange, false, true);
-		#else
-			mDebugRenderer->DrawCapsule(character->GetCenterOfMassTransform(), 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding + character->GetCharacterPadding(), Color::sOrange, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
-		#endif // JPH_DEBUG_RENDERER
+			// Draw the character
+			DrawPaddedCharacter(character->GetShape(), character->GetCharacterPadding(), character->GetCenterOfMassTransform());
 
 			// Update velocity and apply gravity
 			Vec3 velocity;
@@ -703,7 +725,7 @@ void CharacterBaseTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
 	inUI->CreateTextButton(inSubMenu, "Configuration Settings", [this, inUI]() {
 		UIElement *configuration_settings = inUI->CreateMenu();
 
-		inUI->CreateComboBox(configuration_settings, "Shape Type", { "Capsule", "Cylinder", "Box" }, (int)sShapeType, [](int inItem) { sShapeType = (EType)inItem; });
+		inUI->CreateComboBox(configuration_settings, "Shape Type", { "Capsule", "Cylinder", "Box", "Compound" }, (int)sShapeType, [](int inItem) { sShapeType = (EType)inItem; });
 		AddConfigurationSettings(inUI, configuration_settings);
 		inUI->CreateTextButton(configuration_settings, "Accept Changes", [this]() { RestartTest(); });
 		inUI->ShowMenu(configuration_settings);
@@ -797,3 +819,37 @@ void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, RMa
 	horizontal_velocity.SetY(0);
 	mDebugRenderer->DrawText3D(inCharacterTransform.GetTranslation(), StringFormat("State: %s\nMat: %s\nHorizontal Vel: %.1f m/s\nVertical Vel: %.1f m/s", CharacterBase::sToString(ground_state), ground_material->GetDebugName(), (double)horizontal_velocity.Length(), (double)inCharacterVelocity.GetY()), Color::sWhite, 0.25f);
 }
+
+void CharacterBaseTest::DrawPaddedCharacter(const Shape *inShape, float inPadding, RMat44Arg inCenterOfMass)
+{
+	if (inShape->GetSubType() == EShapeSubType::Capsule)
+	{
+		const CapsuleShape *capsule = static_cast<const CapsuleShape *>(inShape);
+		mDebugRenderer->DrawCapsule(inCenterOfMass, capsule->GetHalfHeightOfCylinder(), capsule->GetRadius() + inPadding, Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+	}
+	else if (inShape->GetSubType() == EShapeSubType::Cylinder)
+	{
+		// Not correct as the edges should be rounded
+		const CylinderShape *cylinder = static_cast<const CylinderShape *>(inShape);
+		mDebugRenderer->DrawCylinder(inCenterOfMass, cylinder->GetHalfHeight() + inPadding, cylinder->GetRadius() + inPadding, Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+	}
+	else if (inShape->GetSubType() == EShapeSubType::Box)
+	{
+		// Not correct as the edges should be rounded
+		const BoxShape *box = static_cast<const BoxShape *>(inShape);
+		AABox bounds = box->GetLocalBounds();
+		bounds.ExpandBy(Vec3::sReplicate(inPadding));
+		mDebugRenderer->DrawWireBox(inCenterOfMass, bounds, Color::sGrey);
+	}
+	else if (inShape->GetSubType() == EShapeSubType::RotatedTranslated)
+	{
+		const RotatedTranslatedShape *rt = static_cast<const RotatedTranslatedShape *>(inShape);
+		DrawPaddedCharacter(rt->GetInnerShape(), inPadding, inCenterOfMass);
+	}
+	else if (inShape->GetType() == EShapeType::Compound)
+	{
+		const CompoundShape *compound = static_cast<const CompoundShape *>(inShape);
+		for (const CompoundShape::SubShape &sub_shape : compound->GetSubShapes())
+			DrawPaddedCharacter(sub_shape.mShape, inPadding, inCenterOfMass * sub_shape.GetLocalTransformNoScale(Vec3::sOne()));
+	}
+}

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

@@ -63,6 +63,9 @@ protected:
 	// Add test configuration settings
 	virtual void			AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu) { /* Nothing by default */ }
 
+	// Draw the character + padding
+	void					DrawPaddedCharacter(const Shape *inShape, float inPadding, RMat44Arg inCenterOfMass);
+
 	// Character size
 	static constexpr float	cCharacterHeightStanding = 1.35f;
 	static constexpr float	cCharacterRadiusStanding = 0.3f;
@@ -94,18 +97,19 @@ protected:
 	// List of active characters in the scene so they can collide
 	CharacterVsCharacterCollisionSimple mCharacterVsCharacterCollision;
 
-private:
 	// Shape types
 	enum class EType
 	{
 		Capsule,
 		Cylinder,
-		Box
+		Box,
+		Compound
 	};
 
 	// Character shape type
 	static inline EType		sShapeType = EType::Capsule;
 
+private:
 	// List of possible scene names
 	static const char *		sScenes[];
 

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

@@ -60,14 +60,7 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	mCharacter->GetShape()->Draw(mDebugRenderer, com, Vec3::sOne(), Color::sGreen, false, true);
 #endif // JPH_DEBUG_RENDERER
 
-	// Draw shape including padding (only implemented for capsules right now)
-	if (static_cast<const RotatedTranslatedShape *>(mCharacter->GetShape())->GetInnerShape()->GetSubType() == EShapeSubType::Capsule)
-	{
-		if (mCharacter->GetShape() == mStandingShape)
-			mDebugRenderer->DrawCapsule(com, 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding + mCharacter->GetCharacterPadding(), Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
-		else
-			mDebugRenderer->DrawCapsule(com, 0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching + mCharacter->GetCharacterPadding(), Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
-	}
+	DrawPaddedCharacter(mCharacter->GetShape(), mCharacter->GetCharacterPadding(), com);
 
 	// Remember old position
 	RVec3 old_position = mCharacter->GetPosition();
@@ -95,6 +88,9 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 
 #ifdef JPH_ENABLE_ASSERTS
 	// Validate that our contact list is in sync with that of the character
+	// Note that compound shapes could be non convex so we may detect more contacts than have been reported by the character
+	// as the character only reports contacts as it is sliding through the world. If 2 sub shapes hit at the same time then
+	// most likely only one will be reported as it stops the character and prevents the 2nd one from being seen.
 	uint num_contacts = 0;
 	for (const CharacterVirtual::Contact &c : mCharacter->GetActiveContacts())
 		if (c.mHadCollision)
@@ -102,7 +98,7 @@ void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 			JPH_ASSERT(std::find(mActiveContacts.begin(), mActiveContacts.end(), c) != mActiveContacts.end());
 			num_contacts++;
 		}
-	JPH_ASSERT(num_contacts == mActiveContacts.size());
+	JPH_ASSERT(sShapeType == EType::Compound? num_contacts >= mActiveContacts.size() : num_contacts == mActiveContacts.size());
 #endif
 
 	// Calculate effective velocity