Browse Source

Created an object layer filter implementation that is similar to Bullet's group & mask filtering (#810)

Jorrit Rouwe 1 year ago
parent
commit
e7b634d52c

+ 3 - 0
Jolt/Jolt.cmake

@@ -203,10 +203,12 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseBruteForce.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseBruteForce.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseBruteForce.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseBruteForce.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseLayer.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseLayer.h
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceMask.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceTable.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceTable.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseQuadTree.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseQuadTree.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseQuadTree.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseQuadTree.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseQuery.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/BroadPhaseQuery.h
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterMask.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterTable.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterTable.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/QuadTree.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/QuadTree.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/QuadTree.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/BroadPhase/QuadTree.h
@@ -242,6 +244,7 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/NarrowPhaseStats.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/NarrowPhaseStats.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/NarrowPhaseStats.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/NarrowPhaseStats.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ObjectLayer.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ObjectLayer.h
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/ObjectLayerPairFilterMask.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ObjectLayerPairFilterTable.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ObjectLayerPairFilterTable.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/PhysicsMaterial.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/PhysicsMaterial.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/PhysicsMaterial.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/PhysicsMaterial.h

+ 92 - 0
Jolt/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceMask.h

@@ -0,0 +1,92 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
+#include <Jolt/Physics/Collision/ObjectLayerPairFilterMask.h>
+
+JPH_NAMESPACE_BEGIN
+
+/// BroadPhaseLayerInterface implementation.
+/// This defines a mapping between object and broadphase layers.
+/// This implementation works together with ObjectLayerPairFilterMask and ObjectVsBroadPhaseLayerFilterMask.
+/// A broadphase layer is suitable for an object if its group & inGroupsToInclude is not zero and its group & inGroupsToExclude is zero.
+/// The broadphase layers are iterated from lowest to highest value and the first one that matches is taken. If none match then it takes the last layer.
+class BroadPhaseLayerInterfaceMask : public BroadPhaseLayerInterface
+{
+public:
+	JPH_OVERRIDE_NEW_DELETE
+
+	explicit				BroadPhaseLayerInterfaceMask(uint inNumBroadPhaseLayers)
+	{
+		JPH_ASSERT(inNumBroadPhaseLayers > 0);
+		mMapping.resize(inNumBroadPhaseLayers);
+
+#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
+		mBroadPhaseLayerNames.resize(inNumBroadPhaseLayers, "Undefined");
+#endif // JPH_EXTERNAL_PROFILE || JPH_PROFILE_ENABLED
+	}
+
+	// Configures a broadphase layer.
+	void					ConfigureLayer(BroadPhaseLayer inBroadPhaseLayer, uint32 inGroupsToInclude, uint32 inGroupsToExclude)
+	{
+		JPH_ASSERT((BroadPhaseLayer::Type)inBroadPhaseLayer < (uint)mMapping.size());
+		Mapping &m = mMapping[(BroadPhaseLayer::Type)inBroadPhaseLayer];
+		m.mGroupsToInclude = inGroupsToInclude;
+		m.mGroupsToExclude = inGroupsToExclude;
+	}
+
+	virtual uint			GetNumBroadPhaseLayers() const override
+	{
+		return (uint)mMapping.size();
+	}
+
+	virtual BroadPhaseLayer	GetBroadPhaseLayer(ObjectLayer inLayer) const override
+	{
+		// Try to find the first broadphase layer that matches
+		uint32 group = ObjectLayerPairFilterMask::sGetGroup(inLayer);
+		for (const Mapping &m : mMapping)
+			if ((group & m.mGroupsToInclude) != 0 && (group & m.mGroupsToExclude) == 0)
+				return BroadPhaseLayer(BroadPhaseLayer::Type(&m - mMapping.data()));
+
+		// Fall back to the last broadphase layer
+		return BroadPhaseLayer(BroadPhaseLayer::Type(mMapping.size() - 1));
+	}
+
+	/// Returns true if an object layer should collide with a broadphase layer, this function is being called from ObjectVsBroadPhaseLayerFilterMask
+	inline bool				ShouldCollide(ObjectLayer inLayer1, BroadPhaseLayer inLayer2) const
+	{
+		uint32 mask = ObjectLayerPairFilterMask::sGetMask(inLayer1);
+		const Mapping &m = mMapping[(BroadPhaseLayer::Type)inLayer2];
+		return &m == &mMapping.back() // Last layer may collide with anything
+			|| (m.mGroupsToInclude & mask) != 0; // Mask allows it to collide with objects that could reside in this layer
+	}
+
+#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
+	void					SetBroadPhaseLayerName(BroadPhaseLayer inLayer, const char *inName)
+	{
+		mBroadPhaseLayerNames[(BroadPhaseLayer::Type)inLayer] = inName;
+	}
+
+	virtual const char *	GetBroadPhaseLayerName(BroadPhaseLayer inLayer) const override
+	{
+		return mBroadPhaseLayerNames[(BroadPhaseLayer::Type)inLayer];
+	}
+#endif // JPH_EXTERNAL_PROFILE || JPH_PROFILE_ENABLED
+
+private:
+	struct Mapping
+	{
+		uint32				mGroupsToInclude = 0;
+		uint32				mGroupsToExclude = ~uint32(0);
+	};
+	Array<Mapping>			mMapping;
+
+#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
+	Array<const char *>		mBroadPhaseLayerNames;
+#endif // JPH_EXTERNAL_PROFILE || JPH_PROFILE_ENABLED
+};
+
+JPH_NAMESPACE_END

