瀏覽代碼

Added ability to override the collide function during simulation (#1489)

Added PhysicsSystem::SetSimCollideBodyVsBody. This allows overriding the collision detection between two bodies. It can be used to only store the 1st hit for sensor collisions. This makes sensors cheaper if you only need to know if there is an overlap or not. An example can be found in SimCollideBodyVsBodyTest.

See #1476
Jorrit Rouwe 6 月之前
父節點
當前提交
35dba586c2

+ 17 - 16
Docs/ReleaseNotes.md

@@ -15,15 +15,16 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added an example of a body that's both a sensor and a rigid body in `ContactListenerTest`.
 * Added an example of a body that's both a sensor and a rigid body in `ContactListenerTest`.
 * Added binary serialization to `SkeletalAnimation`.
 * Added binary serialization to `SkeletalAnimation`.
 * Added support for RISC-V, LoongArch and PowerPC (Little Endian) CPUs.
 * Added support for RISC-V, LoongArch and PowerPC (Little Endian) CPUs.
-* Added the ability to add a sub shape at a specified index in a MutableCompoundShape rather than at the end.
-* The Samples and JoltViewer can run on Linux using Vulkan. Make sure to install the Vulkan SDK before compiling (see: Build/ubuntu24_install_vulkan_sdk.sh).
+* Added the ability to add a sub shape at a specified index in a `MutableCompoundShape` rather than at the end.
+* The Samples and JoltViewer can run on Linux using Vulkan. Make sure to install the Vulkan SDK before compiling (see: `Build/ubuntu24_install_vulkan_sdk.sh`).
 * The Samples and JoltViewer can run on macOS using Metal.
 * The Samples and JoltViewer can run on macOS using Metal.
-* Added STLLocalAllocator which is an allocator that can be used in e.g. the Array class. It keeps a fixed size buffer for N elements and only when it runs out of space falls back to the heap.
-* Added support for CharacterVirtual to override the inner rigid body ID. This can be used to make the simulation deterministic in e.g. client/server setups.
-* Added OnContactPersisted, OnContactRemoved, OnCharacterContactPersisted and OnCharacterContactRemoved functions on CharacterContactListener to better match the interface of ContactListener.
-* Every CharacterVirtual now has a CharacterID. This ID can be used to identify the character after removal and is used to make the simulation deterministic in case a character collides with multiple other virtual characters.
-* Added overridable CollisionCollector::OnBodyEnd that is called after all hits for a body have been processed when collecting hits through NarrowPhaseQuery.
-* Added ClosestHitPerBodyCollisionCollector which will report the closest / deepest hit per body that the collision query collides with.
+* Added `STLLocalAllocator` which is an allocator that can be used in e.g. the Array class. It keeps a fixed size buffer for N elements and only when it runs out of space falls back to the heap.
+* Added support for `CharacterVirtual` to override the inner rigid body ID. This can be used to make the simulation deterministic in e.g. client/server setups.
+* Added `OnContactPersisted`, `OnContactRemoved`, `OnCharacterContactPersisted` and `OnCharacterContactRemoved` functions on `CharacterContactListener` to better match the interface of `ContactListener`.
+* Every `CharacterVirtual` now has a `CharacterID`. This ID can be used to identify the character after removal and is used to make the simulation deterministic in case a character collides with multiple other virtual characters.
+* Added overridable `CollisionCollector::OnBodyEnd` that is called after all hits for a body have been processed when collecting hits through `NarrowPhaseQuery`.
+* Added `ClosestHitPerBodyCollisionCollector` which will report the closest / deepest hit per body that the collision query collides with.
+* Added `PhysicsSystem::SetSimCollideBodyVsBody`. This allows overriding the collision detection between two bodies. It can be used to only store the 1st hit for sensor collisions. This makes sensors cheaper if you only need to know if there is an overlap or not. An example can be found in `SimCollideBodyVsBodyTest`.
 
 
 ### Bug fixes
 ### Bug fixes
 
 
@@ -32,14 +33,14 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added overloads for placement new in the `JPH_OVERRIDE_NEW_DELETE` macro, this means it is no longer needed to do `:: new (address) JPH::class_name(constructor_arguments)` but you can do `new (address) JPH::class_name(constructor_arguments)`.
 * Added overloads for placement new in the `JPH_OVERRIDE_NEW_DELETE` macro, this means it is no longer needed to do `:: new (address) JPH::class_name(constructor_arguments)` but you can do `new (address) JPH::class_name(constructor_arguments)`.
 * Fixed a GCC warning `-Wshadow=global`.
 * Fixed a GCC warning `-Wshadow=global`.
 * BodyInterface::AddForce applied a force per soft body vertex rather than to the whole body, this resulted in a soft body accelerating much more compared to a rigid body of the same mass.
 * BodyInterface::AddForce applied a force per soft body vertex rather than to the whole body, this resulted in a soft body accelerating much more compared to a rigid body of the same mass.
-* Removing a sub shape from a MutableCompoundShape would not update the bounding box if the last shape was removed, which can result in a small performance loss.
-* VehicleConstraint would override Body::SetAllowSleeping every frame, making it impossible for client code to configure a vehicle that cannot go to sleep.
-* Fixed CharacterVirtual::Contact::mIsSensorB not being persisted in SaveState.
-* Fixed CharacterVirtual::Contact::mHadContact not being true for collisions with sensors. They will still be marked as mWasDiscarded to prevent any further interaction.
-* Fixed numerical inaccuracy in penetration depth calculation when CollideShapeSettings::mMaxSeparationDistance was set to a really high value (e.g. 1000).
-* Bugfix in Semaphore::Acquire for non-windows platform. The count was updated before waiting, meaning that the counter would become -(number of waiting threads) and the semaphore would not wake up until at least the same amount of releases was done. In practice this meant that the main thread had to do the last (number of threads) jobs, slowing down the simulation a bit.
-* An empty MutableCompoundShape now returns the same local bounding box as EmptyShape (a point at (0, 0, 0)). This prevents floating point overflow exceptions.
-* Fixed a bug in ManifoldBetweenTwoFaces that led to incorrect ContactManifold::mRelativeContactPointsOn2 when the contact normal and the face normal were not roughly parallel. Also it led to possibly jitter in the simulation in that case.
+* Removing a sub shape from a `MutableCompoundShape` would not update the bounding box if the last shape was removed, which can result in a small performance loss.
+* VehicleConstraint would override `Body::SetAllowSleeping` every frame, making it impossible for client code to configure a vehicle that cannot go to sleep.
+* Fixed `CharacterVirtual::Contact::mIsSensorB` not being persisted in SaveState.
+* Fixed `CharacterVirtual::Contact::mHadContact` not being true for collisions with sensors. They will still be marked as mWasDiscarded to prevent any further interaction.
+* Fixed numerical inaccuracy in penetration depth calculation when `CollideShapeSettings::mMaxSeparationDistance` was set to a really high value (e.g. 1000).
+* Bugfix in `Semaphore::Acquire` for non-windows platform. The count was updated before waiting, meaning that the counter would become -(number of waiting threads) and the semaphore would not wake up until at least the same amount of releases was done. In practice this meant that the main thread had to do the last (number of threads) jobs, slowing down the simulation a bit.
+* An empty `MutableCompoundShape` now returns the same local bounding box as `EmptyShape` (a point at (0, 0, 0)). This prevents floating point overflow exceptions.
+* Fixed a bug in ManifoldBetweenTwoFaces that led to incorrect `ContactManifold::mRelativeContactPointsOn2` when the contact normal and the face normal were not roughly parallel. Also it led to possibly jitter in the simulation in that case.
 
 
 ## v5.2.0
 ## v5.2.0
 
 

+ 20 - 10
Jolt/Physics/PhysicsSystem.cpp

@@ -960,6 +960,23 @@ void PhysicsSystem::JobFindCollisions(PhysicsUpdateContext::Step *ioStep, int in
 	}
 	}
 }
 }
 
 
