Browse Source

Added soft keyframed rig example

Jorrit Rouwe 1 week ago
parent
commit
8b5cab66f5

+ 2 - 0
Samples/Samples.cmake

@@ -309,6 +309,8 @@ if (ENABLE_OBJECT_STREAM)
 		${SAMPLES_ROOT}/Tests/Rig/RigPileTest.h
 		${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.cpp
 		${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.h
+		${SAMPLES_ROOT}/Tests/Rig/SoftKeyFramedRigTest.cpp
+		${SAMPLES_ROOT}/Tests/Rig/SoftKeyFramedRigTest.h
 	)
 endif()
 

+ 2 - 0
Samples/SamplesApp.cpp

@@ -286,6 +286,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, CreateRigTest)
 #ifdef JPH_OBJECT_STREAM
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, LoadRigTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, KinematicRigTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftKeyframedRigTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, PoweredRigTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, RigPileTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, LoadSaveRigTest)
@@ -302,6 +303,7 @@ static TestNameAndRTTI sRigTests[] =
 	{ "Load / Save Rig",					JPH_RTTI(LoadSaveRigTest) },
 	{ "Load / Save Binary Rig",				JPH_RTTI(LoadSaveBinaryRigTest) },
 	{ "Kinematic Rig",						JPH_RTTI(KinematicRigTest) },
+	{ "Soft Keyframed Rig",					JPH_RTTI(SoftKeyframedRigTest) },
 	{ "Powered Rig",						JPH_RTTI(PoweredRigTest) },
 	{ "Skeleton Mapper",					JPH_RTTI(SkeletonMapperTest) },
 	{ "Rig Pile",							JPH_RTTI(RigPileTest) },

+ 5 - 24
Samples/Tests/Rig/KinematicRigTest.cpp

@@ -18,19 +18,6 @@ JPH_IMPLEMENT_RTTI_VIRTUAL(KinematicRigTest)
 	JPH_ADD_BASE_CLASS(KinematicRigTest, Test)
 }
 
-const char *KinematicRigTest::sAnimations[] =
-{
-	"neutral",
-	"walk",
-	"sprint",
-	"dead_pose1",
-	"dead_pose2",
-	"dead_pose3",
-	"dead_pose4"
-};
-
-const char *KinematicRigTest::sAnimationName = "walk";
-
 KinematicRigTest::~KinematicRigTest()
 {
 	mRagdoll->RemoveFromPhysicsSystem();
@@ -50,6 +37,10 @@ void KinematicRigTest::Initialize()
 			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);
 
@@ -58,7 +49,7 @@ void KinematicRigTest::Initialize()
 	mRagdoll->AddToPhysicsSystem(EActivation::Activate);
 
 	// Load animation
-	AssetStream stream(String("Human/") + sAnimationName + ".tof", std::ios::in);
+	AssetStream stream("Human/walk.tof", std::ios::in);
 	if (!ObjectStreamIn::sReadObject(stream.Get(), mAnimation))
 		FatalError("Could not open animation");
 
@@ -90,16 +81,6 @@ void KinematicRigTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	mRagdoll->DriveToPoseUsingKinematics(mPose, inParams.mDeltaTime);
 }
 
-void KinematicRigTest::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 KinematicRigTest::SaveState(StateRecorder &inStream) const
 {
 	inStream.Write(mTime);

+ 2 - 11
Samples/Tests/Rig/KinematicRigTest.h

@@ -19,7 +19,8 @@ public:
 	// Description of the test
 	virtual const char *	GetDescription() const override
 	{
-		return "Tests a kinematic ragdoll moving towards a wall of boxes.";
+		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
@@ -31,21 +32,11 @@ public:
 	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;

+ 103 - 0
Samples/Tests/Rig/SoftKeyframedRigTest.cpp

@@ -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);
+}

+ 46 - 0
Samples/Tests/Rig/SoftKeyframedRigTest.h

@@ -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;
+};