Browse Source

Added ability to have a dynamic mesh shape (#169)

Note: This will work as long as the scene only consists of convex objects (mesh vs mesh will come later)
Jorrit Rouwe 3 years ago
parent
commit
3cf3fe55a5

+ 35 - 19
Jolt/Physics/Collision/CollideShape.h

@@ -18,10 +18,10 @@ class CollideShapeResult
 {
 public:
 	/// Default constructor
-							CollideShapeResult() = default;
+								CollideShapeResult() = default;
 
 	/// Constructor
-							CollideShapeResult(Vec3Arg inContactPointOn1, Vec3Arg inContactPointOn2, Vec3Arg inPenetrationAxis, float inPenetrationDepth, const SubShapeID &inSubShapeID1, const SubShapeID &inSubShapeID2, const BodyID &inBodyID2) : 
+								CollideShapeResult(Vec3Arg inContactPointOn1, Vec3Arg inContactPointOn2, Vec3Arg inPenetrationAxis, float inPenetrationDepth, const SubShapeID &inSubShapeID1, const SubShapeID &inSubShapeID2, const BodyID &inBodyID2) : 
 		mContactPointOn1(inContactPointOn1), 
 		mContactPointOn2(inContactPointOn2), 
 		mPenetrationAxis(inPenetrationAxis), 
@@ -33,19 +33,35 @@ public:
 	}
 
 	/// Function required by the CollisionCollector. A smaller fraction is considered to be a 'better hit'. We use -penetration depth to get the hit with the biggest penetration depth
-	inline float			GetEarlyOutFraction() const	{ return -mPenetrationDepth; }
+	inline float				GetEarlyOutFraction() const	{ return -mPenetrationDepth; }
+
+	/// Reverses the hit result, swapping contact point 1 with contact point 2 etc.
+	inline CollideShapeResult	Reversed() const
+	{
+		CollideShapeResult result;
+		result.mContactPointOn2 = mContactPointOn1;
+		result.mContactPointOn1 = mContactPointOn2;
+		result.mPenetrationAxis = -mPenetrationAxis;
+		result.mPenetrationDepth = mPenetrationDepth;
+		result.mSubShapeID2 = mSubShapeID1;
+		result.mSubShapeID1 = mSubShapeID2;
+		result.mBodyID2 = mBodyID2;
+		result.mShape2Face = mShape1Face;
+		result.mShape1Face = mShape2Face;
+		return result;
+	}
 
 	using Face = StaticArray<Vec3, 32>;
 
-	Vec3					mContactPointOn1;			///< Contact point on the surface of shape 1 (in world space)
-	Vec3					mContactPointOn2;			///< Contact point on the surface of shape 2 (in world space). If the penetration depth is 0, this will be the same as mContactPointOn1.
-	Vec3					mPenetrationAxis;			///< Direction to move shape 2 out of collision along the shortest path (magnitude is meaningless, in world space). You can use -mPenetrationAxis.Normalized() as contact normal.
-	float					mPenetrationDepth;			///< Penetration depth (move shape 2 by this distance to resolve the collision)
-	SubShapeID				mSubShapeID1;				///< Sub shape ID that identifies the face on shape 1
-	SubShapeID				mSubShapeID2;				///< Sub shape ID that identifies the face on shape 2
-	BodyID					mBodyID2;					///< BodyID to which shape 2 belongs to
-	Face					mShape1Face;				///< Colliding face on shape 1 (optional result, in world space)
-	Face					mShape2Face;				///< Colliding face on shape 2 (optional result, in world space)
+	Vec3						mContactPointOn1;			///< Contact point on the surface of shape 1 (in world space)
+	Vec3						mContactPointOn2;			///< Contact point on the surface of shape 2 (in world space). If the penetration depth is 0, this will be the same as mContactPointOn1.
+	Vec3						mPenetrationAxis;			///< Direction to move shape 2 out of collision along the shortest path (magnitude is meaningless, in world space). You can use -mPenetrationAxis.Normalized() as contact normal.
+	float						mPenetrationDepth;			///< Penetration depth (move shape 2 by this distance to resolve the collision)
+	SubShapeID					mSubShapeID1;				///< Sub shape ID that identifies the face on shape 1
+	SubShapeID					mSubShapeID2;				///< Sub shape ID that identifies the face on shape 2
+	BodyID						mBodyID2;					///< BodyID to which shape 2 belongs to
+	Face						mShape1Face;				///< Colliding face on shape 1 (optional result, in world space)
+	Face						mShape2Face;				///< Colliding face on shape 2 (optional result, in world space)
 };
 
 /// Settings to be passed with a collision query