+void PhysicsSystem::sDefaultSimCollideBodyVsBody(const Body &inBody1, const Body &inBody2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, CollideShapeSettings &ioCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter)
+{
+	SubShapeIDCreator part1, part2;
+
+	if (inBody1.GetEnhancedInternalEdgeRemovalWithBody(inBody2))
+	{
+		// Collide with enhanced internal edge removal
+		ioCollideShapeSettings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
+		InternalEdgeRemovingCollector::sCollideShapeVsShape(inBody1.GetShape(), inBody2.GetShape(), Vec3::sOne(), Vec3::sOne(), inCenterOfMassTransform1, inCenterOfMassTransform2, part1, part2, ioCollideShapeSettings, ioCollector, inShapeFilter);
+	}
+	else
+	{
+		// Regular collide
+		CollisionDispatch::sCollideShapeVsShape(inBody1.GetShape(), inBody2.GetShape(), Vec3::sOne(), Vec3::sOne(), inCenterOfMassTransform1, inCenterOfMassTransform2, part1, part2, ioCollideShapeSettings, ioCollector, inShapeFilter);
+	}
+}
+
 void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const BodyPair &inBodyPair)
 void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const BodyPair &inBodyPair)
 {
 {
 	JPH_PROFILE_FUNCTION();
 	JPH_PROFILE_FUNCTION();
@@ -1003,13 +1020,10 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 		if (body_pair_handle == nullptr)
 		if (body_pair_handle == nullptr)
 			return; // Out of cache space
 			return; // Out of cache space
 
 
-		// If we want enhanced active edge detection for this body pair
-		bool enhanced_active_edges = body1->GetEnhancedInternalEdgeRemovalWithBody(*body2);
-
 		// Create the query settings
 		// Create the query settings
 		CollideShapeSettings settings;
 		CollideShapeSettings settings;
 		settings.mCollectFacesMode = ECollectFacesMode::CollectFaces;
 		settings.mCollectFacesMode = ECollectFacesMode::CollectFaces;
-		settings.mActiveEdgeMode = mPhysicsSettings.mCheckActiveEdges && !enhanced_active_edges? EActiveEdgeMode::CollideOnlyWithActive : EActiveEdgeMode::CollideWithAll;
+		settings.mActiveEdgeMode = mPhysicsSettings.mCheckActiveEdges? EActiveEdgeMode::CollideOnlyWithActive : EActiveEdgeMode::CollideWithAll;
 		settings.mMaxSeparationDistance = body1->IsSensor() || body2->IsSensor()? 0.0f : mPhysicsSettings.mSpeculativeContactDistance;
 		settings.mMaxSeparationDistance = body1->IsSensor() || body2->IsSensor()? 0.0f : mPhysicsSettings.mSpeculativeContactDistance;
 		settings.mActiveEdgeMovementDirection = body1->GetLinearVelocity() - body2->GetLinearVelocity();
 		settings.mActiveEdgeMovementDirection = body1->GetLinearVelocity() - body2->GetLinearVelocity();
 
 
@@ -1137,9 +1151,7 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 			ReductionCollideShapeCollector collector(this, body1, body2);
 			ReductionCollideShapeCollector collector(this, body1, body2);
 
 
 			// Perform collision detection between the two shapes
 			// 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::sOne(), Vec3::sOne(), transform1, transform2, part1, part2, settings, collector, shape_filter);
+			mSimCollideBodyVsBody(*body1, *body2, transform1, transform2, settings, collector, shape_filter);
 
 
 			// Add the contacts
 			// Add the contacts
 			for (ContactManifold &manifold : collector.mManifolds)
 			for (ContactManifold &manifold : collector.mManifolds)
@@ -1238,9 +1250,7 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 			NonReductionCollideShapeCollector collector(this, ioContactAllocator, body1, body2, body_pair_handle);
 			NonReductionCollideShapeCollector collector(this, ioContactAllocator, body1, body2, body_pair_handle);
 
 
 			// Perform collision detection between the two shapes
 			// 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::sOne(), Vec3::sOne(), transform1, transform2, part1, part2, settings, collector, shape_filter);
+			mSimCollideBodyVsBody(*body1, *body2, transform1, transform2, settings, collector, shape_filter);
 
 
 			constraint_created = collector.mConstraintCreated;
 			constraint_created = collector.mConstraintCreated;
 		}
 		}

