Ajout de Jolt Physics + 1ere version des factory entitecomposants - camera, transform, rigidbody, collider, renderer

This commit is contained in:
Tom Ray
2026-03-22 00:28:03 +01:00
parent 6695d46bcd
commit 48348936a8
1147 changed files with 214331 additions and 353 deletions

View File

@@ -0,0 +1,173 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/BigWorldTest.h>
#include <Jolt/Physics/PhysicsScene.h>
#include <Jolt/Skeleton/Skeleton.h>
#include <Jolt/Skeleton/SkeletalAnimation.h>
#include <Jolt/Skeleton/SkeletonPose.h>
#include <Jolt/Core/StringTools.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Utils/RagdollLoader.h>
#include <Application/DebugUI.h>
#include <Renderer/DebugRendererImp.h>
#include <Layers.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
#include <random>
JPH_IMPLEMENT_RTTI_VIRTUAL(BigWorldTest)
{
JPH_ADD_BASE_CLASS(BigWorldTest, Test)
}
BigWorldTest::~BigWorldTest()
{
for (Pile &pile : mPiles)
for (Ragdoll *r : pile.mRagdolls)
r->RemoveFromPhysicsSystem();
}
void BigWorldTest::Initialize()
{
constexpr int cPileSize = 5;
// Default terrain
Body &floor = CreateMeshTerrain();
RefConst<Shape> shape = floor.GetShape();
// Load ragdoll
Ref<RagdollSettings> settings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Load animation
Ref<SkeletalAnimation> animation;
AssetStream stream("Human/dead_pose1.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), animation))
FatalError("Could not open animation");
SkeletonPose pose;
pose.SetSkeleton(settings->GetSkeleton());
animation->Sample(0.0f, pose);
// Determine rotation for each ragdoll in the pile
default_random_engine random;
uniform_real_distribution<float> angle(0.0f, JPH_PI);
Array<Quat> rotation;
for (int i = 0; i < cPileSize; ++i)
rotation.push_back(Quat::sRotation(Vec3::sAxisY(), angle(random)) * pose.GetJoint(0).mRotation);
// Create piles at various distances
Real distances[] = { 0.0_r, 1.0e3_r, 5.0e3_r, 1.0e4_r, 5.0e4_r, 1.0e5_r, 1.0e6_r, 1.0e7_r, 1.0e8_r };
for (Real distance : distances)
{
// Calculate origin for this simulation assuming we want to be 'distance' away and the same distance along each coordinate axis
RVec3 origin = RVec3::sReplicate(distance) / sqrt(3.0_r);
// Create floor (floor at 0 was already created)
if (distance != 0.0f)
mBodyInterface->CreateAndAddBody(BodyCreationSettings(shape, origin, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
// Create pile of ragdolls
Pile pile;
pile.mOrigin = origin;
for (int i = 0; i < cPileSize; ++i)
{
// Create ragdoll
Ref<Ragdoll> ragdoll = settings->CreateRagdoll(0, 0, mPhysicsSystem);
// Override root
SkeletonPose::JointState &root = pose.GetJoint(0);
root.mTranslation = Vec3::sZero();
root.mRotation = rotation[i];
pose.SetRootOffset(origin + Vec3(0, 2.0f + 0.6f * i, 0));
pose.CalculateJointMatrices();
// Drive to pose
ragdoll->SetPose(pose);
ragdoll->DriveToPoseUsingMotors(pose);
ragdoll->AddToPhysicsSystem(EActivation::Activate);
pile.mRagdolls.push_back(ragdoll);
}
mPiles.push_back(pile);
}
}
void BigWorldTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
int pile_idx = 0;
for (Pile &pile : mPiles)
if (!pile.mOrigin.IsNearZero()) // Pile at 0 is drawn normally
{
// Check if we need to draw this pile
if ((sDrawPileMask & (1 << pile_idx)) != 0)
{
Color color = Color::sGetDistinctColor(pile_idx);
bool first = true;
for (Ragdoll *r : pile.mRagdolls)
{
for (const BodyID &id : r->GetBodyIDs())
{
BodyLockRead lock(mPhysicsSystem->GetBodyLockInterface(), id);
if (lock.Succeeded())
{
const Body &body = lock.GetBody();
// Shift the transform back to the origin
RMat44 transform = body.GetCenterOfMassTransform();
transform.SetTranslation(transform.GetTranslation() - pile.mOrigin);
// Draw distance label for the first body
if (first)
{
mDebugRenderer->DrawText3D(transform.GetTranslation(), pile.GetLabel().c_str(), color, 0.2f);
first = false;
}
#ifdef JPH_DEBUG_RENDERER
// Draw the shape
body.GetShape()->Draw(mDebugRenderer, transform, Vec3::sOne(), color, false, sDrawWireframe);
#endif // JPH_DEBUG_RENDERER
}
}
}
}
pile_idx++;
}
}
void BigWorldTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
// Draw in wireframe?
inUI->CreateCheckBox(inSubMenu, "Draw distant scenes in wireframe", sDrawWireframe, [](UICheckBox::EState inState) { sDrawWireframe = inState == UICheckBox::STATE_CHECKED; });
// Enable / disable drawing of a particular distance
int pile_idx = 0;
for (Pile &pile : mPiles)
if (!pile.mOrigin.IsNearZero())
{
uint32 mask = 1 << pile_idx;
inUI->CreateCheckBox(inSubMenu, "Draw pile at " + pile.GetLabel(), (sDrawPileMask & mask) != 0, [mask](UICheckBox::EState inState) { if (inState == UICheckBox::STATE_CHECKED) sDrawPileMask |= mask; else sDrawPileMask &= ~mask; });
pile_idx++;
}
// Goto pile at a particular distance
for (Pile &pile : mPiles)
inUI->CreateTextButton(inSubMenu, "Goto pile at " + pile.GetLabel(), [this, &pile]() { sPivot = pile.mOrigin; RestartTest(); });
}
RMat44 BigWorldTest::GetCameraPivot(float inCameraHeading, float inCameraPitch) const
{
return RMat44::sTranslation(sPivot);
}
RVec3 BigWorldTest::GetDrawOffset() const
{
return sPivot;
}

View File

@@ -0,0 +1,63 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class BigWorldTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, BigWorldTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Tests the stability of a pile of ragdolls on a terrain at various distances from the origin.\n"
"Renders far away ragdoll piles at the origin in wireframe.";
}
// Destructor
virtual ~BigWorldTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Optional settings menu
virtual bool HasSettingsMenu() const override { return true; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
virtual RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
virtual RVec3 GetDrawOffset() const override;
#ifndef JPH_DOUBLE_PRECISION
virtual String GetStatusString() const override { return "Define JPH_DOUBLE_PRECISION for an accurate simulation!"; }
#endif // JPH_DOUBLE_PRECISION
private:
// If we want to draw the further scenes in wireframe
inline static bool sDrawWireframe = true;
// A bitfield that determines which piles to draw
inline static uint32 sDrawPileMask = ~uint32(0);
// Pivot for the camera
inline static RVec3 sPivot { RVec3::sZero() };
// Bookkeeping for a pile
struct Pile
{
// Distance label for this test
String GetLabel() const { return StringFormat("%.0f km", 1.0e-3 * double(mOrigin.Length())); }
RVec3 mOrigin; ///< Origin for this pile
Array<Ref<Ragdoll>> mRagdolls; ///< Ragdolls in the pile
};
Array<Pile> mPiles;
};

