Explorar o código

Implemented enhanced internal edge removal algorithm (#882)

This should help reduce ghost collisions as described in #874 and #717
Jorrit Rouwe hai 1 ano
pai
achega
94c1ad811b

+ 1 - 0
Jolt/Jolt.cmake

@@ -238,6 +238,7 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/GroupFilter.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/GroupFilterTable.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/GroupFilterTable.h
+	${JOLT_PHYSICS_ROOT}/Physics/Collision/InternalEdgeRemovingCollector.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ManifoldBetweenTwoFaces.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ManifoldBetweenTwoFaces.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/NarrowPhaseQuery.cpp

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

@@ -338,6 +338,7 @@ BodyCreationSettings Body::GetBodyCreationSettings() const
 	result.mUseManifoldReduction = GetUseManifoldReduction();
 	result.mApplyGyroscopicForce = GetApplyGyroscopicForce();
 	result.mMotionQuality = mMotionProperties != nullptr? mMotionProperties->GetMotionQuality() : EMotionQuality::Discrete;
+	result.mEnhancedInternalEdgeRemoval = GetEnhancedInternalEdgeRemoval();
 	result.mAllowSleeping = mMotionProperties != nullptr? GetAllowSleeping() : true;
 	result.mFriction = GetFriction();
 	result.mRestitution = GetRestitution();

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

@@ -105,6 +105,15 @@ public:
 	/// Check if the gyroscopic force is being applied for this body
 	inline bool				GetApplyGyroscopicForce() const									{ return (mFlags.load(memory_order_relaxed) & uint8(EFlags::ApplyGyroscopicForce)) != 0; }
 
+	/// Set to indicate that extra effort should be made to try to remove ghost contacts (collisions with internal edges of a mesh). This is more expensive but makes bodies move smoother over a mesh with convex edges.
+	inline void				SetEnhancedInternalEdgeRemoval(bool inApply)					{ JPH_ASSERT(IsRigidBody()); if (inApply) mFlags.fetch_or(uint8(EFlags::EnhancedInternalEdgeRemoval), memory_order_relaxed); else mFlags.fetch_and(uint8(~uint8(EFlags::EnhancedInternalEdgeRemoval)), memory_order_relaxed); }
+
+	/// Check if enhanced internal edge removal is turned on
+	inline bool				GetEnhancedInternalEdgeRemoval() const							{ return (mFlags.load(memory_order_relaxed) & uint8(EFlags::EnhancedInternalEdgeRemoval)) != 0; }
+
+	/// Checks if the combination of this body and inBody2 should use enhanced internal edge removal
+	inline bool				GetEnhancedInternalEdgeRemovalWithBody(const Body &inBody2) const { return ((mFlags.load(memory_order_relaxed) | inBody2.mFlags.load(memory_order_relaxed)) & uint8(EFlags::EnhancedInternalEdgeRemoval)) != 0; }
+
 	/// Get the bodies motion type.
 	inline EMotionType		GetMotionType() const											{ return mMotionType; }
 
@@ -337,6 +346,7 @@ private:
 		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)
+		EnhancedInternalEdgeRemoval		= 1 << 6,											///< Set this bit to indicate that enhanced internal edge removal should be used for this body (see BodyCreationSettings::mEnhancedInternalEdgeRemoval)
 	};
 
 	// 16 byte aligned

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

@@ -29,6 +29,7 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(BodyCreationSettings)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mUseManifoldReduction)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mApplyGyroscopicForce)
 	JPH_ADD_ENUM_ATTRIBUTE(BodyCreationSettings, mMotionQuality)
+	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mEnhancedInternalEdgeRemoval)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mAllowSleeping)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mFriction)
 	JPH_ADD_ATTRIBUTE(BodyCreationSettings, mRestitution)
@@ -60,6 +61,7 @@ void BodyCreationSettings::SaveBinaryState(StreamOut &inStream) const
 	inStream.Write(mUseManifoldReduction);
 	inStream.Write(mApplyGyroscopicForce);
 	inStream.Write(mMotionQuality);
+	inStream.Write(mEnhancedInternalEdgeRemoval);
 	inStream.Write(mAllowSleeping);
 	inStream.Write(mFriction);
 	inStream.Write(mRestitution);
