Browse Source

Added a 2nd scene to the performance test - convex shapes falling on a mesh (#28)

jrouwe 3 năm trước cách đây
mục cha
commit
0f9972d8c0

+ 111 - 0
PerformanceTest/ConvexVsMeshScene.h

@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+// Jolt includes
+#include <Physics/Collision/Shape/BoxShape.h>
+#include <Physics/Collision/Shape/SphereShape.h>
+#include <Physics/Collision/Shape/ConvexHullShape.h>
+#include <Physics/Collision/Shape/MeshShape.h>
+#include <Physics/Collision/Shape/CapsuleShape.h>
+#include <Physics/Body/BodyCreationSettings.h>
+
+// Local includes
+#include "PerformanceTestScene.h"
+#include "Layers.h"
+
+// A scene that drops a number of convex shapes on a sloping terrain made out of a mesh shape
+class ConvexVsMeshScene : public PerformanceTestScene
+{
+public:
+	virtual const char *	GetName() const override
+	{
+		return "ConvexVsMesh";
+	}
+
+	virtual bool			Load() override
+	{
+		const int n = 100;
+		const float cell_size = 3.0f;
+		const float max_height = 5.0f;
+		float center = n * cell_size / 2;
+
+		// Create vertices
+		const int num_vertices = (n + 1) * (n + 1);
+		VertexList vertices;
+		vertices.resize(num_vertices);
+		for (int x = 0; x <= n; ++x)
+			for (int z = 0; z <= n; ++z)
+			{
+				float height = sin(float(x) * 50.0f / n) * cos(float(z) * 50.0f / n);
+				vertices[z * (n + 1) + x] = Float3(cell_size * x, max_height * height, cell_size * z);
+			}
+
+		// Create regular grid of triangles
+		const int num_triangles = n * n * 2;
+		IndexedTriangleList indices;
+		indices.resize(num_triangles);
+		IndexedTriangle *next = indices.data();
+		for (int x = 0; x < n; ++x)
+			for (int z = 0; z < n; ++z)
+			{
+				int start = (n + 1) * z + x;
+
+				next->mIdx[0] = start;
+				next->mIdx[1] = start + n + 1;
+				next->mIdx[2] = start + 1;
+				next++;
+
+				next->mIdx[0] = start + 1;
+				next->mIdx[1] = start + n + 1;
+				next->mIdx[2] = start + n + 2;
+				next++;
+			}
+
+		// Create mesh shape creation settings
+		mMeshSettings.mMotionType = EMotionType::Static;
+		mMeshSettings.mObjectLayer = Layers::NON_MOVING;
+		mMeshSettings.mPosition = Vec3(-center, max_height, -center);
+		mMeshSettings.mFriction = 0.5f;
+		mMeshSettings.mRestitution = 0.6f;
+		mMeshSettings.SetShapeSettings(new MeshShapeSettings(vertices, indices));
+
+		// Create other shapes
+		mShapes = {
+			new BoxShape(Vec3(0.5f, 0.75f, 1.0f)),
+			new SphereShape(0.5f),
+			new CapsuleShape(0.75f, 0.5f),
+			ConvexHullShapeSettings({ Vec3(0, 1, 0), Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 0, 1), Vec3(0, 0, -1) }).Create().Get(),
+		};
+
+		return true;
+	}
+
+	virtual void			StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) override
+	{
+		// Create background
+		BodyInterface &bi = inPhysicsSystem.GetBodyInterface();
+		bi.CreateAndAddBody(mMeshSettings, EActivation::DontActivate);
+
+		// Construct bodies
+		for (int x = -10; x <= 10; ++x)
+			for (int y = 0; y < (int)mShapes.size(); ++y)
+				for (int z = -10; z <= 10; ++z)
+				{
+					BodyCreationSettings creation_settings;
+					creation_settings.mMotionType = EMotionType::Dynamic;
+					creation_settings.mMotionQuality = inMotionQuality;
+					creation_settings.mObjectLayer = Layers::MOVING;
+					creation_settings.mPosition = Vec3(7.5f * x, 15.0f + 2.0f * y, 7.5f * z);
+					creation_settings.mFriction = 0.5f;
+					creation_settings.mRestitution = 0.6f;
+					creation_settings.SetShape(mShapes[y]);
+					bi.CreateAndAddBody(creation_settings, EActivation::Activate);
+				}
+	}
+
+private:
+	BodyCreationSettings	mMeshSettings;
+	vector<Ref<Shape>>		mShapes;
+};

