Browse Source

Added support for a ShapeFilter during simulation (#1362)

Added PhysicsSystem::SetSimulationShapeFilter. This allows filtering out collisions between sub shapes within a body and can for example be used to have a single body that contains a low detail simulation shape an a high detail collision query shape.
Jorrit Rouwe 8 months ago
parent
commit
0e5fcade0c

+ 14 - 12
Docs/Architecture.md

@@ -12,7 +12,7 @@ We use a pretty traditional physics engine setup, so \ref Body "bodies" in our s
 
 Bodies can either be:
 - [static](@ref EMotionType) (not moving or simulating)
-- [dynamic](@ref EMotionType) (moved by forces) or 
+- [dynamic](@ref EMotionType) (moved by forces) or
 - [kinematic](@ref EMotionType) (moved by velocities only).
 
 Moving bodies have a [MotionProperties](@ref MotionProperties) object that contains information about the movement of the object. Static bodies do not have this to save space (but they can be configured to have it if a static body needs to become dynamic during its lifetime by setting [BodyCreationSettings::mAllowDynamicOrKinematic](@ref BodyCreationSettings::mAllowDynamicOrKinematic)).
@@ -39,7 +39,7 @@ Always use the batch adding functions when possible! Adding many bodies, one at
 
 You can call AddBody, RemoveBody, AddBody, RemoveBody to temporarily remove and later reinsert a body into the simulation.
 
-## Multithreaded Access 
+## Multithreaded Access
 
 Jolt is designed to be accessed from multiple threads so the body interface comes in two flavors: A locking and a non-locking variant. The locking variant uses a mutex array (a fixed size array of mutexes, bodies are associated with a mutex through hashing and multiple bodies use the same mutex, see [MutexArray](@ref MutexArray)) to prevent concurrent access to the same body. The non-locking variant doesn't use mutexes, so requires the user to be careful.
 
@@ -245,10 +245,10 @@ A safer way of scaling shapes is provided by the Shape::ScaleShape function:
 
 	JPH::Shape::ShapeResult my_scaled_shape = my_non_scaled_shape->ScaleShape(JPH::Vec3(x_scale, y_scale, z_scale));
 
-This function will check if a scale is valid for a particular shape and if a scale is not valid, it will produce the closest scale that is valid. 
+This function will check if a scale is valid for a particular shape and if a scale is not valid, it will produce the closest scale that is valid.
 For example, if you scale a CompoundShape that has rotated sub shapes, a non-uniform scale would cause shearing. In that case the Shape::ScaleShape function will create a new compound shape and scale the sub shapes (losing the shear) rather than creating a ScaledShape around the entire CompoundShape.
 
-Updating scaling after a body is created is also possible, but should be done with care. Imagine a sphere in a pipe, scaling the sphere so that it becomes bigger than the pipe creates an impossible situation as there is no way to resolve the collision anymore. 
+Updating scaling after a body is created is also possible, but should be done with care. Imagine a sphere in a pipe, scaling the sphere so that it becomes bigger than the pipe creates an impossible situation as there is no way to resolve the collision anymore.
 Please take a look at the [DynamicScaledShape](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/ScaledShapes/DynamicScaledShape.cpp) demo. The reason that no ScaledShape::SetScale function exists is to ensure thread safety when collision queries are being executed while shapes are modified.
 
 Note that there are many functions that take a scale in Jolt (e.g. CollisionDispatch::sCollideShapeVsShape), usually the shape is scaled relative to its center of mass. The Shape::ScaleShape function scales the shape relative to the origin of the shape.
@@ -420,7 +420,7 @@ The following sections describe the collision detection system in more detail.
 
 ## Broad Phase {#broad-phase}
 
-When bodies are added to the PhysicsSystem, they are inserted in the broad phase ([BroadPhaseQuadTree](@ref BroadPhaseQuadTree)). This provides quick coarse collision detection based on the axis aligned bounding box (AABB) of a body. 
+When bodies are added to the PhysicsSystem, they are inserted in the broad phase ([BroadPhaseQuadTree](@ref BroadPhaseQuadTree)). This provides quick coarse collision detection based on the axis aligned bounding box (AABB) of a body.
 
 ![To quickly test if two objects overlap you can check if their axis aligned bounding boxes overlap. If they do, a check between the actual shapes is needed to be sure.](Images/EllipsoidAABB.png)
 
@@ -491,21 +491,23 @@ Now that we know about the basics, we list the order in which the collision dete
 
 * Broadphase layer: At this stage, the object layer is tested against the broad phase trees that are relevant by checking the [ObjectVsBroadPhaseLayerFilter](@ref ObjectVsBroadPhaseLayerFilter).
 * Object layer: Once the broad phase layer test succeeds, we will test object layers vs object layers through [ObjectLayerPairFilter](@ref ObjectLayerPairFilter) (used for simulation) and [ObjectLayerFilter](@ref ObjectLayerFilter) (used for collision queries). The default implementation of ObjectLayerFilter is DefaultObjectLayerFilter and uses ObjectLayerPairFilter so the behavior is consistent between simulation and collision queries.
-* Group filter: Most expensive filtering (bounding boxes already overlap), used only during simulation. Allows you fine tune collision e.g. by discarding collisions between bodies connected by a constraint. See [GroupFilter](@ref GroupFilter) and implementation for ragdolls [GroupFilterTable](@ref GroupFilterTable).
-* Body filter: This filter is used instead of the group filter if you do collision queries like CastRay. See [BodyFilter](@ref BodyFilter).
-* Shape filter: This filter is used only during collision queries and can be used to filter out individual (sub)shapes. See [ShapeFilter](@ref ShapeFilter).
-* Contact listener: During simulation, after all collision detection work has been performed you can still choose to discard a contact point. This is a very expensive way of rejecting collisions as most of the work is already done. See [ContactListener](@ref ContactListener).
+* [GroupFilter](@ref GroupFilter): Used only during simulation and runs after bounding boxes have found to be overlapping. Allows you fine tune collision e.g. by discarding collisions between bodies connected by a constraint. See e.g. [GroupFilterTable](@ref GroupFilterTable) which implements filtering for bodies within a ragdoll.
+* [BodyFilter](@ref BodyFilter): This filter is used instead of the group filter if you do collision queries like CastRay.
+* [ShapeFilter](@ref ShapeFilter): This filter is used both during queries and simulation and can be used to filter out individual shapes of a compound. To set the shape filter for the simulation use PhysicsSystem::SetSimulationShapeFilter.
+* [ContactListener](@ref ContactListener): During simulation, after all collision detection work has been performed you can still choose to discard a contact point. This is a very expensive way of rejecting collisions as most of the work is already done.
 
 To avoid work, try to filter out collisions as early as possible.
 
 ## Level of Detail {#level-of-detail}
 
 Bodies can only exist in a single layer. If you want a body with a low detail collision shape for simulation (in the example above: MOVING layer) and a high detail collision shape for collision detection (BULLET layer), you'll need to create 2 Bodies.
-
 The low detail body should be dynamic. The high detail body should be kinematic, or if it doesn't interact with other dynamic objects it can also be static.
-
 After calling PhysicsSystem::Update, you'll need to loop over these dynamic bodies and call BodyInterface::MoveKinematic in case the high detail body is kinematic, or BodyInterface::SetPositionAndRotation in case the high detail body is static.
 
+Alternatively, you can put a high detail and a low detail shape in a StaticCompoundShape and use PhysicsSystem::SetSimulationShapeFilter to filter out the high detail shape during simulation.
+Another ShapeFilter would filter out the low detail shape during collision queries (e.g. through NarrowPhaseQuery).
+You can use Shape::GetUserData to determine if a shape is a high or a low detail shape.
+
 ## Continuous Collision Detection {#continuous-collision-detection}
 
 Each body has a motion quality setting ([EMotionQuality](@ref EMotionQuality)). By default the motion quality is [Discrete](@ref Discrete). This means that at the beginning of each simulation step we will perform collision detection and if no collision is found, the body is free to move according to its velocity. This usually works fine for big or slow moving objects. Fast and small objects can easily 'tunnel' through thin objects because they can completely move through them in a single time step. For these objects there is the motion quality [LinearCast](@ref LinearCast). Objects that have this motion quality setting will do the same collision detection at the beginning of the simulation step, but once their new position is known, they will do an additional CastShape to check for any collisions that may have been missed. If this is the case, the object is placed back to where the collision occurred and will remain there until the next time step. This is called 'time stealing' and has the disadvantage that an object may appear to move much slower for a single time step and then speed up again. The alternative, back stepping the entire simulation, is computationally heavy so was not implemented.
@@ -532,7 +534,7 @@ Whenever a body hits an inactive edge, the contact normal is the face normal. Wh
 
 By tweaking MeshShapeSettings::mActiveEdgeCosThresholdAngle or HeightFieldShapeSettings::mActiveEdgeCosThresholdAngle you can determine the angle at which an edge is considered an active edge. By default this is 5 degrees, making this bigger reduces the amount of ghost collisions but can create simulation artifacts if you hit the edge straight on.
 
-To further reduce ghost collisions, you can turn on BodyCreationSettings::mEnhancedInternalEdgeRemoval. When enabling this setting, additional checks will be made at run-time to detect if an edge is active or inactive based on all of the contact points between the two bodies. Beware that this algorithm only considers 2 bodies at a time, so if the two green boxes above belong to two different bodies, the ghost collision can still occur. Use a StaticCompoundShape to combine the boxes in a single body to allow the system to eliminate ghost collisions between the blue and the two green boxes. You can also use this functionality for your custom collision tests by making use of InternalEdgeRemovingCollector. 
+To further reduce ghost collisions, you can turn on BodyCreationSettings::mEnhancedInternalEdgeRemoval. When enabling this setting, additional checks will be made at run-time to detect if an edge is active or inactive based on all of the contact points between the two bodies. Beware that this algorithm only considers 2 bodies at a time, so if the two green boxes above belong to two different bodies, the ghost collision can still occur. Use a StaticCompoundShape to combine the boxes in a single body to allow the system to eliminate ghost collisions between the blue and the two green boxes. You can also use this functionality for your custom collision tests by making use of InternalEdgeRemovingCollector.
 
 # Character Controllers {#character-controllers}
 

+ 1 - 0
Docs/ReleaseNotes.md

@@ -11,6 +11,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added MotionProperties::ScaleToMass. This lets you easily change the mass and inertia tensor of a body after creation.
 * Split up Body::ApplyBuoyancyImpulse into Body::GetSubmergedVolume and Body::ApplyBuoyancyImpulse. This allows you to use the calculated submerged volume for other purposes.
 * Fixed a number of issues when creating very large MeshShapes. MeshShapes of up to 110M triangles are possible now, but the actual maximum is very dependent on how the triangles in the mesh are connected.
+* Added PhysicsSystem::SetSimulationShapeFilter. This allows filtering out collisions between sub shapes within a body and can for example be used to have a single body that contains a low detail simulation shape an a high detail collision query shape.
 
 ### Bug fixes
 

+ 19 - 3
Jolt/Physics/Collision/ShapeFilter.h

@@ -33,7 +33,7 @@ public:
 	/// It is called at each level of the shape hierarchy, so if you have a compound shape with a box, this function will be called twice.
 	/// It will not be called on triangles that are part of another shape, i.e a mesh shape will not trigger a callback per triangle. You can filter out individual triangles in the CollisionCollector::AddHit function by their sub shape ID.
 	/// @param inShape1 1st shape that is colliding
-	/// @param inSubShapeIDOfShape1 The sub shape ID that will lead from the root shape to inShape1 (i.e. the shape that is used to collide or cast against shape 2)
+	/// @param inSubShapeIDOfShape1 The sub shape ID that will lead from the root shape to inShape1 (i.e. the shape that is used to collide or cast against shape 2 or the shape of mBodyID1)
 	/// @param inShape2 2nd shape that is colliding
 	/// @param inSubShapeIDOfShape2 The sub shape ID that will lead from the root shape to inShape2 (i.e. the shape of mBodyID2)
 	virtual bool			ShouldCollide([[maybe_unused]] const Shape *inShape1, [[maybe_unused]] const SubShapeID &inSubShapeIDOfShape1, [[maybe_unused]] const Shape *inShape2, [[maybe_unused]] const SubShapeID &inSubShapeIDOfShape2) const
@@ -41,7 +41,12 @@ public:
 		return true;
 	}
 