View File

@@ -0,0 +1,31 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/CreateRigTest.h>
#include <Application/DebugUI.h>
#include <Utils/RagdollLoader.h>
#include <Layers.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(CreateRigTest)
{
JPH_ADD_BASE_CLASS(CreateRigTest, Test)
}
CreateRigTest::~CreateRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void CreateRigTest::Initialize()
{
// Floor
CreateFloor();
// Create ragdoll
Ref<RagdollSettings> settings = RagdollLoader::sCreate();
mRagdoll = settings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
}

View File

@@ -0,0 +1,32 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class CreateRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CreateRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Demonstrates how to create a ragdoll from code.";
}
// Destructor
virtual ~CreateRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
private:
// Our ragdoll
Ref<Ragdoll> mRagdoll;
};

View File

@@ -0,0 +1,92 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/KinematicRigTest.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/StateRecorder.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(KinematicRigTest)
{
JPH_ADD_BASE_CLASS(KinematicRigTest, Test)
}
KinematicRigTest::~KinematicRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void KinematicRigTest::Initialize()
{
// Floor
CreateFloor();
// Wall
RefConst<Shape> box_shape = new BoxShape(Vec3(0.2f, 0.2f, 0.2f), 0.01f);
for (int i = 0; i < 3; ++i)
for (int j = i / 2; j < 10 - (i + 1) / 2; ++j)
{
RVec3 position(-2.0f + j * 0.4f + (i & 1? 0.2f : 0.0f), 0.2f + i * 0.4f, -2.0f);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(box_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::DontActivate);
}
// Bar to hit head against
// (this should not affect the kinematic ragdoll)
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(2.0f, 0.1f, 0.1f), 0.01f), RVec3(0, 1.5f, -2.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
// Load ragdoll
mRagdollSettings = RagdollLoader::sLoad("Human.tof", EMotionType::Kinematic);
// Create ragdoll
mRagdoll = mRagdollSettings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
// Load animation
AssetStream stream("Human/walk.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), mAnimation))
FatalError("Could not open animation");
// Initialize pose
mPose.SetSkeleton(mRagdollSettings->GetSkeleton());
// Position ragdoll
mAnimation->Sample(0.0f, mPose);
mPose.CalculateJointMatrices();
mRagdoll->SetPose(mPose);
}
void KinematicRigTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Sample previous pose and draw it (ragdoll should have achieved this position)
mAnimation->Sample(mTime, mPose);
mPose.CalculateJointMatrices();
#ifdef JPH_DEBUG_RENDERER
mPose.Draw(*inParams.mPoseDrawSettings, mDebugRenderer);
#endif // JPH_DEBUG_RENDERER
// Update time
mTime += inParams.mDeltaTime;
// Sample new pose
mAnimation->Sample(mTime, mPose);
mPose.CalculateJointMatrices();
mRagdoll->DriveToPoseUsingKinematics(mPose, inParams.mDeltaTime);
}
void KinematicRigTest::SaveState(StateRecorder &inStream) const
{
inStream.Write(mTime);
}
void KinematicRigTest::RestoreState(StateRecorder &inStream)
{
inStream.Read(mTime);
}

