Ver Fonte

Renamed SensorDetectsStatic to CollideKinematicVsNonDynamic and made it work for non-sensors (#841)

This means that kinematic bodies can now get collision callbacks when they collide with other static / kinematic objects. It can also affect the order in which bodies are passed in the ContactListener::OnContactValidate callback.

See: godot-jolt/godot-jolt#682
Jorrit Rouwe há 1 ano atrás
pai
commit
2d607c4161

+ 1 - 1
Jolt/Physics/Body/Body.cpp

@@ -334,7 +334,7 @@ BodyCreationSettings Body::GetBodyCreationSettings() const
 	result.mAllowedDOFs = mMotionProperties != nullptr? mMotionProperties->GetAllowedDOFs() : EAllowedDOFs::All;
 	result.mAllowDynamicOrKinematic = mMotionProperties != nullptr;
 	result.mIsSensor = IsSensor();
-	result.mSensorDetectsStatic = SensorDetectsStatic();
+	result.mCollideKinematicVsNonDynamic = GetCollideKinematicVsNonDynamic();
 	result.mUseManifoldReduction = GetUseManifoldReduction();
 	result.mApplyGyroscopicForce = GetApplyGyroscopicForce();
 	result.mMotionQuality = mMotionProperties != nullptr? mMotionProperties->GetMotionQuality() : EMotionQuality::Discrete;

+ 12 - 10
Jolt/Physics/Body/Body.h

@@ -79,11 +79,13 @@ public:
 	/// Check if this body is a sensor.
 	inline bool				IsSensor() const												{ return (mFlags.load(memory_order_relaxed) & uint8(EFlags::IsSensor)) != 0; }
 
-	// If this sensor detects static objects entering it. Note that the sensor must be kinematic and active for it to detect static objects.
-	inline void				SetSensorDetectsStatic(bool inDetectsStatic)					{ JPH_ASSERT(IsRigidBody()); if (inDetectsStatic) mFlags.fetch_or(uint8(EFlags::SensorDetectsStatic), memory_order_relaxed); else mFlags.fetch_and(uint8(~uint8(EFlags::SensorDetectsStatic)), memory_order_relaxed); }
+	/// If kinematic objects can generate contact points against other kinematic or static objects.
+	/// Note that turning this on can be CPU intensive as much more collision detection work will be done without any effect on the simulation (kinematic objects are not affected by other kinematic/static objects).
+	/// This can be used to make sensors detect static objects. Note that the sensor must be kinematic and active for it to detect static objects.
+	inline void				SetCollideKinematicVsNonDynamic(bool inCollide)					{ JPH_ASSERT(IsRigidBody()); if (inCollide) mFlags.fetch_or(uint8(EFlags::CollideKinematicVsNonDynamic), memory_order_relaxed); else mFlags.fetch_and(uint8(~uint8(EFlags::CollideKinematicVsNonDynamic)), memory_order_relaxed); }
 
-	/// Check if this sensor detects static objects entering it.
-	inline bool				SensorDetectsStatic() const										{ return (mFlags.load(memory_order_relaxed) & uint8(EFlags::SensorDetectsStatic)) != 0; }
+	/// Check if kinematic objects can generate contact points against other kinematic or static objects.
+	inline bool				GetCollideKinematicVsNonDynamic() const							{ return (mFlags.load(memory_order_relaxed) & uint8(EFlags::CollideKinematicVsNonDynamic)) != 0; }
 
 	/// If PhysicsSettings::mUseManifoldReduction is true, this allows turning off manifold reduction for this specific body.
 	/// Manifold reduction by default will combine contacts with similar normals that come from different SubShapeIDs (e.g. different triangles in a mesh shape or different compound shapes).
@@ -329,12 +331,12 @@ private:
 
 	enum class EFlags : uint8
 	{
-		IsSensor				= 1 << 0,													///< If this object is a sensor. A sensor will receive collision callbacks, but will not cause any collision responses and can be used as a trigger volume.
-		SensorDetectsStatic		= 1 << 1,													///< If this sensor detects static objects entering it.
-		IsInBroadPhase			= 1 << 2,													///< Set this bit to indicate that the body is in the broadphase
-		InvalidateContactCache	= 1 << 3,													///< Set this bit to indicate that all collision caches for this body are invalid, will be reset the next simulation step.
-		UseManifoldReduction	= 1 << 4,													///< Set this bit to indicate that this body can use manifold reduction (if PhysicsSettings::mUseManifoldReduction is true)
-		ApplyGyroscopicForce	= 1 << 5,													///< Set this bit to indicate that the gyroscopic force should be applied to this body (aka Dzhanibekov effect, see https://en.wikipedia.org/wiki/Tennis_racket_theorem)
+		IsSensor						= 1 << 0,											///< If this object is a sensor. A sensor will receive collision callbacks, but will not cause any collision responses and can be used as a trigger volume.
+		CollideKinematicVsNonDynamic	= 1 << 1,											///< If kinematic objects can generate contact points against other kinematic or static objects.
+		IsInBroadPhase					= 1 << 2,											///< Set this bit to indicate that the body is in the broadphase
+		InvalidateContactCache			= 1 << 3,											///< Set this bit to indicate that all collision caches for this body are invalid, will be reset the next simulation step.
+		UseManifoldReduction			= 1 << 4,											///< Set this bit to indicate that this body can use manifold reduction (if PhysicsSettings::mUseManifoldReduction is true)
+		ApplyGyroscopicForce			= 1 << 5,											///< Set this bit to indicate that the gyroscopic force should be applied to this body (aka Dzhanibekov effect, see https://en.wikipedia.org/wiki/Tennis_racket_theorem)
 	};
 
 	// 16 byte aligned

+ 11 - 20
Jolt/Physics/Body/Body.inl

@@ -27,29 +27,20 @@ RMat44 Body::GetInverseCenterOfMassTransform() const
 	return RMat44::sInverseRotationTranslation(mRotation, mPosition);
 }
 