+ 62 - 0
PerformanceTest/Layers.h

@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Physics/Collision/ObjectLayer.h>
+#include <Physics/Collision/BroadPhase/BroadPhaseLayer.h>
+
+/// Layer that objects can be in, determines which other objects it can collide with
+namespace Layers
+{
+	static constexpr uint8 NON_MOVING = 0;
+	static constexpr uint8 MOVING = 1;
+	static constexpr uint8 NUM_LAYERS = 2;
+};
+
+/// Function that determines if two object layers can collide
+inline bool ObjectCanCollide(ObjectLayer inObject1, ObjectLayer inObject2)
+{
+	switch (inObject1)
+	{
+	case Layers::NON_MOVING:
+		return inObject2 == Layers::MOVING; // Non moving only collides with moving
+	case Layers::MOVING:
+		return true; // Moving collides with everything
+	default:
+		JPH_ASSERT(false);
+		return false;
+	}
+};
+
+/// Broadphase layers
+namespace BroadPhaseLayers
+{
+	static constexpr BroadPhaseLayer NON_MOVING(0);
+	static constexpr BroadPhaseLayer MOVING(1);
+};
+
+/// Function that determines if two broadphase layers can collide
+inline bool BroadPhaseCanCollide(ObjectLayer inLayer1, BroadPhaseLayer inLayer2)
+{
+	switch (inLayer1)
+	{
+	case Layers::NON_MOVING:
+		return inLayer2 == BroadPhaseLayers::MOVING;
+	case Layers::MOVING:
+		return true;	
+	default:
+		JPH_ASSERT(false);
+		return false;
+	}
+}
+
+/// Create mapping table from layer to broadphase layer
+inline ObjectToBroadPhaseLayer GetObjectToBroadPhaseLayer()
+{
+	ObjectToBroadPhaseLayer object_to_broadphase;
+	object_to_broadphase.resize(Layers::NUM_LAYERS);
+	object_to_broadphase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING;
+	object_to_broadphase[Layers::MOVING] = BroadPhaseLayers::MOVING;
+	return object_to_broadphase;
+}

+ 4 - 0
PerformanceTest/PerformanceTest.cmake

@@ -5,6 +5,10 @@ set(PERFORMANCE_TEST_ROOT ${PHYSICS_REPO_ROOT}/PerformanceTest)
 set(PERFORMANCE_TEST_SRC_FILES
 	${PERFORMANCE_TEST_ROOT}/PerformanceTest.cpp
 	${PERFORMANCE_TEST_ROOT}/PerformanceTest.cmake
+	${PERFORMANCE_TEST_ROOT}/PerformanceTestScene.h
+	${PERFORMANCE_TEST_ROOT}/RagdollScene.h
+	${PERFORMANCE_TEST_ROOT}/ConvexVsMeshScene.h
+	${PERFORMANCE_TEST_ROOT}/Layers.h
 )
 
 # Group source files

+ 57 - 175
PerformanceTest/PerformanceTest.cpp

@@ -8,10 +8,6 @@
 #include <Core/JobSystemThreadPool.h>
 #include <Physics/PhysicsSettings.h>
 #include <Physics/PhysicsSystem.h>
