/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include "SystemComponentFixture.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace EMotionFX { class MorphTargetRuntimeFixture : public SystemComponentFixture { public: void ScaleMesh(Mesh* mesh) { const uint32 vertexCount = mesh->GetNumVertices(); AZ::Vector3* positions = static_cast(mesh->FindOriginalVertexData(EMotionFX::Mesh::ATTRIB_POSITIONS)); for (uint32 vertexNum = 0; vertexNum < vertexCount; ++vertexNum) { positions[vertexNum] *= m_scaleFactor; } } void AddParam(const char* name, const AZ::TypeId& type, const AZStd::string& defaultValue) { EMotionFX::Parameter* parameter = EMotionFX::ParameterFactory::Create(type); parameter->SetName(name); MCore::ReflectionSerializer::DeserializeIntoMember(parameter, "defaultValue", defaultValue); m_animGraph->AddParameter(parameter); } void SetUp() override { SystemComponentFixture::SetUp(); m_actor = ActorFactory::CreateAndInit("testActor"); m_morphSetup = MorphSetup::Create(); m_actor->SetMorphSetup(0, m_morphSetup); AZStd::unique_ptr morphActor = m_actor->Clone(); Mesh* morphMesh = morphActor->GetMesh(0, 0); ScaleMesh(morphMesh); MorphTargetStandard* morphTarget = MorphTargetStandard::Create( /*captureTransforms=*/ false, /*captureMeshDeforms=*/ true, m_actor.get(), morphActor.get(), "morphTarget" ); m_morphSetup->AddMorphTarget(morphTarget); // Without this call, the bind pose does not know about newly added // morph target (mMorphWeights.GetLength() == 0) m_actor->ResizeTransformData(); m_actor->PostCreateInit(/*makeGeomLodsCompatibleWithSkeletalLODs=*/false, /*generateOBBs=*/false, /*convertUnitType=*/false); m_animGraph = AZStd::make_unique(); AddParam("FloatParam", azrtti_typeid(), "0.0"); BlendTreeParameterNode* parameterNode = aznew BlendTreeParameterNode(); BlendTreeMorphTargetNode* morphTargetNode = aznew BlendTreeMorphTargetNode(); morphTargetNode->SetMorphTargetNames({"morphTarget"}); BlendTreeFinalNode* finalNode = aznew BlendTreeFinalNode(); BlendTree* blendTree = aznew BlendTree(); blendTree->SetName("testBlendTree"); blendTree->AddChildNode(parameterNode); blendTree->AddChildNode(morphTargetNode); blendTree->AddChildNode(finalNode); blendTree->SetFinalNodeId(finalNode->GetId()); m_stateMachine = aznew AnimGraphStateMachine(); m_stateMachine->SetName("rootStateMachine"); m_animGraph->SetRootStateMachine(m_stateMachine); m_stateMachine->AddChildNode(blendTree); m_stateMachine->SetEntryState(blendTree); m_stateMachine->InitAfterLoading(m_animGraph.get()); // Create the connections once the port indices are known. The // parameter node's ports are not known until after // InitAfterLoading() is called morphTargetNode->AddConnection( parameterNode, parameterNode->FindOutputPortIndex("FloatParam"), BlendTreeMorphTargetNode::PORTID_INPUT_WEIGHT ); finalNode->AddConnection( morphTargetNode, BlendTreeMorphTargetNode::PORTID_OUTPUT_POSE, BlendTreeFinalNode::PORTID_INPUT_POSE ); m_motionSet = AZStd::make_unique(); m_motionSet->SetName("testMotionSet"); m_actorInstance = Integration::EMotionFXPtr::MakeFromNew(ActorInstance::Create(m_actor.get())); m_animGraphInstance = AnimGraphInstance::Create(m_animGraph.get(), m_actorInstance.get(), m_motionSet.get()); m_actorInstance->SetAnimGraphInstance(m_animGraphInstance); } void TearDown() override { m_actor.reset(); m_actorInstance.reset(); m_motionSet.reset(); m_animGraph.reset(); SystemComponentFixture::TearDown(); } // The members that are EMotionFXPtrs are the ones that are owned by // the test fixture. The others are created by the fixture but owned by // the EMotionFX runtime. AZStd::unique_ptr m_actor; MorphSetup* m_morphSetup = nullptr; AZStd::unique_ptr m_animGraph; AnimGraphStateMachine* m_stateMachine = nullptr; AZStd::unique_ptr m_motionSet; Integration::EMotionFXPtr m_actorInstance; AnimGraphInstance* m_animGraphInstance = nullptr; const float m_scaleFactor = 10.0f; }; TEST_F(MorphTargetRuntimeFixture, TestMorphTargetMeshRuntime) { const float fps = 30.0f; const float updateInterval = 1.0f / fps; const Mesh* mesh = m_actor->GetMesh(0, 0); const uint32 vertexCount = mesh->GetNumOrgVertices(); const AZ::Vector3* positions = static_cast(mesh->FindVertexData(EMotionFX::Mesh::ATTRIB_POSITIONS)); const AZStd::vector neutralPoints = [vertexCount, &positions](){ AZStd::vector p; for (uint32 vertexNum = 0; vertexNum < vertexCount; ++vertexNum) { p.emplace_back(positions[vertexNum]); } return p; }(); const AZStd::array weights { {0.0f, 0.5f, 1.0f, 0.0f} }; AZStd::vector gotWeightedPoints; AZStd::vector expectedWeightedPoints; for (const float weight : weights) { gotWeightedPoints.clear(); expectedWeightedPoints.clear(); static_cast(m_animGraphInstance->FindParameter("FloatParam"))->SetValue(weight); GetEMotionFX().Update(updateInterval); m_actorInstance->UpdateMeshDeformers(updateInterval); for (uint32 vertexNum = 0; vertexNum < vertexCount; ++vertexNum) { gotWeightedPoints.emplace_back(positions[vertexNum]); } for (const AZ::Vector3& neutralPoint : neutralPoints) { const AZ::Vector3 delta = (neutralPoint * m_scaleFactor) - neutralPoint; expectedWeightedPoints.emplace_back(neutralPoint + delta * weight); } EXPECT_THAT(gotWeightedPoints, ::testing::Pointwise(IsClose(), expectedWeightedPoints)); } } } // namespace EMotionFX