+ 21 - 0
Jolt/Physics/PhysicsSystem.h

@@ -76,6 +76,24 @@ public:
 	void						SetSimShapeFilter(const SimShapeFilter *inShapeFilter)		{ mSimShapeFilter = inShapeFilter; }
 	void						SetSimShapeFilter(const SimShapeFilter *inShapeFilter)		{ mSimShapeFilter = inShapeFilter; }
 	const SimShapeFilter *		GetSimShapeFilter() const									{ return mSimShapeFilter; }
 	const SimShapeFilter *		GetSimShapeFilter() const									{ return mSimShapeFilter; }
 
 
+	/// Advanced use only: This function is similar to CollisionDispatch::sCollideShapeVsShape but only used to collide bodies during simulation.
+	/// inBody1 The first body to collide.
+	/// inBody2 The second body to collide.
+	/// inCenterOfMassTransform1 The center of mass transform of the first body (note this will not be the actual world space position of the body, it will be made relative to some position so we can drop down to single precision).
+	/// inCenterOfMassTransform2 The center of mass transform of the second body.
+	/// ioCollideShapeSettings Settings that control the collision detection. Note that the implementation can freely overwrite the shape settings if needed, the caller provides a temporary that will not be used after the function returns.
+	/// ioCollector The collector that will receive the contact points.
+	/// inShapeFilter The shape filter that can be used to exclude shapes from colliding with each other.
+	using SimCollideBodyVsBody = void (*)(const Body &inBody1, const Body &inBody2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, CollideShapeSettings &ioCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter);
+
+	/// Advanced use only: Set the function that will be used to collide two bodies during simulation.
+	/// This function is expected to eventually call CollideShapeCollector::AddHit all contact points between the shapes of body 1 and 2 in their given transforms.
+	void						SetSimCollideBodyVsBody(SimCollideBodyVsBody inBodyVsBody)	{ mSimCollideBodyVsBody = inBodyVsBody; }
+	SimCollideBodyVsBody		GetSimCollideBodyVsBody() const								{ return mSimCollideBodyVsBody; }
+
+	/// Advanced use only: Default function that is used to collide two bodies during simulation.
+	static void					sDefaultSimCollideBodyVsBody(const Body &inBody1, const Body &inBody2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, CollideShapeSettings &ioCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter);
+
 	/// Control the main constants of the physics simulation
 	/// Control the main constants of the physics simulation
 	void						SetPhysicsSettings(const PhysicsSettings &inSettings)		{ mPhysicsSettings = inSettings; }
 	void						SetPhysicsSettings(const PhysicsSettings &inSettings)		{ mPhysicsSettings = inSettings; }
 	const PhysicsSettings &		GetPhysicsSettings() const									{ return mPhysicsSettings; }
 	const PhysicsSettings &		GetPhysicsSettings() const									{ return mPhysicsSettings; }
