From f779821ac0653b6e8cbe8b5c473d7f000642f477 Mon Sep 17 00:00:00 2001 From: AMZN-stankowi Date: Wed, 19 May 2021 13:02:31 -0700 Subject: [PATCH] =?UTF-8?q?Helios=20-=20LYN-3250=20-=20Fixed=20morph=20tar?= =?UTF-8?q?gets=20for=20meshes=20that=20had=20multiple=20=E2=80=A6=20(#696?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Helios - LYN-3250 - Fixed morph targets for meshes that had multiple materials (#374) Fixed morph targets for meshes that had multiple materials and were split by AssImp: Recombined them into one mesh in the O3DE scene graph, so the behavior would match FBX SDK. --- .../Importers/AssImpAnimationImporter.cpp | 79 +++++++----- .../AssImpBitangentStreamImporter.cpp | 78 +++++++----- .../Importers/AssImpBlendShapeImporter.cpp | 99 +++++++++------ .../Importers/AssImpBoneImporter.cpp | 24 ++-- .../Importers/AssImpColorStreamImporter.cpp | 66 ++++++---- .../Importers/AssImpImporterUtilities.cpp | 2 +- .../Importers/AssImpMaterialImporter.cpp | 2 +- .../Importers/AssImpMeshImporter.cpp | 2 +- .../Importers/AssImpSkinImporter.cpp | 2 +- .../Importers/AssImpSkinWeightsImporter.cpp | 61 ++++----- .../Importers/AssImpSkinWeightsImporter.h | 1 + .../Importers/AssImpTangentStreamImporter.cpp | 80 +++++++----- .../Importers/AssImpTransformImporter.cpp | 2 +- .../Importers/AssImpUvMapImporter.cpp | 117 +++++++++++++----- .../Utilities/AssImpMeshImporterUtilities.cpp | 36 ++++-- .../Utilities/AssImpMeshImporterUtilities.h | 7 +- .../Importers/Utilities/RenamedNodesMap.cpp | 6 +- .../SDKWrapper/AssImpSceneWrapper.cpp | 4 +- .../SceneCore/Utilities/DebugOutput.cpp | 10 ++ .../SceneCore/Utilities/DebugOutput.h | 1 + .../SceneData/GraphData/BlendShapeData.cpp | 18 +++ .../SceneAPI/SceneData/GraphData/MeshData.cpp | 32 ++--- .../SceneAPI/SceneData/GraphData/MeshData.h | 5 - .../GraphData/GraphDataBehaviorTests.cpp | 2 - .../MeshOptimizer/MeshOptimizerComponent.cpp | 2 +- 25 files changed, 457 insertions(+), 281 deletions(-) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp index 0ee25195bc..38f7de89c6 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp @@ -260,7 +260,7 @@ namespace AZ { AZ_TraceContext("Importer", "Animation"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); // Add check for animation layers at the scene level. @@ -387,11 +387,10 @@ namespace AZ } Events::ProcessingResultCombiner combinedAnimationResult; - for (AZ::u32 meshIndex = 0; meshIndex < currentNode->mNumMeshes; ++meshIndex) + if (context.m_sourceNode.ContainsMesh()) { - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[meshIndex]]; - - if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(mesh->mName.C_Str()); + const aiMesh* firstMesh = scene->mMeshes[currentNode->mMeshes[0]]; + if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(firstMesh->mName.C_Str()); channelsForMeshName != meshMorphAnimations.end()) { const auto [nodeIterName, channels] = *channelsForMeshName; @@ -399,7 +398,7 @@ namespace AZ { const auto& [animation, morphAnimation] = animAndMorphAnim; combinedAnimationResult += ImportBlendShapeAnimation( - context, animation, morphAnimation, mesh); + context, animation, morphAnimation, firstMesh); } } } @@ -413,32 +412,39 @@ namespace AZ if (boneAnimations.empty() && !meshMorphAnimations.empty()) { const aiAnimation* animation = scene->mAnimations[0]; - - // Morph animations need a regular animation on the node, as well. - // If there is no bone animation on the current node, then generate one here. - AZStd::shared_ptr createdAnimationData = - AZStd::make_shared(); - - const size_t numKeyframes = animation->mDuration + 1; // +1 because we start at 0 and the last keyframe is at mDuration instead of mDuration-1 - createdAnimationData->ReserveKeyFrames(numKeyframes); - - const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond; - createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames); - - // Set every frame of the animation to the start location of the node. - aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode); - DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform); - context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform); - context.m_sourceSceneSystem.ConvertUnit(localTransform); - for (AZ::u32 time = 0; time <= animation->mDuration; ++time) + for (AZ::u32 channelIndex = 0; channelIndex < animation->mNumMorphMeshChannels; ++channelIndex) { - createdAnimationData->AddKeyFrame(localTransform); + const aiMeshMorphAnim* nodeAnim = animation->mMorphMeshChannels[channelIndex]; + // Morph animations need a regular animation on the node, as well. + // If there is no bone animation on the current node, then generate one here. + AZStd::shared_ptr createdAnimationData = + AZStd::make_shared(); + + const size_t numKeyframes = GetNumKeyFrames( + nodeAnim->mNumKeys, + animation->mDuration, + animation->mTicksPerSecond); + createdAnimationData->ReserveKeyFrames(numKeyframes); + + const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond; + createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames); + + // Set every frame of the animation to the start location of the node. + aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode); + DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform); + context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform); + context.m_sourceSceneSystem.ConvertUnit(localTransform); + for (AZ::u32 time = 0; time <= numKeyframes; ++time) + { + createdAnimationData->AddKeyFrame(localTransform); + } + + const AZStd::string stubBoneAnimForMorphName(AZStd::string::format("%s%s", nodeName.c_str(), nodeAnim->mName.C_Str())); + Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild( + context.m_currentGraphPosition, stubBoneAnimForMorphName.c_str(), AZStd::move(createdAnimationData)); + context.m_scene.GetGraph().MakeEndPoint(addNode); } - - Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild( - context.m_currentGraphPosition, nodeName.c_str(), AZStd::move(createdAnimationData)); - context.m_scene.GetGraph().MakeEndPoint(addNode); - + return combinedAnimationResult.GetResult(); } decltype(boneAnimations) parentFillerAnimations; @@ -446,8 +452,8 @@ namespace AZ // Go through all the animations and make sure we create animations for bones who's parents don't have an animation for (auto&& anim : boneAnimations) { - aiNode* node = scene->mRootNode->FindNode(anim.first.c_str()); - aiNode* parent = node->mParent; + const aiNode* node = scene->mRootNode->FindNode(anim.first.c_str()); + const aiNode* parent = node->mParent; while (parent && parent != scene->mRootNode) { @@ -598,7 +604,8 @@ namespace AZ // Keyframes generated for every single frame of the animation. typedef AZStd::map> ValueToKeyDataMap; ValueToKeyDataMap valueToKeyDataMap; - + // Key time can be less than zero, normalize to have zero be the lowest time. + double keyOffset = 0; for (int keyIdx = 0; keyIdx < meshMorphAnim->mNumKeys; keyIdx++) { aiMeshMorphKey& key = meshMorphAnim->mKeys[keyIdx]; @@ -609,6 +616,10 @@ namespace AZ valueToKeyDataMap[currentValue].insert( AZStd::upper_bound(valueToKeyDataMap[currentValue].begin(), valueToKeyDataMap[currentValue].end(),thisKey), thisKey); + if (key.mTime < keyOffset) + { + keyOffset = key.mTime; + } } } @@ -631,7 +642,7 @@ namespace AZ const double time = GetTimeForFrame(frame, animation->mTicksPerSecond); float weight = 0; - if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx)) + if (!SampleKeyFrame(weight, keys, keys.size(), time + keyOffset, keyIdx)) { return Events::ProcessingResult::Failure; } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp index c2b1f20035..2ce9bc14f4 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp @@ -25,7 +25,6 @@ #include #include - namespace AZ { namespace SceneAPI @@ -44,7 +43,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); // LYN-2576 + serializeContext->Class()->Version(3); // LYN-3250 } } @@ -55,62 +54,79 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) + const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex) { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); + return scene->mMeshes[meshIndex]->HasTangentsAndBitangents(); + }; - size_t vertexCount = parentMeshData->GetVertexCount(); - - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes) - { - AZ_Error(Utilities::ErrorWindow, false, - "Tried to construct bitangent stream attribute for invalid or non-mesh parent data, mesh index is invalid"); - return Events::ProcessingResult::Failure; - } - - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; - - if (!mesh->HasTangentsAndBitangents()) + // If there are no bitangents on any meshes, there's nothing to import in this function. + const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!anyMeshHasTangentsAndBitangents) { return Events::ProcessingResult::Ignored; } + // AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material. + // This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes, + // and the engine has code to do this later. + const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!allMeshesHaveTangentsAndBitangents) + { + const char* mixedBitangentsError = + "Node with name %s has meshes with and without bitangents. " + "Placeholder incorrect bitangents will be generated to allow the data to process, " + "but the source art needs to be fixed to correct this. Either apply bitangents to all meshes on this node, " + "or remove all bitangents from all meshes on this node."; + AZ_Error( + Utilities::ErrorWindow, false, mixedBitangentsError, currentNode->mName.C_Str()); + } + + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); + AZStd::shared_ptr bitangentStream = AZStd::make_shared(); - // AssImp only has one bitangentStream per mesh. bitangentStream->SetBitangentSetIndex(0); bitangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx); bitangentStream->ReserveContainerSpace(vertexCount); - - for (int v = 0; v < mesh->mNumVertices; ++v) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - const Vector3 bitangent( - AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v])); - bitangentStream->AppendBitangent(bitangent); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (!mesh->HasTangentsAndBitangents()) + { + // This node has mixed meshes with and without bitangents. + // An error was already thrown above. Output stub bitangents so + // the mesh can still be output in some form, even if the data isn't correct. + // The bitangent count needs to match the vertex count on the associated mesh node. + bitangentStream->AppendBitangent(Vector3::CreateAxisY()); + } + else + { + const Vector3 bitangent( + AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v])); + bitangentStream->AppendBitangent(bitangent); + } + } } - AZStd::string nodeName(AZStd::string::format("%s",m_defaultNodeName)); Containers::SceneGraph::NodeIndex newIndex = - context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); + context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName); Events::ProcessingResult bitangentResults; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, nodeName.c_str()); + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, m_defaultNodeName); bitangentResults = Events::Process(dataPopulated); if (bitangentResults != Events::ProcessingResult::Failure) { bitangentResults = AddAttributeDataNodeWithContexts(dataPopulated); } - return bitangentResults; } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp index c0399329d3..34266a3e29 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp @@ -74,37 +74,51 @@ namespace AZ { return meshDataResult.GetError(); } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); - int parentMeshIndex = parentMeshData->GetSdkMeshIndex(); Events::ProcessingResultCombiner combinedBlendShapeResult; + // 1. Loop through meshes & anims + // Create storage: Anim to meshes + // 2. Loop through anims & meshes + // Create an anim mesh for each anim, with meshes re-combined. + // 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>> animToMeshToAnimMeshIndices; for (int nodeMeshIdx = 0; nodeMeshIdx < numMesh; nodeMeshIdx++) { int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[nodeMeshIdx]; const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx]; - - // Each mesh gets its own node in the scene graph, so only generate - // morph targets for the current mesh. - if (parentMeshIndex != nodeMeshIdx || !aiMesh->mNumAnimMeshes) - { - continue; - } - for (int animIdx = 0; animIdx < aiMesh->mNumAnimMeshes; animIdx++) { - AZStd::shared_ptr blendShapeData = - AZStd::make_shared(); - aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[animIdx]; - AZStd::string nodeName(aiAnimMesh->mName.C_Str()); - size_t dotIndex = nodeName.rfind('.'); - if (dotIndex != AZStd::string::npos) - { - nodeName.erase(0, dotIndex + 1); - } - RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape"); - AZ_TraceContext("Blend shape name", nodeName); + animToMeshToAnimMeshIndices[aiAnimMesh->mName.C_Str()].emplace_back(nodeMeshIdx, animIdx); + } + } + + for (const auto& animToMeshIndex : animToMeshToAnimMeshIndices) + { + AZStd::shared_ptr blendShapeData = + AZStd::make_shared(); + + // 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); + size_t dotIndex = nodeName.rfind('.'); + if (dotIndex != AZStd::string::npos) + { + nodeName.erase(0, dotIndex + 1); + } + 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) + { + 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]; AZStd::bitset uvSetUsedFlags; for (AZ::u8 uvSetIndex = 0; uvSetIndex < SceneData::GraphData::BlendShapeData::MaxNumUVSets; ++uvSetIndex) @@ -128,7 +142,7 @@ namespace AZ context.m_sourceSceneSystem.ConvertUnit(vertex); blendShapeData->AddPosition(vertex); - blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx); + blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx + vertexOffset, vertIdx + vertexOffset); // Add normals if (aiAnimMesh->HasNormals()) @@ -191,33 +205,36 @@ namespace AZ } for (int idx = 0; idx < face.mNumIndices; ++idx) { - blendFace.vertexIndex[idx] = face.mIndices[idx]; + blendFace.vertexIndex[idx] = face.mIndices[idx] + vertexOffset; } blendShapeData->AddFace(blendFace); } + vertexOffset += aiMesh->mNumVertices; - // Report problem if no vertex or face converted to MeshData - if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0) - { - AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str()); - return Events::ProcessingResult::Failure; - } - Containers::SceneGraph::NodeIndex newIndex = - context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); - - Events::ProcessingResult blendShapeResult; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName); - blendShapeResult = Events::Process(dataPopulated); - - if (blendShapeResult != Events::ProcessingResult::Failure) - { - blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated); - } - combinedBlendShapeResult += blendShapeResult; } + + // Report problem if no vertex or face converted to MeshData + if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0) + { + AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str()); + return Events::ProcessingResult::Failure; + } + + Containers::SceneGraph::NodeIndex newIndex = + context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); + + Events::ProcessingResult blendShapeResult; + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName); + blendShapeResult = Events::Process(dataPopulated); + + if (blendShapeResult != Events::ProcessingResult::Failure) + { + blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated); + } + combinedBlendShapeResult += blendShapeResult; } return combinedBlendShapeResult.GetResult(); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp index 4467d6933b..5b43941715 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp @@ -46,8 +46,8 @@ namespace AZ } void EnumBonesInNode( - const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, - AZStd::unordered_map& boneLookup) + const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, + AZStd::unordered_map& boneLookup) { /* From AssImp Documentation a) Create a map or a similar container to store which nodes are necessary for the skeleton. Pre-initialise it for all nodes with a "no". @@ -62,14 +62,14 @@ namespace AZ for (unsigned meshIndex = 0; meshIndex < node->mNumMeshes; ++meshIndex) { - aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]]; + const aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]]; for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex) { - aiBone* bone = mesh->mBones[boneIndex]; + const aiBone* bone = mesh->mBones[boneIndex]; - aiNode* boneNode = scene->mRootNode->FindNode(bone->mName); - aiNode* boneParent = boneNode->mParent; + const aiNode* boneNode = scene->mRootNode->FindNode(bone->mName); + const aiNode* boneParent = boneNode->mParent; mainBoneList[bone->mName.C_Str()] = boneNode; boneLookup[bone->mName.C_Str()] = bone; @@ -85,8 +85,8 @@ namespace AZ } void EnumChildren( - const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, - AZStd::unordered_map& boneLookup) + const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, + AZStd::unordered_map& boneLookup) { EnumBonesInNode(scene, node, mainBoneList, boneLookup); @@ -102,7 +102,7 @@ namespace AZ { AZ_TraceContext("Importer", "Bone"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (IsPivotNode(currentNode->mName)) @@ -118,8 +118,8 @@ namespace AZ } else { - AZStd::unordered_map mainBoneList; - AZStd::unordered_map boneLookup; + AZStd::unordered_map mainBoneList; + AZStd::unordered_map boneLookup; EnumChildren(scene, scene->mRootNode, mainBoneList, boneLookup); if (mainBoneList.find(currentNode->mName.C_Str()) != mainBoneList.end()) @@ -172,7 +172,7 @@ namespace AZ } aiMatrix4x4 transform = currentNode->mTransformation; - aiNode* parent = currentNode->mParent; + const aiNode* parent = currentNode->mParent; while (parent) { diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp index 7ebdb55363..75fa39105f 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -44,7 +45,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); // LYN-2576 + serializeContext->Class()->Version(3); // LYN-3250 } } @@ -55,43 +56,64 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) - { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); + // This node has at least one mesh, verify that the color channel counts are the same for all meshes. + const int expectedColorChannels = scene->mMeshes[currentNode->mMeshes[0]]->GetNumColorChannels(); + const bool allMeshesHaveSameNumberOfColorChannels = + AZStd::all_of(currentNode->mMeshes + 1, currentNode->mMeshes + currentNode->mNumMeshes, [scene, expectedColorChannels](const unsigned int meshIndex) + { + return scene->mMeshes[meshIndex]->GetNumColorChannels() == expectedColorChannels; + }); - size_t vertexCount = parentMeshData->GetVertexCount(); + AZ_Error( + Utilities::ErrorWindow, + allMeshesHaveSameNumberOfColorChannels, + "Color channel counts for node %s has meshes with different color channel counts. " + "The color channel count for the first mesh will be used, and placeholder incorrect color values " + "will be generated to allow the data to process, but the source art needs to be fixed to correct this. " + "All meshes on this node should have the same number of color channels.", + currentNode->mName.C_Str()); - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - if (sdkMeshIndex < 0) + if (expectedColorChannels == 0) { - AZ_Error(Utilities::ErrorWindow, false, - "Tried to construct color stream attribute for invalid or non-mesh parent data, mesh index is missing"); - return Events::ProcessingResult::Failure; + return Events::ProcessingResult::Ignored; } - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); Events::ProcessingResultCombiner combinedVertexColorResults; - for (int colorSetIndex = 0; colorSetIndex < mesh->GetNumColorChannels(); ++colorSetIndex) + for (int colorSetIndex = 0; colorSetIndex < expectedColorChannels; ++colorSetIndex) { + AZStd::shared_ptr vertexColors = AZStd::make_shared(); vertexColors->ReserveContainerSpace(vertexCount); - for (int v = 0; v < mesh->mNumVertices; ++v) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - AZ::SceneAPI::DataTypes::Color vertexColor( - AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v])); - vertexColors->AppendColor(vertexColor); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (colorSetIndex < mesh->GetNumColorChannels()) + { + AZ::SceneAPI::DataTypes::Color vertexColor( + AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v])); + vertexColors->AppendColor(vertexColor); + } + else + { + // An error was already emitted if this mesh has less color channels + // than other meshes on the parent node. Append an arbitrary color value, fully opaque black, + // so the mesh can still be processed. + // It's better to let the engine load a partially valid mesh than to completely fail. + vertexColors->AppendColor(AZ::SceneAPI::DataTypes::Color(0.0f,0.0f,0.0f,1.0f)); + } + } } - AZStd::string nodeName(AZStd::string::format("%s%d",m_defaultNodeName,colorSetIndex)); + AZStd::string nodeName(AZStd::string::format("%s%d", m_defaultNodeName, colorSetIndex)); Containers::SceneGraph::NodeIndex newIndex = context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); @@ -106,9 +128,7 @@ namespace AZ combinedVertexColorResults += colorMapResults; } - return combinedVertexColorResults.GetResult(); - } } // namespace FbxSceneBuilder diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp index 80ac01ebdf..e79eaa09aa 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp @@ -69,7 +69,7 @@ namespace AZ aiMatrix4x4 GetConcatenatedLocalTransform(const aiNode* currentNode) { - aiNode* parent = currentNode->mParent; + const aiNode* parent = currentNode->mParent; aiMatrix4x4 combinedTransform = currentNode->mTransformation; while (parent) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp index e314804ea1..a912b90e34 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp @@ -62,7 +62,7 @@ namespace AZ for (int idx = 0; idx < context.m_sourceNode.m_assImpNode->mNumMeshes; ++idx) { int meshIndex = context.m_sourceNode.m_assImpNode->mMeshes[idx]; - aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex]; + const aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex]; AZ_Assert(assImpMesh, "Asset Importer Mesh should not be null."); int materialIndex = assImpMesh->mMaterialIndex; AZ_TraceContext("Material Index", materialIndex); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp index cafb96934d..193a1f9fd5 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp @@ -45,7 +45,7 @@ namespace AZ { AZ_TraceContext("Importer", "Mesh"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (!context.m_sourceNode.ContainsMesh() || IsSkinnedMesh(*currentNode, *scene)) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp index 145dc9a457..f4a5fd0f93 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp @@ -45,7 +45,7 @@ namespace AZ { AZ_TraceContext("Importer", "Skin"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (!context.m_sourceNode.ContainsMesh() || !IsSkinnedMesh(*currentNode, *scene)) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp index abcbf10b4a..d8503857cc 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp @@ -51,7 +51,7 @@ namespace AZ { AZ_TraceContext("Importer", "Skin Weights"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if(currentNode->mNumMeshes <= 0) @@ -59,35 +59,21 @@ namespace AZ return Events::ProcessingResult::Ignored; } - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) - { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); - - int parentMeshIndex = parentMeshData->GetSdkMeshIndex(); - Events::ProcessingResultCombiner combinedSkinWeightsResult; + // Don't create this until a bone with weights is encountered + Containers::SceneGraph::NodeIndex weightsIndexForMesh; + AZStd::string skinWeightName; + AZStd::shared_ptr skinWeightData; + + const uint64_t totalVertices = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); + + int vertexCount = 0; for(unsigned nodeMeshIndex = 0; nodeMeshIndex < currentNode->mNumMeshes; ++nodeMeshIndex) { - if (nodeMeshIndex != parentMeshIndex) - { - // Only generate skinning data for the parent mesh. - // Each AssImp mesh is assigned to a unique node, - // so the skinning data should be generated as a child node - // for the associated parent mesh. - continue; - } int sceneMeshIndex = currentNode->mMeshes[nodeMeshIndex]; const aiMesh* mesh = scene->mMeshes[sceneMeshIndex]; - // Don't create this until a bone with weights is encountered - Containers::SceneGraph::NodeIndex weightsIndexForMesh; - AZStd::string skinWeightName; - AZStd::shared_ptr skinWeightData; - for(unsigned b = 0; b < mesh->mNumBones; ++b) { const aiBone* bone = mesh->mBones[b]; @@ -100,7 +86,6 @@ namespace AZ if (!weightsIndexForMesh.IsValid()) { skinWeightName = s_skinWeightName; - skinWeightName += AZStd::to_string(nodeMeshIndex); RenamedNodesMap::SanitizeNodeName(skinWeightName, context.m_scene.GetGraph(), context.m_currentGraphPosition); weightsIndexForMesh = @@ -116,23 +101,25 @@ namespace AZ } Pending pending; pending.m_bone = bone; - pending.m_numVertices = mesh->mNumVertices; + pending.m_numVertices = totalVertices; pending.m_skinWeightData = skinWeightData; + pending.m_vertOffset = vertexCount; m_pendingSkinWeights.push_back(pending); } - - Events::ProcessingResult skinWeightsResult; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName); - skinWeightsResult = Events::Process(dataPopulated); - - if (skinWeightsResult != Events::ProcessingResult::Failure) - { - skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated); - } - - combinedSkinWeightsResult += skinWeightsResult; + vertexCount += mesh->mNumVertices; } + Events::ProcessingResult skinWeightsResult; + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName); + skinWeightsResult = Events::Process(dataPopulated); + + if (skinWeightsResult != Events::ProcessingResult::Failure) + { + skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated); + } + + combinedSkinWeightsResult += skinWeightsResult; + return combinedSkinWeightsResult.GetResult(); } @@ -153,7 +140,7 @@ namespace AZ link.boneId = boneId; link.weight = it.m_bone->mWeights[weight].mWeight; - it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId, link); + it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId + it.m_vertOffset, link); } } const auto result = m_pendingSkinWeights.empty() ? Events::ProcessingResult::Ignored : Events::ProcessingResult::Success; diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h index f048fe0af2..655c838701 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h @@ -61,6 +61,7 @@ namespace AZ { const aiBone* m_bone = nullptr; unsigned m_numVertices = 0; + unsigned m_vertOffset = 0; AZStd::shared_ptr m_skinWeightData; }; diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp index 992a6a6ab1..47b7e410b4 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -44,7 +45,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); // LYN-2576 + serializeContext->Class()->Version(3); // LYN-3250 } } @@ -55,62 +56,79 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) + + const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex) { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); + return scene->mMeshes[meshIndex]->HasTangentsAndBitangents(); + }; - size_t vertexCount = parentMeshData->GetVertexCount(); - - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes) - { - AZ_Error(Utilities::ErrorWindow, false, - "Tried to construct tangent stream attribute for invalid or non-mesh parent data, mesh index is invalid"); - return Events::ProcessingResult::Failure; - } - - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; - - if (!mesh->HasTangentsAndBitangents()) + // If there are no tangents on any meshes, there's nothing to import in this function. + const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!anyMeshHasTangentsAndBitangents) { return Events::ProcessingResult::Ignored; } + // AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material. + // This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes, + // and the engine has code to do this later. + const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!allMeshesHaveTangentsAndBitangents) + { + const char* mixedTangentsError = + "Node with name %s has meshes with and without tangents. " + "Placeholder incorrect tangents will be generated to allow the data to process, " + "but the source art needs to be fixed to correct this. Either apply tangents to all meshes on this node, " + "or remove all tangents from all meshes on this node."; + AZ_Error( + Utilities::ErrorWindow, false, mixedTangentsError, currentNode->mName.C_Str()); + } + + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); + AZStd::shared_ptr tangentStream = AZStd::make_shared(); - // AssImp only has one tangentStream per mesh. tangentStream->SetTangentSetIndex(0); tangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx); tangentStream->ReserveContainerSpace(vertexCount); - - for (int v = 0; v < mesh->mNumVertices; ++v) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - // Vector4's constructor that takes in a vector3 sets w to 1.0f automatically. - const Vector4 tangent(AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v])); - tangentStream->AppendTangent(tangent); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (!mesh->HasTangentsAndBitangents()) + { + // This node has mixed meshes with and without tangents. + // An error was already thrown above. Output stub tangents so + // the mesh can still be output in some form, even if the data isn't correct. + // The tangent count needs to match the vertex count on the associated mesh node. + tangentStream->AppendTangent(Vector4(0.f, 1.f, 0.f, 1.f)); + } + else + { + const Vector4 tangent( + AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v])); + tangentStream->AppendTangent(tangent); + } + } } - AZStd::string nodeName(AZStd::string::format("%s", m_defaultNodeName)); Containers::SceneGraph::NodeIndex newIndex = - context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); + context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName); Events::ProcessingResult tangentResults; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, nodeName.c_str()); + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, m_defaultNodeName); tangentResults = Events::Process(dataPopulated); if (tangentResults != Events::ProcessingResult::Failure) { tangentResults = AddAttributeDataNodeWithContexts(dataPopulated); } - return tangentResults; } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp index 84c0e3e18c..5357c32fa9 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp @@ -50,7 +50,7 @@ namespace AZ Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context) { AZ_TraceContext("Importer", "transform"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName)) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp index f5f47b233d..e37a4f4285 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp @@ -12,17 +12,19 @@ #include #include +#include +#include #include #include #include #include #include #include -#include -#include +#include #include #include -#include +#include +#include #include #include @@ -45,7 +47,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(3); // LYN-2506 + serializeContext->Class()->Version(4); // LYN-3250 } } @@ -56,28 +58,53 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) + // 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. + bool foundTextureCoordinates = false; + AZStd::array meshesPerTextureCoordinateIndex = {}; + for (int localMeshIndex = 0; localMeshIndex < currentNode->mNumMeshes; ++localMeshIndex) { - return meshDataResult.GetError(); + aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[localMeshIndex]]; + for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex) + { + if (!mesh->mTextureCoords[texCoordIndex]) + { + continue; + } + ++meshesPerTextureCoordinateIndex[texCoordIndex]; + foundTextureCoordinates = true; + } } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); - size_t vertexCount = parentMeshData->GetVertexCount(); + if (!foundTextureCoordinates) + { + return Events::ProcessingResult::Ignored; + } - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - AZ_Assert(sdkMeshIndex >= 0, - "Tried to construct uv stream attribute for invalid or non-mesh parent data, mesh index is missing"); + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex) + { + int meshesWithIndex = meshesPerTextureCoordinateIndex[texCoordIndex]; + AZ_Error( + Utilities::ErrorWindow, + meshesWithIndex == 0 || meshesWithIndex == currentNode->mNumMeshes, + "Texture coordinate index %d for node %s is not on all meshes on this node. " + "Placeholder arbitrary texture values will be generated to allow the data to process, but the source art " + "needs to be fixed to correct this. All meshes on this node should have the same number of texture coordinate channels.", + texCoordIndex, + currentNode->mName.C_Str()); + } Events::ProcessingResultCombiner combinedUvMapResults; - for (int texCoordIndex = 0; texCoordIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++texCoordIndex) + for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex) { - if (!mesh->mTextureCoords[texCoordIndex]) + // No meshes have this texture coordinate index, skip it. + if (meshesPerTextureCoordinateIndex[texCoordIndex] == 0) { continue; } @@ -85,24 +112,55 @@ namespace AZ AZStd::shared_ptr uvMap = AZStd::make_shared(); uvMap->ReserveContainerSpace(vertexCount); - + bool customNameFound = false; AZStd::string name(AZStd::string::format("%s%d", m_defaultNodeName, texCoordIndex)); - if (mesh->mTextureCoordsNames[texCoordIndex].length) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - name = mesh->mTextureCoordsNames[texCoordIndex].C_Str(); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + if(mesh->mTextureCoords[texCoordIndex]) + { + if (mesh->mTextureCoordsNames[texCoordIndex].length > 0) + { + if (!customNameFound) + { + name = mesh->mTextureCoordsNames[texCoordIndex].C_Str(); + customNameFound = true; + } + else + { + AZ_Warning(Utilities::WarningWindow, + strcmp(name.c_str(), mesh->mTextureCoordsNames[texCoordIndex].C_Str()) == 0, + "Node %s has conflicting mesh coordinate names at index %d, %s and %s. Using %s.", + currentNode->mName.C_Str(), + texCoordIndex, + name.c_str(), + mesh->mTextureCoordsNames[texCoordIndex].C_Str(), + name.c_str()); + } + } + } + + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (mesh->mTextureCoords[texCoordIndex]) + { + AZ::Vector2 vertexUV( + mesh->mTextureCoords[texCoordIndex][v].x, + // The engine's V coordinate is reverse of how it's stored in the FBX file. + 1.0f - mesh->mTextureCoords[texCoordIndex][v].y); + uvMap->AppendUV(vertexUV); + } + else + { + // An error was already emitted if the UV channels for all meshes on this node do not match. + // Append an arbitrary UV value so that the mesh can still be processed. + // It's better to let the engine load a partially valid mesh than to completely fail. + uvMap->AppendUV(AZ::Vector2::CreateZero()); + } + } } uvMap->SetCustomName(name.c_str()); - - for (int v = 0; v < mesh->mNumVertices; ++v) - { - AZ::Vector2 vertexUV( - mesh->mTextureCoords[texCoordIndex][v].x, - // The engine's V coordinate is reverse of how it's stored in the FBX file. - 1.0f - mesh->mTextureCoords[texCoordIndex][v].y); - uvMap->AppendUV(vertexUV); - } - Containers::SceneGraph::NodeIndex newIndex = context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, name.c_str()); @@ -116,6 +174,7 @@ namespace AZ } combinedUvMapResults += uvMapResults; + } return combinedUvMapResults.GetResult(); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp index 59821336e0..c90fe7d1f3 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder { - bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, + bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, const AZStd::function()>& makeMeshFunc) { AZStd::unordered_map assImpMatIndexToLYIndex; @@ -34,17 +35,18 @@ namespace AZ::SceneAPI::FbxSceneBuilder { return false; } + auto newMesh = makeMeshFunc(); + newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters()); + newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters()); + + // 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. + int vertOffset = 0; for (int m = 0; m < currentNode->mNumMeshes; ++m) { - auto newMesh = makeMeshFunc(); - - newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters()); - newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters()); - - newMesh->SetSdkMeshIndex(m); - - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]]; + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]]; // Lumberyard materials are created in order based on mesh references in the scene if (assImpMatIndexToLYIndex.find(mesh->mMaterialIndex) == assImpMatIndexToLYIndex.end()) @@ -59,7 +61,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder sceneSystem.SwapVec3ForUpAxis(vertex); sceneSystem.ConvertUnit(vertex); newMesh->AddPosition(vertex); - newMesh->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx); + newMesh->SetVertexIndexToControlPointIndexMap(vertIdx + vertOffset, vertIdx + vertOffset); if (mesh->HasNormals()) { @@ -86,14 +88,15 @@ namespace AZ::SceneAPI::FbxSceneBuilder } for (int idx = 0; idx < face.mNumIndices; ++idx) { - meshFace.vertexIndex[idx] = face.mIndices[idx]; + meshFace.vertexIndex[idx] = face.mIndices[idx] + vertOffset; } newMesh->AddFace(meshFace, assImpMatIndexToLYIndex[mesh->mMaterialIndex]); } + vertOffset += mesh->mNumVertices; - meshes.push_back(newMesh); } + meshes.push_back(newMesh); return true; } @@ -127,4 +130,13 @@ namespace AZ::SceneAPI::FbxSceneBuilder azrtti_cast(parentData); return AZ::Success(parentMeshData); } + + uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene) + { + return AZStd::accumulate(node.mMeshes, node.mMeshes + node.mNumMeshes, uint64_t{ 0u }, + [&scene](auto runningTotal, unsigned int meshIndex) + { + return runningTotal + scene.mMeshes[meshIndex]->mNumVertices; + }); + } } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h index c0b5c044cb..3c7d3d2102 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h @@ -44,11 +44,16 @@ namespace AZ namespace FbxSceneBuilder { - bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, + bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, const AZStd::function()>& makeMeshFunc); typedef AZ::Outcome GetMeshDataFromParentResult; GetMeshDataFromParentResult GetMeshDataFromParent(AssImpSceneNodeAppendedContext& context); + + // If a node in the original scene file has a mesh with multiple materials on it, the associated AssImp + // node will have multiple meshes on it, broken apart per material. This returns the total number + // of vertices on all meshes on the given node. + uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene); } } } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp index abc095d583..b67696e3cc 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp @@ -27,6 +27,7 @@ namespace AZ Containers::SceneGraph::NodeIndex parentNode, const char* defaultName) { AZ_TraceContext("Node name", name); + const AZStd::string originalNodeName(name); bool isNameUpdated = false; // Nodes can't have an empty name, except of the root, otherwise nodes can't be referenced. @@ -56,7 +57,7 @@ namespace AZ // can't reference the same parent in that case. This is to make sure the node can be quickly found as // the full path will be unique. To fix any issues, an index is appended. size_t index = 1; - size_t offset = name.length(); + const size_t offset = name.length(); while (graph.Find(parentNode, name).IsValid()) { // Remove the previously tried extension. @@ -71,7 +72,8 @@ namespace AZ if (isNameUpdated) { AZ_TraceContext("New node name", name); - AZ_TracePrintf(Utilities::WarningWindow, "The name of the node was invalid or conflicting and was updated."); + AZ_TracePrintf(Utilities::WarningWindow, "The name of the node '%s' was invalid or conflicting and was updated to '%s'.", + originalNodeName.c_str(), name.c_str()); } return isNameUpdated; diff --git a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp index eccbe729c6..2cda1e68ae 100644 --- a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp +++ b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp @@ -38,12 +38,14 @@ namespace AZ { AZ_TracePrintf(SceneAPI::Utilities::LogWindow, "AssImpSceneWrapper::LoadSceneFromFile %s", fileName); AZ_TraceContext("Filename", fileName); + // aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this, + // this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release. + // There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph. m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false); m_sceneFileName = fileName; m_assImpScene = m_importer.ReadFile(fileName, aiProcess_Triangulate //Triangulates all faces of all meshes - | aiProcess_JoinIdenticalVertices //Identifies and joins identical vertex data sets for the imported meshes | aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value //dropping the least important and re-normalizing | aiProcess_GenNormals); //Generate normals for meshes diff --git a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp index e99788d570..f3c01df2e3 100644 --- a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp @@ -19,6 +19,16 @@ namespace AZ::SceneAPI::Utilities m_output += AZStd::string::format("\t%s: %s\n", name, data); } + void DebugOutput::WriteArray(const char* name, const unsigned int* data, int size) + { + m_output += AZStd::string::format("\t%s: ", name); + for (int index = 0; index < size; ++index) + { + m_output += AZStd::string::format("%d, ", data[index]); + } + m_output += AZStd::string::format("\n"); + } + void DebugOutput::Write(const char* name, const AZStd::string& data) { Write(name, data.c_str()); diff --git a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h index a598c996c9..83dc9dd6ab 100644 --- a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h +++ b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h @@ -29,6 +29,7 @@ namespace AZ::SceneAPI::Utilities void Write(const char* name, const AZStd::vector>& data); SCENE_CORE_API void Write(const char* name, const char* data); + SCENE_CORE_API void WriteArray(const char* name, const unsigned int* data, int size); SCENE_CORE_API void Write(const char* name, const AZStd::string& data); SCENE_CORE_API void Write(const char* name, double data); SCENE_CORE_API void Write(const char* name, uint64_t data); diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp index 902928d404..7d81166829 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp +++ b/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp @@ -285,8 +285,26 @@ namespace AZ void BlendShapeData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const { output.Write("Positions", m_positions); + int index = 0; + for (const auto& position : m_positions) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), position); + ++index; + } + index = 0; output.Write("Normals", m_normals); + for (const auto& normal : m_normals) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), normal); + ++index; + } + index = 0; output.Write("Faces", m_faces); + for (const auto& face : m_faces) + { + output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3); + ++index; + } } } // GraphData } // SceneData diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp index 8bf49b2898..8f464240c9 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp +++ b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp @@ -45,7 +45,6 @@ namespace AZ behaviorContext->Class() ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) ->Attribute(AZ::Script::Attributes::Module, "scene") - ->Method("GetSdkMeshIndex", &MeshData::GetSdkMeshIndex) ->Method("GetControlPointIndex", &MeshData::GetControlPointIndex) ->Method("GetUsedControlPointCount", &MeshData::GetUsedControlPointCount) ->Method("GetUsedPointIndexForControlPoint", &MeshData::GetUsedPointIndexForControlPoint) @@ -77,10 +76,6 @@ namespace AZ void MeshData::CloneAttributesFrom(const IGraphObject* sourceObject) { IMeshData::CloneAttributesFrom(sourceObject); - if (const auto* typedSource = azrtti_cast(sourceObject)) - { - SetSdkMeshIndex(typedSource->GetSdkMeshIndex()); - } } void MeshData::AddPosition(const AZ::Vector3& position) @@ -111,15 +106,6 @@ namespace AZ m_faceMaterialIds.push_back(faceMaterialId); } - void MeshData::SetSdkMeshIndex(int sdkMeshIndex) - { - m_sdkMeshIndex = sdkMeshIndex; - } - int MeshData::GetSdkMeshIndex() const - { - return m_sdkMeshIndex; - } - void MeshData::SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex) { m_vertexIndexToControlPointIndexMap[vertexIndex] = controlPointIndex; @@ -206,8 +192,26 @@ namespace AZ void MeshData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const { output.Write("Positions", m_positions); + int index = 0; + for (const auto& position : m_positions) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), position); + ++index; + } + index = 0; output.Write("Normals", m_normals); + for (const auto& normal : m_normals) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), normal); + ++index; + } + index = 0; output.Write("FaceList", m_faceList); + for (const auto& face : m_faceList) + { + output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3); + ++index; + } output.Write("FaceMaterialIds", m_faceMaterialIds); } } diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h index c5197d6cb4..4321096857 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h +++ b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h @@ -49,9 +49,6 @@ namespace AZ SCENE_DATA_API void AddFace(const AZ::SceneAPI::DataTypes::IMeshData::Face& face, unsigned int faceMaterialId = AZ::SceneAPI::DataTypes::IMeshData::s_invalidMaterialId); - SCENE_DATA_API void SetSdkMeshIndex(int sdkMeshIndex); - SCENE_DATA_API int GetSdkMeshIndex() const; - SCENE_DATA_API void SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex); SCENE_DATA_API size_t GetUsedControlPointCount() const override; SCENE_DATA_API int GetControlPointIndex(int vertexIndex) const override; @@ -80,8 +77,6 @@ namespace AZ AZStd::unordered_map m_vertexIndexToControlPointIndexMap; AZStd::unordered_map m_controlPointToUsedVertexIndexMap; - - int m_sdkMeshIndex = -1; }; } } diff --git a/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp b/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp index 79c6cfea7e..84018bbc9a 100644 --- a/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp +++ b/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp @@ -57,7 +57,6 @@ namespace AZ meshData->AddNormal(Vector3{0.1f, 0.2f, 0.3f}); meshData->AddNormal(Vector3{0.4f, 0.5f, 0.6f}); meshData->SetOriginalUnitSizeInMeters(10.0f); - meshData->SetSdkMeshIndex(1337); meshData->SetUnitSizeInMeters(0.5f); meshData->SetVertexIndexToControlPointIndexMap(0, 10); meshData->SetVertexIndexToControlPointIndexMap(1, 11); @@ -252,7 +251,6 @@ namespace AZ ExpectExecute("TestExpectFloatEquals(meshData:GetNormal(1).z, 0.6)"); ExpectExecute("TestExpectFloatEquals(meshData:GetOriginalUnitSizeInMeters(), 10.0)"); ExpectExecute("TestExpectFloatEquals(meshData:GetUnitSizeInMeters(), 0.5)"); - ExpectExecute("TestExpectIntegerEquals(meshData:GetSdkMeshIndex(), 1337)"); ExpectExecute("TestExpectIntegerEquals(meshData:GetUsedControlPointCount(), 4)"); ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(0), 10)"); ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(1), 11)"); diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 592a8e2a75..45908eb2d5 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -135,7 +135,7 @@ namespace AZ::SceneGenerationComponents { for (size_t controlPointIndex = 0; controlPointIndex < skinData.get().GetVertexCount(); ++controlPointIndex) { - const int usedPointIndex = meshData->GetUsedPointIndexForControlPoint(aznumeric_caster(controlPointIndex)); + const int usedPointIndex = meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(aznumeric_caster(controlPointIndex))); const size_t linkCount = skinData.get().GetLinkCount(controlPointIndex); if (usedPointIndex < 0 || linkCount == 0)