+ 35 - 0
Jolt/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterMask.h

@@ -0,0 +1,35 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceMask.h>
+
+JPH_NAMESPACE_BEGIN
+
+/// Class that determines if an object layer can collide with a broadphase layer.
+/// This implementation works together with BroadPhaseLayerInterfaceMask and ObjectLayerPairFilterMask
+class ObjectVsBroadPhaseLayerFilterMask : public ObjectVsBroadPhaseLayerFilter
+{
+public:
+	JPH_OVERRIDE_NEW_DELETE
+
+/// Constructor
+					ObjectVsBroadPhaseLayerFilterMask(const BroadPhaseLayerInterfaceMask &inBroadPhaseLayerInterface) :
+		mBroadPhaseLayerInterface(inBroadPhaseLayerInterface)
+	{
+	}
+
+	/// Returns true if an object layer should collide with a broadphase layer
+	virtual bool	ShouldCollide(ObjectLayer inLayer1, BroadPhaseLayer inLayer2) const override
+	{
+		// Just defer to BroadPhaseLayerInterface
+		return mBroadPhaseLayerInterface.ShouldCollide(inLayer1, inLayer2);
+	}
+
+private:
+	const BroadPhaseLayerInterfaceMask &mBroadPhaseLayerInterface;
+};
+
+JPH_NAMESPACE_END

+ 52 - 0
Jolt/Physics/Collision/ObjectLayerPairFilterMask.h

@@ -0,0 +1,52 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Collision/ObjectLayer.h>
+
+JPH_NAMESPACE_BEGIN
+
+/// Filter class to test if two objects can collide based on their object layer. Used while finding collision pairs.
+/// Uses group bits and mask bits. Two layers can collide if Object1.Group & Object2.Mask is non-zero and Object2.Group & Object1.Mask is non-zero.
+/// The behavior is similar to that in e.g. Bullet.
+/// This implementation works together with BroadPhaseLayerInterfaceMask and ObjectVsBroadPhaseLayerFilterMask
+class ObjectLayerPairFilterMask : public ObjectLayerPairFilter
+{
+public:
+	JPH_OVERRIDE_NEW_DELETE
+
+	/// Number of bits for the group and mask bits
+	static constexpr uint32 cNumBits = JPH_OBJECT_LAYER_BITS / 2;
+	static constexpr uint32	cMask = (1 << cNumBits) - 1;
+
+	/// Construct an ObjectLayer from a group and mask bits
+	static ObjectLayer		sGetObjectLayer(uint32 inGroup, uint32 inMask = cMask)
+	{
+		JPH_ASSERT((inGroup & ~cMask) == 0);
+		JPH_ASSERT((inMask & ~cMask) == 0);
+		return ObjectLayer((inGroup & cMask) | (inMask << cNumBits));
+	}
+
+	/// Get the group bits from an ObjectLayer
+	static inline uint32	sGetGroup(ObjectLayer inObjectLayer)
+	{
+		return uint32(inObjectLayer) & cMask;
+	}
+
+	/// Get the mask bits from an ObjectLayer
+	static inline uint32	sGetMask(ObjectLayer inObjectLayer)
+	{
+		return uint32(inObjectLayer) >> cNumBits;
+	}
+
+	/// Returns true if two layers can collide
+	virtual bool			ShouldCollide(ObjectLayer inObject1, ObjectLayer inObject2) const override
+	{
+		return (sGetGroup(inObject1) & sGetMask(inObject2)) != 0
+			&& (sGetGroup(inObject2) & sGetMask(inObject1)) != 0;
+	}
+};
+
+JPH_NAMESPACE_END