@@ -308,6 +326,9 @@ private:
 	/// The shape filter that is used to filter out sub shapes during simulation
 	/// The shape filter that is used to filter out sub shapes during simulation
 	const SimShapeFilter *		mSimShapeFilter = nullptr;
 	const SimShapeFilter *		mSimShapeFilter = nullptr;
 
 
+	/// The collision function that is used to collide two shapes during simulation
+	SimCollideBodyVsBody		mSimCollideBodyVsBody = &sDefaultSimCollideBodyVsBody;
+
 	/// Simulation settings
 	/// Simulation settings
 	PhysicsSettings				mPhysicsSettings;
 	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/EnhancedInternalEdgeRemovalTest.h
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.cpp
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.cpp
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.h
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.h
+	${SAMPLES_ROOT}/Tests/General/SimCollideBodyVsBodyTest.cpp
+	${SAMPLES_ROOT}/Tests/General/SimCollideBodyVsBodyTest.h
 	${SAMPLES_ROOT}/Tests/General/SimShapeFilterTest.cpp
 	${SAMPLES_ROOT}/Tests/General/SimShapeFilterTest.cpp
 	${SAMPLES_ROOT}/Tests/General/SimShapeFilterTest.h
 	${SAMPLES_ROOT}/Tests/General/SimShapeFilterTest.h
 	${SAMPLES_ROOT}/Tests/General/CenterOfMassTest.cpp
 	${SAMPLES_ROOT}/Tests/General/CenterOfMassTest.cpp