@@ -91,6 +93,7 @@ void BodyCreationSettings::RestoreBinaryState(StreamIn &inStream)
 	inStream.Read(mUseManifoldReduction);
 	inStream.Read(mApplyGyroscopicForce);
 	inStream.Read(mMotionQuality);
+	inStream.Read(mEnhancedInternalEdgeRemoval);
 	inStream.Read(mAllowSleeping);
 	inStream.Read(mFriction);
 	inStream.Read(mRestitution);

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

@@ -98,6 +98,7 @@ public:
 	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
+	bool					mEnhancedInternalEdgeRemoval = false;							///< Set to indicate that extra effort should be made to try to remove ghost contacts (collisions with internal edges of a mesh). This is more expensive but makes bodies move smoother over a mesh with convex edges.
 	bool					mAllowSleeping = true;											///< If this body can go to sleep or not
 	float					mFriction = 0.2f;												///< Friction of the body (dimensionless number, usually between 0 and 1, 0 = no friction, 1 = friction force equals force that presses the two bodies together). Note that bodies can have negative friction but the combined friction (see PhysicsSystem::SetCombineFriction) should never go below zero.
 	float					mRestitution = 0.0f;											///< Restitution of body (dimensionless number, usually between 0 and 1, 0 = completely inelastic collision response, 1 = completely elastic collision response). Note that bodies can have negative restitution but the combined restitution (see PhysicsSystem::SetCombineRestitution) should never go below zero.

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

