Fixed issues with blend shape animations (#3080)

Duplicate blend shape animations are now handled correctly.
Invalid animation targets are now an error instead of a crash in the builder.

Signed-off-by: stankowi <4838196+AMZN-stankowi@users.noreply.github.com>
This commit is contained in:
AMZN-stankowi
2021-08-16 10:18:45 -07:00
committed by GitHub
parent e2b4d8e502
commit b88a7faf64
3 changed files with 107 additions and 15 deletions
@@ -70,6 +70,9 @@ namespace AZ
// This results in the loss of the offset matrix data for nodes without a mesh which is required for the Transform Importer.
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false);
// The remove empty bones flag is on by default, but doesn't do anything internal to AssImp right now.
// This is here as a bread crumb to save others times investigating issues with empty bones.
// m_importer.SetPropertyBool(AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES, false);
m_sceneFileName = fileName;
m_assImpScene = m_importer.ReadFile(fileName,
aiProcess_Triangulate //Triangulates all faces of all meshes
@@ -147,7 +147,9 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AssImpAnimationImporter, SceneCore::LoadingComponent>()->Version(5); // [LYN-4226] Invert PostRotation matrix in animation chains
// Revision 5: [LYN-4226] Invert PostRotation matrix in animation chains
// Revision 6: Handle duplicate blend shape animations
serializeContext->Class<AssImpAnimationImporter, SceneCore::LoadingComponent>()->Version(6);
}
}
@@ -631,6 +633,15 @@ namespace AZ
for (const auto& [meshIdx, keys] : valueToKeyDataMap)
{
if (static_cast<AZ::u32>(meshIdx) >= mesh->mNumAnimMeshes)
{
AZ_Error(
"AnimationImporter", false,
"Mesh %s has an animation mesh index reference of %d, but only has %d animation meshes. Skipping importing this. This is an error in the source scene file that should be corrected.",
mesh->mName.C_Str(), meshIdx, mesh->mNumAnimMeshes);
continue;
}
AZStd::shared_ptr<SceneData::GraphData::BlendShapeAnimationData> morphAnimNode =
AZStd::make_shared<SceneData::GraphData::BlendShapeAnimationData>();
@@ -656,12 +667,29 @@ namespace AZ
morphAnimNode->AddKeyFrame(weight);
}
// Some DCC tools, like Maya, include a full path separated by '.' in the node names.
// For example, "cone_skin_blendShapeNode.cone_squash"
// Downstream processing doesn't want anything but the last part of that node name,
// so find the last '.' and remove anything before it.
const size_t dotIndex = nodeName.find_last_of('.');
nodeName = nodeName.substr(dotIndex + 1);
morphAnimNode->SetBlendShapeName(nodeName.data());
AZStd::string animNodeName(AZStd::string::format("%s_%s", s_animationNodeName, nodeName.data()));
// Duplicates can exist if an anim mesh had a name with a suffix like .001, in that case
// AssImp will strip off that suffix. Note that this behavior is separate from the
// scan for a period in the node name that came before this.
AZStd::string originalNodeName(AZStd::string::format("%s_%s", s_animationNodeName, nodeName.data()));
AZStd::string animNodeName(originalNodeName);
if (RenamedNodesMap::SanitizeNodeName(
animNodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, originalNodeName.c_str()))
{
AZ_Warning(
"AnimationImporter", false,
"Duplicate animations were found with the name %s on mesh %s. The duplicate will be named %s.",
originalNodeName.c_str(), mesh->mName.C_Str(), animNodeName.c_str());
}
Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
context.m_currentGraphPosition, animNodeName.c_str(), AZStd::move(morphAnimNode));
context.m_scene.GetGraph().MakeEndPoint(addNode);
@@ -40,7 +40,9 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AssImpBlendShapeImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-2576
// Revision 3: Fixed an issue where jack.fbx was failing to process
// Revision 4: Handle duplicate blend shape animations
serializeContext->Class<AssImpBlendShapeImporter, SceneCore::LoadingComponent>()->Version(4);
}
}
@@ -80,7 +82,32 @@ namespace AZ
// AssImp separates meshes that have multiple materials.
// This code re-combines them to match previous FBX SDK behavior,
// so they can be separated by engine code instead.
AZStd::map<AZStd::string_view, AZStd::vector<AZStd::pair<int, int>>> animToMeshToAnimMeshIndices;
// Can't de-dupe nodes in the first loop because we can't generate names until we create nodes later.
// Because meshes are split on material at this point and need to be recombined, we can be in a position where
// There is a legit duped anim mesh that needs to be combined based on the outer non-anim mesh,
// or this is a duplicately named anim mesh that needs to be de-duped. There is also the case where both are true,
// it's a duplicate name and the non-anim mesh has to be deduped.
// Helper struct to track an anim mesh and its associated mesh.
struct AnimMeshAndSceneMeshIndex
{
AnimMeshAndSceneMeshIndex(const aiAnimMesh* aiAnimMesh, const aiMesh* aiMesh)
: m_aiAnimMesh(aiAnimMesh)
, m_aiMesh(aiMesh)
{
}
const aiAnimMesh* m_aiAnimMesh = nullptr;
const aiMesh* m_aiMesh = nullptr;
};
// Helper struct to track all anim meshes at an index for all scene meshes.
struct AnimMeshAndSceneMeshes
{
AZStd::vector<AnimMeshAndSceneMeshIndex> m_animMeshAndSceneMeshIndex;
};
// Map the animation index to the list of anim meshes at that index, and the mesh associated with those anim meshes.
AZStd::map<int, AnimMeshAndSceneMeshes> animMeshIndexToSceneMeshes;
for (int nodeMeshIdx = 0; nodeMeshIdx < numMesh; nodeMeshIdx++)
{
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[nodeMeshIdx];
@@ -88,20 +115,57 @@ namespace AZ
for (unsigned int animIdx = 0; animIdx < aiMesh->mNumAnimMeshes; animIdx++)
{
aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[animIdx];
animToMeshToAnimMeshIndices[aiAnimMesh->mName.C_Str()].emplace_back(nodeMeshIdx, animIdx);
// This code executes if:
// A mesh in the FBX file had multiple materials and blend shapes.
// This means that AssImp splits that mesh to one material per mesh.
// AssImp creates a set of anim meshes for each mesh based on that split.
// This verifies that those anim mesh arrays are in the same order across all split meshes, if it fails
// it means this logic needs to be updated, but it also catches that here earlier in an obvious way,
// instead of failing later in a harder to track way.
if (animMeshIndexToSceneMeshes.contains(animIdx))
{
const AnimMeshAndSceneMeshIndex& firstExistingAnim(
animMeshIndexToSceneMeshes[animIdx].m_animMeshAndSceneMeshIndex[0]);
if (strcmp(
firstExistingAnim.m_aiAnimMesh->mName.C_Str(),
aiAnimMesh->mName.C_Str()) != 0)
{
AZ_Error(
Utilities::ErrorWindow, false,
"Meshes %s and %s on node %s have mismatched animations %s and %s at index %d. This can be resolved by "
"either manually separating meshes by material in the source scene file, or by updating this logic to "
"handle out of order animation indices.",
firstExistingAnim.m_aiMesh->mName.C_Str(),
aiMesh->mName.C_Str(),
context.m_sourceNode.GetName(),
firstExistingAnim.m_aiAnimMesh->mName.C_Str(),
aiAnimMesh->mName.C_Str(), animIdx);
return Events::ProcessingResult::Failure;
}
}
animMeshIndexToSceneMeshes[animIdx].m_animMeshAndSceneMeshIndex.emplace_back(
AnimMeshAndSceneMeshIndex(aiAnimMesh, aiMesh));
}
}
for (const auto& animToMeshIndex : animToMeshToAnimMeshIndices)
for (const auto& animMeshToSceneMeshes : animMeshIndexToSceneMeshes)
{
AZStd::shared_ptr<SceneData::GraphData::BlendShapeData> blendShapeData =
AZStd::make_shared<SceneData::GraphData::BlendShapeData>();
if (animMeshToSceneMeshes.second.m_animMeshAndSceneMeshIndex.size() == 0)
{
AZ_Error(Utilities::ErrorWindow, false, "Blend shape animations were expected but missing on node %s.",
context.m_sourceNode.GetName());
return Events::ProcessingResult::Failure;
}
// Some DCC tools, like Maya, include a full path separated by '.' in the node names.
// For example, "cone_skin_blendShapeNode.cone_squash"
// Downstream processing doesn't want anything but the last part of that node name,
// so find the last '.' and remove anything before it.
AZStd::string nodeName(animToMeshIndex.first);
AZStd::string nodeName(animMeshToSceneMeshes.second.m_animMeshAndSceneMeshIndex[0].m_aiAnimMesh->mName.C_Str());
size_t dotIndex = nodeName.rfind('.');
if (dotIndex != AZStd::string::npos)
{
@@ -109,12 +173,11 @@ namespace AZ
}
int vertexOffset = 0;
RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
AZ_TraceContext("Blend shape name", nodeName);
for (const auto& meshIndex : animToMeshIndex.second)
for (const auto& animMeshAndSceneIndex : animMeshToSceneMeshes.second.m_animMeshAndSceneMeshIndex)
{
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[meshIndex.first];
const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
const aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[meshIndex.second];
const aiAnimMesh* aiAnimMesh = animMeshAndSceneIndex.m_aiAnimMesh;
const aiMesh* aiMesh = animMeshAndSceneIndex.m_aiMesh;
AZStd::bitset<SceneData::GraphData::BlendShapeData::MaxNumUVSets> uvSetUsedFlags;
for (AZ::u8 uvSetIndex = 0; uvSetIndex < SceneData::GraphData::BlendShapeData::MaxNumUVSets; ++uvSetIndex)
@@ -199,6 +262,7 @@ namespace AZ
face.mNumIndices);
continue;
}
for (unsigned int idx = 0; idx < face.mNumIndices; ++idx)
{
blendFace.vertexIndex[idx] = face.mIndices[idx] + vertexOffset;
@@ -207,11 +271,8 @@ namespace AZ
blendShapeData->AddFace(blendFace);
}
vertexOffset += aiMesh->mNumVertices;
}
// Report problem if no vertex or face converted to MeshData
if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
{