-	/// Set by the collision detection functions to the body ID of the body that we're colliding against before calling the ShouldCollide function
+	/// Used during PhysicsSystem::Update only. Set to the body ID of inShape1 before calling ShouldCollide.
+	/// Provides context to the filter to indicate which body is colliding.
+	mutable BodyID			mBodyID1;
+
+	/// Used during PhysicsSystem::Update, NarrowPhase queries and TransformedShape queries. Set to the body ID of inShape2 before calling ShouldCollide.
+	/// Provides context to the filter to indicate which body is colliding.
 	mutable BodyID			mBodyID2;
 };
 
@@ -52,7 +57,18 @@ public:
 	/// Constructor
 	explicit				ReversedShapeFilter(const ShapeFilter &inFilter) : mFilter(inFilter)
 	{
-		mBodyID2 = inFilter.mBodyID2;
+		if (inFilter.mBodyID1.IsInvalid())
+		{
+			// If body 1 is not set then we're coming from a regular query and we should not swap the bodies
+			// because conceptually we're still colliding a shape against a body and not a body against a body.
+			mBodyID2 = inFilter.mBodyID2;
+		}
+		else
+		{
+			// If both bodies have been filled in then we swap the bodies
+			mBodyID1 = inFilter.mBodyID2;
+			mBodyID2 = inFilter.mBodyID1;
+		}
 	}
 
 	virtual bool			ShouldCollide(const Shape *inShape2, const SubShapeID &inSubShapeIDOfShape2) const override

