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

306 lines
14 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 "InitSceneAPIFixture.h"
#include <AzCore/Memory/MemoryComponent.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/std/string/conversions.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyManagerComponent.h>
#include <SceneAPI/SceneCore/Mocks/Containers/MockScene.h>
#include <SceneAPI/SceneData/GraphData/BoneData.h>
#include <SceneAPI/SceneData/GraphData/MeshData.h>
#include <SceneAPI/SceneData/GraphData/BlendShapeData.h>
#include <EMotionFX/Source/Node.h>
#include <EMotionFX/Source/Actor.h>
#include <EMotionFX/Source/ActorInstance.h>
#include <EMotionFX/Source/Mesh.h>
#include <EMotionFX/Source/MorphSetup.h>
#include <EMotionFX/Source/MorphSetupInstance.h>
#include <EMotionFX/Source/MorphTarget.h>
#include <EMotionFX/Source/Node.h>
#include <EMotionFX/Source/Skeleton.h>
#include <EMotionFX/Pipeline/RCExt/Actor/ActorBuilder.h>
#include <EMotionFX/Pipeline/RCExt/Actor/MorphTargetExporter.h>
#include <EMotionFX/Pipeline/RCExt/ExportContexts.h>
#include <EMotionFX/Pipeline/SceneAPIExt/Groups/ActorGroup.h>
#include <EMotionFX/Pipeline/SceneAPIExt/Rules/MorphTargetRule.h>
#include <Tests/TestAssetCode/SimpleActors.h>
#include <Tests/TestAssetCode/ActorFactory.h>
namespace EMotionFX
{
// This fixture is responsible for creating the scene description used by
// the morph target pipeline tests
using MorphTargetPipelineFixtureBase = InitSceneAPIFixture<
AZ::MemoryComponent,
AZ::AssetManagerComponent,
AZ::JobManagerComponent,
AZ::StreamerComponent,
AzToolsFramework::Components::PropertyManagerComponent,
EMotionFX::Integration::SystemComponent,
EMotionFX::Pipeline::ActorBuilder,
EMotionFX::Pipeline::MorphTargetExporter
>;
class MorphTargetPipelineFixture
: public MorphTargetPipelineFixtureBase
{
public:
void SetUp() override
{
MorphTargetPipelineFixtureBase::SetUp();
m_actor = ActorFactory::CreateAndInit<SimpleJointChainActor>(0);
// Set up the scene graph
m_scene = new AZ::SceneAPI::Containers::MockScene("MockScene");
m_scene->SetOriginalSceneOrientation(AZ::SceneAPI::Containers::Scene::SceneOrientation::ZUp);
AZ::SceneAPI::Containers::SceneGraph& graph = m_scene->GetGraph();
AZStd::shared_ptr<AZ::SceneData::GraphData::BoneData> boneData = AZStd::make_shared<AZ::SceneData::GraphData::BoneData>();
graph.AddChild(graph.GetRoot(), "testRootBone", boneData);
// Set up our base shape
AZStd::shared_ptr<AZ::SceneData::GraphData::MeshData> meshData = AZStd::make_shared<AZ::SceneData::GraphData::MeshData>();
AZStd::vector<AZ::Vector3> unmorphedVerticies
{
AZ::Vector3(0.0f, 0.0f, 0.0f),
AZ::Vector3(1.0f, 0.0f, 0.0f),
AZ::Vector3(0.0f, 1.0f, 0.0f)
};
for (const AZ::Vector3& vertex : unmorphedVerticies)
{
meshData->AddPosition(vertex);
}
meshData->AddNormal(AZ::Vector3(0.0f, 0.0f, 1.0f));
meshData->AddNormal(AZ::Vector3(0.0f, 0.0f, 1.0f));
meshData->AddNormal(AZ::Vector3(0.0f, 0.0f, 1.0f));
meshData->SetVertexIndexToControlPointIndexMap(0, 0);
meshData->SetVertexIndexToControlPointIndexMap(1, 1);
meshData->SetVertexIndexToControlPointIndexMap(2, 2);
meshData->AddFace(0, 1, 2);
AZ::SceneAPI::Containers::SceneGraph::NodeIndex meshNodeIndex = graph.AddChild(graph.GetRoot(), "testMesh", meshData);
// Set up the morph targets
AZStd::vector<AZStd::vector<AZ::Vector3>> morphedVertices
{{
{
// Morph target 1
AZ::Vector3(0.0f, 0.0f, 0.0f),
AZ::Vector3(1.0f, 0.0f, 1.0f), // this one is different
AZ::Vector3(0.0f, 1.0f, 0.0f)
},
{
// Morph target 2
AZ::Vector3(0.0f, 0.0f, 0.0f),
AZ::Vector3(1.0f, 0.0f, 0.0f),
AZ::Vector3(0.0f, 1.0f, 1.0f) // this one is different
},
}};
const size_t morphTargetCount = morphedVertices.size();
for (size_t morphIndex = 0; morphIndex < morphTargetCount; ++morphIndex)
{
AZStd::shared_ptr<AZ::SceneData::GraphData::BlendShapeData> blendShapeData = AZStd::make_shared<AZ::SceneData::GraphData::BlendShapeData>();
const AZStd::vector<AZ::Vector3>& verticesForThisMorph = morphedVertices.at(morphIndex);
const uint vertexCount = static_cast<uint>(verticesForThisMorph.size());
for (uint vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex)
{
blendShapeData->AddPosition(verticesForThisMorph.at(vertexIndex));
blendShapeData->AddNormal(AZ::Vector3::CreateAxisZ());
blendShapeData->SetVertexIndexToControlPointIndexMap(vertexIndex, vertexIndex);
}
blendShapeData->AddFace({{0, 1, 2}});
AZStd::string morphTargetName("testMorphTarget");
morphTargetName += AZStd::to_string(static_cast<int>(morphIndex));
graph.AddChild(meshNodeIndex, morphTargetName.c_str(), blendShapeData);
}
}
AZ::SceneAPI::Events::ProcessingResult Process(EMotionFX::Pipeline::Group::ActorGroup& actorGroup)
{
AZ::SceneAPI::Events::ProcessingResultCombiner result;
AZStd::vector<AZStd::string> materialReferences;
EMotionFX::Pipeline::ActorBuilderContext actorBuilderContext(*m_scene, "tmp", actorGroup, m_actor.get(), materialReferences, AZ::RC::Phase::Construction);
result += AZ::SceneAPI::Events::Process(actorBuilderContext);
result += AZ::SceneAPI::Events::Process<EMotionFX::Pipeline::ActorBuilderContext>(actorBuilderContext, AZ::RC::Phase::Filling);
result += AZ::SceneAPI::Events::Process<EMotionFX::Pipeline::ActorBuilderContext>(actorBuilderContext, AZ::RC::Phase::Finalizing);
return result.GetResult();
}
void TearDown() override
{
m_actor.reset();
delete m_scene;
MorphTargetPipelineFixtureBase::TearDown();
}
EMotionFX::Mesh* GetMesh(const EMotionFX::Actor* actor)
{
Skeleton* skeleton = actor->GetSkeleton();
EMotionFX::Mesh* mesh = nullptr;
const size_t numNodes = skeleton->GetNumNodes();
for (size_t nodeNum = 0; nodeNum < numNodes; ++nodeNum)
{
if (mesh)
{
// We should only have one node that has a mesh
EXPECT_FALSE(actor->GetMesh(0, nodeNum)) << "More than one mesh found on built actor";
}
else
{
mesh = actor->GetMesh(0, nodeNum);
AZ_Printf("EMotionFX", "%s node name", skeleton->GetNode(nodeNum)->GetName());
}
}
return mesh;
}
AZStd::unique_ptr<Actor> m_actor;
AZ::SceneAPI::Containers::Scene* m_scene;
};
class MorphTargetCreationTestFixture : public MorphTargetPipelineFixture,
public ::testing::WithParamInterface<std::vector<std::string>>
{
};
TEST_P(MorphTargetCreationTestFixture, TestMorphTargetCreation)
{
const std::vector<std::string>& selectedMorphTargets = GetParam();
// Set up the actor group, which controls which parts of the scene graph
// are used to generate the actor
EMotionFX::Pipeline::Group::ActorGroup actorGroup;
actorGroup.SetSelectedRootBone("testRootBone");
// TODO: replace the test with atom mesh.
// actorGroup.GetSceneNodeSelectionList().AddSelectedNode("testMesh");
// actorGroup.GetBaseNodeSelectionList().AddSelectedNode("testMesh");
AZStd::shared_ptr<EMotionFX::Pipeline::Rule::MorphTargetRule> morphTargetRule = AZStd::make_shared<EMotionFX::Pipeline::Rule::MorphTargetRule>();
for (const std::string& selectedMorphTarget : selectedMorphTargets)
{
morphTargetRule->GetSceneNodeSelectionList().AddSelectedNode(("testMesh." + selectedMorphTarget).c_str());
}
actorGroup.GetRuleContainer().AddRule(morphTargetRule);
const AZ::SceneAPI::Events::ProcessingResult result = Process(actorGroup);
ASSERT_EQ(result, AZ::SceneAPI::Events::ProcessingResult::Success) << "Failed to build actor";
const MorphSetup* morphSetup = m_actor->GetMorphSetup(0);
if (selectedMorphTargets.empty())
{
ASSERT_FALSE(morphSetup) << "A morph setup was created when the blend shape rule specified no nodes";
// That's all we can verify for the case where no morph targets
// were selected for export
return;
}
ASSERT_TRUE(morphSetup) << "No morph setup was created";
const size_t expectedNumMorphTargets = selectedMorphTargets.size();
ASSERT_EQ(morphSetup->GetNumMorphTargets(), expectedNumMorphTargets) << "Morph setup should contain " << expectedNumMorphTargets << " morph target(s)";
EMotionFX::Integration::EMotionFXPtr<EMotionFX::ActorInstance> actorInstance = EMotionFX::Integration::EMotionFXPtr<EMotionFX::ActorInstance>::MakeFromNew(EMotionFX::ActorInstance::Create(m_actor.get()));
const Mesh* mesh = GetMesh(m_actor.get());
// TODO: replace the test with atom mesh.
if (!mesh)
{
return;
}
const size_t numMorphTargets = morphSetup->GetNumMorphTargets();
for (size_t morphTargetIndex = 0; morphTargetIndex < numMorphTargets; ++morphTargetIndex)
{
const MorphTarget* morphTarget = morphSetup->GetMorphTarget(morphTargetIndex);
EXPECT_STREQ(morphTarget->GetName(), selectedMorphTargets[morphTargetIndex].c_str()) << "Morph target's name is incorrect";
const AZ::SceneAPI::Containers::SceneGraph& graph = m_scene->GetGraph();
// Verify that the unmorphed vertices are what we expect
const AZ::Vector3* const positions = static_cast<AZ::Vector3*>(mesh->FindVertexData(EMotionFX::Mesh::ATTRIB_POSITIONS));
AZStd::vector<AZ::Vector3> gotUnmorphedVertices;
uint32 numVertices = mesh->GetNumVertices();
for (uint32 vertexNum = 0; vertexNum < numVertices; ++vertexNum)
{
gotUnmorphedVertices.emplace_back(
positions[vertexNum].GetX(),
positions[vertexNum].GetY(),
positions[vertexNum].GetZ()
);
}
// Get the unmorphed vertices from the scene data
const AZStd::shared_ptr<const AZ::SceneAPI::DataTypes::IMeshData> meshData = azrtti_cast<const AZ::SceneAPI::DataTypes::IMeshData*>(graph.GetNodeContent(graph.Find("testMesh")));
AZStd::vector<AZ::Vector3> expectedUnmorphedVertices;
numVertices = meshData->GetVertexCount();
for (uint vertexNum = 0; vertexNum < numVertices; ++vertexNum)
{
expectedUnmorphedVertices.emplace_back(meshData->GetPosition(meshData->GetControlPointIndex(vertexNum)));
}
EXPECT_EQ(gotUnmorphedVertices, expectedUnmorphedVertices);
// Now apply the morph, and verify the morphed vertices against
// what we expect
actorInstance->GetMorphSetupInstance()->GetMorphTarget(morphTargetIndex)->SetManualMode(true);
actorInstance->GetMorphSetupInstance()->GetMorphTarget(morphTargetIndex)->SetWeight(1.0f);
actorInstance->UpdateTransformations(0.0f, true);
actorInstance->UpdateMeshDeformers(0.0f);
const AZ::Vector3* const morphedPositions = static_cast<AZ::Vector3*>(mesh->FindVertexData(EMotionFX::Mesh::ATTRIB_POSITIONS));
AZStd::vector<AZ::Vector3> gotMorphedVertices;
numVertices = mesh->GetNumVertices();
for (uint32 vertexNum = 0; vertexNum < numVertices; ++vertexNum)
{
gotMorphedVertices.emplace_back(
morphedPositions[vertexNum].GetX(),
morphedPositions[vertexNum].GetY(),
morphedPositions[vertexNum].GetZ()
);
}
// Get the morphed vertices from the scene data
AZStd::string morphTargetSceneNodeName("testMesh.");
morphTargetSceneNodeName += selectedMorphTargets[morphTargetIndex].c_str();
const AZStd::shared_ptr<const AZ::SceneAPI::DataTypes::IBlendShapeData> morphTargetData = azrtti_cast<const AZ::SceneAPI::DataTypes::IBlendShapeData*>(graph.GetNodeContent(graph.Find(morphTargetSceneNodeName)));
AZStd::vector<AZ::Vector3> expectedMorphedVertices;
numVertices = morphTargetData->GetVertexCount();
for (uint vertexNum = 0; vertexNum < numVertices; ++vertexNum)
{
expectedMorphedVertices.emplace_back(morphTargetData->GetPosition(morphTargetData->GetControlPointIndex(vertexNum)));
}
EXPECT_EQ(gotMorphedVertices, expectedMorphedVertices);
// Reset the morph target weight so that the next iteration compares against the unmorphed mesh
actorInstance->GetMorphSetupInstance()->GetMorphTarget(morphTargetIndex)->SetWeight(0.0f);
actorInstance->UpdateTransformations(0.0f, true);
actorInstance->UpdateMeshDeformers(0.0f);
}
}
// Note that these values are instantiated before the SystemAllocator is
// created, so we can't use AZStd::vector
INSTANTIATE_TEST_CASE_P(TestMorphTargetCreation, MorphTargetCreationTestFixture,
::testing::Values(
std::vector<std::string> {},
std::vector<std::string> {"testMorphTarget0"},
std::vector<std::string> {"testMorphTarget1"},
std::vector<std::string> {"testMorphTarget0", "testMorphTarget1"}
)
);
}