View File

@@ -0,0 +1,45 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Skeleton/Skeleton.h>
#include <Jolt/Skeleton/SkeletalAnimation.h>
#include <Jolt/Skeleton/SkeletonPose.h>
#include <Utils/RagdollLoader.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class KinematicRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, KinematicRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Tests a kinematic ragdoll moving towards a wall of boxes.\n"
"Also demonstrates that kinematic bodies don't get influenced by static bodies.";
}
// Destructor
virtual ~KinematicRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
private:
float mTime = 0.0f;
Ref<RagdollSettings> mRagdollSettings;
Ref<Ragdoll> mRagdoll;
Ref<SkeletalAnimation> mAnimation;
SkeletonPose mPose;
};

View File

@@ -0,0 +1,53 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/LoadRigTest.h>
#include <Application/DebugUI.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(LoadRigTest)
{
JPH_ADD_BASE_CLASS(LoadRigTest, Test)
}
LoadRigTest::ConstraintNameAndType LoadRigTest::sTypes[] =
{
{ "Fixed", EConstraintOverride::TypeFixed },
{ "Point", EConstraintOverride::TypePoint },
{ "Hinge", EConstraintOverride::TypeHinge },
{ "Slider", EConstraintOverride::TypeSlider },
{ "Cone", EConstraintOverride::TypeCone },
{ "Ragdoll", EConstraintOverride::TypeRagdoll },
};
EConstraintOverride LoadRigTest::sConstraintType = EConstraintOverride::TypeRagdoll;
LoadRigTest::~LoadRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void LoadRigTest::Initialize()
{
// Floor
CreateFloor();
// Load ragdoll
mRagdollSettings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic, sConstraintType);
// Create ragdoll
mRagdoll = mRagdollSettings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
}
void LoadRigTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
inUI->CreateTextButton(inSubMenu, "Constraint Type", [this, inUI]() {
UIElement *constraint_type = inUI->CreateMenu();
for (uint i = 0; i < size(sTypes); ++i)
inUI->CreateTextButton(constraint_type, sTypes[i].mName, [this, i]() { sConstraintType = sTypes[i].mType; RestartTest(); });
inUI->ShowMenu(constraint_type);
});
}

View File

@@ -0,0 +1,50 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
#include <Utils/RagdollLoader.h>
class LoadRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, LoadRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Loads a ragdoll from disc and simulates it.";
}
// Destructor
virtual ~LoadRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
// Optional settings menu
virtual bool HasSettingsMenu() const override { return true; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
private:
// Our ragdoll
Ref<RagdollSettings> mRagdollSettings;
Ref<Ragdoll> mRagdoll;
struct ConstraintNameAndType
{
const char * mName;
EConstraintOverride mType;
};
// List of possible constraint types and their names
static ConstraintNameAndType sTypes[];
// Type of constraints to create for this test
static EConstraintOverride sConstraintType;
};

View File

@@ -0,0 +1,57 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/LoadSaveBinaryRigTest.h>
#include <Jolt/Core/StreamWrapper.h>
#include <Jolt/Physics/Constraints/DistanceConstraint.h>
#include <Utils/Log.h>
#include <Utils/RagdollLoader.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(LoadSaveBinaryRigTest)
{
JPH_ADD_BASE_CLASS(LoadSaveBinaryRigTest, Test)
}
LoadSaveBinaryRigTest::~LoadSaveBinaryRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void LoadSaveBinaryRigTest::Initialize()
{
// Floor
CreateFloor();
stringstream data;
{
// Load ragdoll
Ref<RagdollSettings> settings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Add an additional constraint between the left and right arm to test loading/saving of additional constraints
const Skeleton *skeleton = settings->GetSkeleton();
int left_arm = skeleton->GetJointIndex("L_Wrist_sjnt_0");
int right_arm = skeleton->GetJointIndex("R_Wrist_sjnt_0");
Ref<DistanceConstraintSettings> constraint = new DistanceConstraintSettings;
constraint->mSpace = EConstraintSpace::LocalToBodyCOM;
constraint->mMaxDistance = 0.1f;
constraint->mMinDistance = 0.1f;
settings->mAdditionalConstraints.push_back(RagdollSettings::AdditionalConstraint(left_arm, right_arm , constraint));
// Save it to a binary stream
StreamOutWrapper stream_out(data);
settings->SaveBinaryState(stream_out, true /* Save shape */, true /* Save group filter */);
}
StreamInWrapper stream_in(data);
RagdollSettings::RagdollResult result = RagdollSettings::sRestoreFromBinaryState(stream_in);
if (result.HasError())
FatalError(result.GetError().c_str());
// Create ragdoll
mRagdoll = result.Get()->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
}