+ 2 - 0
Samples/SamplesApp.cpp

@@ -105,6 +105,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ContactListenerTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ModifyMassTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ModifyMassTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ActivateDuringUpdateTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ActivateDuringUpdateTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SensorTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SensorTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SimCollideBodyVsBodyTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DynamicMeshTest)
 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, TwoDFunnelTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, AllowedDOFsTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, AllowedDOFsTest)
@@ -152,6 +153,7 @@ static TestNameAndRTTI sGeneralTests[] =
 	{ "Modify Mass",						JPH_RTTI(ModifyMassTest) },
 	{ "Modify Mass",						JPH_RTTI(ModifyMassTest) },
 	{ "Activate During Update",				JPH_RTTI(ActivateDuringUpdateTest) },
 	{ "Activate During Update",				JPH_RTTI(ActivateDuringUpdateTest) },
 	{ "Sensor",								JPH_RTTI(SensorTest) },
 	{ "Sensor",								JPH_RTTI(SensorTest) },
+	{ "Override Body Vs Body Collision",	JPH_RTTI(SimCollideBodyVsBodyTest) },
 	{ "Dynamic Mesh",						JPH_RTTI(DynamicMeshTest) },
 	{ "Dynamic Mesh",						JPH_RTTI(DynamicMeshTest) },
 	{ "Allowed Degrees of Freedom",			JPH_RTTI(AllowedDOFsTest) },
 	{ "Allowed Degrees of Freedom",			JPH_RTTI(AllowedDOFsTest) },
 	{ "Shape Filter (Collision Detection)",	JPH_RTTI(ShapeFilterTest) },
 	{ "Shape Filter (Collision Detection)",	JPH_RTTI(ShapeFilterTest) },

+ 227 - 0
Samples/Tests/General/SimCollideBodyVsBodyTest.cpp