@@ -53,19 +69,19 @@ class CollideSettingsBase
 {
 public:
 	/// How active edges (edges that a moving object should bump into) are handled
-	EActiveEdgeMode			mActiveEdgeMode				= EActiveEdgeMode::CollideOnlyWithActive;
+	EActiveEdgeMode				mActiveEdgeMode				= EActiveEdgeMode::CollideOnlyWithActive;
 
 	/// If colliding faces should be collected or only the collision point
-	ECollectFacesMode		mCollectFacesMode			= ECollectFacesMode::NoFaces;
+	ECollectFacesMode			mCollectFacesMode			= ECollectFacesMode::NoFaces;
 
 	/// If objects are closer than this distance, they are considered to be colliding (used for GJK) (unit: meter)
-	float					mCollisionTolerance			= cDefaultCollisionTolerance;
+	float						mCollisionTolerance			= cDefaultCollisionTolerance;
 
 	/// A factor that determines the accuracy of the penetration depth calculation. If the change of the squared distance is less than tolerance * current_penetration_depth^2 the algorithm will terminate. (unit: dimensionless)
-	float					mPenetrationTolerance		= cDefaultPenetrationTolerance;
+	float						mPenetrationTolerance		= cDefaultPenetrationTolerance;
 
 	/// When mActiveEdgeMode is CollideOnlyWithActive a movement direction can be provided. When hitting an inactive edge, the system will select the triangle normal as penetration depth only if it impedes the movement less than with the calculated penetration depth.
-	Vec3					mActiveEdgeMovementDirection = Vec3::sZero();
+	Vec3						mActiveEdgeMovementDirection = Vec3::sZero();
 };
 
 /// Settings to be passed with a collision query
@@ -73,10 +89,10 @@ class CollideShapeSettings : public CollideSettingsBase
 {
 public:
 	/// When > 0 contacts in the vicinity of the query shape can be found. All nearest contacts that are not further away than this distance will be found (uint: meter)
-	float					mMaxSeparationDistance		= 0.0f;
+	float						mMaxSeparationDistance		= 0.0f;
 
 	/// How backfacing triangles should be treated
-	EBackFaceMode			mBackFaceMode				= EBackFaceMode::IgnoreBackFaces;
+	EBackFaceMode				mBackFaceMode				= EBackFaceMode::IgnoreBackFaces;
 };
 
 JPH_NAMESPACE_END

+ 91 - 0
Jolt/Physics/Collision/CollisionDispatch.cpp

@@ -4,10 +4,101 @@
 #include <Jolt/Jolt.h>
 
 #include <Jolt/Physics/Collision/CollisionDispatch.h>
+#include <Jolt/Physics/Collision/CastResult.h>
 
 JPH_NAMESPACE_BEGIN
 
 CollisionDispatch::CollideShape CollisionDispatch::sCollideShape[NumSubShapeTypes][NumSubShapeTypes];
 CollisionDispatch::CastShape CollisionDispatch::sCastShape[NumSubShapeTypes][NumSubShapeTypes];
 