+ 21 - 5
Jolt/Physics/PhysicsSystem.cpp

@@ -1019,6 +1019,12 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 		settings.mMaxSeparationDistance = body1->IsSensor() || body2->IsSensor()? 0.0f : mPhysicsSettings.mSpeculativeContactDistance;
 		settings.mActiveEdgeMovementDirection = body1->GetLinearVelocity() - body2->GetLinearVelocity();
 
+		// Create shape filter
+		ShapeFilter default_shape_filter;
+		const ShapeFilter &shape_filter = mSimulationShapeFilter != nullptr? *mSimulationShapeFilter : default_shape_filter;
+		shape_filter.mBodyID1 = body1->GetID();
+		shape_filter.mBodyID2 = body2->GetID();
+
 		// Get transforms relative to body1
 		RVec3 offset = body1->GetCenterOfMassPosition();
 		Mat44 transform1 = Mat44::sRotation(body1->GetRotation());
@@ -1140,7 +1146,7 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 			// Perform collision detection between the two shapes
 			SubShapeIDCreator part1, part2;
 			auto f = enhanced_active_edges? InternalEdgeRemovingCollector::sCollideShapeVsShape : CollisionDispatch::sCollideShapeVsShape;
-			f(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector, { });
+			f(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector, shape_filter);
 
 			// Add the contacts
 			for (ContactManifold &manifold : collector.mManifolds)