-inline static bool sIsValidSensorBodyPair(const Body &inSensor, const Body &inOther)
-{
-	// If the sensor is not an actual sensor then this is not a valid pair
-	if (!inSensor.IsSensor())
-		return false;
-
-	if (inSensor.SensorDetectsStatic())
-		return !inOther.IsDynamic(); // If the other body is dynamic, the pair will be handled when the bodies are swapped, otherwise we'll detect the collision twice
-	else
-		return inOther.IsKinematic(); // Only kinematic bodies are valid
-}
-
 inline bool Body::sFindCollidingPairsCanCollide(const Body &inBody1, const Body &inBody2)
 {
 	// First body should never be a soft body
 	JPH_ASSERT(!inBody1.IsSoftBody());
 
 	// One of these conditions must be true
+	// - We always allow detecting collisions between kinematic and non-dynamic bodies
 	// - One of the bodies must be dynamic to collide
-	// - A sensor can collide with non-dynamic bodies
-	if ((!inBody1.IsDynamic() && !inBody2.IsDynamic())
-		&& !sIsValidSensorBodyPair(inBody1, inBody2)
-		&& !sIsValidSensorBodyPair(inBody2, inBody1))
+	// - A kinematic object can collide with a sensor
+	if (!inBody1.GetCollideKinematicVsNonDynamic()
+		&& !inBody2.GetCollideKinematicVsNonDynamic()
+		&& (!inBody1.IsDynamic() && !inBody2.IsDynamic())
+		&& !(inBody1.IsKinematic() && inBody2.IsSensor())
+		&& !(inBody2.IsKinematic() && inBody1.IsSensor()))
 		return false;
 
 	// Check that body 1 is active
@@ -58,9 +49,9 @@ inline bool Body::sFindCollidingPairsCanCollide(const Body &inBody1, const Body
 
 	// If the pair A, B collides we need to ensure that the pair B, A does not collide or else we will handle the collision twice.
 	// If A is the same body as B we don't want to collide (1)
-	// If A is dynamic and B is static we should collide (2)
-	// If A is dynamic / kinematic and B is dynamic / kinematic we should only collide if (kinematic vs kinematic is ruled out by the if above)
-	//	- A is active and B is not yet active (3)
+	// If A is dynamic / kinematic and B is static we should collide (2)
+	// If A is dynamic / kinematic and B is dynamic / kinematic we should only collide if
+	//	- A is active and B is not active (3)
 	//	- A is active and B will become active during this simulation step (4)
 	//	- A is active and B is active, we require a condition that makes A, B collide and B, A not (5)
 	//
@@ -80,7 +71,7 @@ inline bool Body::sFindCollidingPairsCanCollide(const Body &inBody1, const Body
 		return false;
 	JPH_ASSERT(inBody1.GetID() != inBody2.GetID(), "Read the comment above, A and B are the same body which should not be possible!");
 
-	// Bodies in the same group don't collide
+	// Check collision group filter
 	if (!inBody1.GetCollisionGroup().CanCollide(inBody2.GetCollisionGroup()))
 		return false;
 

+ 3 - 3
Jolt/Physics/Body/BodyCreationSettings.cpp

@@ -25,7 +25,7 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(BodyCreationSettings)
 	JPH_ADD_ENUM_ATTRIBUTE(BodyCreationSettings, mAllowedDOFs)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mAllowDynamicOrKinematic)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mIsSensor)
-	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mSensorDetectsStatic)
+	JPH_ADD_ATTRIBUTE_WITH_ALIAS(BodyCreationSettings, mCollideKinematicVsNonDynamic, "mSensorDetectsStatic") // This is the old name to keep backwards compatibility
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mUseManifoldReduction)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mApplyGyroscopicForce)
 	JPH_ADD_ENUM_ATTRIBUTE(BodyCreationSettings, mMotionQuality)