+void CollisionDispatch::sInit()
+{
+	for (uint i = 0; i < NumSubShapeTypes; ++i)
+		for (uint j = 0; j < NumSubShapeTypes; ++j)
+		{
+			if (sCollideShape[i][j] == nullptr)
+				sCollideShape[i][j] = [](const Shape *, const Shape *, Vec3Arg, Vec3Arg, Mat44Arg, Mat44Arg, const SubShapeIDCreator &, const SubShapeIDCreator &, const CollideShapeSettings &, CollideShapeCollector &, const ShapeFilter &)
+				{
+					JPH_ASSERT(false, "Unsupported shape pair");
+				};
+
+			if (sCastShape[i][j] == nullptr)
+				sCastShape[i][j] = [](const ShapeCast &, const ShapeCastSettings &, const Shape *, Vec3Arg, const ShapeFilter &, Mat44Arg, const SubShapeIDCreator &, const SubShapeIDCreator &, CastShapeCollector &)
+				{
+					JPH_ASSERT(false, "Unsupported shape pair");
+				};
+		}
+}
+
+void CollisionDispatch::sReversedCollideShape(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)
+{
+	// A collision collector that flips the collision results
+	class ReversedCollector : public CollideShapeCollector
+	{
+	public:
+		explicit				ReversedCollector(CollideShapeCollector &ioCollector) :
+			mCollector(ioCollector)
+		{
+			SetContext(ioCollector.GetContext());
+		}
+
+		virtual void			AddHit(const CollideShapeResult &inResult) override
+		{
+			// Add the reversed hit
+			mCollector.AddHit(inResult.Reversed());
+
+			// If our chained collector updated its early out fraction, we need to follow
+			UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
+		}
+
+	private:
+		CollideShapeCollector &	mCollector;
+	};
+
+	ReversedCollector collector(ioCollector);
+	sCollideShapeVsShape(inShape2, inShape1, inScale2, inScale1, inCenterOfMassTransform2, inCenterOfMassTransform1, inSubShapeIDCreator2, inSubShapeIDCreator1, inCollideShapeSettings, collector, inShapeFilter);
+}
+
+void CollisionDispatch::sReversedCastShape(const ShapeCast &inShapeCast, const ShapeCastSettings &inShapeCastSettings, const Shape *inShape, Vec3Arg inScale, const ShapeFilter &inShapeFilter, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, CastShapeCollector &ioCollector)
+{
+	// A collision collector that flips the collision results
+	class ReversedCollector : public CastShapeCollector
+	{
+	public:
+		explicit				ReversedCollector(CastShapeCollector &ioCollector, Vec3Arg inWorldDirection) :
+			mCollector(ioCollector),
+			mWorldDirection(inWorldDirection)
+		{
+			SetContext(ioCollector.GetContext());
+		}
+
+		virtual void			AddHit(const ShapeCastResult &inResult) override
+		{
+			// Add the reversed hit
+			mCollector.AddHit(inResult.Reversed(mWorldDirection));
+
+			// If our chained collector updated its early out fraction, we need to follow
+			UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
+		}
+
+	private:
+		CastShapeCollector &	mCollector;
+		Vec3					mWorldDirection;
+	};
+
+	// Reverse the shape cast (shape cast is in local space to shape 2)
+	Mat44 com_start_inv = inShapeCast.mCenterOfMassStart.InversedRotationTranslation();
+	ShapeCast local_shape_cast(inShape, inScale, com_start_inv, -com_start_inv.Multiply3x3(inShapeCast.mDirection));
+
+	// Calculate the center of mass of shape 1 at start of sweep
+	Mat44 shape1_com = inCenterOfMassTransform2 * inShapeCast.mCenterOfMassStart;
+
+	// Calculate the world space direction vector of the shape cast
+	Vec3 world_direction = -inCenterOfMassTransform2.Multiply3x3(inShapeCast.mDirection);
+
+	// Forward the cast
+	ReversedCollector collector(ioCollector, world_direction);
+	sCastShapeVsShapeLocalSpace(local_shape_cast, inShapeCastSettings, inShapeCast.mShape, inShapeCast.mScale, inShapeFilter, shape1_com, inSubShapeIDCreator2, inSubShapeIDCreator1, collector);
+}
+
 JPH_NAMESPACE_END