@@ -1241,7 +1247,7 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 			// Perform collision detection between the two shapes
 			SubShapeIDCreator part1, part2;
 			auto f = enhanced_active_edges? InternalEdgeRemovingCollector::sCollideShapeVsShape : CollisionDispatch::sCollideShapeVsShape;
-			f(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector, { });
+			f(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector, shape_filter);
 
 			constraint_created = collector.mConstraintCreated;
 		}
@@ -1814,12 +1820,13 @@ void PhysicsSystem::JobFindCCDContacts(const PhysicsUpdateContext *ioContext, Ph
 		class CCDBroadPhaseCollector : public CastShapeBodyCollector
 		{
 		public:
-										CCDBroadPhaseCollector(const CCDBody &inCCDBody, const Body &inBody1, const RShapeCast &inShapeCast, ShapeCastSettings &inShapeCastSettings, CCDNarrowPhaseCollector &ioCollector, const BodyManager &inBodyManager, PhysicsUpdateContext::Step *inStep, float inDeltaTime) :
+										CCDBroadPhaseCollector(const CCDBody &inCCDBody, const Body &inBody1, const RShapeCast &inShapeCast, ShapeCastSettings &inShapeCastSettings, const ShapeFilter &inShapeFilter, CCDNarrowPhaseCollector &ioCollector, const BodyManager &inBodyManager, PhysicsUpdateContext::Step *inStep, float inDeltaTime) :
 				mCCDBody(inCCDBody),
 				mBody1(inBody1),
 				mBody1Extent(inShapeCast.mShapeWorldBounds.GetExtent()),
 				mShapeCast(inShapeCast),
 				mShapeCastSettings(inShapeCastSettings),
+				mShapeFilter(inShapeFilter),
 				mCollector(ioCollector),
 				mBodyManager(inBodyManager),
 				mStep(inStep),
@@ -1871,12 +1878,15 @@ void PhysicsSystem::JobFindCCDContacts(const PhysicsUpdateContext *ioContext, Ph
 				mCollector.mValidateBodyPair = true;
 				mCollector.mRejectAll = false;
 
+				// Set body ID on shape filter
+				mShapeFilter.mBodyID2 = inResult.mBodyID;
+
 				// Provide direction as hint for the active edges algorithm
 				mShapeCastSettings.mActiveEdgeMovementDirection = direction;
 
 				// Do narrow phase collision check
 				RShapeCast relative_cast(mShapeCast.mShape, mShapeCast.mScale, mShapeCast.mCenterOfMassStart, direction, mShapeCast.mShapeWorldBounds);
-				body2.GetTransformedShape().CastShape(relative_cast, mShapeCastSettings, mShapeCast.mCenterOfMassStart.GetTranslation(), mCollector);
+				body2.GetTransformedShape().CastShape(relative_cast, mShapeCastSettings, mShapeCast.mCenterOfMassStart.GetTranslation(), mCollector, mShapeFilter);
 
 				// Update early out fraction based on narrow phase collector
 				if (!mCollector.mRejectAll)
@@ -1888,15 +1898,21 @@ void PhysicsSystem::JobFindCCDContacts(const PhysicsUpdateContext *ioContext, Ph
 			Vec3						mBody1Extent;
 			RShapeCast					mShapeCast;
 			ShapeCastSettings &			mShapeCastSettings;
+			const ShapeFilter &			mShapeFilter;
 			CCDNarrowPhaseCollector &	mCollector;
 			const BodyManager &			mBodyManager;
 			PhysicsUpdateContext::Step *mStep;
 			float						mDeltaTime;
 		};
 
+		// Create shape filter
+		ShapeFilter default_shape_filter;
+		const ShapeFilter &shape_filter = mSimulationShapeFilter != nullptr? *mSimulationShapeFilter : default_shape_filter;
+		shape_filter.mBodyID1 = ccd_body.mBodyID1;
+
 		// Check if we collide with any other body. Note that we use the non-locking interface as we know the broadphase cannot be modified at this point.
 		RShapeCast shape_cast(body.GetShape(), Vec3::sReplicate(1.0f), body.GetCenterOfMassTransform(), ccd_body.mDeltaPosition);
-		CCDBroadPhaseCollector bp_collector(ccd_body, body, shape_cast, settings, np_collector, mBodyManager, ioStep, ioContext->mStepDeltaTime);
+		CCDBroadPhaseCollector bp_collector(ccd_body, body, shape_cast, settings, shape_filter, np_collector, mBodyManager, ioStep, ioContext->mStepDeltaTime);
 		mBroadPhase->CastAABoxNoLock({ shape_cast.mShapeWorldBounds, shape_cast.mDirection }, bp_collector, broadphase_layer_filter, object_layer_filter);
 
 		// Check if there was a hit

+ 11 - 0
Jolt/Physics/PhysicsSystem.h

@@ -67,6 +67,14 @@ public:
 	void						SetCombineRestitution(ContactConstraintManager::CombineFunction inCombineRestition) { mContactManager.SetCombineRestitution(inCombineRestition); }
 	ContactConstraintManager::CombineFunction GetCombineRestitution() const					{ return mContactManager.GetCombineRestitution(); }
 
+	/// Set/get the shape filter that will be used during simulation. This can be used to exclude shapes within a body from colliding with each other.
+	/// E.g. if you have a high detail and a low detail collision model, you can attach them to the same body in a StaticCompoundShape and use the ShapeFilter
+	/// to exclude the high detail collision model when simulating and exclude the low detail collision model when casting rays. Note that in this case
+	/// you would need to pass the inverse of inShapeFilter to the CastRay function. Pass a nullptr to disable the shape filter.
+	/// The PhysicsSystem does not own the ShapeFilter, make sure it stays alive during the lifetime of the PhysicsSystem.
+	void						SetSimulationShapeFilter(const ShapeFilter *inShapeFilter)	{ mSimulationShapeFilter = inShapeFilter; }
+	const ShapeFilter *			GetSimulationShapeFilter() const							{ return mSimulationShapeFilter; }
+
 	/// Control the main constants of the physics simulation
 	void						SetPhysicsSettings(const PhysicsSettings &inSettings)		{ mPhysicsSettings = inSettings; }
 	const PhysicsSettings &		GetPhysicsSettings() const									{ return mPhysicsSettings; }
@@ -294,6 +302,9 @@ private:
 	/// The soft body contact listener
 	SoftBodyContactListener *	mSoftBodyContactListener = nullptr;
 
+	/// The shape filter that is used to filter out sub shapes during simulation
+	const ShapeFilter *			mSimulationShapeFilter = nullptr;
+
 	/// Simulation settings
 	PhysicsSettings				mPhysicsSettings;
 

+ 2 - 0
Samples/Samples.cmake

@@ -87,6 +87,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/General/EnhancedInternalEdgeRemovalTest.h
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.cpp
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.h
+	${SAMPLES_ROOT}/Tests/General/SimulationShapeFilterTest.cpp
+	${SAMPLES_ROOT}/Tests/General/SimulationShapeFilterTest.h
 	${SAMPLES_ROOT}/Tests/General/CenterOfMassTest.cpp
 	${SAMPLES_ROOT}/Tests/General/CenterOfMassTest.h
 	${SAMPLES_ROOT}/Tests/General/ChangeMotionQualityTest.cpp

+ 3 - 1
Samples/SamplesApp.cpp

@@ -107,6 +107,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DynamicMeshTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, TwoDFunnelTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, AllowedDOFsTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ShapeFilterTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SimulationShapeFilterTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, GyroscopicForceTest)
 #ifdef JPH_OBJECT_STREAM
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, LoadSaveSceneTest)
@@ -151,7 +152,8 @@ static TestNameAndRTTI sGeneralTests[] =
 	{ "Sensor",								JPH_RTTI(SensorTest) },
 	{ "Dynamic Mesh",						JPH_RTTI(DynamicMeshTest) },
 	{ "Allowed Degrees of Freedom",			JPH_RTTI(AllowedDOFsTest) },
-	{ "Shape Filter",						JPH_RTTI(ShapeFilterTest) },
+	{ "Shape Filter (Collision Detection)",	JPH_RTTI(ShapeFilterTest) },
+	{ "Shape Filter (Simulation)",			JPH_RTTI(SimulationShapeFilterTest) },
 	{ "Gyroscopic Force",					JPH_RTTI(GyroscopicForceTest) },
 };
 