@@ -56,7 +56,7 @@ void BodyCreationSettings::SaveBinaryState(StreamOut &inStream) const
 	inStream.Write(mAllowedDOFs);
 	inStream.Write(mAllowDynamicOrKinematic);
 	inStream.Write(mIsSensor);
-	inStream.Write(mSensorDetectsStatic);
+	inStream.Write(mCollideKinematicVsNonDynamic);
 	inStream.Write(mUseManifoldReduction);
 	inStream.Write(mApplyGyroscopicForce);
 	inStream.Write(mMotionQuality);
@@ -87,7 +87,7 @@ void BodyCreationSettings::RestoreBinaryState(StreamIn &inStream)
 	inStream.Read(mAllowedDOFs);
 	inStream.Read(mAllowDynamicOrKinematic);
 	inStream.Read(mIsSensor);
-	inStream.Read(mSensorDetectsStatic);
+	inStream.Read(mCollideKinematicVsNonDynamic);
 	inStream.Read(mUseManifoldReduction);
 	inStream.Read(mApplyGyroscopicForce);
 	inStream.Read(mMotionQuality);

+ 1 - 1
Jolt/Physics/Body/BodyCreationSettings.h

@@ -94,7 +94,7 @@ public:
 	EAllowedDOFs			mAllowedDOFs = EAllowedDOFs::All;								///< Which degrees of freedom this body has (can be used to limit simulation to 2D)
 	bool					mAllowDynamicOrKinematic = false;								///< When this body is created as static, this setting tells the system to create a MotionProperties object so that the object can be switched to kinematic or dynamic
 	bool					mIsSensor = false;												///< If this body is a sensor. A sensor will receive collision callbacks, but will not cause any collision responses and can be used as a trigger volume. See description at Body::SetIsSensor.
-	bool					mSensorDetectsStatic = false;									///< If this sensor detects static objects entering it. Note that the sensor must be kinematic and active for it to detect static objects.
+	bool					mCollideKinematicVsNonDynamic = false;							///< If kinematic objects can generate contact points against other kinematic or static objects. See description at Body::SetCollideKinematicVsNonDynamic.
 	bool					mUseManifoldReduction = true;									///< If this body should use manifold reduction (see description at Body::SetUseManifoldReduction)
 	bool					mApplyGyroscopicForce = false;									///< Set to indicate that the gyroscopic force should be applied to this body (aka Dzhanibekov effect, see https://en.wikipedia.org/wiki/Tennis_racket_theorem)
 	EMotionQuality			mMotionQuality = EMotionQuality::Discrete;						///< Motion quality, or how well it detects collisions when it has a high velocity