+ 9 - 0
Jolt/Physics/Collision/CollisionDispatch.h

@@ -73,12 +73,21 @@ public:
 	/// Function that casts a shape vs another shape (see sCastShapeVsShapeLocalSpace)
 	using CastShape = void (*)(const ShapeCast &inShapeCast, const ShapeCastSettings &inShapeCastSettings, const Shape *inShape, Vec3Arg inScale, const ShapeFilter &inShapeFilter, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, CastShapeCollector &ioCollector);
 
+	/// Initialize all collision functions with a function that asserts and returns no collision
+	static void				sInit();
+
 	/// Register a collide shape function in the collision table
 	static void				sRegisterCollideShape(EShapeSubType inType1, EShapeSubType inType2, CollideShape inFunction)	{ sCollideShape[(int)inType1][(int)inType2] = inFunction; }
 
 	/// Register a cast shape function in the collision table
 	static void				sRegisterCastShape(EShapeSubType inType1, EShapeSubType inType2, CastShape inFunction)			{ sCastShape[(int)inType1][(int)inType2] = inFunction; }
 
+	/// An implementation of CollideShape that swaps inShape1 and inShape2 and swaps the result back, can be registered if the collision function only exists the other way around
+	static void				sReversedCollideShape(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);
+
+	/// An implementation of CastShape that swaps inShape1 and inShape2 and swaps the result back, can be registered if the collision function only exists the other way around
+	static void				sReversedCastShape(const ShapeCast &inShapeCast, const ShapeCastSettings &inShapeCastSettings, const Shape *inShape, Vec3Arg inScale, const ShapeFilter &inShapeFilter, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, CastShapeCollector &ioCollector);
+
 private:
 	static CollideShape		sCollideShape[NumSubShapeTypes][NumSubShapeTypes];
 	static CastShape		sCastShape[NumSubShapeTypes][NumSubShapeTypes];

+ 3 - 0
Jolt/Physics/Collision/Shape/MeshShape.cpp

@@ -1184,6 +1184,9 @@ void MeshShape::sRegister()
 	{
 		CollisionDispatch::sRegisterCollideShape(s, EShapeSubType::Mesh, sCollideConvexVsMesh);
 		CollisionDispatch::sRegisterCastShape(s, EShapeSubType::Mesh, sCastConvexVsMesh);
+
+		CollisionDispatch::sRegisterCastShape(EShapeSubType::Mesh, s, CollisionDispatch::sReversedCastShape);
+		CollisionDispatch::sRegisterCollideShape(EShapeSubType::Mesh, s, CollisionDispatch::sReversedCollideShape);
 	}
 
 	// Specialized collision functions

+ 29 - 0
Jolt/Physics/Collision/ShapeCast.h

@@ -92,6 +92,35 @@ public:
 	/// Function required by the CollisionCollector. A smaller fraction is considered to be a 'better hit'. For rays/cast shapes we can just use the collision fraction. The fraction and penetration depth are combined in such a way that deeper hits at fraction 0 go first.
 	inline float				GetEarlyOutFraction() const			{ return mFraction > 0.0f? mFraction : -mPenetrationDepth; }
 