+ 3 - 0
UnitTests/LoggingContactListener.h

@@ -5,6 +5,9 @@
 #pragma once
 #pragma once
 
 
 #include <Jolt/Physics/Collision/ContactListener.h>
 #include <Jolt/Physics/Collision/ContactListener.h>
+#include <Jolt/Physics/Body/Body.h>
+#include <Jolt/Core/Mutex.h>
+#include <Jolt/Core/UnorderedSet.h>
 
 
 // Contact listener that just logs the calls made to it for later validation
 // Contact listener that just logs the calls made to it for later validation
 class LoggingContactListener : public ContactListener
 class LoggingContactListener : public ContactListener

+ 161 - 0
UnitTests/Physics/ObjectLayerPairFilterMaskTests.cpp

@@ -0,0 +1,161 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include "UnitTestFramework.h"
+#include "Layers.h"
+#include "LoggingContactListener.h"
+#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceMask.h>
+#include <Jolt/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterMask.h>
+#include <Jolt/Physics/Collision/ObjectLayerPairFilterMask.h>
+#include <Jolt/Physics/PhysicsSystem.h>
+#include <Jolt/Core/JobSystemSingleThreaded.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+
+TEST_SUITE("ObjectLayerPairFilterMaskTests")
+{
+	TEST_CASE("ObjectLayerPairFilterMaskTest")
+	{
+		// Some example layers
+		constexpr uint32 FilterDefault = 1;
+		constexpr uint32 FilterStatic = 2;
+		constexpr uint32 FilterDebris = 4;
+		constexpr uint32 FilterSensor = 8;
+		constexpr uint32 FilterAll = FilterDefault | FilterStatic | FilterDebris | FilterSensor;
+
+		ObjectLayerPairFilterMask pair_filter;
+
+		ObjectLayer layer1 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterDefault, FilterAll);
+		ObjectLayer layer2 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic, FilterAll);
+		CHECK(pair_filter.ShouldCollide(layer1, layer2));
+		CHECK(pair_filter.ShouldCollide(layer2, layer1));
+
+		layer1 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterDefault, FilterStatic);
+		layer2 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic, FilterDefault);
+		CHECK(pair_filter.ShouldCollide(layer1, layer2));
+		CHECK(pair_filter.ShouldCollide(layer2, layer1));
+
+		layer1 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterDefault, FilterDefault);
+		layer2 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic, FilterDefault);
+		CHECK(!pair_filter.ShouldCollide(layer1, layer2));
+		CHECK(!pair_filter.ShouldCollide(layer2, layer1));
+
+		layer1 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterDefault, FilterStatic);
+		layer2 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic, FilterStatic);
+		CHECK(!pair_filter.ShouldCollide(layer1, layer2));
+		CHECK(!pair_filter.ShouldCollide(layer2, layer1));
+
+		layer1 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterDefault | FilterDebris, FilterAll);
+		layer2 = ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic, FilterStatic);
+		CHECK(!pair_filter.ShouldCollide(layer1, layer2));
+		CHECK(!pair_filter.ShouldCollide(layer2, layer1));
+
+		BroadPhaseLayerInterfaceMask bp_interface(4);
+		bp_interface.ConfigureLayer(BroadPhaseLayer(0), FilterDefault, 0); // Default goes to 0
+		bp_interface.ConfigureLayer(BroadPhaseLayer(1), FilterStatic, FilterSensor); // Static but not sensor goes to 1
+		bp_interface.ConfigureLayer(BroadPhaseLayer(2), FilterStatic, 0); // Everything else static goes to 2
+		// Last layer is for everything else
+
+		CHECK(bp_interface.GetBroadPhaseLayer(ObjectLayerPairFilterMask::sGetObjectLayer(FilterDefault)) == BroadPhaseLayer(0));
+		CHECK(bp_interface.GetBroadPhaseLayer(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll)) == BroadPhaseLayer(0));
+		CHECK(bp_interface.GetBroadPhaseLayer(ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic)) == BroadPhaseLayer(1));
+		CHECK(bp_interface.GetBroadPhaseLayer(ObjectLayerPairFilterMask::sGetObjectLayer(FilterStatic | FilterSensor)) == BroadPhaseLayer(2));
+		CHECK(bp_interface.GetBroadPhaseLayer(ObjectLayerPairFilterMask::sGetObjectLayer(FilterDebris)) == BroadPhaseLayer(3));
+
+		ObjectVsBroadPhaseLayerFilterMask bp_filter(bp_interface);
+
+		CHECK(bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterDefault), BroadPhaseLayer(0)));
+		CHECK(!bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterDefault), BroadPhaseLayer(1)));
+		CHECK(!bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterDefault), BroadPhaseLayer(2)));
+		CHECK(bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterDefault), BroadPhaseLayer(3)));
+
+		CHECK(!bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterStatic), BroadPhaseLayer(0)));
+		CHECK(bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterStatic), BroadPhaseLayer(1)));
+		CHECK(bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterStatic), BroadPhaseLayer(2)));
+		CHECK(bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterStatic), BroadPhaseLayer(3)));
+
+		CHECK(!bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterSensor), BroadPhaseLayer(0)));
+		CHECK(!bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterSensor), BroadPhaseLayer(1)));
+		CHECK(!bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterSensor), BroadPhaseLayer(2)));
+		CHECK(bp_filter.ShouldCollide(ObjectLayerPairFilterMask::sGetObjectLayer(FilterAll, FilterSensor), BroadPhaseLayer(3)));
+	}
+
+	TEST_CASE("ThreeFloorTest")
+	{
+		// Define the group bits
+		constexpr uint32 GROUP_STATIC = 1;
+		constexpr uint32 GROUP_FLOOR1 = 2;
+		constexpr uint32 GROUP_FLOOR2 = 4;
+		constexpr uint32 GROUP_FLOOR3 = 8;
+		constexpr uint32 GROUP_ALL = GROUP_STATIC | GROUP_FLOOR1 | GROUP_FLOOR2 | GROUP_FLOOR3;
+
+		ObjectLayerPairFilterMask pair_filter;
+
+		constexpr uint NUM_BROAD_PHASE_LAYERS = 2;
+		BroadPhaseLayer BP_LAYER_STATIC(0);
+		BroadPhaseLayer BP_LAYER_DYNAMIC(1);
+		BroadPhaseLayerInterfaceMask bp_interface(NUM_BROAD_PHASE_LAYERS);
+		bp_interface.ConfigureLayer(BP_LAYER_STATIC, GROUP_STATIC, 0); // Anything that has the static bit set goes into the static broadphase layer
+		bp_interface.ConfigureLayer(BP_LAYER_DYNAMIC, GROUP_FLOOR1 | GROUP_FLOOR2 | GROUP_FLOOR3, 0); // Anything that has one of the floor bits set goes into the dynamic broadphase layer
+
+		ObjectVsBroadPhaseLayerFilterMask bp_filter(bp_interface);
+
+		PhysicsSystem system;
+		system.Init(1024, 0, 1024, 1024, bp_interface, bp_filter, pair_filter);
+		BodyInterface &body_interface = system.GetBodyInterface();
+
+		// Create 3 floors, each colliding with a different group
+		RefConst<Shape> floor_shape = new BoxShape(Vec3(10, 0.1f, 10));
+		BodyID ground = body_interface.CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(20, 0.1f, 20)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_STATIC, GROUP_FLOOR1)), EActivation::DontActivate);
+		BodyID floor1 = body_interface.CreateAndAddBody(BodyCreationSettings(floor_shape, RVec3(0, 2, 0), Quat::sIdentity(), EMotionType::Static, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_STATIC, GROUP_FLOOR1)), EActivation::DontActivate);
+		BodyID floor2 = body_interface.CreateAndAddBody(BodyCreationSettings(floor_shape, RVec3(0, 4, 0), Quat::sIdentity(), EMotionType::Static, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_STATIC, GROUP_FLOOR2)), EActivation::DontActivate);
+		BodyID floor3 = body_interface.CreateAndAddBody(BodyCreationSettings(floor_shape, RVec3(0, 6, 0), Quat::sIdentity(), EMotionType::Static, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_STATIC, GROUP_FLOOR3)), EActivation::DontActivate);
+
+		// Create dynamic bodies, each colliding with a different floor
+		RefConst<Shape> box_shape = new BoxShape(Vec3::sReplicate(0.5f));
+		BodyID dynamic_floor1 = body_interface.CreateAndAddBody(BodyCreationSettings(box_shape, RVec3(0, 8, 0), Quat::sIdentity(), EMotionType::Dynamic, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_FLOOR1, GROUP_ALL)), EActivation::Activate);
+		BodyID dynamic_floor2 = body_interface.CreateAndAddBody(BodyCreationSettings(box_shape, RVec3(0, 9, 0), Quat::sIdentity(), EMotionType::Dynamic, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_FLOOR2, GROUP_ALL)), EActivation::Activate);
+		BodyID dynamic_floor3 = body_interface.CreateAndAddBody(BodyCreationSettings(box_shape, RVec3(0, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_FLOOR3, GROUP_ALL)), EActivation::Activate);
+		BodyID dynamic_ground = body_interface.CreateAndAddBody(BodyCreationSettings(box_shape, RVec3(15, 8, 0), Quat::sIdentity(), EMotionType::Dynamic, ObjectLayerPairFilterMask::sGetObjectLayer(GROUP_FLOOR1, GROUP_ALL)), EActivation::Activate);
+
+		// Start listening to collision events
+		LoggingContactListener listener;
+		system.SetContactListener(&listener);
+
+		// Simulate long enough for all objects to fall on the ground
+		TempAllocatorImpl allocator(4 * 1024 * 1024);
+		JobSystemSingleThreaded job_system(cMaxPhysicsJobs);
+		for (int i = 0; i < 100; ++i)
+			system.Update(1.0f/ 60.0f, 1, &allocator, &job_system);
+
+		// Dynamic 1 should rest on floor 1
+		CHECK(listener.Contains(LoggingContactListener::EType::Add, dynamic_floor1, floor1));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor1, floor2));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor1, floor3));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor1, ground));
+		float tolerance = 1.1f * system.GetPhysicsSettings().mPenetrationSlop;
+		CHECK_APPROX_EQUAL(body_interface.GetPosition(dynamic_floor1), RVec3(0, 2.6_r, 0), tolerance);
+
+		// Dynamic 2 should rest on floor 2
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor2, floor1));
+		CHECK(listener.Contains(LoggingContactListener::EType::Add, dynamic_floor2, floor2));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor2, floor3));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor2, ground));
+		CHECK_APPROX_EQUAL(body_interface.GetPosition(dynamic_floor2), RVec3(0, 4.6_r, 0), tolerance);
+
+		// Dynamic 3 should rest on floor 3
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor3, floor1));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor3, floor2));
+		CHECK(listener.Contains(LoggingContactListener::EType::Add, dynamic_floor3, floor3));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_floor3, ground));
+		CHECK_APPROX_EQUAL(body_interface.GetPosition(dynamic_floor3), RVec3(0, 6.6_r, 0), tolerance);
+
+		// Dynamic 4 should rest on the ground floor
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_ground, floor1));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_ground, floor2));
+		CHECK(!listener.Contains(LoggingContactListener::EType::Add, dynamic_ground, floor3));
+		CHECK(listener.Contains(LoggingContactListener::EType::Add, dynamic_ground, ground));
+		CHECK_APPROX_EQUAL(body_interface.GetPosition(dynamic_ground), RVec3(15, 0.6_r, 0), tolerance);
+	}
+}

+ 1 - 0
UnitTests/UnitTests.cmake

@@ -48,6 +48,7 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Physics/HingeConstraintTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/HingeConstraintTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/MotionQualityLinearCastTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/MotionQualityLinearCastTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/ObjectLayerPairFilterTableTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/ObjectLayerPairFilterTableTests.cpp
+	${UNIT_TESTS_ROOT}/Physics/ObjectLayerPairFilterMaskTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/OffsetCenterOfMassShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/OffsetCenterOfMassShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PathConstraintTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PathConstraintTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PhysicsDeterminismTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PhysicsDeterminismTests.cpp