View File

@@ -0,0 +1,32 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class LoadSaveBinaryRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, LoadSaveBinaryRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Loads a ragdoll from disc, writes it to a binary stream, loads it again and simulates it.";
}
// Destructor
virtual ~LoadSaveBinaryRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
private:
// Our ragdoll
Ref<Ragdoll> mRagdoll;
};

View File

@@ -0,0 +1,61 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/LoadSaveRigTest.h>
#include <Jolt/Physics/Constraints/DistanceConstraint.h>
#include <Jolt/ObjectStream/ObjectStreamOut.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Utils/Log.h>
#include <Utils/RagdollLoader.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(LoadSaveRigTest)
{
JPH_ADD_BASE_CLASS(LoadSaveRigTest, Test)
}
LoadSaveRigTest::~LoadSaveRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void LoadSaveRigTest::Initialize()
{
// Floor
CreateFloor();
stringstream data;
{
// Load ragdoll
Ref<RagdollSettings> settings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Add an additional constraint between the left and right arm to test loading/saving of additional constraints
const Skeleton *skeleton = settings->GetSkeleton();
int left_arm = skeleton->GetJointIndex("L_Wrist_sjnt_0");
int right_arm = skeleton->GetJointIndex("R_Wrist_sjnt_0");
Ref<DistanceConstraintSettings> constraint = new DistanceConstraintSettings;
constraint->mSpace = EConstraintSpace::LocalToBodyCOM;
constraint->mMaxDistance = 0.1f;
constraint->mMinDistance = 0.1f;
settings->mAdditionalConstraints.push_back(RagdollSettings::AdditionalConstraint(left_arm, right_arm , constraint));
// Write ragdoll
if (!ObjectStreamOut::sWriteObject(data, ObjectStream::EStreamType::Text, *settings))
FatalError("Failed to save ragdoll");
}
// Read ragdoll back in
Ref<RagdollSettings> settings;
if (!ObjectStreamIn::sReadObject(data, settings))
FatalError("Failed to load ragdoll");
// Parent joint indices are not stored so need to be calculated again
settings->GetSkeleton()->CalculateParentJointIndices();
// Create ragdoll
mRagdoll = settings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
}

View File

@@ -0,0 +1,32 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class LoadSaveRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, LoadSaveRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Loads a ragdoll from disc, writes it to an object stream, loads it again and simulates it.";
}
// Destructor
virtual ~LoadSaveRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
private:
// Our ragdoll
Ref<Ragdoll> mRagdoll;
};

View File

@@ -0,0 +1,104 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/PoweredRigTest.h>
#include <Jolt/Physics/StateRecorder.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Application/DebugUI.h>
#include <Utils/RagdollLoader.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(PoweredRigTest)
{
JPH_ADD_BASE_CLASS(PoweredRigTest, Test)
}
const char *PoweredRigTest::sAnimations[] =
{
"neutral",
"walk",
"sprint",
"dead_pose1",
"dead_pose2",
"dead_pose3",
"dead_pose4"
};
const char *PoweredRigTest::sAnimationName = "sprint";
PoweredRigTest::~PoweredRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void PoweredRigTest::Initialize()
{
// Floor
CreateFloor();
// Load ragdoll
mRagdollSettings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Create ragdoll
mRagdoll = mRagdollSettings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
// Load animation
AssetStream stream(String("Human/") + sAnimationName + ".tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), mAnimation))
FatalError("Could not open animation");
// Initialize pose
mPose.SetSkeleton(mRagdollSettings->GetSkeleton());
// Position ragdoll
mAnimation->Sample(0.0f, mPose);
mPose.CalculateJointMatrices();
mRagdoll->SetPose(mPose);
}
void PoweredRigTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Update time
mTime += inParams.mDeltaTime;
// Sample new pose
mAnimation->Sample(mTime, mPose);
// Place the root joint on the first body so that we draw the pose in the right place
RVec3 root_offset;
SkeletonPose::JointState &joint = mPose.GetJoint(0);
joint.mTranslation = Vec3::sZero(); // All the translation goes into the root offset
mRagdoll->GetRootTransform(root_offset, joint.mRotation);
mPose.SetRootOffset(root_offset);
mPose.CalculateJointMatrices();
#ifdef JPH_DEBUG_RENDERER
mPose.Draw(*inParams.mPoseDrawSettings, mDebugRenderer);
#endif // JPH_DEBUG_RENDERER
mRagdoll->DriveToPoseUsingMotors(mPose);
}
void PoweredRigTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
inUI->CreateTextButton(inSubMenu, "Select Animation", [this, inUI]() {
UIElement *animation_name = inUI->CreateMenu();
for (uint i = 0; i < size(sAnimations); ++i)
inUI->CreateTextButton(animation_name, sAnimations[i], [this, i]() { sAnimationName = sAnimations[i]; RestartTest(); });
inUI->ShowMenu(animation_name);
});
}
void PoweredRigTest::SaveState(StateRecorder &inStream) const
{
inStream.Write(mTime);
}
void PoweredRigTest::RestoreState(StateRecorder &inStream)
{
inStream.Read(mTime);
}