+ 2 - 2
Jolt/Physics/Body/BodyManager.cpp

@@ -198,8 +198,8 @@ Body *BodyManager::AllocateBody(const BodyCreationSettings &inBodyCreationSettin
 	body->mMotionType = inBodyCreationSettings.mMotionType;
 	if (inBodyCreationSettings.mIsSensor)
 		body->SetIsSensor(true);
-	if (inBodyCreationSettings.mSensorDetectsStatic)
-		body->SetSensorDetectsStatic(true);
+	if (inBodyCreationSettings.mCollideKinematicVsNonDynamic)
+		body->SetCollideKinematicVsNonDynamic(true);
 	if (inBodyCreationSettings.mUseManifoldReduction)
 		body->SetUseManifoldReduction(true);
 	if (inBodyCreationSettings.mApplyGyroscopicForce)

+ 1 - 1
Jolt/Physics/Collision/ContactListener.h

@@ -77,7 +77,7 @@ public:
 	/// This is a rather expensive time to reject a contact point since a lot of the collision detection has happened already, make sure you
 	/// filter out the majority of undesired body pairs through the ObjectLayerPairFilter that is registered on the PhysicsSystem.
 	/// Note that this callback is called when all bodies are locked, so don't use any locking functions!
-	/// The order of body 1 and 2 is undefined, but when one of the two bodies is dynamic it will be body 1.
+	/// Body 1 will have a motion type that is larger or equal than body 2's motion type (order from large to small: dynamic -> kinematic -> static). When motion types are equal, they are ordered by BodyID.
 	/// The collision result (inCollisionResult) is reported relative to inBaseOffset.
 	virtual ValidateResult	OnContactValidate([[maybe_unused]] const Body &inBody1, [[maybe_unused]] const Body &inBody2, [[maybe_unused]] RVec3Arg inBaseOffset, [[maybe_unused]] const CollideShapeResult &inCollisionResult) { return ValidateResult::AcceptAllContactsForThisBodyPair; }
 

+ 9 - 14
Jolt/Physics/PhysicsSystem.cpp

@@ -974,13 +974,12 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 		return;
 	}
 
-	// Ensure that body1 is dynamic, this ensures that we do the collision detection in the space of a moving body, which avoids accuracy problems when testing a very large static object against a small dynamic object
-	// Ensure that body1 id < body2 id for dynamic vs dynamic
-	// Keep body order unchanged when colliding with a sensor
-	if ((!body1->IsDynamic() || (body2->IsDynamic() && inBodyPair.mBodyB < inBodyPair.mBodyA))
-		&& !body2->IsSensor())
+	// Ensure that body1 has the higher motion type (i.e. dynamic trumps kinematic), this ensures that we do the collision detection in the space of a moving body,
+	// which avoids accuracy problems when testing a very large static object against a small dynamic object
+	// Ensure that body1 id < body2 id when motion types are the same.
+	if (body1->GetMotionType() < body2->GetMotionType()
+		|| (body1->GetMotionType() == body2->GetMotionType() && inBodyPair.mBodyB < inBodyPair.mBodyA))
 		swap(body1, body2);
-	JPH_ASSERT(body1->IsDynamic() || body2->IsSensor());
 
 	// Check if the contact points from the previous frame are reusable and if so copy them
 	bool pair_handled = false, constraint_created = false;