@@ -0,0 +1,227 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/General/SimCollideBodyVsBodyTest.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
+#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
+#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
+#include <Jolt/Physics/Collision/CollisionDispatch.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Jolt/Core/STLLocalAllocator.h>
+#include <Layers.h>
+#include <Renderer/DebugRendererImp.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(SimCollideBodyVsBodyTest)
+{
+	JPH_ADD_BASE_CLASS(SimCollideBodyVsBodyTest, Test)
+}
+
+template <class LeafCollector>
+static void sCollideBodyVsBodyPerBody(const Body &inBody1, const Body &inBody2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, CollideShapeSettings &ioCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter)
+{
+	if (inBody1.IsSensor() || inBody2.IsSensor())
+	{
+		LeafCollector collector;
+		SubShapeIDCreator part1, part2;
+		CollisionDispatch::sCollideShapeVsShape(inBody1.GetShape(), inBody2.GetShape(), Vec3::sOne(), Vec3::sOne(), inCenterOfMassTransform1, inCenterOfMassTransform2, part1, part2, ioCollideShapeSettings, collector);
+		if (collector.HadHit())
+			ioCollector.AddHit(collector.mHit);
+	}
+	else
+	{
+		// If not a sensor: fall back to the default
+		PhysicsSystem::sDefaultSimCollideBodyVsBody(inBody1, inBody2, inCenterOfMassTransform1, inCenterOfMassTransform2, ioCollideShapeSettings, ioCollector, inShapeFilter);
+	}
+}
+
+template <class LeafCollector>
+static void sCollideBodyVsBodyPerLeaf(const Body &inBody1, const Body &inBody2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, CollideShapeSettings &ioCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter)
+{
+	if (inBody1.IsSensor() || inBody2.IsSensor())
+	{
+		// Tracks information we need about a leaf shape
+		struct LeafShape
+		{
+								LeafShape() = default;
+
+								LeafShape(const AABox &inBounds, Mat44Arg inCenterOfMassTransform, Vec3Arg inScale, const Shape *inShape, const SubShapeIDCreator &inSubShapeIDCreator) :
+				mBounds(inBounds),
+				mCenterOfMassTransform(inCenterOfMassTransform),
+				mScale(inScale),
+				mShape(inShape),
+				mSubShapeIDCreator(inSubShapeIDCreator)
+			{
+			}
+
+			AABox				mBounds;
+			Mat44				mCenterOfMassTransform;
+			Vec3				mScale;
+			const Shape *		mShape;
+			SubShapeIDCreator	mSubShapeIDCreator;
+		};
+
+		// A collector that stores the information we need from a leaf shape in an array that is usually on the stack but can fall back to the heap if needed
+		class MyCollector : public TransformedShapeCollector
+		{
+		public:
+			void				AddHit(const TransformedShape &inShape) override
+			{
+				mHits.emplace_back(inShape.GetWorldSpaceBounds(), inShape.GetCenterOfMassTransform().ToMat44(), inShape.GetShapeScale(), inShape.mShape, inShape.mSubShapeIDCreator);
+			}
+
+			Array<LeafShape, STLLocalAllocator<LeafShape, 32>> mHits;
+		};
+
+		// Get bounds of both shapes
+		AABox bounds1 = inBody1.GetShape()->GetWorldSpaceBounds(inCenterOfMassTransform1, Vec3::sOne());
+		AABox bounds2 = inBody2.GetShape()->GetWorldSpaceBounds(inCenterOfMassTransform2, Vec3::sOne());
+
+		// Get leaf shapes that overlap with the bounds of the other shape
+		SubShapeIDCreator part1, part2;
+		MyCollector leaf_shapes1, leaf_shapes2;
+		inBody1.GetShape()->CollectTransformedShapes(bounds2, inCenterOfMassTransform1.GetTranslation(), inCenterOfMassTransform1.GetQuaternion(), Vec3::sOne(), part1, leaf_shapes1, inShapeFilter);
+		inBody2.GetShape()->CollectTransformedShapes(bounds1, inCenterOfMassTransform2.GetTranslation(), inCenterOfMassTransform2.GetQuaternion(), Vec3::sOne(), part2, leaf_shapes2, inShapeFilter);
+
+		// Now test each leaf shape against each other leaf
+		for (const LeafShape &leaf1 : leaf_shapes1.mHits)
+			for (const LeafShape &leaf2 : leaf_shapes2.mHits)
+				if (leaf1.mBounds.Overlaps(leaf2.mBounds))
+				{
+					LeafCollector collector;
+					CollisionDispatch::sCollideShapeVsShape(leaf1.mShape, leaf2.mShape, leaf1.mScale, leaf2.mScale, leaf1.mCenterOfMassTransform, leaf2.mCenterOfMassTransform, leaf1.mSubShapeIDCreator, leaf2.mSubShapeIDCreator, ioCollideShapeSettings, collector);
+					if (collector.HadHit())
+						ioCollector.AddHit(collector.mHit);
+				}
+	}
+	else
+	{
+		// If not a sensor: fall back to the default
+		PhysicsSystem::sDefaultSimCollideBodyVsBody(inBody1, inBody2, inCenterOfMassTransform1, inCenterOfMassTransform2, ioCollideShapeSettings, ioCollector, inShapeFilter);
+	}
+}
+
+void SimCollideBodyVsBodyTest::Initialize()
+{
+	// Create pyramid with flat top
+	MeshShapeSettings pyramid;
+	pyramid.mTriangleVertices = { Float3(1, 0, 1), Float3(1, 0, -1), Float3(-1, 0, -1), Float3(-1, 0, 1), Float3(0.1f, 1, 0.1f), Float3(0.1f, 1, -0.1f), Float3(-0.1f, 1, -0.1f), Float3(-0.1f, 1, 0.1f) };
+	pyramid.mIndexedTriangles = { IndexedTriangle(0, 1, 4), IndexedTriangle(4, 1, 5), IndexedTriangle(1, 2, 5), IndexedTriangle(2, 6, 5), IndexedTriangle(2, 3, 6), IndexedTriangle(3, 7, 6), IndexedTriangle(3, 0, 7), IndexedTriangle(0, 4, 7), IndexedTriangle(4, 5, 6), IndexedTriangle(4, 6, 7) };
+	pyramid.SetEmbedded();
+
+	// Create floor of many pyramids
+	StaticCompoundShapeSettings compound;
+	for (int x = -10; x <= 10; ++x)
+		for (int z = -10; z <= 10; ++z)
+			compound.AddShape(Vec3(x * 2.0f, 0, z * 2.0f), Quat::sIdentity(), &pyramid);
+	compound.SetEmbedded();
+
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(&compound, RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+	// A kinematic sensor that also detects static bodies
+	BodyCreationSettings sensor_settings(new BoxShape(Vec3::sReplicate(10.0f)), RVec3(0, 5, 0), Quat::sIdentity(), EMotionType::Kinematic, Layers::MOVING); // Put in a layer that collides with static
+	sensor_settings.mIsSensor = true;
+	sensor_settings.mCollideKinematicVsNonDynamic = true;
+	sensor_settings.mUseManifoldReduction = false;
+	mSensorID = mBodyInterface->CreateAndAddBody(sensor_settings, EActivation::Activate);
+
+	// Dynamic bodies
+	for (int i = 0; i < 10; ++i)
+		mBodyIDs.push_back(mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.1f, 0.5f, 0.2f)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate));
+}
+
+void SimCollideBodyVsBodyTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	// Update time
+	mTime += inParams.mDeltaTime;
+
+	const char *mode_string = nullptr;
+	int mode = int(mTime / 3.0f) % 5;
+	switch (mode)
+	{
+	default:
+		mode_string = "Sensor: Collect all contact points";
+		mPhysicsSystem->SetSimCollideBodyVsBody(&PhysicsSystem::sDefaultSimCollideBodyVsBody);
+		break;
+
+	case 1:
+		mode_string = "Sensor: Collect any contact point per body";
+		mPhysicsSystem->SetSimCollideBodyVsBody(&sCollideBodyVsBodyPerBody<AnyHitCollisionCollector<CollideShapeCollector>>);
+		break;
+
+	case 2:
+		mode_string = "Sensor: Collect deepest contact point per body";
+		mPhysicsSystem->SetSimCollideBodyVsBody(&sCollideBodyVsBodyPerBody<ClosestHitCollisionCollector<CollideShapeCollector>>);
+		break;
+
+	case 3:
+		mode_string = "Sensor: Collect any contact point per leaf shape";
+		mPhysicsSystem->SetSimCollideBodyVsBody(&sCollideBodyVsBodyPerLeaf<AnyHitCollisionCollector<CollideShapeCollector>>);
+		break;
+
+	case 4:
+		mode_string = "Sensor: Collect deepest contact point per leaf shape";
+		mPhysicsSystem->SetSimCollideBodyVsBody(&sCollideBodyVsBodyPerLeaf<ClosestHitCollisionCollector<CollideShapeCollector>>);
+		break;
+	}
+	DebugRenderer::sInstance->DrawText3D(RVec3(0, 5, 0), mode_string);
+
+	// If the mode changes
+	if (mode != mPrevMode)
+	{
+		// Start all bodies from the top
+		for (int i = 0; i < (int)mBodyIDs.size(); ++i)
+		{
+			BodyID id = mBodyIDs[i];
+			mBodyInterface->SetPositionRotationAndVelocity(id, RVec3(-4.9_r + i * 1.0_r, 5.0_r, 0), Quat::sIdentity(), Vec3::sZero(), Vec3::sZero());
+			mBodyInterface->ActivateBody(id);
+		}
+
+		// Invalidate collisions with sensor to refresh contacts
+		mBodyInterface->InvalidateContactCache(mSensorID);
+
+		mPrevMode = mode;
+	}
+}
+
+void SimCollideBodyVsBodyTest::OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings)
+{
+	if (!inBody1.IsSensor())
+	{
+		mDebugRenderer->DrawWirePolygon(RMat44::sTranslation(inManifold.mBaseOffset), inManifold.mRelativeContactPointsOn1, Color::sGreen, 0.01f);
+		Vec3 average = Vec3::sZero();
+		for (const Vec3 &p : inManifold.mRelativeContactPointsOn1)
+			average += p;
+		average /= (float)inManifold.mRelativeContactPointsOn1.size();
+		mDebugRenderer->DrawArrow(inManifold.mBaseOffset + average, inManifold.mBaseOffset + average - inManifold.mWorldSpaceNormal, Color::sYellow, 0.1f);
+	}
+	if (!inBody2.IsSensor())
+	{
+		mDebugRenderer->DrawWirePolygon(RMat44::sTranslation(inManifold.mBaseOffset), inManifold.mRelativeContactPointsOn2, Color::sGreen, 0.01f);
+		Vec3 average = Vec3::sZero();
+		for (const Vec3 &p : inManifold.mRelativeContactPointsOn2)
+			average += p;
+		average /= (float)inManifold.mRelativeContactPointsOn2.size();
+		mDebugRenderer->DrawArrow(inManifold.mBaseOffset + average, inManifold.mBaseOffset + average + inManifold.mWorldSpaceNormal, Color::sYellow, 0.1f);
+	}
+}
+
+void SimCollideBodyVsBodyTest::OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings)
+{
+	OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
+}
+
+void SimCollideBodyVsBodyTest::SaveState(StateRecorder &inStream) const
+{
+	inStream.Write(mPrevMode);
+	inStream.Write(mTime);
+}
+
+void SimCollideBodyVsBodyTest::RestoreState(StateRecorder &inStream)
+{
+	inStream.Read(mPrevMode);
+	inStream.Read(mTime);
+}

+ 38 - 0
Samples/Tests/General/SimCollideBodyVsBodyTest.h

@@ -0,0 +1,38 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+// Test that overrides the collide body vs body function on the simulation to reduce the number of contact points generated
+// between sensors and other objects in the simulation. This can be useful to improve performance if you don't need to know
+// about all contact points and are only interested in an overlap/no-overlap result.
+class SimCollideBodyVsBodyTest : public Test, public ContactListener
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SimCollideBodyVsBodyTest)
+
+	// See: Test
+	virtual void		Initialize() override;
+	virtual void		PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+
+	// If this test implements a contact listener, it should be returned here
+	virtual ContactListener *GetContactListener() override	{ return this; }
+
+	// See: ContactListener
+	virtual void		OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override;
+	virtual void		OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override;
+
+	// Saving / restoring state for replay
+	virtual void		SaveState(StateRecorder &inStream) const override;
+	virtual void		RestoreState(StateRecorder &inStream) override;
+
+private:
+	int					mPrevMode = -1;						// Previous mode
+	float				mTime = 0.0f;						// Total elapsed time
+
+	BodyID				mSensorID;							// Body ID of the sensor
+	BodyIDVector		mBodyIDs;							// List of dynamic bodies
+};