+	/// Reverses the hit result, swapping contact point 1 with contact point 2 etc.
+	/// @param inWorldSpaceCastDirection Direction of the shape cast in world space
+	ShapeCastResult				Reversed(Vec3Arg inWorldSpaceCastDirection) const
+	{
+		// Calculate by how much to shift the contact points
+		Vec3 delta = mFraction * inWorldSpaceCastDirection;
+
+		ShapeCastResult result;
+		result.mContactPointOn2 = mContactPointOn1 - delta;
+		result.mContactPointOn1 = mContactPointOn2 - delta;
+		result.mPenetrationAxis = -mPenetrationAxis;
+		result.mPenetrationDepth = mPenetrationDepth;
+		result.mSubShapeID2 = mSubShapeID1;
+		result.mSubShapeID1 = mSubShapeID2;
+		result.mBodyID2 = mBodyID2;
+		result.mFraction = mFraction;
+		result.mIsBackFaceHit = mIsBackFaceHit;
+
+		result.mShape2Face.resize(mShape1Face.size());
+		for (Face::size_type i = 0; i < mShape1Face.size(); ++i)
+			result.mShape2Face[i] = mShape1Face[i] - delta;
+
+		result.mShape1Face.resize(mShape2Face.size());
+		for (Face::size_type i = 0; i < mShape2Face.size(); ++i)
+			result.mShape1Face[i] = mShape2Face[i] - delta;
+
+		return result;
+	}
+
 	float						mFraction;							///< This is the fraction where the shape hit the other shape: CenterOfMassOnHit = Start + value * (End - Start)
 	bool						mIsBackFaceHit;						///< True if the shape was hit from the back side
 };

+ 4 - 0
Jolt/RegisterTypes.cpp

@@ -6,6 +6,7 @@
 #include <Jolt/RegisterTypes.h>
 #include <Jolt/Core/Factory.h>
 #include <Jolt/Core/RTTI.h>
+#include <Jolt/Physics/Collision/CollisionDispatch.h>
 #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/BoxShape.h>
@@ -65,6 +66,9 @@ void RegisterTypes()
 {
 	JPH_ASSERT(Factory::sInstance != nullptr, "Need to create a factory first!");
 
+	// Initialize dispatcher
+	CollisionDispatch::sInit();
+
 	// Register base classes first so that we can specialize them later
 	CompoundShape::sRegister();
 	ConvexShape::sRegister();

+ 4 - 0
Samples/Samples.cmake

@@ -81,6 +81,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/General/ContactManifoldTest.h
 	${SAMPLES_ROOT}/Tests/General/DampingTest.cpp
 	${SAMPLES_ROOT}/Tests/General/DampingTest.h
+	${SAMPLES_ROOT}/Tests/General/DynamicMeshTest.cpp
+	${SAMPLES_ROOT}/Tests/General/DynamicMeshTest.h
 	${SAMPLES_ROOT}/Tests/General/FrictionTest.cpp
 	${SAMPLES_ROOT}/Tests/General/FrictionTest.h
 	${SAMPLES_ROOT}/Tests/General/FrictionPerTriangleTest.cpp
@@ -197,6 +199,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Utils/ContactListenerImpl.h
 	${SAMPLES_ROOT}/Utils/RagdollLoader.cpp
 	${SAMPLES_ROOT}/Utils/RagdollLoader.h
+	${SAMPLES_ROOT}/Utils/ShapeCreator.cpp
+	${SAMPLES_ROOT}/Utils/ShapeCreator.h
 )
 
 if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")

+ 8 - 1
Samples/SamplesApp.cpp

@@ -35,6 +35,7 @@
 #include <Jolt/Physics/Constraints/DistanceConstraint.h>
 #include <Jolt/Physics/Character/CharacterVirtual.h>
 #include <Utils/Log.h>
+#include <Utils/ShapeCreator.h>
 #include <Renderer/DebugRendererImp.h>
 
 //-----------------------------------------------------------------------------