+ 3 - 6
Samples/Tests/General/ShapeFilterTest.cpp

@@ -49,17 +49,14 @@ void ShapeFilterTest::PostPhysicsUpdate(float inDeltaTime)
 	class MyShapeFilter : public ShapeFilter
 	{
 	public:
-		// Not used in this example
-		virtual bool	ShouldCollide(const Shape *inShape2, const SubShapeID &inSubShapeIDOfShape2) const override
-		{
-			return true;
-		}
-
 		virtual bool	ShouldCollide(const Shape *inShape1, const SubShapeID &inSubShapeID1, const Shape *inShape2, const SubShapeID &inSubShapeID2) const override
 		{
 			return inShape1->GetUserData() != mUserDataOfShapeToIgnore;
 		}
 
+		// We're not interested in the other overload as it is not used by ray casts
+		using ShapeFilter::ShouldCollide;
+
 		uint64			mUserDataOfShapeToIgnore = (uint64)ShapeIdentifier::Sphere;
 	};
 

+ 58 - 0
Samples/Tests/General/SimulationShapeFilterTest.cpp

@@ -0,0 +1,58 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/General/SimulationShapeFilterTest.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
+#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(SimulationShapeFilterTest)
+{
+	JPH_ADD_BASE_CLASS(SimulationShapeFilterTest, Test)
+}
+
+SimulationShapeFilterTest::~SimulationShapeFilterTest()
+{
+	// Unregister shape filter
+	mPhysicsSystem->SetSimulationShapeFilter(nullptr);
+}
+
+void SimulationShapeFilterTest::Initialize()
+{
+	// Register shape filter
+	mPhysicsSystem->SetSimulationShapeFilter(&mShapeFilter);
+
+	// Floor
+	CreateFloor();
+
+	// Platform
+	mShapeFilter.mPlatformID = mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(5.0f, 0.5f, 5.0f)), RVec3(0, 7.5f, 0), Quat::sRotation(Vec3::sAxisX(), 0.25f * JPH_PI), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+	// Compound shape
+	Ref<Shape> capsule = new CapsuleShape(2, 0.1f);
+	capsule->SetUserData(1); // Don't want the capsule to collide with the platform
+	Ref<Shape> sphere = new SphereShape(0.5f);
+	sphere->SetUserData(1); // Don't want the sphere to collide with the platform
+	Ref<Shape> box = new BoxShape(Vec3::sReplicate(0.5f));
+	Ref<StaticCompoundShapeSettings> compound = new StaticCompoundShapeSettings;
+	compound->AddShape(Vec3::sZero(), Quat::sIdentity(), capsule);
+	compound->AddShape(Vec3(0, -2, 0), Quat::sIdentity(), sphere);
+	compound->AddShape(Vec3(0, 2, 0), Quat::sIdentity(), box);
+	mShapeFilter.mCompoundID = mBodyInterface->CreateAndAddBody(BodyCreationSettings(compound, RVec3(0, 15, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
+}
+
+bool SimulationShapeFilterTest::Filter::ShouldCollide(const Shape *inShape1, const SubShapeID &inSubShapeIDOfShape1, const Shape *inShape2, const SubShapeID &inSubShapeIDOfShape2) const
+{
+	// If the platform is colliding with the compound, filter out collisions where the shape has user data 1
+	if (mBodyID1 == mPlatformID && mBodyID2 == mCompoundID)
+		return inShape2->GetUserData() != 1;
+	else if (mBodyID1 == mCompoundID && mBodyID2 == mPlatformID)
+		return inShape1->GetUserData() != 1;
+	return true;
+}

+ 36 - 0
Samples/Tests/General/SimulationShapeFilterTest.h

@@ -0,0 +1,36 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+// This test shows how to use a shape filter during the simulation to disable contacts between certain sub shapes
+class SimulationShapeFilterTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SimulationShapeFilterTest)
+
+	// Destructor
+	virtual				~SimulationShapeFilterTest() override;
+
+	// See: Test
+	virtual void		Initialize() override;
+
+private:
+	// A demo of the shape filter
+	class Filter : public ShapeFilter
+	{
+	public:
+		virtual bool	ShouldCollide(const Shape *inShape1, const SubShapeID &inSubShapeIDOfShape1, const Shape *inShape2, const SubShapeID &inSubShapeIDOfShape2) const override;
+
+		// We're not interested in the other overload as it is only used by collision queries and not by the simulation
+		using ShapeFilter::ShouldCollide;
+
+		BodyID			mPlatformID;
+		BodyID			mCompoundID;
+	};
+
+	Filter				mShapeFilter;
+};