@@ -204,6 +204,8 @@ Body *BodyManager::AllocateBody(const BodyCreationSettings &inBodyCreationSettin
 		body->SetUseManifoldReduction(true);
 	if (inBodyCreationSettings.mApplyGyroscopicForce)
 		body->SetApplyGyroscopicForce(true);
+	if (inBodyCreationSettings.mEnhancedInternalEdgeRemoval)
+		body->SetEnhancedInternalEdgeRemoval(true);
 	SetBodyObjectLayerInternal(*body, inBodyCreationSettings.mObjectLayer);
 	body->mObjectLayer = inBodyCreationSettings.mObjectLayer;
 	body->mCollisionGroup = inBodyCreationSettings.mCollisionGroup;

+ 4 - 5
Jolt/Physics/Collision/CollideConvexVsTriangles.cpp

@@ -136,11 +136,10 @@ void CollideConvexVsTriangles::Collide(Vec3Arg inV0, Vec3Arg inV1, Vec3Arg inV2,
 		mShape1->GetSupportingFace(SubShapeID(), -penetration_axis, mScale1, mTransform1, result.mShape1Face);
 
 		// Get face of the triangle
-		triangle.GetSupportingFace(penetration_axis, result.mShape2Face);
-
-		// Convert to world space
-		for (Vec3 &p : result.mShape2Face)
-			p = mTransform1 * p;
+		result.mShape2Face.resize(3);
+		result.mShape2Face[0] = mTransform1 * v0;
+		result.mShape2Face[1] = mTransform1 * v1;
+		result.mShape2Face[2] = mTransform1 * v2;
 	}
 
 	// Notify the collector

+ 11 - 1
Jolt/Physics/Collision/CollideSphereVsTriangles.cpp

@@ -103,7 +103,17 @@ void CollideSphereVsTriangles::Collide(Vec3Arg inV0, Vec3Arg inV1, Vec3Arg inV2,
 	// Create collision result
 	CollideShapeResult result(point1, point2, penetration_axis_world, penetration_depth, mSubShapeID1, inSubShapeID2, TransformedShape::sGetBodyID(mCollector.GetContext()));
 
-	// Note: We don't gather faces here because that's only useful if both shapes have a face. Since the sphere always has only 1 contact point, the manifold is always a point.
+	// Gather faces
+	if (mCollideShapeSettings.mCollectFacesMode == ECollectFacesMode::CollectFaces)
+	{
+		// The sphere doesn't have a supporting face
+
+		// Get face of triangle 2
+		result.mShape2Face.resize(3);
+		result.mShape2Face[0] = mTransform2 * (mSphereCenterIn2 + v0);
+		result.mShape2Face[1] = mTransform2 * (mSphereCenterIn2 + v1);
+		result.mShape2Face[2] = mTransform2 * (mSphereCenterIn2 + v2);
+	}
 
 	// Notify the collector
 	JPH_IF_TRACK_NARROWPHASE_STATS(TrackNarrowPhaseCollector track;)

+ 208 - 0
Jolt/Physics/Collision/InternalEdgeRemovingCollector.h

@@ -0,0 +1,208 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Core/QuickSort.h>
+#include <Jolt/Physics/Collision/CollisionDispatch.h>
+
+//#define JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+
+#ifdef JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+#include <Jolt/Renderer/DebugRenderer.h>
+#endif // JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+
+JPH_NAMESPACE_BEGIN
+
+// Removes internal edges from collision results. Can be used to filter out 'ghost collisions'.
+// Based on: Contact generation for meshes - Pierre Terdiman (https://www.codercorner.com/MeshContacts.pdf)
+class InternalEdgeRemovingCollector : public CollideShapeCollector
+{
+	static constexpr uint cMaxDelayedResults = 16;
+	static constexpr uint cMaxVoidedFeatures = 128;
+
+	/// Check if a vertex is voided
+	inline bool				IsVoided(Vec3 inV) const
+	{
+		for (const Float3 &vf : mVoidedFeatures)
+			if (inV.IsClose(Vec3::sLoadFloat3Unsafe(vf), 1.0e-8f))
+				return true;
+		return false;
+	}
+
+	/// Add all vertices of a face to the voided features
+	inline void				VoidFeatures(const CollideShapeResult &inResult)
+	{
+		for (const Vec3 &v : inResult.mShape2Face)
+			if (!IsVoided(v))
+			{
+				if (mVoidedFeatures.size() == cMaxVoidedFeatures)
+					break;
+				Float3 f;
+				v.StoreFloat3(&f);
+				mVoidedFeatures.push_back(f);
+			}
+	}
+
+	/// Call the chained collector
+	inline void				Chain(const CollideShapeResult &inResult)
+	{
+		mChainedCollector.AddHit(inResult);
+	}
+
+	/// Call the chained collector and void all features of inResult
+	inline void				ChainAndVoid(const CollideShapeResult &inResult)
+	{
+		Chain(inResult);
+		VoidFeatures(inResult);
+
+	#ifdef JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+		DebugRenderer::sInstance->DrawWirePolygon(RMat44::sIdentity(), inResult.mShape2Face, Color::sGreen);
+		DebugRenderer::sInstance->DrawArrow(RVec3(inResult.mContactPointOn2), RVec3(inResult.mContactPointOn2) + inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero()), Color::sGreen, 0.1f);
+	#endif // JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+	}
+
+public:
+	/// Constructor, configures a collector to be called with all the results that do not hit internal edges
+	explicit				InternalEdgeRemovingCollector(CollideShapeCollector &inChainedCollector) :
+		mChainedCollector(inChainedCollector)
+	{
+	}
+
+	// See: CollideShapeCollector::AddHit
+	virtual void			AddHit(const CollideShapeResult &inResult) override
+	{
+		// We only support welding when the shape is a triangle or has more vertices so that we can calculate a normal
+		if (inResult.mShape2Face.size() < 3)
+			return ChainAndVoid(inResult);
+
+		// Get the triangle normal of shape 2 face
+		Vec3 triangle_normal = (inResult.mShape2Face[1] - inResult.mShape2Face[0]).Cross(inResult.mShape2Face[2] - inResult.mShape2Face[0]);
+		float triangle_normal_len = triangle_normal.Length();
+		if (triangle_normal_len < 1e-6f)
+			return ChainAndVoid(inResult);
+
+		// If the triangle normal matches the contact normal within 1 degree, we can process the contact immediately
+		// We make the assumption here that if the contact normal and the triangle normal align that the we're dealing with a 'face contact'
+		Vec3 contact_normal = -inResult.mPenetrationAxis;
+		float contact_normal_len = inResult.mPenetrationAxis.Length();
+		if (triangle_normal.Dot(contact_normal) > 0.999848f * contact_normal_len * triangle_normal_len) // cos(1 degree)
+			return ChainAndVoid(inResult);
+
+		// Delayed processing
+		if (mDelayedResults.size() == cMaxDelayedResults)
+			return ChainAndVoid(inResult);
+		mDelayedResults.push_back(inResult);
+	}
+
+	/// After all hits have been added, call this function to process the delayed results
+	void					Flush()
+	{
+		// Sort on biggest penetration depth first
+		uint sorted_indices[cMaxDelayedResults];
+		for (uint i = 0; i < uint(mDelayedResults.size()); ++i)
+			sorted_indices[i] = i;
+		QuickSort(sorted_indices, sorted_indices + mDelayedResults.size(), [this](uint inLHS, uint inRHS) { return mDelayedResults[inLHS].mPenetrationDepth > mDelayedResults[inRHS].mPenetrationDepth; });
+
+		// Loop over all results
+		for (uint i = 0; i < uint(mDelayedResults.size()); ++i)
+		{
+			const CollideShapeResult &r = mDelayedResults[sorted_indices[i]];
+
+			// Determine which vertex or which edge is the closest to the contact point
+			float best_dist_sq = FLT_MAX;
+			uint best_v1_idx = 0;
+			uint best_v2_idx = 0;
+			uint num_v = uint(r.mShape2Face.size());
+			uint v1_idx = num_v - 1;
+			Vec3 v1 = r.mShape2Face[v1_idx] - r.mContactPointOn2;
+			for (uint v2_idx = 0; v2_idx < num_v; ++v2_idx)
+			{
+				Vec3 v2 = r.mShape2Face[v2_idx] - r.mContactPointOn2;
+				Vec3 v1_v2 = v2 - v1;
+				float denominator = v1_v2.LengthSq();
+				if (denominator < Square(FLT_EPSILON))
+				{
+					// Degenerate, assume v1 is closest, v2 will be tested in a later iteration
+					float v1_len_sq = v1.LengthSq();
+					if (v1_len_sq < best_dist_sq)
+					{
+						best_dist_sq = v1_len_sq;
+						best_v1_idx = v1_idx;
+						best_v2_idx = v1_idx;
+					}
+				}
+				else
+				{
+					// Taken from ClosestPoint::GetBaryCentricCoordinates
+					float fraction = -v1.Dot(v1_v2) / denominator;
+					if (fraction < 1.0e-6f)
+					{
+						// Closest lies on v1
+						float v1_len_sq = v1.LengthSq();
+						if (v1_len_sq < best_dist_sq)
+						{
+							best_dist_sq = v1_len_sq;
+							best_v1_idx = v1_idx;
+							best_v2_idx = v1_idx;
+						}
+					}
+					else if (fraction < 1.0f - 1.0e-6f)
+					{
+						// Closest lies on the line segment v1, v2
+						Vec3 closest = v1 + fraction * v1_v2;
+						float closest_len_sq = closest.LengthSq();
+						if (closest_len_sq < best_dist_sq)
+						{
+							best_dist_sq = closest_len_sq;
+							best_v1_idx = v1_idx;
+							best_v2_idx = v2_idx;
+						}
+					}
+					// else closest is v2, but v2 will be tested in a later iteration
+				}
+
+				v1_idx = v2_idx;
+				v1 = v2;
+			}
+
+			// Check if this vertex/edge is voided
+			bool voided = IsVoided(r.mShape2Face[best_v1_idx])
+				&& (best_v1_idx == best_v2_idx || IsVoided(r.mShape2Face[best_v2_idx]));
+
+		#ifdef JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+			Color color = voided? Color::sRed : Color::sYellow;
+			DebugRenderer::sInstance->DrawText3D(RVec3(r.mContactPointOn2), StringFormat("%d: %g", i, r.mPenetrationDepth), color, 0.1f);
+			DebugRenderer::sInstance->DrawWirePolygon(RMat44::sIdentity(), r.mShape2Face, color);
+			DebugRenderer::sInstance->DrawArrow(RVec3(r.mContactPointOn2), RVec3(r.mContactPointOn2) + r.mPenetrationAxis.NormalizedOr(Vec3::sZero()), color, 0.1f);
+			DebugRenderer::sInstance->DrawMarker(RVec3(r.mShape2Face[best_v1_idx]), IsVoided(r.mShape2Face[best_v1_idx])? Color::sRed : Color::sYellow, 0.1f);
+			DebugRenderer::sInstance->DrawMarker(RVec3(r.mShape2Face[best_v2_idx]), IsVoided(r.mShape2Face[best_v2_idx])? Color::sRed : Color::sYellow, 0.1f);
+		#endif // JPH_INTERNAL_EDGE_REMOVING_COLLECTOR_DEBUG
+
+			// No voided features, accept the contact
+			if (!voided)
+				Chain(r);
+
+			// Void the features of this face
+			VoidFeatures(r);
+		}
+	}
+
+	/// Version of CollisionDispatch::sCollideShapeVsShape that removes internal edges
+	static void				sCollideShapeVsShape(const Shape *inShape1, const Shape *inShape2, Vec3Arg inScale1, Vec3Arg inScale2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, const CollideShapeSettings &inCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter = { })
+	{
+		JPH_ASSERT(inCollideShapeSettings.mCollectFacesMode == ECollectFacesMode::CollectFaces); // Won't work without collecting faces
+
+		InternalEdgeRemovingCollector wrapper(ioCollector);
+		CollisionDispatch::sCollideShapeVsShape(inShape1, inShape2, inScale1, inScale2, inCenterOfMassTransform1, inCenterOfMassTransform2, inSubShapeIDCreator1, inSubShapeIDCreator2, inCollideShapeSettings, wrapper, inShapeFilter);
+		wrapper.Flush();
+	}
+
+private:
+	CollideShapeCollector &	mChainedCollector;
+	StaticArray<Float3, cMaxVoidedFeatures> mVoidedFeatures; // Read with Vec3::sLoadFloat3Unsafe so must not be the last member
+	StaticArray<CollideShapeResult, cMaxDelayedResults> mDelayedResults;
+};
+
+JPH_NAMESPACE_END

+ 5 - 2
Jolt/Physics/PhysicsSystem.cpp

@@ -19,6 +19,7 @@
 #include <Jolt/Physics/Collision/CollideConvexVsTriangles.h>
 #include <Jolt/Physics/Collision/ManifoldBetweenTwoFaces.h>
 #include <Jolt/Physics/Collision/Shape/ConvexShape.h>
+#include <Jolt/Physics/Collision/InternalEdgeRemovingCollector.h>
 #include <Jolt/Physics/Constraints/CalculateSolverSteps.h>
 #include <Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h>
 #include <Jolt/Physics/DeterminismLog.h>
@@ -1124,7 +1125,8 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 
 			// Perform collision detection between the two shapes
 			SubShapeIDCreator part1, part2;
-			CollisionDispatch::sCollideShapeVsShape(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector);
+			auto f = body1->GetEnhancedInternalEdgeRemovalWithBody(*body2)? InternalEdgeRemovingCollector::sCollideShapeVsShape : CollisionDispatch::sCollideShapeVsShape;
+			f(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector, { });
 
 			// Add the contacts
 			for (ContactManifold &manifold : collector.mManifolds)
@@ -1224,7 +1226,8 @@ void PhysicsSystem::ProcessBodyPair(ContactAllocator &ioContactAllocator, const
 
 			// Perform collision detection between the two shapes
 			SubShapeIDCreator part1, part2;
-			CollisionDispatch::sCollideShapeVsShape(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector);
+			auto f = body1->GetEnhancedInternalEdgeRemovalWithBody(*body2)? InternalEdgeRemovingCollector::sCollideShapeVsShape : CollisionDispatch::sCollideShapeVsShape;
+			f(body1->GetShape(), body2->GetShape(), Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), transform1, transform2, part1, part2, settings, collector, { });
 
 			constraint_created = collector.mConstraintCreated;
 		}

+ 2 - 0
Samples/Samples.cmake

@@ -81,6 +81,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/General/AllowedDOFsTest.h
 	${SAMPLES_ROOT}/Tests/General/BigVsSmallTest.cpp
 	${SAMPLES_ROOT}/Tests/General/BigVsSmallTest.h
+	${SAMPLES_ROOT}/Tests/General/EnhancedInternalEdgeRemovalTest.cpp
+	${SAMPLES_ROOT}/Tests/General/EnhancedInternalEdgeRemovalTest.h
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.cpp
 	${SAMPLES_ROOT}/Tests/General/ShapeFilterTest.h
 	${SAMPLES_ROOT}/Tests/General/CenterOfMassTest.cpp

+ 2 - 0
Samples/SamplesApp.cpp

@@ -94,6 +94,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, LoadSaveSceneTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, LoadSaveBinaryTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, BigVsSmallTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ActiveEdgesTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, EnhancedInternalEdgeRemovalTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MultithreadedTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ContactListenerTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ModifyMassTest)
@@ -134,6 +135,7 @@ static TestNameAndRTTI sGeneralTests[] =
 	{ "Load/Save Binary",					JPH_RTTI(LoadSaveBinaryTest) },
 	{ "Big vs Small",						JPH_RTTI(BigVsSmallTest) },
 	{ "Active Edges",						JPH_RTTI(ActiveEdgesTest) },
