You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/EMotionFX/Code/Tests/BlendTreeFootIKNodeTests.cpp

388 lines
17 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "JackGraphFixture.h"
#include <EMotionFX/Source/AnimGraph.h>
#include <EMotionFX/Source/AnimGraphStateMachine.h>
#include <EMotionFX/Source/BlendTree.h>
#include <EMotionFX/Source/BlendTreeFootIKNode.h>
#include <EMotionFX/Source/AnimGraphBindPoseNode.h>
#include <EMotionFX/Source/AnimGraphStateMachine.h>
#include <EMotionFX/Source/EMotionFXManager.h>
#include <EMotionFX/Source/Parameter/FloatSliderParameter.h>
#include <EMotionFX/Source/Parameter/ParameterFactory.h>
#include <EMotionFX/Source/Parameter/ValueParameter.h>
#include <EMotionFX/Source/BlendTreeParameterNode.h>
#include <EMotionFX/Source/TransformData.h>
#include <EMotionFX/Source/Pose.h>
#include <EMotionFX/Source/AnimGraphPose.h>
#include <EMotionFX/Source/ActorInstance.h>
#include <EMotionFX/Source/Skeleton.h>
#include <MCore/Source/AzCoreConversions.h>
#include <Integration/AnimationBus.h>
#include <AzCore/Math/Plane.h>
namespace EMotionFX
{
class BlendTreeFootIKNodeTests
: public JackGraphFixture
, private EMotionFX::Integration::RaycastRequestBus::Handler
{
public:
RaycastRequests::RaycastResult Raycast([[maybe_unused]] AZ::EntityId entityId, const RaycastRequests::RaycastRequest& rayRequest) override
{
RaycastRequests::RaycastResult result;
//
// z
//
// a xxxxxxxx (z = m_rightSideHeight)
// x xxxxxxxx
// i xxxxxxxx
// s --------|---------- (z = m_leftSideHeight)
// 0 (x-axis)
//
// The right side (as seen from the character's eyes), has a given height, as well as the left side.
// The split is at x coordinate 0. So everything with a value smaller than 0 will have 'm_rightSideHeight' and everything
// with with a value >= 0.0f will have a height of 'm_leftSideHeight'.
const AZ::Vector3 planeNormal = (m_sceneTransform.TransformPoint(AZ::Vector3(0.0f, 0.0f, 1.0f))).GetNormalizedSafe();
const AZ::Vector3 pointOnUpperPlane = m_sceneTransform.TransformPoint(AZ::Vector3(0.0f, 0.0f, m_rightSideHeight));
const AZ::Vector3 pointOnLowerPlane = m_sceneTransform.TransformPoint(AZ::Vector3(0.0f, 0.0f, m_leftSideHeight));
const AZ::Plane upperPlane = AZ::Plane::CreateFromNormalAndPoint(planeNormal, pointOnUpperPlane);
const AZ::Plane lowerPlane = AZ::Plane::CreateFromNormalAndPoint(planeNormal, pointOnLowerPlane);
AZ::Vector3 intersectionUpper = AZ::Vector3::CreateZero();
AZ::Vector3 intersectionLower = AZ::Vector3::CreateZero();
const bool intersectedUpper = upperPlane.IntersectSegment(rayRequest.m_start, rayRequest.m_start + rayRequest.m_direction * rayRequest.m_distance, intersectionUpper);
const bool intersectedLower = lowerPlane.IntersectSegment(rayRequest.m_start, rayRequest.m_start + rayRequest.m_direction * rayRequest.m_distance, intersectionLower);
if (intersectedUpper && intersectionUpper.GetX() < 0)
{
result.m_intersected = true;
result.m_position = intersectionUpper;
}
else if (intersectedLower)
{
result.m_intersected = true;
result.m_position = intersectionLower;
}
result.m_normal = planeNormal;
return result;
}
void TearDown() override
{
JackGraphFixture::TearDown();
EMotionFX::Integration::RaycastRequestBus::Handler::BusDisconnect();
}
void ConstructGraph() override
{
JackGraphFixture::ConstructGraph();
// Create a weight parameter.
m_weightParameter = static_cast<FloatSliderParameter*>(ParameterFactory::Create(azrtti_typeid<FloatSliderParameter>()));
m_weightParameter->SetName("IK Weight");
m_weightParameter->SetDefaultValue(1.0f);
m_animGraph->AddParameter(m_weightParameter);
// Create the blend tree.
BlendTree* blendTree = aznew BlendTree();
m_animGraph->GetRootStateMachine()->AddChildNode(blendTree);
m_animGraph->GetRootStateMachine()->SetEntryState(blendTree);
// Add a final node.
BlendTreeFinalNode* finalNode = aznew BlendTreeFinalNode();
blendTree->AddChildNode(finalNode);
// Add a foot IK node and connect it to the final node.
m_ikNode = aznew BlendTreeFootIKNode();
m_ikNode->SetForceUseRaycastBus(true);
m_ikNode->SetLeftFootJointName(s_leftFootJointName);
m_ikNode->SetRightFootJointName(s_rightFootJointName);
m_ikNode->SetLeftToeJointName(s_leftToeJointName);
m_ikNode->SetRightToeJointName(s_rightToeJointName);
m_ikNode->SetHipJointName(s_hipJointName);
blendTree->AddChildNode(m_ikNode);
finalNode->AddConnection(m_ikNode, BlendTreeFootIKNode::OUTPUTPORT_POSE, BlendTreeFinalNode::INPUTPORT_POSE);
// Create the parameter node.
m_parameterNode = aznew BlendTreeParameterNode();
blendTree->AddChildNode(m_parameterNode);
AnimGraphBindPoseNode* bindPoseNode = aznew AnimGraphBindPoseNode();
blendTree->AddChildNode(bindPoseNode);
m_ikNode->AddConnection(bindPoseNode, AnimGraphBindPoseNode::OUTPUTPORT_RESULT, BlendTreeFootIKNode::INPUTPORT_POSE);
// Connect the weight parameter to the weight of the IK node.
m_ikNode->AddUnitializedConnection(m_parameterNode, 0 /* Weight parameter */, BlendTreeFootIKNode::INPUTPORT_WEIGHT);
}
void SetUp() override
{
JackGraphFixture::SetUp();
// Disable raycasts in other handlers, and take over control (muahhahaha *evil laugh*).
EMotionFX::Integration::RaycastRequestBus::Broadcast(&EMotionFX::Integration::RaycastRequests::DisableRayRequests);
EMotionFX::Integration::RaycastRequestBus::Handler::BusConnect();
}
void ValidateFootHeight(BlendTreeFootIKNode::LegId legId, const char* jointName, float height, float tolerance)
{
// Check the left foot height.
size_t footIndex = InvalidIndex;
Skeleton* skeleton = m_actor->GetSkeleton();
skeleton->FindNodeAndIndexByName(jointName, footIndex);
ASSERT_NE(footIndex, InvalidIndex);
EMotionFX::Transform transform = m_actorInstance->GetTransformData()->GetCurrentPose()->GetWorldSpaceTransform(footIndex);
const BlendTreeFootIKNode::UniqueData* uniqueData = static_cast<const BlendTreeFootIKNode::UniqueData*>(m_animGraphInstance->FindOrCreateUniqueNodeData(m_ikNode));
const float correction = (m_actorInstance->GetWorldSpaceTransform().m_rotation.TransformVector(AZ::Vector3(0.0f, 0.0f, uniqueData->m_legs[legId].m_footHeight))).GetZ();
const float pos = transform.m_position.GetZ() - correction;
EXPECT_NEAR(pos, height, tolerance);
}
void ValidateFeetHeights(float leftFootHeight, float rightFootHeight, float tolerance=0.02f)
{
ValidateFootHeight(BlendTreeFootIKNode::LegId::Left, BlendTreeFootIKNodeTests::s_leftFootJointName, leftFootHeight, tolerance);
ValidateFootHeight(BlendTreeFootIKNode::LegId::Right, BlendTreeFootIKNodeTests::s_rightFootJointName, rightFootHeight, tolerance);
}
void ResetActorInstanceTransform()
{
EMotionFX::Transform transform;
transform.Identity();
m_actorInstance->SetLocalSpaceTransform(transform);
}
void SetSceneHeights(float leftSideHeight, float rightSideHeight)
{
m_leftSideHeight = leftSideHeight;
m_rightSideHeight = rightSideHeight;
}
void SimulateFrames(size_t numFrames=60)
{
for (size_t i = 0; i < numFrames; ++i)
{
Evaluate(1.0f / 60.0f);
}
}
void SetSceneTransform(const AZ::Transform& transform)
{
m_sceneTransform = transform;
}
public:
static const char* s_leftFootJointName;
static const char* s_leftToeJointName;
static const char* s_rightFootJointName;
static const char* s_rightToeJointName;
static const char* s_hipJointName;
BlendTreeFootIKNode* m_ikNode = nullptr;
BlendTreeParameterNode* m_parameterNode = nullptr;
FloatSliderParameter* m_weightParameter = nullptr;
AZ::Transform m_sceneTransform = AZ::Transform::CreateIdentity();
float m_leftSideHeight = 0.0f;
float m_rightSideHeight = 0.3f;
};
const char* BlendTreeFootIKNodeTests::s_leftFootJointName = "l_ankle";
const char* BlendTreeFootIKNodeTests::s_leftToeJointName = "l_ball";
const char* BlendTreeFootIKNodeTests::s_rightFootJointName = "r_ankle";
const char* BlendTreeFootIKNodeTests::s_rightToeJointName = "r_ball";
const char* BlendTreeFootIKNodeTests::s_hipJointName = "Bip01__pelvis";
TEST_F(BlendTreeFootIKNodeTests, Integrity)
{
// Process a bunch of frames, so that we are sure the IK fully blended in.
SimulateFrames();
// Do some integrity checks.
BlendTreeFootIKNode::UniqueData* uniqueData = static_cast<BlendTreeFootIKNode::UniqueData*>(m_animGraphInstance->FindOrCreateUniqueNodeData(m_ikNode));
ASSERT_TRUE(uniqueData != nullptr);
ASSERT_TRUE(!uniqueData->GetHasError());
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_jointIndices[BlendTreeFootIKNode::LegJointId::UpperLeg], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_jointIndices[BlendTreeFootIKNode::LegJointId::Knee], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_jointIndices[BlendTreeFootIKNode::LegJointId::Foot], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_jointIndices[BlendTreeFootIKNode::LegJointId::Toe], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_jointIndices[BlendTreeFootIKNode::LegJointId::UpperLeg], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_jointIndices[BlendTreeFootIKNode::LegJointId::Knee], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_jointIndices[BlendTreeFootIKNode::LegJointId::Foot], InvalidIndex);
ASSERT_NE(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_jointIndices[BlendTreeFootIKNode::LegJointId::Toe], InvalidIndex);
ASSERT_NE(uniqueData->m_hipJointIndex, InvalidIndex);
// Make sure the weights are fully active.
ASSERT_FLOAT_EQ(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_weight, 1.0f);
ASSERT_FLOAT_EQ(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_weight, 1.0f);
// Make sure the leg length is about correct.
EXPECT_NEAR(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_legLength, 0.898f, 0.003f);
EXPECT_NEAR(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_legLength, 0.898f, 0.003f);
// Check the foot height offset.
EXPECT_NEAR(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Left].m_footHeight, 0.093f, 0.002f);
EXPECT_NEAR(uniqueData->m_legs[BlendTreeFootIKNode::LegId::Right].m_footHeight, 0.093f, 0.002f);
}
TEST_F(BlendTreeFootIKNodeTests, LegIK)
{
// Disable hip adjustment.
m_ikNode->SetAdjustHip(false);
// Tests where the leg can reach the target position just fine.
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.3f);
SetSceneHeights(0.0f, 0.1f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.1f);
SetSceneHeights(0.0f, 0.4f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.4f);
SetSceneHeights(0.0f, 0.0f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.0f);
// Unreachable left leg, would need hip adjustment.
SetSceneHeights(-0.3f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.3f);
// Right leg is unreachable.
SetSceneHeights(0.0f, 3.0f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.0f);
}
TEST_F(BlendTreeFootIKNodeTests, HipAdjustment)
{
// Enable hip adjustment.
m_ikNode->SetAdjustHip(true);
EMotionFX::Transform transform;
transform.Identity();
m_actorInstance->SetLocalSpaceTransform(transform);
// Tests where the leg can reach the target position just fine, make sure the hip adjustment doesn't break it.
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.3f);
SetSceneHeights(0.0f, 0.1f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.1f);
SetSceneHeights(0.0f, 0.4f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.4f);
SetSceneHeights(0.0f, 0.0f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.0f);
// Normally unreachable left leg, would need hip adjustment.
// Hip adjustment is now enabled, so we should be able to reach it.
SetSceneHeights(-0.3f, 0.3f);
SimulateFrames();
ValidateFeetHeights(-0.3f, 0.3f);
SetSceneHeights(-0.3f, 0.0f);
SimulateFrames();
ValidateFeetHeights(-0.3f, 0.0f);
SetSceneHeights(-m_ikNode->GetMaxHipAdjustment(), 0.0f);
SimulateFrames();
ValidateFeetHeights(-m_ikNode->GetMaxHipAdjustment(), 0.0f);
// Right leg is unreachable. Even moving the hips, as it is just too far.
SetSceneHeights(-2.0f, 0.0f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.0f);
}
TEST_F(BlendTreeFootIKNodeTests, UpVectorSupport)
{
// Disable hip adjustment.
m_ikNode->SetAdjustHip(false);
// Rotate our scene.
// Flip it 180 degrees over the x axis. Please keep in mind our tests only work with 180 degrees flipped or not.
// A more robust method would be needed to really test it correctly otherwise.
AZ::Transform sceneTransform = AZ::Transform::CreateRotationX(MCore::Math::pi);
SetSceneTransform(sceneTransform);
// Rotate the actor instance 180 degrees over the X axis as well.
EMotionFX::Transform transform;
transform.Identity();
transform.m_rotation = AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3(1.0f, 0.0f, 0.0f), MCore::Math::pi);
m_actorInstance->SetLocalSpaceTransform(transform);
// Tests where the leg can reach the target position just fine, make sure the hip adjustment doesn't break it.
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, -0.3f);
SetSceneHeights(0.0f, 0.2f);
SimulateFrames();
ValidateFeetHeights(0.0f, -0.2f);
SetSceneHeights(0.0f, 0.4f);
SimulateFrames();
ValidateFeetHeights(0.0f, -0.4f);
// Now check with hips included.
// Enable hip adjustment.
m_ikNode->SetAdjustHip(true);
SetSceneHeights(-0.2f, 0.4f);
SimulateFrames();
ValidateFeetHeights(0.2f, -0.4f);
SetSceneHeights(-0.4f, 0.0f);
SimulateFrames();
ValidateFeetHeights(0.4f, 0.0f);
}
TEST_F(BlendTreeFootIKNodeTests, InputWeight)
{
MCore::AttributeFloat* weightParam = m_animGraphInstance->GetParameterValueChecked<MCore::AttributeFloat>(0);
weightParam->SetValue(1.0f);
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.3f);
// This should disable the IK.
weightParam->SetValue(0.0f);
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.0f);
}
TEST_F(BlendTreeFootIKNodeTests, DisabledState)
{
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.3f);
// Disable the node. It should now act as passthrough.
m_ikNode->SetIsEnabled(false);
SetSceneHeights(0.0f, 0.3f);
SimulateFrames();
ValidateFeetHeights(0.0f, 0.0f);
}
} // namespace EMotionFX