View File

@@ -0,0 +1,53 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Skeleton/Skeleton.h>
#include <Jolt/Skeleton/SkeletalAnimation.h>
#include <Jolt/Skeleton/SkeletonPose.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class PoweredRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, PoweredRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Demonstrates how to use motors to drive a ragdoll to a pose.";
}
// Destructor
virtual ~PoweredRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Optional settings menu
virtual bool HasSettingsMenu() const override { return true; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
private:
// List of possible animation names
static const char * sAnimations[];
// Filename of animation to load for this test
static const char * sAnimationName;
float mTime = 0.0f;
Ref<RagdollSettings> mRagdollSettings;
Ref<Ragdoll> mRagdoll;
Ref<SkeletalAnimation> mAnimation;
SkeletonPose mPose;
};

View File

@@ -0,0 +1,151 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/RigPileTest.h>
#include <Jolt/Physics/PhysicsScene.h>
#include <Jolt/Physics/Collision/RayCast.h>
#include <Jolt/Physics/Collision/CastResult.h>
#include <Jolt/Skeleton/Skeleton.h>
#include <Jolt/Skeleton/SkeletalAnimation.h>
#include <Jolt/Skeleton/SkeletonPose.h>
#include <Jolt/Core/StringTools.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Utils/RagdollLoader.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
#include <random>
JPH_IMPLEMENT_RTTI_VIRTUAL(RigPileTest)
{
JPH_ADD_BASE_CLASS(RigPileTest, Test)
}
const char *RigPileTest::sScenes[] =
{
"PerlinMesh",
"PerlinHeightField",
"Terrain1",
"Terrain2",
};
#ifdef JPH_DEBUG
const char *RigPileTest::sSceneName = "PerlinMesh";
int RigPileTest::sPileSize = 5;
int RigPileTest::sNumPilesPerAxis = 2;
#else
const char *RigPileTest::sSceneName = "Terrain1";
int RigPileTest::sPileSize = 10;
int RigPileTest::sNumPilesPerAxis = 4;
#endif
RigPileTest::~RigPileTest()
{
for (Ragdoll *r : mRagdolls)
r->RemoveFromPhysicsSystem();
}
void RigPileTest::Initialize()
{
if (strcmp(sSceneName, "PerlinMesh") == 0)
{
// Default terrain
CreateMeshTerrain();
}
else if (strcmp(sSceneName, "PerlinHeightField") == 0)
{
// Default terrain
CreateHeightFieldTerrain();
}
else
{
// Load scene
Ref<PhysicsScene> scene;
AssetStream stream(String(sSceneName) + ".bof", std::ios::in | std::ios::binary);
if (!ObjectStreamIn::sReadObject(stream.Get(), scene))
FatalError("Failed to load scene");
for (BodyCreationSettings &body : scene->GetBodies())
body.mObjectLayer = Layers::NON_MOVING;
scene->FixInvalidScales();
scene->CreateBodies(mPhysicsSystem);
}
// Load ragdoll
Ref<RagdollSettings> settings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Load animation
const int cAnimationCount = 4;
Ref<SkeletalAnimation> animation[cAnimationCount];
for (int i = 0; i < cAnimationCount; ++i)
{
AssetStream stream(StringFormat("Human/dead_pose%d.tof", i + 1), std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), animation[i]))
FatalError("Could not open animation");
}
const float cHorizontalSeparation = 4.0f;
const float cVerticalSeparation = 0.6f;
// Limit the size of the piles so we don't go over 160 ragdolls
int pile_size = min(sPileSize, 160 / Square(sNumPilesPerAxis));
// Create piles
default_random_engine random;
uniform_real_distribution<float> angle(0.0f, JPH_PI);
CollisionGroup::GroupID group_id = 1;
for (int row = 0; row < sNumPilesPerAxis; ++row)
for (int col = 0; col < sNumPilesPerAxis; ++col)
{
// Determine start location of ray
RVec3 start = RVec3(cHorizontalSeparation * (col - (sNumPilesPerAxis - 1) / 2.0f), 100, cHorizontalSeparation * (row - (sNumPilesPerAxis - 1) / 2.0f));
// Cast ray down to terrain
RayCastResult hit;
Vec3 ray_direction(0, -200, 0);
RRayCast ray { start, ray_direction };
if (mPhysicsSystem->GetNarrowPhaseQuery().CastRay(ray, hit, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING)))
start = ray.GetPointOnRay(hit.mFraction);
for (int i = 0; i < pile_size; ++i)
{
// Create ragdoll
Ref<Ragdoll> ragdoll = settings->CreateRagdoll(group_id++, 0, mPhysicsSystem);
// Sample pose
SkeletonPose pose;
pose.SetSkeleton(settings->GetSkeleton());
animation[random() % cAnimationCount]->Sample(0.0f, pose);
// Override root
pose.SetRootOffset(start);
SkeletonPose::JointState &root = pose.GetJoint(0);
root.mTranslation = Vec3(0, cVerticalSeparation * (i + 1), 0);
root.mRotation = Quat::sRotation(Vec3::sAxisY(), angle(random)) * root.mRotation;
pose.CalculateJointMatrices();
// Drive to pose
ragdoll->SetPose(pose);
ragdoll->DriveToPoseUsingMotors(pose);
ragdoll->AddToPhysicsSystem(EActivation::Activate);
mRagdolls.push_back(ragdoll);
}
}
}
void RigPileTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
inUI->CreateTextButton(inSubMenu, "Select Scene", [this, inUI]() {
UIElement *scene_name = inUI->CreateMenu();
for (uint i = 0; i < size(sScenes); ++i)
inUI->CreateTextButton(scene_name, sScenes[i], [this, i]() { sSceneName = sScenes[i]; RestartTest(); });
inUI->ShowMenu(scene_name);
});
inUI->CreateSlider(inSubMenu, "Num Ragdolls Per Pile", float(sPileSize), 1, 160, 1, [](float inValue) { sPileSize = (int)inValue; });
inUI->CreateSlider(inSubMenu, "Num Piles Per Axis", float(sNumPilesPerAxis), 1, 4, 1, [](float inValue) { sNumPilesPerAxis = (int)inValue; });
}