+	{ "Enhanced Internal Edge Removal",		JPH_RTTI(EnhancedInternalEdgeRemovalTest) },
 	{ "Multithreaded",						JPH_RTTI(MultithreadedTest) },
 	{ "Contact Listener",					JPH_RTTI(ContactListenerTest) },
 	{ "Modify Mass",						JPH_RTTI(ModifyMassTest) },

+ 1 - 0
Samples/Tests/General/ActiveEdgesTest.cpp

@@ -92,6 +92,7 @@ void ActiveEdgesTest::Initialize()
 
 	// Mesh
 	BodyCreationSettings mesh_settings(&mesh_shape, RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+	// Instead of settting mActiveEdgeCosThresholdAngle you can also set: mesh_settings.mEnhancedInternalEdgeRemoval = true
 	mesh_settings.mFriction = 0.0f;
 	Body &mesh = *mBodyInterface->CreateBody(mesh_settings);
 	mBodyInterface->AddBody(mesh.GetID(), EActivation::DontActivate);

+ 114 - 0
Samples/Tests/General/EnhancedInternalEdgeRemovalTest.cpp

@@ -0,0 +1,114 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/General/EnhancedInternalEdgeRemovalTest.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
+#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
+#include <Jolt/Geometry/Triangle.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(EnhancedInternalEdgeRemovalTest)
+{
+	JPH_ADD_BASE_CLASS(EnhancedInternalEdgeRemovalTest, Test)
+}
+
+void EnhancedInternalEdgeRemovalTest::CreateSlidingObjects(RVec3Arg inStart)
+{
+	// Slide the shapes over the grid of boxes
+	RVec3 pos = inStart - RVec3(0, 0, 8.5_r);
+	for (int enhanced_removal = 0; enhanced_removal < 2; ++enhanced_removal)
+	{
+		// A box
+		BodyCreationSettings box_bcs(new BoxShape(Vec3::sReplicate(2.0f)), pos, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+		box_bcs.mLinearVelocity = Vec3(20, 0, 0);
+		box_bcs.mEnhancedInternalEdgeRemoval = enhanced_removal == 1;
+		mBodyInterface->CreateAndAddBody(box_bcs, EActivation::Activate);
+		pos += RVec3(0, 0, 5.0_r);
+
+		// A sphere
+		BodyCreationSettings sphere_bcs(new SphereShape(2.0f), pos, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+		sphere_bcs.mLinearVelocity = Vec3(20, 0, 0);
+		sphere_bcs.mEnhancedInternalEdgeRemoval = enhanced_removal == 1;
+		mBodyInterface->CreateAndAddBody(sphere_bcs, EActivation::Activate);
+		pos += RVec3(0, 0, 7.0_r);
+	}
+}
+
+void EnhancedInternalEdgeRemovalTest::Initialize()
+{
+	// This test creates a grid of connected boxes and tests that objects don't hit the internal edges
+	{
+		StaticCompoundShapeSettings compound_settings;
+		compound_settings.SetEmbedded();
+		constexpr float size = 2.0f;
+		RefConst<Shape> box_shape = new BoxShape(Vec3::sReplicate(0.5f * size));
+		for (int x = -10; x < 10; ++x)
+			for (int z = -10; z < 10; ++z)
+				compound_settings.AddShape(Vec3(size * x, 0, size * z), Quat::sIdentity(), box_shape);
+		mBodyInterface->CreateAndAddBody(BodyCreationSettings(&compound_settings, RVec3(0, -1, -40), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+		CreateSlidingObjects(RVec3(-18, 1.9_r, -40.0_r));
+	}
+
+	// This tests if objects do not collide with internal edges
+	{
+		// Create a dense grid of triangles so that we have a large chance of hitting an internal edge
+		constexpr float size = 2.0f;
+		TriangleList triangles;
+		for (int x = -10; x < 10; ++x)
+			for (int z = -10; z < 10; ++z)
+			{
+				float x1 = size * x;
+				float z1 = size * z;
+				float x2 = x1 + size;
+				float z2 = z1 + size;
+
+				Float3 v1 = Float3(x1, 0, z1);
+				Float3 v2 = Float3(x2, 0, z1);
+				Float3 v3 = Float3(x1, 0, z2);
+				Float3 v4 = Float3(x2, 0, z2);
+
+				triangles.push_back(Triangle(v1, v3, v4));
+				triangles.push_back(Triangle(v1, v4, v2));
+			}
+
+		MeshShapeSettings mesh_settings(triangles);
+		mesh_settings.mActiveEdgeCosThresholdAngle = FLT_MAX; // Turn off regular active edge determination so that we only rely on the mEnhancedInternalEdgeRemoval flag
+		mesh_settings.SetEmbedded();
+		mBodyInterface->CreateAndAddBody(BodyCreationSettings(&mesh_settings, RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+		CreateSlidingObjects(RVec3(-18, 1.9_r, 0));
+	}
+
+	// This test tests that we only ignore edges that are shared with voided triangles
+	{
+		// Create an L shape mesh lying on its back
+		TriangleList triangles;
+		constexpr float height = 0.5f;
+		constexpr float half_width = 5.0f;
+		constexpr float half_length = 2.0f;
+		triangles.push_back(Triangle(Float3(-half_length, 0, half_width), Float3(half_length, 0, -half_width), Float3(-half_length, 0, -half_width)));
+		triangles.push_back(Triangle(Float3(-half_length, 0, half_width), Float3(half_length, 0, half_width), Float3(half_length, 0, -half_width)));
+		triangles.push_back(Triangle(Float3(half_length, height, half_width), Float3(half_length, height, -half_width), Float3(half_length, 0, half_width)));
+		triangles.push_back(Triangle(Float3(half_length, 0, half_width), Float3(half_length, height, -half_width), Float3(half_length, 0, -half_width)));
+		mBodyInterface->CreateAndAddBody(BodyCreationSettings(new MeshShapeSettings(triangles), RVec3(0, 0, 30), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+		// Roll a sphere towards the edge pointing upwards
+		float z = 28.0f;
+		for (int enhanced_removal = 0; enhanced_removal < 2; ++enhanced_removal)
+		{
+			// A sphere
+			BodyCreationSettings sphere_bcs(new SphereShape(1.0f), RVec3(0, 1, z), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+			sphere_bcs.mLinearVelocity = Vec3(20, 0, 0);
+			sphere_bcs.mEnhancedInternalEdgeRemoval = enhanced_removal == 1;
+			mBodyInterface->CreateAndAddBody(sphere_bcs, EActivation::Activate);
+			z += 4.0f;
+		}
+	}
+}

+ 20 - 0
Samples/Tests/General/EnhancedInternalEdgeRemovalTest.h

@@ -0,0 +1,20 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+/// Demonstrates the enhanced internal edge removal mode by sliding two shapes over a mesh that has only active edges
+class EnhancedInternalEdgeRemovalTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, EnhancedInternalEdgeRemovalTest)
+
+	// See: Test
+	virtual void	Initialize() override;
+
+private:
+	void			CreateSlidingObjects(RVec3Arg inStart);
+};