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/BlendTreeBlendNNodeTests.cpp

407 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 "AnimGraphFixture.h"
#include <EMotionFX/Source/AnimGraph.h>
#include <EMotionFX/Source/AnimGraphStateMachine.h>
#include <EMotionFX/Source/AnimGraphMotionNode.h>
#include <EMotionFX/Source/BlendTree.h>
#include <EMotionFX/Source/BlendTreeBlendNNode.h>
#include <EMotionFX/Source/BlendTreeFloatConstantNode.h>
#include <EMotionFX/Source/EMotionFXManager.h>
#include <EMotionFX/Source/MotionData/NonUniformMotionData.h>
#include <EMotionFX/Source/MotionSet.h>
#include <EMotionFX/Source/MotionInstance.h>
#include <EMotionFX/Source/Motion.h>
#include <EMotionFX/Source/Parameter/FloatSliderParameter.h>
#include <EMotionFX/Source/Parameter/ParameterFactory.h>
#include <EMotionFX/Source/Parameter/ValueParameter.h>
#include <EMotionFX/Source/BlendTreeParameterNode.h>
namespace EMotionFX
{
class BlendTreeBlendNNodeTests : public AnimGraphFixture
{
public:
void TearDown() override
{
if (m_motionNodes)
{
delete m_motionNodes;
}
AnimGraphFixture::TearDown();
}
void ConstructGraph() override
{
AnimGraphFixture::ConstructGraph();
m_blendTreeAnimGraph = AnimGraphFactory::Create<OneBlendTreeNodeAnimGraph>();
m_rootStateMachine = m_blendTreeAnimGraph->GetRootStateMachine();
m_blendTree = m_blendTreeAnimGraph->GetBlendTreeNode();
m_blendNNode = aznew BlendTreeBlendNNode();
m_blendTree->AddChildNode(m_blendNNode);
BlendTreeFinalNode* finalNode = aznew BlendTreeFinalNode();
m_blendTree->AddChildNode(finalNode);
finalNode->AddConnection(m_blendNNode, BlendTreeBlendNNode::PORTID_OUTPUT_POSE, BlendTreeFinalNode::PORTID_INPUT_POSE);
const uint16 motionNodeCount = 3;
for (uint16 i = 0; i < motionNodeCount; ++i)
{
AnimGraphMotionNode* motionNode = aznew AnimGraphMotionNode();
m_blendTree->AddChildNode(motionNode);
m_blendNNode->AddConnection(motionNode, AnimGraphMotionNode::PORTID_OUTPUT_POSE, i);
m_motionNodes->push_back(motionNode);
}
m_blendNNode->UpdateParamWeights();
m_blendNNode->SetParamWeightsEquallyDistributed(-1.0f, 1.0f);
Parameter* parameter = ParameterFactory::Create(azrtti_typeid<FloatSliderParameter>());
parameter->SetName("parameter_test");
m_blendTreeAnimGraph->AddParameter(parameter);
BlendTreeParameterNode* parameterNode = aznew BlendTreeParameterNode();
m_blendTree->AddChildNode(parameterNode);
m_blendNNode->AddUnitializedConnection(parameterNode, 0, BlendTreeBlendNNode::INPUTPORT_WEIGHT);
m_blendTreeAnimGraph->InitAfterLoading();
}
void SetUp() override
{
m_motionNodes = new AZStd::vector<AnimGraphMotionNode*>();
AnimGraphFixture::SetUp();
m_animGraphInstance->Destroy();
m_animGraphInstance = m_blendTreeAnimGraph->GetAnimGraphInstance(m_actorInstance, m_motionSet);
for (size_t i = 0; i < m_motionNodes->size(); ++i)
{
// The motion set keeps track of motions by their name. Each motion
// within the motion set must have a unique name.
AZStd::string motionId = AZStd::string::format("testSkeletalMotion%zu", i);
Motion* motion = aznew Motion(motionId.c_str());
motion->SetMotionData(aznew NonUniformMotionData());
motion->GetMotionData()->SetDuration(1.0f);
MotionSet::MotionEntry* motionEntry = aznew MotionSet::MotionEntry(motion->GetName(), motion->GetName(), motion);
m_motionSet->AddMotionEntry(motionEntry);
(*m_motionNodes)[i]->AddMotionId(motionId.c_str());
}
}
AZStd::unique_ptr<OneBlendTreeNodeAnimGraph> m_blendTreeAnimGraph;
AZStd::vector<AnimGraphMotionNode*>* m_motionNodes = nullptr;
BlendTreeBlendNNode* m_blendNNode = nullptr;
BlendTree* m_blendTree = nullptr;
};
TEST_F(BlendTreeBlendNNodeTests, RandomizeMotion)
{
bool success = true;
MCore::AttributeFloat* testParameter = m_animGraphInstance->GetParameterValueChecked<MCore::AttributeFloat>(0);
testParameter->SetValue(-10.0f);
Evaluate();
AnimGraphNode* outNodeA;
AnimGraphNode* outNodeB;
uint32 outIndexA;
uint32 outIndexB;
float outWeight;
m_blendNNode->FindBlendNodes(m_animGraphInstance, &outNodeA, &outNodeB, &outIndexA, &outIndexB, &outWeight);
success = success && outNodeA == outNodeB;
success = success && outNodeA == static_cast<AnimGraphNode*>(m_motionNodes->front());
success = success && outWeight <= 0;
testParameter->SetValue(-1.0f);
Evaluate();
m_blendNNode->FindBlendNodes(m_animGraphInstance, &outNodeA, &outNodeB, &outIndexA, &outIndexB, &outWeight);
success = success && outNodeA == outNodeB;
success = success && outNodeA == static_cast<AnimGraphNode*>((*m_motionNodes).front());
success = success && outWeight <= 0.0f;
testParameter->SetValue(-0.5f);
Evaluate();
const float tolerance = 0.001f;
m_blendNNode->FindBlendNodes(m_animGraphInstance, &outNodeA, &outNodeB, &outIndexA, &outIndexB, &outWeight);
success = success && outNodeA != outNodeB;
success = success && outNodeA == static_cast<AnimGraphNode*>(m_motionNodes->front());
success = success && outNodeB == static_cast<AnimGraphNode*>((*m_motionNodes)[1]);
success = success && outWeight > 0.5f - tolerance && outWeight < 0.5f + tolerance;
testParameter->SetValue(0.5f);
Evaluate();
m_blendNNode->FindBlendNodes(m_animGraphInstance, &outNodeA, &outNodeB, &outIndexA, &outIndexB, &outWeight);
success = success && outNodeA != outNodeB;
success = success && outNodeA == static_cast<AnimGraphNode*>((*m_motionNodes)[1]);
success = success && outNodeB == static_cast<AnimGraphNode*>((*m_motionNodes)[2]);
success = success && outWeight > 0.5f - tolerance && outWeight < 0.5f + tolerance;
testParameter->SetValue(1.0f);
Evaluate();
m_blendNNode->FindBlendNodes(m_animGraphInstance, &outNodeA, &outNodeB, &outIndexA, &outIndexB, &outWeight);
success = success && outNodeA == outNodeB;
success = success && outNodeA == static_cast<AnimGraphNode*>((*m_motionNodes).back());
success = success && outWeight <= 0.0f;
testParameter->SetValue(10.0f);
Evaluate();
m_blendNNode->FindBlendNodes(m_animGraphInstance, &outNodeA, &outNodeB, &outIndexA, &outIndexB, &outWeight);
success = success && outNodeA == outNodeB;
success = success && outNodeA == static_cast<AnimGraphNode*>(m_motionNodes->back());
success = success && outWeight <= 0;
ASSERT_TRUE(success);
}
///////////////////////////////////////////////////////////////////////////
struct BlendNSyncTestParam
{
AZ::u32 m_motionNodeCount;
float m_minWeight;
float m_maxWeight;
float m_testWeight;
};
class BlendTreeBlendNNodeSyncTestFixture : public AnimGraphFixture
, public ::testing::WithParamInterface<BlendNSyncTestParam>
{
public:
void ConstructGraph() override
{
AnimGraphFixture::ConstructGraph();
m_blendTreeAnimGraph = AnimGraphFactory::Create<OneBlendTreeNodeAnimGraph>();
m_rootStateMachine = m_blendTreeAnimGraph->GetRootStateMachine();
m_blendTree = m_blendTreeAnimGraph->GetBlendTreeNode();
/*
+----------+
| Motion 1 +-----------+
+----------+ |
|
+----------+ >+---------+ +-------+
| Motion 2 +----------->| Blend N +-------------->+ Final |
+----------+ ------>| | +-------+
| >+---------+
+----------+ | |
| Motion N +-----+ |
+----------+ |
|
+-------------+ |
| Const Float +--------+
+-------------+
*/
const BlendNSyncTestParam& param = GetParam();
m_blendNNode = aznew BlendTreeBlendNNode();
m_blendTree->AddChildNode(m_blendNNode);
BlendTreeFinalNode* finalNode = aznew BlendTreeFinalNode();
m_blendTree->AddChildNode(finalNode);
finalNode->AddConnection(m_blendNNode, BlendTreeBlendNNode::PORTID_OUTPUT_POSE, BlendTreeFinalNode::PORTID_INPUT_POSE);
ASSERT_TRUE(param.m_motionNodeCount <= 10) << "The blend N node only has 10 pose inputs.";
for (uint16 i = 0; i < param.m_motionNodeCount; ++i)
{
AnimGraphMotionNode* motionNode = aznew AnimGraphMotionNode();
m_blendTree->AddChildNode(motionNode);
m_blendNNode->AddConnection(motionNode, AnimGraphMotionNode::PORTID_OUTPUT_POSE, i);
m_motionNodes.push_back(motionNode);
}
m_blendNNode->UpdateParamWeights();
m_blendNNode->SetParamWeightsEquallyDistributed(param.m_minWeight, param.m_maxWeight);
m_blendNNode->SetSyncMode(AnimGraphObject::SYNCMODE_CLIPBASED);
m_floatNode = aznew BlendTreeFloatConstantNode();
m_blendTree->AddChildNode(m_floatNode);
m_blendNNode->AddConnection(m_floatNode, BlendTreeFloatConstantNode::OUTPUTPORT_RESULT, BlendTreeBlendNNode::INPUTPORT_WEIGHT);
m_blendTreeAnimGraph->InitAfterLoading();
}
void SetUp() override
{
AnimGraphFixture::SetUp();
m_animGraphInstance->Destroy();
m_animGraphInstance = m_blendTreeAnimGraph->GetAnimGraphInstance(m_actorInstance, m_motionSet);
for (size_t i = 0; i < m_motionNodes.size(); ++i)
{
const AZStd::string motionId = AZStd::string::format("testSkeletalMotion%zu", i);
Motion* motion = aznew Motion(motionId.c_str());
motion->SetMotionData(aznew NonUniformMotionData());
motion->GetMotionData()->SetDuration(i + 1.0f);
MotionSet::MotionEntry * motionEntry = aznew MotionSet::MotionEntry(motion->GetName(), motion->GetName(), motion);
m_motionSet->AddMotionEntry(motionEntry);
m_motionNodes[i]->AddMotionId(motionId.c_str());
m_motionNodes[i]->RecursiveOnChangeMotionSet(m_animGraphInstance, m_motionSet); // Trigger create motion instance.
m_motionNodes[i]->PickNewActiveMotion(m_animGraphInstance);
}
}
public:
AZStd::unique_ptr<OneBlendTreeNodeAnimGraph> m_blendTreeAnimGraph;
std::vector<AnimGraphMotionNode*> m_motionNodes;
BlendTree* m_blendTree = nullptr;
BlendTreeFloatConstantNode* m_floatNode = nullptr;
BlendTreeBlendNNode* m_blendNNode = nullptr;
};
// Make sure we don't crash when we have no inputs
// Also make sure removing connections on BlendN doesn't crash
TEST_F(BlendTreeBlendNNodeTests, NoInputsNoCrashTest)
{
// Remove all input connections of the blendN node.
while (m_blendNNode->GetNumConnections() > 0)
{
const BlendTreeConnection* connection = m_blendNNode->GetConnection(0);
m_blendNNode->RemoveConnection(
connection->GetSourceNode(),
connection->GetSourcePort(),
connection->GetTargetPort());
}
// Update EMFX, which crashed before in the above mentioned Jira bug reports.
GetEMotionFX().Update(0.1f);
}
TEST_P(BlendTreeBlendNNodeSyncTestFixture, PlaySpeedAndTimeSyncTests)
{
const float epsilon = 0.0001f;
const BlendNSyncTestParam& param = GetParam();
ASSERT_TRUE(param.m_maxWeight > param.m_minWeight) << "Invalid test weight range. The min weight is bigger than the max weight";
const float weightRange = param.m_maxWeight - param.m_minWeight;
m_floatNode->SetValue(param.m_testWeight);
GetEMotionFX().Update(0.0f);
const size_t simulationTime = m_motionNodes.size() + 1;
const size_t sampleRate = 24;
const float timeDelta = 1.0f / static_cast<float>(sampleRate);
const size_t numFramesToSimulate = simulationTime * sampleRate;
for (size_t frame = 0; frame < numFramesToSimulate; ++frame)
{
GetEMotionFX().Update(timeDelta);
float blendWeight;
AnimGraphNode* nodeA;
AnimGraphNode* nodeB;
uint32 poseIndexA;
uint32 poseIndexB;
m_blendNNode->FindBlendNodes(m_animGraphInstance, &nodeA, &nodeB, &poseIndexA, &poseIndexB, &blendWeight);
// Check if the correct motions are picked and blended.
const float normalizedWeight = (param.m_testWeight - param.m_minWeight) / weightRange;
const int motionIndexA = static_cast<int>(AZ::Lerp(0.0f, static_cast<float>(m_motionNodes.size() - 1), normalizedWeight));
EXPECT_TRUE(motionIndexA < m_motionNodes.size());
EXPECT_TRUE(m_motionNodes[motionIndexA] == nodeA);
if (nodeA != nodeB)
{
const int motionIndexB = motionIndexA + 1;
EXPECT_TRUE(motionIndexB < m_motionNodes.size());
EXPECT_TRUE(m_motionNodes[motionIndexB] == nodeB);
const float playSpeedA = nodeA->GetPlaySpeed(m_animGraphInstance);
const float playTimeA = nodeA->GetCurrentPlayTime(m_animGraphInstance);
const float durationA = nodeA->GetDuration(m_animGraphInstance);
const float playSpeedB = nodeB->GetPlaySpeed(m_animGraphInstance);
const float playTimeB = nodeB->GetCurrentPlayTime(m_animGraphInstance);
const float durationB = nodeB->GetDuration(m_animGraphInstance);
const float playSpeedN = m_blendNNode->GetPlaySpeed(m_animGraphInstance);
const float playTimeN = m_blendNNode->GetCurrentPlayTime(m_animGraphInstance);
const float durationN = m_blendNNode->GetDuration(m_animGraphInstance);
ASSERT_TRUE(durationA > 0.0f && durationB > 0.0f) << "Invalid test data, motion nodes should have a max time bigger than 0.0";
// Node A is the primary sync node, so the blend N node has to mimic it.
EXPECT_NEAR(playSpeedA, playSpeedN, epsilon);
EXPECT_NEAR(playTimeA, playTimeN, epsilon);
EXPECT_NEAR(durationA, durationN, epsilon);
// Node B gets synced to the blend N node which got synced to node A.
const float timeRatio = durationA / durationB;
const float timeRatio2 = durationB / durationA;
const float factorA = AZ::Lerp(1.0f, timeRatio, blendWeight);
const float factorB = AZ::Lerp(timeRatio2, 1.0f, blendWeight);
const float primaryMotionPlaySpeed = m_motionNodes[motionIndexA]->GetDefaultPlaySpeed();
const float interpolatedSpeed = AZ::Lerp(playSpeedA, primaryMotionPlaySpeed, blendWeight);
const float expectedSecondaryPlaySpeed = interpolatedSpeed * factorB;
EXPECT_NEAR(playSpeedB, expectedSecondaryPlaySpeed, epsilon);
const float normalizedPlayTimeA = playTimeA / durationA;
const float normalizedPlayTimeB = playTimeB / durationB;
EXPECT_NEAR(normalizedPlayTimeA, normalizedPlayTimeB, epsilon);
}
}
}
std::vector<BlendNSyncTestParam> blendNNodeSyncTestData
{
{
2,
0.0f,
1.0f,
0.25f
},
{
2,
0.0f,
1.0f,
0.5f
},
{
2,
0.0f,
1.0f,
0.75f
},
{
3,
0.0f,
1.0f,
0.0f
},
{
3,
0.0f,
2.0f,
1.5f
},
{
5,
0.0f,
4.0f,
2.25f
},
{
10,
0.0f,
10.0f,
7.75f
},
{
3,
-1.0f,
1.0f,
0.25f
}
};
INSTANTIATE_TEST_CASE_P(BlendTreeBlendNNode,
BlendTreeBlendNNodeSyncTestFixture,
::testing::ValuesIn(blendNNodeSyncTestData));
} // namespace EMotionFX