@@ -81,6 +82,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(MultithreadedTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(ContactListenerTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(ActivateDuringUpdateTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(SensorTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(DynamicMeshTest)
 
 static TestNameAndRTTI sGeneralTests[] =
 {
@@ -111,6 +113,7 @@ static TestNameAndRTTI sGeneralTests[] =
 	{ "Contact Listener",					JPH_RTTI(ContactListenerTest) },
 	{ "Activate During Update",				JPH_RTTI(ActivateDuringUpdateTest) },
 	{ "Sensor",								JPH_RTTI(SensorTest) },
+	{ "Dynamic Mesh",						JPH_RTTI(DynamicMeshTest) },
 };
 
 JPH_DECLARE_RTTI_FOR_FACTORY(DistanceConstraintTest)
@@ -415,7 +418,7 @@ SamplesApp::SamplesApp()
 	mDebugUI->CreateTextButton(main_menu, "Mouse Probe", [this]() { 
 		UIElement *probe_options = mDebugUI->CreateMenu();
 		mDebugUI->CreateComboBox(probe_options, "Mode", { "Pick", "Ray", "RayCollector", "CollidePoint", "CollideShape", "CastShape", "TransfShape", "GetTriangles", "BP Ray", "BP Box", "BP Sphere", "BP Point", "BP OBox", "BP Cast Box" }, (int)mProbeMode, [this](int inItem) { mProbeMode = (EProbeMode)inItem; });
-		mDebugUI->CreateComboBox(probe_options, "Shape", { "Sphere", "Box", "ConvexHull", "Capsule", "TaperedCapsule", "Cylinder", "Triangle", "StaticCompound", "StaticCompound2", "MutableCompound" }, (int)mProbeShape, [=](int inItem) { mProbeShape = (EProbeShape)inItem; });
+		mDebugUI->CreateComboBox(probe_options, "Shape", { "Sphere", "Box", "ConvexHull", "Capsule", "TaperedCapsule", "Cylinder", "Triangle", "StaticCompound", "StaticCompound2", "MutableCompound", "Mesh" }, (int)mProbeShape, [=](int inItem) { mProbeShape = (EProbeShape)inItem; });
 		mDebugUI->CreateCheckBox(probe_options, "Scale Shape", mScaleShape, [this](UICheckBox::EState inState) { mScaleShape = inState == UICheckBox::STATE_CHECKED; });
 		mDebugUI->CreateSlider(probe_options, "Scale X", mShapeScale.GetX(), -5.0f, 5.0f, 0.1f, [this](float inValue) { mShapeScale.SetX(inValue); });
 		mDebugUI->CreateSlider(probe_options, "Scale Y", mShapeScale.GetY(), -5.0f, 5.0f, 0.1f, [this](float inValue) { mShapeScale.SetY(inValue); });
@@ -753,6 +756,10 @@ RefConst<Shape> SamplesApp::CreateProbeShape()
 			shape = compound_settings.Create().Get();
 		}
 		break;
+
+	case EProbeShape::Mesh:
+		shape = ShapeCreator::CreateTorusMesh(2.0f, 0.25f);
+		break;
 	}
 
 	JPH_ASSERT(shape != nullptr);

+ 1 - 0
Samples/SamplesApp.h

@@ -173,6 +173,7 @@ private:
 		StaticCompound,
 		StaticCompound2,
 		MutableCompound,
+		Mesh,
 	};
 
 	// Probe settings

+ 45 - 0
Samples/Tests/General/DynamicMeshTest.cpp

@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/General/DynamicMeshTest.h>
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+#include <Utils/ShapeCreator.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(DynamicMeshTest) 
+{ 
+	JPH_ADD_BASE_CLASS(DynamicMeshTest, Test) 
+}
+
+void DynamicMeshTest::Initialize()
+{
+	// Floor
+	CreateFloor();
+
+	constexpr float cTorusRadius = 3.0f;
+	constexpr float cTubeRadius = 1.0f;
+
+	// Create torus
+	RefConst<Shape> mesh_shape = ShapeCreator::CreateTorusMesh(cTorusRadius, cTubeRadius);
+	BodyCreationSettings settings(mesh_shape, Vec3(0, 10, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+
+	// Mesh cannot calculate its mass, we must provide it
+	settings.mOverrideMassProperties = EOverrideMassProperties::MassAndInertiaProvided;
+	settings.mMassPropertiesOverride.SetMassAndInertiaOfSolidBox(2.0f * Vec3(cTorusRadius, cTubeRadius, cTorusRadius), 1000.0f);
+
+	mBodyInterface->CreateAndAddBody(settings, EActivation::Activate);
+
+	// Wall of blocks
+	RefConst<Shape> box_shape = new BoxShape(Vec3::sReplicate(0.5f));
+	for (int i = 0; i < 7; ++i)
+		for (int j = i / 2; j < 7 - (i + 1) / 2; ++j)
+		{
+			Vec3 position(-3.5f + j * 1.0f + (i & 1? 0.5f : 0.0f), 0.5f + i * 1.0f, 0.0f);
+			Body &wall = *mBodyInterface->CreateBody(BodyCreationSettings(box_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
+			mBodyInterface->AddBody(wall.GetID(), EActivation::Activate);
+		}
+}

+ 16 - 0
Samples/Tests/General/DynamicMeshTest.h

@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+// This test drops a mesh shape on a box
+class DynamicMeshTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(DynamicMeshTest)
+
+	// See: Test
+	virtual void		Initialize() override;
+};

+ 39 - 0
Samples/Utils/ShapeCreator.cpp

@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Utils/ShapeCreator.h>
+
+namespace ShapeCreator {
+
+ShapeRefC CreateTorusMesh(float inTorusRadius, float inTubeRadius, uint inTorusSegments, uint inTubeSegments)
+{
+	uint cNumVertices = inTorusSegments * inTubeSegments;
+
+	// Create torus
+	MeshShapeSettings mesh;
+	mesh.mTriangleVertices.reserve(cNumVertices);
+	mesh.mIndexedTriangles.reserve(cNumVertices * 2);
+	for (uint torus_segment = 0; torus_segment < inTorusSegments; ++torus_segment)
+	{
+		Mat44 rotation = Mat44::sRotation(Vec3::sAxisY(), float(torus_segment) * 2.0f * JPH_PI / inTorusSegments);
+		for (uint tube_segment = 0; tube_segment < inTubeSegments; ++tube_segment)
+		{
+			// Create vertices
+			float tube_angle = float(tube_segment) * 2.0f * JPH_PI / inTubeSegments;
+			Vec3 pos = rotation * Vec3(inTorusRadius + inTubeRadius * sin(tube_angle), inTubeRadius * cos(tube_angle), 0);
+			Float3 v;
+			pos.StoreFloat3(&v);
+			mesh.mTriangleVertices.push_back(v);
+
+			// Create indices
+			uint start_idx = torus_segment * inTubeSegments + tube_segment;
+			mesh.mIndexedTriangles.emplace_back(start_idx, (start_idx + 1) % cNumVertices, (start_idx + inTubeSegments) % cNumVertices);
+			mesh.mIndexedTriangles.emplace_back((start_idx + 1) % cNumVertices, (start_idx + inTubeSegments + 1) % cNumVertices, (start_idx + inTubeSegments) % cNumVertices);
+		}
+	}
+	return mesh.Create().Get();
+}
+
+};

+ 17 - 0
Samples/Utils/ShapeCreator.h

@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
+
+namespace ShapeCreator
+{
+	/// Create a mesh shape in the shape of a torus
+	/// @param inTorusRadius Radius of the torus ring
+	/// @param inTubeRadius Radius of the torus tube
+	/// @param inTorusSegments Number of segments around the torus
+	/// @param inTubeSegments Number of segments around the tube of the torus
+	/// @return The mesh shape
+	ShapeRefC	CreateTorusMesh(float inTorusRadius, float inTubeRadius, uint inTorusSegments = 16, uint inTubeSegments = 16);
+};