+ 95 - 0
UnitTests/Physics/ShapeFilterTests.cpp

@@ -0,0 +1,95 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include "UnitTestFramework.h"
+#include "PhysicsTestContext.h"
+#include "Layers.h"
+#include "LoggingContactListener.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/Body/BodyCreationSettings.h>
+
+TEST_SUITE("ShapeFilterTests")
+{
+	// Tests two spheres in one simulated body, one collides with a static platform, the other doesn't
+	TEST_CASE("TestSimulationShapeFilter")
+	{
+		// Test once per motion quality type
+		for (int q = 0; q < 2; ++q)
+		{
+			PhysicsTestContext c;
+
+			// Log contacts
+			LoggingContactListener contact_listener;
+			c.GetSystem()->SetContactListener(&contact_listener);
+
+			// Install shape filter
+			class Filter : public ShapeFilter
+			{
+			public:
+				virtual bool	ShouldCollide(const Shape *inShape1, const SubShapeID &inSubShapeIDOfShape1, const Shape *inShape2, const SubShapeID &inSubShapeIDOfShape2) const override
+				{
+					// If the platform is colliding with the compound, filter out collisions where the shape has user data 1
+					if (mBodyID1 == mPlatformID && mBodyID2 == mCompoundID)
+						return inShape2->GetUserData() != 1;
+					else if (mBodyID1 == mCompoundID && mBodyID2 == mPlatformID)
+						return inShape1->GetUserData() != 1;
+					return true;
+				}
+
+				// We're not interested in the other overload as it is only used by collision queries and not by the simulation
+				using ShapeFilter::ShouldCollide;
+
+				BodyID			mPlatformID;
+				BodyID			mCompoundID;
+			};
+			Filter shape_filter;
+			c.GetSystem()->SetSimulationShapeFilter(&shape_filter);
+
+			// Floor
+			BodyID floor_id = c.CreateFloor().GetID();
+
+			// Platform
+			BodyInterface &bi = c.GetBodyInterface();
+			shape_filter.mPlatformID = bi.CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(10, 0.5f, 10)), RVec3(0, 3.5f, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+			// Compound shape that starts above platform
+			Ref<Shape> sphere = new SphereShape(0.5f);
+			sphere->SetUserData(1); // Don't want sphere to collide with the platform
+			Ref<Shape> sphere2 = new SphereShape(0.5f);
+			Ref<StaticCompoundShapeSettings> compound_settings = new StaticCompoundShapeSettings;
+			compound_settings->AddShape(Vec3(0, -2, 0), Quat::sIdentity(), sphere);
+			compound_settings->AddShape(Vec3(0, 2, 0), Quat::sIdentity(), sphere2);
+			Ref<StaticCompoundShape> compound = StaticCast<StaticCompoundShape>(compound_settings->Create().Get());
+			BodyCreationSettings bcs(compound, RVec3(0, 7, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+			if (q == 1)
+			{
+				// For the 2nd iteration activate CCD
+				bcs.mMotionQuality = EMotionQuality::LinearCast;
+				bcs.mLinearVelocity = Vec3(0, -50, 0);
+			}
+			shape_filter.mCompoundID = bi.CreateAndAddBody(bcs, EActivation::Activate);
+
+			// Get sub shape IDs
+			SubShapeID sphere_id = compound->GetSubShapeIDFromIndex(0, SubShapeIDCreator()).GetID();
+			SubShapeID sphere2_id = compound->GetSubShapeIDFromIndex(1, SubShapeIDCreator()).GetID();
+
+			// Simulate for 2 seconds
+			c.Simulate(2.0f);
+
+			// The compound should now be resting with sphere on the platform and the sphere2 on the floor
+			CHECK_APPROX_EQUAL(bi.GetPosition(shape_filter.mCompoundID), RVec3(0, 2.5f, 0), 1.01f * c.GetSystem()->GetPhysicsSettings().mPenetrationSlop);
+			CHECK_APPROX_EQUAL(bi.GetRotation(shape_filter.mCompoundID), Quat::sIdentity());
+
+			// Check that sphere2 collided with the platform but sphere did not
+			CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, shape_filter.mPlatformID, SubShapeID(), shape_filter.mCompoundID, sphere2_id));
+			CHECK(!contact_listener.Contains(LoggingContactListener::EType::Add, shape_filter.mPlatformID, SubShapeID(), shape_filter.mCompoundID, sphere_id));
+
+			// Check that sphere2 didn't collide with the floor but that the sphere did
+			CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, floor_id, SubShapeID(), shape_filter.mCompoundID, sphere_id));
+			CHECK(!contact_listener.Contains(LoggingContactListener::EType::Add, floor_id, SubShapeID(), shape_filter.mCompoundID, sphere2_id));
+		}
+	}
+}

+ 1 - 0
UnitTests/UnitTests.cmake

@@ -64,6 +64,7 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Physics/PhysicsTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/RayShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/SensorTests.cpp
+	${UNIT_TESTS_ROOT}/Physics/ShapeFilterTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/ShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/SixDOFConstraintTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/SliderConstraintTests.cpp