-#include <Physics/Ragdoll/Ragdoll.h>
-#include <Physics/PhysicsScene.h>
-#include <Physics/Collision/CastResult.h>
-#include <Physics/Collision/RayCast.h>
 #ifdef JPH_DEBUG_RENDERER
 	#include <Renderer/DebugRendererRecorder.h>
 	#include <Core/StreamWrapper.h>
@@ -21,64 +17,20 @@
 #include <iostream>
 #include <thread>
 #include <chrono>
+#include <memory>
 
 using namespace JPH;
 using namespace std;
 
-namespace Layers
-{
-	static constexpr uint8 NON_MOVING = 0;
-	static constexpr uint8 MOVING = 1;
-	static constexpr uint8 NUM_LAYERS = 2;
-};
-
-bool MyObjectCanCollide(ObjectLayer inObject1, ObjectLayer inObject2)
-{
-	switch (inObject1)
-	{
-	case Layers::NON_MOVING:
-		return inObject2 == Layers::MOVING; // Non moving only collides with moving
-	case Layers::MOVING:
-		return true; // Moving collides with everything
-	default:
-		JPH_ASSERT(false);
-		return false;
-	}
-};
+// Local includes
+#include "RagdollScene.h"
+#include "ConvexVsMeshScene.h"
 
-namespace BroadPhaseLayers
-{
-	static constexpr BroadPhaseLayer NON_MOVING(0);
-	static constexpr BroadPhaseLayer MOVING(1);
-};
-
-bool MyBroadPhaseCanCollide(ObjectLayer inLayer1, BroadPhaseLayer inLayer2)
-{
-	switch (inLayer1)
-	{
-	case Layers::NON_MOVING:
-		return inLayer2 == BroadPhaseLayers::MOVING;
-	case Layers::MOVING:
-		return true;	
-	default:
-		JPH_ASSERT(false);
-		return false;
-	}
-}
+// Time step for physics
+constexpr float cDeltaTime = 1.0f / 60.0f;
 
-// Test configuration
-const float cHorizontalSeparation = 4.0f;
-const float cVerticalSeparation = 0.6f;
-#ifdef _DEBUG
-	const int cPileSize = 5;
-	const int cNumRows = 2;
-	const int cNumCols = 2;
-#else
-	const int cPileSize = 10;
-	const int cNumRows = 4;
-	const int cNumCols = 4;
-#endif
-const float cDeltaTime = 1.0f / 60.0f;
+// Number of iterations to run the test
+constexpr uint cMaxIterations = 500;
 
 // Program entry point
 int main(int argc, char** argv)
@@ -88,43 +40,56 @@ int main(int argc, char** argv)
 	int specified_threads = -1;
 	bool enable_profiler = false;
 	bool enable_debug_renderer = false;