@@ -1035,10 +1034,8 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 
 				virtual void	AddHit(const CollideShapeResult &inResult) override
 				{
-					// One of the following should be true:
-					// - Body 1 is dynamic and body 2 may be dynamic, static or kinematic
-					// - Body 1 is not dynamic in which case body 2 should be a sensor
-					JPH_ASSERT(mBody1->IsDynamic() || mBody2->IsSensor());
+					// The first body should be the one with the highest motion type
+					JPH_ASSERT(mBody1->GetMotionType() >= mBody2->GetMotionType());
 					JPH_ASSERT(!ShouldEarlyOut());
 
 					// Test if we want to accept this hit
@@ -1160,10 +1157,8 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 
 				virtual void	AddHit(const CollideShapeResult &inResult) override
 				{
-					// One of the following should be true:
-					// - Body 1 is dynamic and body 2 may be dynamic, static or kinematic
-					// - Body 1 is not dynamic in which case body 2 should be a sensor
-					JPH_ASSERT(mBody1->IsDynamic() || mBody2->IsSensor());
+					// The first body should be the one with the highest motion type
+					JPH_ASSERT(mBody1->GetMotionType() >= mBody2->GetMotionType());
 					JPH_ASSERT(!ShouldEarlyOut());
 
 					// Test if we want to accept this hit

+ 1 - 1
Samples/Tests/General/SensorTest.cpp

@@ -56,7 +56,7 @@ void SensorTest::Initialize()
 		// A kinematic sensor that also detects static bodies
 		BodyCreationSettings sensor_settings(new BoxShape(Vec3::sReplicate(5.0f)), RVec3(25, 5.1f, 0), Quat::sIdentity(), EMotionType::Kinematic, Layers::MOVING); // Put in a layer that collides with static
 		sensor_settings.mIsSensor = true;
-		sensor_settings.mSensorDetectsStatic = true;
+		sensor_settings.mCollideKinematicVsNonDynamic = true;
 		mSensorID[SensorDetectingStatic] = mBodyInterface->CreateAndAddBody(sensor_settings, EActivation::Activate);
 	}
 

+ 4 - 2
Samples/Utils/ContactListenerImpl.cpp

@@ -12,8 +12,10 @@
 
 ValidateResult ContactListenerImpl::OnContactValidate(const Body &inBody1, const Body &inBody2, RVec3Arg inBaseOffset, const CollideShapeResult &inCollisionResult)
 {
-	// Expect body 1 to be dynamic (or one of the bodies must be a sensor)
-	if (!inBody1.IsDynamic() && !inBody1.IsSensor() && !inBody2.IsSensor())
+	// Check ordering contract between body 1 and body 2
+	bool contract = inBody1.GetMotionType() >= inBody2.GetMotionType()
+		|| (inBody1.GetMotionType() == inBody2.GetMotionType() && inBody1.GetID() < inBody2.GetID());
+	if (!contract)
 		JPH_BREAKPOINT;
 
 	ValidateResult result;

+ 3 - 2
UnitTests/LoggingContactListener.h

@@ -33,8 +33,9 @@ public:
 
 	virtual ValidateResult			OnContactValidate(const Body &inBody1, const Body &inBody2, RVec3Arg inBaseOffset, const CollideShapeResult &inCollisionResult) override
 	{
-		// Check contract that body 1 is dynamic or that body2 is not dynamic
-		bool contract = inBody1.IsDynamic() || !inBody2.IsDynamic();
+		// Check ordering contract between body 1 and body 2
+		bool contract = inBody1.GetMotionType() >= inBody2.GetMotionType()
+			|| (inBody1.GetMotionType() == inBody2.GetMotionType() && inBody1.GetID() < inBody2.GetID());
 		CHECK(contract);
 
 		lock_guard lock(mLogMutex);

+ 72 - 0
UnitTests/Physics/ContactListenerTests.cpp

@@ -602,4 +602,76 @@ TEST_SUITE("ContactListenerTests")
 				CHECK_APPROX_EQUAL(bi.GetLinearVelocity(sphere_id), -sphere_settings.mLinearVelocity * sphere_settings.mRestitution);
 			}
 		}
+
+	TEST_CASE("TestCollideKinematicVsNonDynamic")
+	{
+		for (EMotionType m1 = EMotionType::Static; m1 <= EMotionType::Dynamic; m1 = EMotionType((int)m1 + 1))
+			for (int allow1 = 0; allow1 < 2; ++allow1)
+				for (int active1 = 0; active1 < 2; ++active1)
+					for (EMotionType m2 = EMotionType::Static; m2 <= EMotionType::Dynamic; m2 = EMotionType((int)m2 + 1))
+						for (int allow2 = 0; allow2 < 2; ++allow2)
+							for (int active2 = 0; active2 < 2; ++active2)
+								if ((m1 != EMotionType::Static && active1) || (m2 != EMotionType::Static && active2))
+								{
+									PhysicsTestContext c;
+									c.ZeroGravity();
+
+									const Vec3 cInitialVelocity1(m1 != EMotionType::Static && active1 != 0? 1.0f : 0.0f, 0, 0);
+									const Vec3 cInitialVelocity2(m2 != EMotionType::Static && active2 != 0? -1.0f : 0.0f, 0, 0);
+
+									// Create two spheres that are colliding initially
+									BodyCreationSettings bcs(new SphereShape(1.0f), RVec3::sZero(), Quat::sIdentity(), m1, m1 != EMotionType::Static? Layers::MOVING : Layers::NON_MOVING);
+									bcs.mPosition = RVec3(-0.5_r, 0, 0);
+									bcs.mLinearVelocity = cInitialVelocity1;
+									bcs.mCollideKinematicVsNonDynamic = allow1 != 0;
+									Body &body1 = *c.GetBodyInterface().CreateBody(bcs);
+									c.GetBodyInterface().AddBody(body1.GetID(), active1 != 0? EActivation::Activate : EActivation::DontActivate);
+
+									bcs.mMotionType = m2;
+									bcs.mObjectLayer = m2 != EMotionType::Static? Layers::MOVING : Layers::NON_MOVING;
+									bcs.mPosition = RVec3(0.5_r, 0, 0);
+									bcs.mLinearVelocity = cInitialVelocity2;
+									bcs.mCollideKinematicVsNonDynamic = allow2 != 0;
+									Body &body2 = *c.GetBodyInterface().CreateBody(bcs);
+									c.GetBodyInterface().AddBody(body2.GetID(), active2 != 0? EActivation::Activate : EActivation::DontActivate);
+
+									// Set listener
+									LoggingContactListener listener;
+									c.GetSystem()->SetContactListener(&listener);
+
+									// Step
+									c.SimulateSingleStep();
+
+									if ((allow1 || allow2) // In this case we always get a callback
+										|| (m1 == EMotionType::Dynamic || m2 == EMotionType::Dynamic)) // Otherwise we only get a callback when one of the bodies is dynamic
+									{
+										// Check that we received a callback
+										CHECK(listener.GetEntryCount() == 2);
+										CHECK(listener.Contains(EType::Validate, body1.GetID(), body2.GetID()));
+										CHECK(listener.Contains(EType::Add, body1.GetID(), body2.GetID()));
+									}
+									else
+									{
+										// No collision events should have been received
+										CHECK(listener.GetEntryCount() == 0);
+									}
+
+									// Velocities should only change if the body is dynamic
+									if (m1 == EMotionType::Dynamic)
+									{
+										CHECK(body1.GetLinearVelocity() != cInitialVelocity1);
+										CHECK(body1.IsActive());
+									}
+									else
+										CHECK(body1.GetLinearVelocity() == cInitialVelocity1);
+
+									if (m2 == EMotionType::Dynamic)
+									{
+										CHECK(body2.GetLinearVelocity() != cInitialVelocity2);
+										CHECK(body2.IsActive());
+									}
+									else
+										CHECK(body2.GetLinearVelocity() == cInitialVelocity2);
+								}
+	}
 }

+ 2 - 2
UnitTests/Physics/SensorTests.cpp

@@ -631,7 +631,7 @@ TEST_SUITE("SensorTests")
 		listener.Clear();
 
 		// Start detecting static
-		sensor.SetSensorDetectsStatic(true);
+		sensor.SetCollideKinematicVsNonDynamic(true);
 
 		// After a single step we should detect both static bodies
 		c.SimulateSingleStep();
@@ -641,7 +641,7 @@ TEST_SUITE("SensorTests")
 		listener.Clear();
 
 		// Stop detecting static
-		sensor.SetSensorDetectsStatic(false);
+		sensor.SetCollideKinematicVsNonDynamic(false);
 
 		// After a single step we should stop detecting both static bodies
 		c.SimulateSingleStep();