View File

@@ -0,0 +1,48 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class RigPileTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, RigPileTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Tests the performance of a pile of ragdolls on a terrain.";
}
// Destructor
virtual ~RigPileTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
// Optional settings menu
virtual bool HasSettingsMenu() const override { return true; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
private:
// List of possible scene names
static const char * sScenes[];
// Filename of animation to load for this test
static const char * sSceneName;
// Number of ragdolls per pile
static int sPileSize;
// Number of piles per axis
static int sNumPilesPerAxis;
// All active ragdolls
Array<Ref<Ragdoll>> mRagdolls;
};

View File

@@ -0,0 +1,157 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/SkeletonMapperTest.h>
#include <Jolt/Physics/StateRecorder.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Renderer/DebugRendererImp.h>
#include <Layers.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
#include <Application/DebugUI.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(SkeletonMapperTest)
{
JPH_ADD_BASE_CLASS(SkeletonMapperTest, Test)
}
SkeletonMapperTest::~SkeletonMapperTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void SkeletonMapperTest::Initialize()
{
// Floor
CreateFloor();
// Load ragdoll
mRagdollSettings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Create ragdoll
mRagdoll = mRagdollSettings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
// Load neutral animation for ragdoll
Ref<SkeletalAnimation> neutral_ragdoll;
{
AssetStream stream("Human/neutral.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), neutral_ragdoll))
FatalError("Could not open neutral animation");
}
// Load animation skeleton
Ref<Skeleton> animation_skeleton;
{
AssetStream stream("Human/skeleton_hd.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), animation_skeleton))
FatalError("Could not open skeleton_hd");
animation_skeleton->CalculateParentJointIndices();
}
// Load neutral animation
Ref<SkeletalAnimation> neutral_animation;
{
AssetStream stream("Human/neutral_hd.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), neutral_animation))
FatalError("Could not open neutral_hd animation");
}
// Load test animation
{
AssetStream stream("Human/jog_hd.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), mAnimation))
FatalError("Could not open jog_hd animation");
}
// Initialize pose
mAnimatedPose.SetSkeleton(animation_skeleton);
mRagdollPose.SetSkeleton(mRagdollSettings->GetSkeleton());
// Calculate neutral poses and initialize skeleton mapper
neutral_ragdoll->Sample(0.0f, mRagdollPose);
mRagdollPose.CalculateJointMatrices();
neutral_animation->Sample(0.0f, mAnimatedPose);
mAnimatedPose.CalculateJointMatrices();
mRagdollToAnimated.Initialize(mRagdollPose.GetSkeleton(), mRagdollPose.GetJointMatrices().data(), mAnimatedPose.GetSkeleton(), mAnimatedPose.GetJointMatrices().data());
// Optionally lock translations (this can be used to prevent ragdolls from stretching)
// Try wildly dragging the ragdoll by the head (using spacebar) to see how the ragdoll stretches under stress
if (sLockTranslations)
mRagdollToAnimated.LockAllTranslations(mAnimatedPose.GetSkeleton(), mAnimatedPose.GetJointMatrices().data());
// Calculate initial pose and set it
CalculateRagdollPose();
mRagdoll->SetPose(mRagdollPose);
}
void SkeletonMapperTest::CalculateRagdollPose()
{
// Sample new animated pose
mAnimation->Sample(mTime, mAnimatedPose);
mAnimatedPose.CalculateJointMatrices();
// Map to ragdoll pose
mRagdollToAnimated.MapReverse(mAnimatedPose.GetJointMatrices().data(), mRagdollPose.GetJointMatrices().data());
mRagdollPose.CalculateJointStates();
}
void SkeletonMapperTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Update time
mTime += inParams.mDeltaTime;
// Drive the ragdoll pose and drive motors to reach it
CalculateRagdollPose();
mRagdoll->DriveToPoseUsingMotors(mRagdollPose);
#ifdef JPH_DEBUG_RENDERER
// Draw animated skeleton
mAnimatedPose.Draw(*inParams.mPoseDrawSettings, mDebugRenderer);
mDebugRenderer->DrawText3D(mAnimatedPose.GetRootOffset() + mAnimatedPose.GetJointMatrix(0).GetTranslation(), "Animated", Color::sWhite, 0.2f);
// Draw mapped skeleton
RMat44 offset = RMat44::sTranslation(RVec3(1.0f, 0, 0));
mRagdollPose.Draw(*inParams.mPoseDrawSettings, mDebugRenderer, offset);
mDebugRenderer->DrawText3D(offset * (mAnimatedPose.GetRootOffset() + mAnimatedPose.GetJointMatrix(0).GetTranslation()), "Reverse Mapped", Color::sWhite, 0.2f);
#endif // JPH_DEBUG_RENDERER
// Get ragdoll pose in model space
RVec3 root_offset;
Array<Mat44> pose1_model(mRagdollPose.GetJointCount());
mRagdoll->GetPose(root_offset, pose1_model.data());
// Get animated pose in local space
Array<Mat44> pose2_local(mAnimatedPose.GetJointCount());
mAnimatedPose.CalculateLocalSpaceJointMatrices(pose2_local.data());
// Map ragdoll to animated pose, filling in the extra joints using the local space animated pose
SkeletonPose pose2_world;
pose2_world.SetSkeleton(mAnimatedPose.GetSkeleton());
pose2_world.SetRootOffset(root_offset);
mRagdollToAnimated.Map(pose1_model.data(), pose2_local.data(), pose2_world.GetJointMatrices().data());
#ifdef JPH_DEBUG_RENDERER
// Draw mapped pose
pose2_world.Draw(*inParams.mPoseDrawSettings, mDebugRenderer, offset);
mDebugRenderer->DrawText3D(offset * pose2_world.GetJointMatrix(1).GetTranslation(), "Mapped", Color::sWhite, 0.2f);
#endif // JPH_DEBUG_RENDERER
}
void SkeletonMapperTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
inUI->CreateCheckBox(inSubMenu, "Lock Translations", sLockTranslations, [this](UICheckBox::EState inState) { sLockTranslations = inState == UICheckBox::STATE_CHECKED; RestartTest(); });
}
void SkeletonMapperTest::SaveState(StateRecorder &inStream) const
{
inStream.Write(mTime);
}
void SkeletonMapperTest::RestoreState(StateRecorder &inStream)
{
inStream.Read(mTime);
}

View File

@@ -0,0 +1,55 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Skeleton/Skeleton.h>
#include <Jolt/Skeleton/SkeletalAnimation.h>
#include <Jolt/Skeleton/SkeletonPose.h>
#include <Jolt/Skeleton/SkeletonMapper.h>
#include <Utils/RagdollLoader.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class SkeletonMapperTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SkeletonMapperTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Shows how you can map a high detail animation skeleton on a low detail physics skeleton and back.";
}
// Destructor
virtual ~SkeletonMapperTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Optional settings menu
virtual bool HasSettingsMenu() const override { return true; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
private:
inline static bool sLockTranslations = false;
void CalculateRagdollPose();
float mTime = 0.0f;
Ref<RagdollSettings> mRagdollSettings;
Ref<Ragdoll> mRagdoll;
Ref<SkeletalAnimation> mAnimation;
SkeletonMapper mRagdollToAnimated;
SkeletonPose mAnimatedPose;
SkeletonPose mRagdollPose;
};

View File

@@ -0,0 +1,103 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Rig/SoftKeyframedRigTest.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/StateRecorder.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(SoftKeyframedRigTest)
{
JPH_ADD_BASE_CLASS(SoftKeyframedRigTest, Test)
}
SoftKeyframedRigTest::~SoftKeyframedRigTest()
{
mRagdoll->RemoveFromPhysicsSystem();
}
void SoftKeyframedRigTest::Initialize()
{
// Floor
CreateFloor();
// Wall
RefConst<Shape> box_shape = new BoxShape(Vec3(0.2f, 0.2f, 0.2f), 0.01f);
for (int i = 0; i < 3; ++i)
for (int j = i / 2; j < 10 - (i + 1) / 2; ++j)
{
RVec3 position(-2.0f + j * 0.4f + (i & 1? 0.2f : 0.0f), 0.2f + i * 0.4f, -2.0f);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(box_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::DontActivate);
}
// Bar to hit head against
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(2.0f, 0.1f, 0.1f), 0.01f), RVec3(0, 1.5f, -2.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
// Load ragdoll
mRagdollSettings = RagdollLoader::sLoad("Human.tof", EMotionType::Dynamic);
// Limit max velocity of the bodies to avoid excessive jittering when the head hits the bar
// Note that this also limits how fast an animation can be and as a result you can see
// the ragdolls lag behind when the animation loops.
// Note that the velocity doesn't need to be limited at body level, it can also be done
// by calculating the needed velocities and clamping them instead of calling DriveToPoseUsingKinematics.
for (BodyCreationSettings &bcs : mRagdollSettings->mParts)
bcs.mMaxLinearVelocity = 10.0f;
// Create ragdoll
mRagdoll = mRagdollSettings->CreateRagdoll(0, 0, mPhysicsSystem);
mRagdoll->AddToPhysicsSystem(EActivation::Activate);
// Load animation
AssetStream stream("Human/walk.tof", std::ios::in);
if (!ObjectStreamIn::sReadObject(stream.Get(), mAnimation))
FatalError("Could not open animation");
// Initialize pose
mPose.SetSkeleton(mRagdollSettings->GetSkeleton());
// Position ragdoll
mAnimation->Sample(0.0f, mPose);
mPose.CalculateJointMatrices();
mRagdoll->SetPose(mPose);
}
void SoftKeyframedRigTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Sample previous pose and draw it (ragdoll should have achieved this position)
mAnimation->Sample(mTime, mPose);
mPose.CalculateJointMatrices();
#ifdef JPH_DEBUG_RENDERER
mPose.Draw(*inParams.mPoseDrawSettings, mDebugRenderer);
#endif // JPH_DEBUG_RENDERER
// Update time
mTime += inParams.mDeltaTime;
// Sample new pose
mAnimation->Sample(mTime, mPose);
mPose.CalculateJointMatrices();
// Drive the ragdoll by setting velocities
mRagdoll->DriveToPoseUsingKinematics(mPose, inParams.mDeltaTime);
// Cancel gravity that will be applied in the next step
mRagdoll->AddLinearVelocity(mPhysicsSystem->GetGravity() * inParams.mDeltaTime);
}
void SoftKeyframedRigTest::SaveState(StateRecorder &inStream) const
{
inStream.Write(mTime);
}
void SoftKeyframedRigTest::RestoreState(StateRecorder &inStream)
{
inStream.Read(mTime);
}

View File

@@ -0,0 +1,46 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Skeleton/Skeleton.h>
#include <Jolt/Skeleton/SkeletalAnimation.h>
#include <Jolt/Skeleton/SkeletonPose.h>
#include <Utils/RagdollLoader.h>
#include <Jolt/Physics/Ragdoll/Ragdoll.h>
class SoftKeyframedRigTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SoftKeyframedRigTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Tests a soft keyframed ragdoll moving towards a wall of boxes.\n"
"This applies velocities to dynamic bodies to force the ragdoll to follow an animation.\n"
"Since the bodies are dynamic, they will collide with static objects.";
}
// Destructor
virtual ~SoftKeyframedRigTest() override;
// Number used to scale the terrain and camera movement to the scene
virtual float GetWorldScale() const override { return 0.2f; }
virtual void Initialize() override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
private:
float mTime = 0.0f;
Ref<RagdollSettings> mRagdollSettings;
Ref<Ragdoll> mRagdoll;
Ref<SkeletalAnimation> mAnimation;
SkeletonPose mPose;
};