-	for (int arg = 1; arg < argc; ++arg)
+	unique_ptr<PerformanceTestScene> scene;
+	for (int argidx = 1; argidx < argc; ++argidx)
 	{
-		if (strncmp(argv[arg], "-q=", 3) == 0)
+		const char *arg = argv[argidx];
+
+		if (strncmp(arg, "-s=", 3) == 0)
 		{
-			// Parse quality
-			if (strcmp(argv[arg] + 3, "discrete") == 0)
+			// Parse scene
+			if (strcmp(arg + 3, "Ragdoll") == 0)
+				scene = unique_ptr<PerformanceTestScene>(new RagdollScene);
+			else if (strcmp(arg + 3, "ConvexVsMesh") == 0)
+				scene = unique_ptr<PerformanceTestScene>(new ConvexVsMeshScene);
+			else
 			{
-				specified_quality = 0;
+				cerr << "Invalid scene" << endl;
+				return 1;
 			}
-			else if (strcmp(argv[arg] + 3, "linearcast") == 0)
-			{
+		}
+		else if (strncmp(arg, "-q=", 3) == 0)
+		{
+			// Parse quality
+			if (strcmp(arg + 3, "Discrete") == 0)
+				specified_quality = 0;
+			else if (strcmp(arg + 3, "LinearCast") == 0)
 				specified_quality = 1;
-			}
 			else
 			{
 				cerr << "Invalid quality" << endl;
 				return 1;
 			}
 		}
-		else if (strncmp(argv[arg], "-t=", 3) == 0)
+		else if (strncmp(arg, "-t=", 3) == 0)
 		{
 			// Parse threads
-			specified_threads = atoi(argv[arg] + 3);
+			specified_threads = atoi(arg + 3);
 		}
-		else if (strcmp(argv[arg], "-p") == 0)
+		else if (strcmp(arg, "-p") == 0)
 		{
 			enable_profiler = true;
 		}
-		else if (strcmp(argv[arg], "-r") == 0)
+		else if (strcmp(arg, "-r") == 0)
 		{
 			enable_debug_renderer = true;
 		}
-		else if (strcmp(argv[arg], "-h") == 0)
+		else if (strcmp(arg, "-h") == 0)
 		{
 			// Print usage
-			cerr << "Usage: PerformanceTest [-q=<quality>] [-t=<threads>] [-p] [-r]" << endl
-				 << "-q: Test only with specified quality (discrete, linearcast)" << endl
+			cerr << "Usage: PerformanceTest [-s=<scene>] [-q=<quality>] [-t=<threads>] [-p] [-r]" << endl
+				 << "-s: Select scene (Ragdoll, ConvexVsMesh)" << endl
+				 << "-q: Test only with specified quality (Discrete, LinearCast)" << endl
 				 << "-t: Test only with N threads" << endl
 				 << "-p: Write out profiles" << endl
 				 << "-r: Record debug renderer output for JoltViewer" << endl;
@@ -138,54 +103,17 @@ int main(int argc, char** argv)
 	// Create temp allocator
 	TempAllocatorImpl temp_allocator(10 * 1024 * 1024);
 
-	// Load ragdoll
-	Ref<RagdollSettings> ragdoll_settings;
-	if (!ObjectStreamIn::sReadObject("Assets/Human.tof", ragdoll_settings))
-	{
-		cerr << "Unable to load ragdoll" << endl;
-		return 1;
-	}
-	for (BodyCreationSettings &body : ragdoll_settings->mParts)
-		body.mObjectLayer = Layers::MOVING;
-
-	// Init ragdoll
-	ragdoll_settings->GetSkeleton()->CalculateParentJointIndices();
-	ragdoll_settings->Stabilize();
-	ragdoll_settings->CalculateBodyIndexToConstraintIndex();
-	ragdoll_settings->CalculateConstraintIndexToBodyIdxPair();
-
-	// Load animation
-	Ref<SkeletalAnimation> animation;
-	if (!ObjectStreamIn::sReadObject("Assets/Human/dead_pose1.tof", animation))
-	{
-		cerr << "Unable to load animation" << endl;
+	// Load the scene
+	if (scene == nullptr)
+		scene = unique_ptr<PerformanceTestScene>(new RagdollScene);
+	if (!scene->Load())
 		return 1;
-	}
 
-	// Sample pose
-	SkeletonPose pose;
-	pose.SetSkeleton(ragdoll_settings->GetSkeleton());
-	animation->Sample(0.0f, pose);
-
-	// Read the scene
-	Ref<PhysicsScene> scene;
-	if (!ObjectStreamIn::sReadObject("Assets/terrain2.bof", scene))
-	{
-		cerr << "Unable to load terrain" << endl;
-		return 1;
-	}
-	for (BodyCreationSettings &body : scene->GetBodies())
-	{
-		body.mObjectLayer = Layers::NON_MOVING;
-		body.mAllowSleeping = false;
-	}
-	scene->FixInvalidScales();
+	// Output scene we're running
+	cout << "Running scene: " << scene->GetName() << endl;
 
 	// Create mapping table from object layer to broadphase layer
-	ObjectToBroadPhaseLayer object_to_broadphase;
-	object_to_broadphase.resize(Layers::NUM_LAYERS);
-	object_to_broadphase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING;
-	object_to_broadphase[Layers::MOVING] = BroadPhaseLayers::MOVING;
+	ObjectToBroadPhaseLayer object_to_broadphase = GetObjectToBroadPhaseLayer();
 
 	// Start profiling this thread
 	JPH_PROFILE_THREAD_START("Main");
@@ -193,8 +121,6 @@ int main(int argc, char** argv)
 	// Trace header
 	cout << "Motion Quality, Thread Count, Steps / Second, Hash" << endl;
 
-	constexpr uint cMaxIterations = 500;
-
 	// Iterate motion qualities
 	for (uint mq = 0; mq < 2; ++mq)
 	{
@@ -206,10 +132,6 @@ int main(int argc, char** argv)
 		EMotionQuality motion_quality = mq == 0? EMotionQuality::Discrete : EMotionQuality::LinearCast;
 		string motion_quality_str = mq == 0? "Discrete" : "LinearCast";
 
-		// Set motion quality on ragdoll
-		for (BodyCreationSettings &body : ragdoll_settings->mParts)
-			body.mMotionQuality = motion_quality;
-
 		// Determine which thread counts to test
 		vector<uint> thread_permutations;
 		if (specified_threads > 0)
@@ -226,50 +148,10 @@ int main(int argc, char** argv)
 
 			// Create physics system
 			PhysicsSystem physics_system;
-			physics_system.Init(10240, 0, 65536, 10240, object_to_broadphase, MyBroadPhaseCanCollide, MyObjectCanCollide);
-
-			// Add background geometry
-			scene->CreateBodies(&physics_system);
-
-			// Create ragdoll piles
-			vector<Ref<Ragdoll>> ragdolls;
-			mt19937 random;
-			uniform_real_distribution<float> angle(0.0f, JPH_PI);
-			CollisionGroup::GroupID group_id = 1;
-			for (int row = 0; row < cNumRows; ++row)
-				for (int col = 0; col < cNumCols; ++col)
-				{
-					// Determine start location of ray
-					Vec3 start = Vec3(cHorizontalSeparation * (col - (cNumCols - 1) / 2.0f), 100, cHorizontalSeparation * (row - (cNumRows - 1) / 2.0f));
-
-					// Cast ray down to terrain
-					RayCastResult hit;
-					Vec3 ray_direction(0, -200, 0);
-					RayCast ray { start, ray_direction };
-					if (physics_system.GetNarrowPhaseQuery().CastRay(ray, hit, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING)))
-						start = start + hit.mFraction * ray_direction;
-
-					for (int i = 0; i < cPileSize; ++i)
-					{
-						// Create ragdoll
-						Ref<Ragdoll> ragdoll = ragdoll_settings->CreateRagdoll(group_id++, nullptr, &physics_system);
-	
-						// Override root
-						SkeletonPose pose_copy = pose;
-						SkeletonPose::JointState &root = pose_copy.GetJoint(0);
-						root.mTranslation = start + Vec3(0, cVerticalSeparation * (i + 1), 0);
-						root.mRotation = Quat::sRotation(Vec3::sAxisY(), angle(random)) * root.mRotation;
-						pose_copy.CalculateJointMatrices();
-
-						// Drive to pose
-						ragdoll->SetPose(pose_copy);
-						ragdoll->DriveToPoseUsingMotors(pose_copy);
-						ragdoll->AddToPhysicsSystem(EActivation::Activate);
-
-						// Keep reference
-						ragdolls.push_back(ragdoll);
-					}
-				}
+			physics_system.Init(10240, 0, 65536, 10240, object_to_broadphase, BroadPhaseCanCollide, ObjectCanCollide);
+
+			// Start test scene
+			scene->StartTest(physics_system, motion_quality);
 
 		#ifdef JPH_DEBUG_RENDERER
 			// Open output
@@ -319,17 +201,17 @@ int main(int argc, char** argv)
 			// Calculate hash of all positions and rotations of the bodies
 			size_t hash = 0;
 			BodyInterface &bi = physics_system.GetBodyInterfaceNoLock();
-			for (Ragdoll *ragdoll : ragdolls)
-				for (BodyID id : ragdoll->GetBodyIDs())
-				{
-					Vec3 pos = bi.GetPosition(id);
-					Quat rot = bi.GetRotation(id);
-					hash_combine(hash, pos.GetX(), pos.GetY(), pos.GetZ(), rot.GetX(), rot.GetY(), rot.GetZ(), rot.GetW());
-				}
+			BodyIDVector body_ids;
+			physics_system.GetBodies(body_ids);
+			for (BodyID id : body_ids)
+			{
+				Vec3 pos = bi.GetPosition(id);
+				Quat rot = bi.GetRotation(id);
+				hash_combine(hash, pos.GetX(), pos.GetY(), pos.GetZ(), rot.GetX(), rot.GetY(), rot.GetZ(), rot.GetW());
+			}
 
-			// Remove ragdolls
-			for (Ragdoll *ragdoll : ragdolls)
-				ragdoll->RemoveFromPhysicsSystem();
+			// Stop test scene
+			scene->StopTest(physics_system);
 
 			// Trace stat line
 			cout << motion_quality_str << ", " << num_threads + 1 << ", " << double(cMaxIterations) / (1.0e-9 * total_duration.count()) << ", " << hash << endl;

+ 24 - 0
PerformanceTest/PerformanceTestScene.h

@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+// Base class for a test scene to test performance
+class PerformanceTestScene
+{
+public:
+	// Virtual destructor
+	virtual					~PerformanceTestScene()							{ }
+
+	// Get name of test for debug purposes
+	virtual const char *	GetName() const = 0;
+
+	// Load assets for the scene
+	virtual bool			Load()											{ return true; }
+
+	// Start a new test by adding objects to inPhysicsSystem
+	virtual void			StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) = 0;
+
+	// Stop a test and remove objects from inPhysicsSystem
+	virtual void			StopTest(PhysicsSystem &inPhysicsSystem)		{ }
+};

+ 142 - 0
PerformanceTest/RagdollScene.h

@@ -0,0 +1,142 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+// Jolt includes
+#include <Physics/Ragdoll/Ragdoll.h>
+#include <Physics/PhysicsScene.h>
+#include <Physics/Collision/CastResult.h>
+#include <Physics/Collision/RayCast.h>
+
+// Local includes
+#include "PerformanceTestScene.h"
+#include "Layers.h"
+
+// A scene that loads a part of a Horizon Zero Dawn level and drops many ragdolls on the terrain (motors enabled)
+class RagdollScene : public PerformanceTestScene
+{
+public:
+	virtual const char *	GetName() const override
+	{
+		return "Ragdoll";
+	}
+
+	virtual bool			Load() override
+	{
+		// Load ragdoll
+		if (!ObjectStreamIn::sReadObject("Assets/Human.tof", mRagdollSettings))
+		{
+			cerr << "Unable to load ragdoll" << endl;
+			return false;
+		}
+		for (BodyCreationSettings &body : mRagdollSettings->mParts)
+			body.mObjectLayer = Layers::MOVING;
+
+		// Init ragdoll
+		mRagdollSettings->GetSkeleton()->CalculateParentJointIndices();
+		mRagdollSettings->Stabilize();
+		mRagdollSettings->CalculateBodyIndexToConstraintIndex();
+		mRagdollSettings->CalculateConstraintIndexToBodyIdxPair();
+
+		// Load animation
+		if (!ObjectStreamIn::sReadObject("Assets/Human/dead_pose1.tof", mAnimation))
+		{
+			cerr << "Unable to load animation" << endl;
+			return false;
+		}
+
+		// Sample pose
+		mPose.SetSkeleton(mRagdollSettings->GetSkeleton());
+		mAnimation->Sample(0.0f, mPose);
+
+		// Read the background scene
+		if (!ObjectStreamIn::sReadObject("Assets/terrain2.bof", mBackground))
+		{
+			cerr << "Unable to load terrain" << endl;
+			return false;
+		}
+		for (BodyCreationSettings &body : mBackground->GetBodies())
+			body.mObjectLayer = Layers::NON_MOVING;
+		mBackground->FixInvalidScales();
+
+		return true;
+	}
+
+	virtual void			StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) override
+	{
+		// Test configuration
+		const float cHorizontalSeparation = 4.0f;
+		const float cVerticalSeparation = 0.6f;
+		#ifdef _DEBUG
+			const int cPileSize = 5;
+			const int cNumRows = 2;
+			const int cNumCols = 2;
+		#else
+			const int cPileSize = 10;
+			const int cNumRows = 4;
+			const int cNumCols = 4;
+		#endif
+
+		// Set motion quality on ragdoll
+		for (BodyCreationSettings &body : mRagdollSettings->mParts)
+			body.mMotionQuality = inMotionQuality;
+
+		// Add background geometry
+		mBackground->CreateBodies(&inPhysicsSystem);
+
+		// Create ragdoll piles
+		mt19937 random;
+		uniform_real_distribution<float> angle(0.0f, JPH_PI);
+		CollisionGroup::GroupID group_id = 1;
+		for (int row = 0; row < cNumRows; ++row)
+			for (int col = 0; col < cNumCols; ++col)
+			{
+				// Determine start location of ray
+				Vec3 start = Vec3(cHorizontalSeparation * (col - (cNumCols - 1) / 2.0f), 100, cHorizontalSeparation * (row - (cNumRows - 1) / 2.0f));
+
+				// Cast ray down to terrain
+				RayCastResult hit;
+				Vec3 ray_direction(0, -200, 0);
+				RayCast ray { start, ray_direction };
+				if (inPhysicsSystem.GetNarrowPhaseQuery().CastRay(ray, hit, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING)))
+					start = start + hit.mFraction * ray_direction;
+
+				for (int i = 0; i < cPileSize; ++i)
+				{
+					// Create ragdoll
+					Ref<Ragdoll> ragdoll = mRagdollSettings->CreateRagdoll(group_id++, nullptr, &inPhysicsSystem);
+	
+					// Override root
+					SkeletonPose pose_copy = mPose;
+					SkeletonPose::JointState &root = pose_copy.GetJoint(0);
+					root.mTranslation = start + Vec3(0, cVerticalSeparation * (i + 1), 0);
+					root.mRotation = Quat::sRotation(Vec3::sAxisY(), angle(random)) * root.mRotation;
+					pose_copy.CalculateJointMatrices();
+
+					// Drive to pose
+					ragdoll->SetPose(pose_copy);
+					ragdoll->DriveToPoseUsingMotors(pose_copy);
+					ragdoll->AddToPhysicsSystem(EActivation::Activate);
+
+					// Keep reference
+					mRagdolls.push_back(ragdoll);
+				}
+			}
+	}
+
+	virtual void			StopTest(PhysicsSystem &inPhysicsSystem) override
+	{
+		// Remove ragdolls
+		for (Ragdoll *ragdoll : mRagdolls)
+			ragdoll->RemoveFromPhysicsSystem();
+		mRagdolls.clear();
+	}
+
+private:
+	Ref<RagdollSettings>	mRagdollSettings;
+	Ref<SkeletalAnimation>	mAnimation;
+	SkeletonPose			mPose;
+	Ref<PhysicsScene>		mBackground;
+	vector<Ref<Ragdoll>>	mRagdolls;
+};