Ver Fonte

Fix several issues with importing actor assets (#18056) (#18119)

* Fix several issues with importing actor assets

* Fixes a crash when viewing the import settings of a scene.  The crash
was caused by Qt objects interacting with non-qt threads.
* Fixes a crash caused by inactive entities being constructed in a
background thread during scene settings (ie, the prefab builder stuff)
interacting with the real actual loaded entities and undo stack in the
editor.
* Fixes a crash caused by morph targets that are empty still making it
into the data.
* Fixes a crash caused by a mixture of skinned and unskinned meshes
being combined in a skinned mesh asset by default generating skin info
if they are part of a skin export but lack it.
* Adds additional debug output for future debugging of these issues,
this debug output is not enabled by default and uses the existing
"debug output" checkbox in the Asset Processor GUI to enable it.  When
active, dumps a lot more detail from the scene and export into the
log.

Signed-off-by: Nicholas Lawson <[email protected]>
Nicholas Lawson há 1 ano atrás
pai
commit
4a3ee11c43

+ 9 - 3
Code/Editor/Plugins/EditorAssetImporter/AssetImporterWindow.cpp

@@ -255,13 +255,14 @@ void AssetImporterWindow::OpenFileInternal(const AZStd::string& filePath)
         s_browseTag,
         [this, filePath]()
         {
+            // this is invoked across threads, so ensure that nothing touches the main thread that isn't thread safe.
+            // Qt objects, in particular, should talk via timers or queued connections.
             m_assetImporterDocument->LoadScene(filePath);
-
-            QTimer::singleShot(0, [&]() { UpdateSceneDisplay({}); });
+            QMetaObject::invokeMethod(this, &AssetImporterWindow::UpdateDefaultSceneDisplay, Qt::QueuedConnection);
         },
         [this]()
         {
-            QTimer::singleShot(0, [&]() { HandleAssetLoadingCompleted();});
+            QMetaObject::invokeMethod(this, &AssetImporterWindow::HandleAssetLoadingCompleted, Qt::QueuedConnection);
         }, this);
         
     QFileInfo fileInfo(filePath.c_str());
@@ -621,6 +622,11 @@ void AssetImporterWindow::OverlayLayerRemoved()
     }
 }
 
+void AssetImporterWindow::UpdateDefaultSceneDisplay()
+{
+    UpdateSceneDisplay({});
+}
+
 void AssetImporterWindow::UpdateSceneDisplay(const AZStd::shared_ptr<AZ::SceneAPI::Containers::Scene> scene) const
 {
     QString sceneHeaderText;

+ 1 - 0
Code/Editor/Plugins/EditorAssetImporter/AssetImporterWindow.h

@@ -119,6 +119,7 @@ private slots:
     
     void OverlayLayerAdded();
     void OverlayLayerRemoved();
+    void UpdateDefaultSceneDisplay();
     void UpdateSceneDisplay(const AZStd::shared_ptr<AZ::SceneAPI::Containers::Scene> scene = {}) const;
 
     void FileChanged(QString path);

+ 12 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorComponentBase.cpp

@@ -130,10 +130,19 @@ namespace AzToolsFramework
 
         void EditorComponentBase::SetDirty()
         {
-            if (GetEntity())
+            // Don't mark things for dirty that are not active.
+            // Entities can be inactive for a number of reasons, for example, in a prefab file
+            // being constructed on another thread, and while these might still invoke SetDirty
+            // in response to properties changing, only actualized entities should interact with
+            // the undo/redo system or the prefab change tracker for overrides, which is what
+            // marking things dirty is for.
+            if (AZ::Entity* entity = GetEntity();entity)
             {
-                AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(
-                    &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, GetEntity()->GetId());
+                if (entity->GetState() == AZ::Entity::State::Active)
+                {
+                    AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entity->GetId());
+                }
+
             }
             else
             {

+ 11 - 0
Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp

@@ -12,6 +12,7 @@
 #include <AzCore/IO/SystemFile.h>
 #include <AzCore/Serialization/Json/JsonUtils.h>
 #include <AzCore/Serialization/Utils.h>
+#include <AzCore/Settings/SettingsRegistry.h>
 #include <AzFramework/StringFunc/StringFunc.h>
 #include <SceneAPI/SceneCore/Containers/Scene.h>
 #include <SceneAPI/SceneCore/Containers/Views/PairIterator.h>
@@ -22,6 +23,16 @@
 
 namespace AZ::SceneAPI::Utilities
 {
+    bool IsDebugEnabled()
+    {
+        bool resultValue = false;
+        if (auto* registry = AZ::SettingsRegistry::Get())
+        {
+            registry->Get(resultValue, AZ::SceneAPI::Utilities::Key_AssetProcessorInDebugOutput);
+        }
+        return resultValue;
+    }
+
     bool SaveToJson(const AZStd::string& fileName, const DebugSceneGraph& graph);
 
     void DebugNode::Reflect(AZ::ReflectContext* context)

+ 3 - 0
Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h

@@ -37,6 +37,9 @@ namespace AZ::SceneAPI::Utilities
 {
     constexpr int SceneGraphVersion = 1;
 
+    //! IsDebugEnabled - returns true if additional debug output is desired from scene processing.
+    SCENE_CORE_API bool IsDebugEnabled(); 
+
     struct DebugNode
     {
         AZ_TYPE_INFO(DebugNode, "{490B9D4C-1847-46EB-BEBC-49812E104626}");

+ 17 - 11
Code/Tools/SceneAPI/SceneUI/Handlers/ProcessingHandlers/AsyncOperationProcessingHandler.cpp

@@ -11,6 +11,8 @@
 #include <AzToolsFramework/Debug/TraceContext.h>
 #include <SceneAPI/SceneUI/Handlers/ProcessingHandlers/AsyncOperationProcessingHandler.h>
 
+#include <QTimer>
+
 namespace AZ
 {
     namespace SceneAPI
@@ -27,22 +29,26 @@ namespace AZ
             void AsyncOperationProcessingHandler::BeginProcessing()
             {
                 emit StatusMessageUpdated("Waiting for background processes to complete...");
-                m_thread.reset(
-                    new AZStd::thread(
-                        [this]()
-                        {
-                            AZ_TraceContext("Tag", m_traceTag);
-                            m_operationToRun();
-                            EBUS_QUEUE_FUNCTION(AZ::TickBus, AZStd::bind(&AsyncOperationProcessingHandler::OnBackgroundOperationComplete, this));
-                        }
-                    )
+                // Note that the use of a QThread instead of an AZStd::thread is intentional here, as signals, slots, timers, and other parts
+                // of Qt will cause weird behavior and crashes if invoked from a non-QThread.  Qt tries its best to compensate, but without
+                // a QThread as context, it may not correctly be able to invoke cross-thread event queues, or safely store objects in QThreadStorage.
+                m_thread = QThread::create(
+                    [this]()
+                    {
+                        AZ_TraceContext("Tag", m_traceTag);
+                        m_operationToRun();
+                        QMetaObject::invokeMethod(this, &AsyncOperationProcessingHandler::OnBackgroundOperationComplete, Qt::QueuedConnection);
+                    }
                 );
+                m_thread->start();
             }
 
             void AsyncOperationProcessingHandler::OnBackgroundOperationComplete()
             {
-                m_thread->detach();
-                m_thread.reset(nullptr);
+                m_thread->quit(); // signal the thread's event pump to exit (at this point, its almost certainly already completed)
+                m_thread->wait(); // wait for the thread to clean up any state, as well as actually join (ie, exit) so that it is no longer running.
+                delete m_thread;
+                m_thread = nullptr;
 
                 emit StatusMessageUpdated("Processing complete");
                 if (m_onComplete)

+ 3 - 2
Code/Tools/SceneAPI/SceneUI/Handlers/ProcessingHandlers/AsyncOperationProcessingHandler.h

@@ -13,6 +13,7 @@
 #include <AzCore/std/functional.h>
 #include <AzCore/std/smart_ptr/unique_ptr.h>
 #include <SceneAPI/SceneUI/SceneUIConfiguration.h>
+#include <QThread>
 #endif
 
 namespace AZStd
@@ -34,14 +35,14 @@ namespace AZ
                 ~AsyncOperationProcessingHandler() override = default;
                 void BeginProcessing() override;
 
-            private:
+            private Q_SLOTS:
                 void OnBackgroundOperationComplete();
 
             private:
                 AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
                 AZStd::function<void()> m_operationToRun;
                 AZStd::function<void()> m_onComplete;
-                AZStd::unique_ptr<AZStd::thread> m_thread;
+                QThread* m_thread = nullptr;
                 AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
             };
         }

+ 218 - 30
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp

@@ -62,7 +62,6 @@ static constexpr AZStd::string_view MismatchedVertexLayoutsAreErrorsKey{ "/O3DE/
   */
 #define AZ_RPI_MESHES_SHARE_COMMON_BUFFERS
 
-
 namespace AZ
 {
     class Aabb;
@@ -86,7 +85,10 @@ namespace AZ
             if (auto* serialize = azrtti_cast<SerializeContext*>(context))
             {
                 serialize->Class<ModelAssetBuilderComponent, SceneAPI::SceneCore::ExportingComponent>()
-                    ->Version(38);  // Pad Skinning mesh buffers to respect appropriate alignment
+                    ->Version(39);
+
+                // v38 - Pad Skinning mesh buffers to respect appropriate alignment
+                // v39 - Automatically generate missing skinning data when skinned and unskinned data is mixed
             }
         }
 
@@ -293,6 +295,7 @@ namespace AZ
                     // this process transparent for the end-asset generated, the name assigned to the source mesh
                     // content will not include the "_optimized" prefix or the group name.
                     sourceMesh.m_name = sceneGraph.GetNodeName(originalUnoptimizedMeshIndex).GetName();
+                    sourceMesh.m_parentName = sceneGraph.GetNodeName(sceneGraph.GetNodeParent(originalUnoptimizedMeshIndex)).GetName();
 
                     // Add the MeshData to the source mesh
                     AddToMeshContent(viewIt.second, sourceMesh);
@@ -343,6 +346,29 @@ namespace AZ
             AZStd::vector<Data::Asset<ModelLodAsset>> lodAssets;
             lodAssets.resize(sourceMeshContentListsByLod.size());
 
+            // in debug mode, start by outputting the lods we intend to actually export
+            // do not do so if AZ_ENABLE_TRACING is not defined, since this would be creating variables and loops
+            // for no reason, since AZ_Info is a no-op.
+#if defined(AZ_ENABLE_TRACING)
+            if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+            {
+                AZ_Info(s_builderName, "Model '%s' will be exported with %zu LODs\n", m_modelName.c_str(), sourceMeshContentListsByLod.size());
+                for (size_t lodIndex = 0; lodIndex < sourceMeshContentListsByLod.size(); ++lodIndex)
+                {
+                    AZ_Info(s_builderName, "  LOD %zu\n", lodIndex);
+                    for (const SourceMeshContent& sourceMesh : sourceMeshContentListsByLod[lodIndex])
+                    {
+                        AZ_Info(s_builderName, "    SOURCE MESH '%s' - parent '%s'\n", sourceMesh.m_name.GetCStr(), sourceMesh.m_parentName.c_str());
+                        AZ_Info(s_builderName, "       # Vertices:    %zu\n", sourceMesh.m_meshData->GetVertexCount());
+                        AZ_Info(s_builderName, "       # Faces:       %zu\n", sourceMesh.m_meshData->GetFaceCount());
+                        AZ_Info(s_builderName, "       # Is Morphed:  %s\n",  sourceMesh.m_isMorphed ? "true" : "false");
+                        AZ_Info(s_builderName, "       # Cloth Data:  %zu\n", sourceMesh.m_meshClothData.size());
+                        AZ_Info(s_builderName, "       # Skin  Data:  %zu\n", sourceMesh.m_skinData.size());
+                    }
+                }
+            }
+#endif // AZ_ENABLE_TRACING
+
             // Joint name to joint index map used for the skinning influences.
             AZStd::unordered_map<AZStd::string, uint16_t> jointNameToIndexMap;
 
@@ -373,6 +399,11 @@ namespace AZ
                 AZStd::string lodAssetName = GetAssetFullName(ModelLodAsset::TYPEINFO_Uuid());
                 lodAssetCreator.Begin(CreateAssetId(lodAssetName));
 
+                if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                {
+                    AZ_Info(s_builderName, "  Creating LOD %d\n", lodIndex);
+                }
+
                 {
                     AZ::Outcome<ProductMeshContentList> productMeshListOutcome =
                         SourceMeshListToProductMeshList(context, sourceMeshContentList, jointNameToIndexMap, morphTargetMetaCreator);
@@ -391,6 +422,7 @@ namespace AZ
                     AZStd::shared_ptr<const SceneAPI::SceneData::StaticMeshAdvancedRule> staticMeshAdvancedRule = context.m_group.GetRuleContainerConst().FindFirstByType<SceneAPI::SceneData::StaticMeshAdvancedRule>();
                     if (staticMeshAdvancedRule && !staticMeshAdvancedRule->MergeMeshes())
                     {
+                        AZ_Info(s_builderName, "        Merging meshes disabled by advanced mesh rule.\n");
                         // If the merge meshes option is disabled in the advanced mesh rule, don't merge meshes
                         canMergeMeshes = false;
                     }
@@ -405,13 +437,51 @@ namespace AZ
                                 // If we keep track of the ordering changes in MergeMeshesByMaterialUid and then re-mapped the MORPHTARGET_VERTEXINDICES buffer
                                 // we could potentially enable merging meshes that are morphed. But for now, disable merging.
                                 canMergeMeshes = false;
+                                AZ_Info(s_builderName, "   Scene contains morph data, disabling mesh merge.\n");
                                 break;
                             }
                         }
                     }
+#if defined(AZ_ENABLE_TRACING)
+                    auto printProductMesh = [&](const ProductMeshContent& productMesh)
+                    {
+                        AZ_Info(s_builderName, "      Mesh '%s'\n", productMesh.m_name.GetCStr());
+                        AZ_Info(s_builderName, "         # Indices: %zu\n", productMesh.m_indices.size());
+                        AZ_Info(s_builderName, "         # Positions: %zu\n", productMesh.m_positions.size());
+                        AZ_Info(s_builderName, "         # Normals: %zu\n", productMesh.m_normals.size());
+                        AZ_Info(s_builderName, "         # Tangents: %zu\n", productMesh.m_tangents.size());
+                        AZ_Info(s_builderName, "         # Bitangents: %zu\n", productMesh.m_bitangents.size());
+                        AZ_Info(s_builderName, "         # UV sets: %zu\n", productMesh.m_uvSets.size());
+                        AZ_Info(s_builderName, "         # Color sets: %zu\n", productMesh.m_colorSets.size());
+                        AZ_Info(s_builderName, "         # Cloth floats: %zu\n", productMesh.m_clothData.size());
+                        AZ_Info(s_builderName, "         # Material UID: %" PRIu64 "\n", productMesh.m_materialUid);
+                        AZ_Info(s_builderName, "         # Skin Influences Per Vertex: %" PRIu32 "\n", productMesh.m_influencesPerVertex);
+                        AZ_Info(s_builderName, "         # Skin Joint Indices: %zu\n", productMesh.m_skinJointIndices.size());
+                        AZ_Info(s_builderName, "         # Skin Weights: %zu\n", productMesh.m_skinWeights.size());
+                        AZ_Info(s_builderName, "         # Morph Target Data Size: %zu\n", productMesh.m_morphTargetVertexData.size());
+                        AZ_Info(s_builderName, "         # Can Be Merged fn returns: %s\n", productMesh.CanBeMerged() ? "true" : "false");
+                    };
+
+                    auto printProductListFn = [&](const char* introMessage)
+                    {
+                        if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                        {
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "%s", introMessage);
+                            // loop over the productMeshListOutcome and output the contents in debug trace
+                            for (const ProductMeshContent& mesh : lodMeshes)
+                            {
+                                printProductMesh(mesh);
+                            }
+                        }
+                    };
+
+                    printProductListFn("  Product list --- Before merging meshes:\n");
+#endif // AZ_ENABLE_TRACING
 
                     if (canMergeMeshes)
                     {
+                        AZ_Info(s_builderName, "        Merging meshes...");
+
                         productMeshListOutcome = MergeMeshesByMaterialUid(lodMeshes);
 
                         if (!productMeshListOutcome.IsSuccess())
@@ -419,6 +489,15 @@ namespace AZ
                             return AZ::SceneAPI::Events::ProcessingResult::Failure;
                         }
                         lodMeshes = productMeshListOutcome.GetValue();
+
+#if defined(AZ_ENABLE_TRACING)
+                        printProductListFn("  Product list --- After merging meshes:\n");
+#endif
+                    }
+                    else
+                    {
+                        AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "        Mesh merging is diabled.");
+
                     }
 
 #if defined(AZ_RPI_MESHES_SHARE_COMMON_BUFFERS)
@@ -429,6 +508,14 @@ namespace AZ
                     ProductMeshContent mergedMesh;
                     MergeMeshesToCommonBuffers(lodMeshes, mergedMesh, lodMeshViews);
 
+#if defined(AZ_ENABLE_TRACING)
+                    if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                    {
+                        AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "Final Common buffer merged content:\n");
+                        printProductMesh(mergedMesh);
+                    }
+#endif
+
                     BufferAssetView indexBuffer;
                     AZStd::vector<ModelLodAsset::Mesh::StreamBufferInfo> streamBuffers;
 
@@ -494,6 +581,16 @@ namespace AZ
             // Fill the skin meta asset
             if (!jointNameToIndexMap.empty())
             {
+                if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                {
+                    AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "Skinning influences found. Creating skin meta-asset with this data:\n");
+#if defined(AZ_ENABLE_TRACING)
+                    for (const auto& joint : jointNameToIndexMap)
+                    {
+                        AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "  Joint '%s' has index %" PRIu16 "\n", joint.first.c_str(), joint.second);
+                    }
+#endif
+                }
                 SkinMetaAssetCreator skinCreator;
                 skinCreator.Begin(SkinMetaAsset::ConstructAssetId(modelAssetId, modelAssetName));
 
@@ -609,11 +706,16 @@ namespace AZ
             // will have a 1-1 relationship with the source data. This ensures morph target indices
             // don't need to be re-mapped, as long as the meshes aren't merged later
             // We just can't output a mesh that has faces with multiple materials.
+
+            bool generateMissingSkinningData = false;
+
             for (size_t i = 0; i < sourceMeshList.size(); ++i)
             {
                 const SourceMeshContent& sourceMeshContent = sourceMeshList[i];
                 FacesByMaterialUid& productsByMaterialUid = productList[i];
 
+                generateMissingSkinningData = generateMissingSkinningData || (!sourceMeshContent.m_skinData.empty());
+
                 meshTransforms.push_back(sourceMeshContent.m_worldTransform);
 
                 const auto& meshData = sourceMeshContent.m_meshData;
@@ -652,6 +754,14 @@ namespace AZ
             {
                 m_skinRuleSettings.m_maxInfluencesPerVertex = skinRule->GetMaxWeightsPerVertex();
                 m_skinRuleSettings.m_weightThreshold = skinRule->GetWeightThreshold();
+
+                // in addition, if we have a skin rule on this specific mesh group assume the intention is for skinning
+                // This will cause it to treat meshes parented to bones (instead of skinned to bones) still as skinned meshes,
+                // and generate appropriate data such that they can follow those bones around.  This is one way to make it so that
+                // a scene that contains entirely rigid objects arranged in a heirarchy by parenting instead of skinning can still be
+                // animated as if it is an actor with skin weights, if desired - just add a Skin rule to any mesh group you want to
+                // export as a skin.
+                generateMissingSkinningData = true;
             }
 
             
@@ -805,7 +915,7 @@ namespace AZ
                         clothData.reserve(vertexCount * ClothDataFloatsPerVert);
                     }
 
-                    const bool hasSkinData = !sourceMesh.m_skinData.empty();
+                    bool hasSkinData = !sourceMesh.m_skinData.empty();
                     if (hasSkinData)
                     {
                         // Skinned meshes require that positions, normals, tangents, bitangents, all exist and have the same number
@@ -828,6 +938,29 @@ namespace AZ
                         productMesh.m_skinJointIndices.reserve(totalInfluences);
                         productMesh.m_skinWeights.reserve(totalInfluences);
                     }
+                    else if (generateMissingSkinningData)
+                    {
+                        // Another issue is that while everything in input sourceMeshList is represents one logical output mesh (split by
+                        // material UID), and may be merged into one actual mesh (per material id), some input sub-meshes might have
+                        // skinning data and some might not. Artists can use their DCC tools to attach meshes directly bones via parenting,
+                        // for example. The meshes will not have any skin weight or index data, but will still be expected to move as if
+                        // they are skinned to the bone they are parented to.  To handle this, when we encounter any mesh that has skinning
+                        // data in the input list, we will make sure that EVERY mesh in the output list of this function has skinning data,
+                        // and synthesize it if its missing.
+
+                        // Note that the shader eventually used only handles odd numbers of weights, because it packs them as uint16s into
+                        // the lower and upper half of a uint32
+                        productMesh.m_influencesPerVertex = 2;
+                        productMesh.m_skinJointIndices.reserve(vertexCount * productMesh.m_influencesPerVertex);
+                        productMesh.m_skinWeights.reserve(vertexCount * productMesh.m_influencesPerVertex);
+                        hasSkinData = true;
+
+                        if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                        {
+                            AZ_Info(s_builderName, "Source mesh '%s' has no skin data, but others in the same merge do.  Will Synthesize skin data.", sourceMesh.m_name.GetCStr());
+                            AZ_Info(s_builderName, "    Reserved space for: %" PRIu32 " influences per vertex\n", productMesh.m_influencesPerVertex);
+                        }
+                    }
 
                     for (const auto& itr : oldToNewIndices)
                     {
@@ -928,7 +1061,9 @@ namespace AZ
                         // Gather skinning influences
                         if (hasSkinData)
                         {
-                            // Warn about excess of skin influences once per-source mesh.
+                            // Copy the vertex skinning data from the source mesh, to the product mesh.
+                            // this populates the productMesh.m_skinJointIndices and productMesh.m_skinWeights arrays as well as creates the
+                            // jointNameToIndexMap.
                             GatherVertexSkinningInfluences(sourceMesh, productMesh, jointNameToIndexMap, oldIndex);
                         }
                     }// for each vertex in old to new indices
@@ -938,6 +1073,17 @@ namespace AZ
                     // We also need to align to 16 and 12 byte boundary in order to respect RGB32 and RGBA32 buffer views.
                     if (hasSkinData)
                     {
+                        if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                        {
+                            AZ_Info(s_builderName, "  Aligning Buffers for mesh '%s' with vertexCount %zu\n", productMesh.m_name.GetCStr(), vertexCount);
+                            AZ_Info(s_builderName, "  Before:      %zu positions\n", positions.size());
+                            AZ_Info(s_builderName, "               %zu normals\n", normals.size());
+                            AZ_Info(s_builderName, "               %zu tangents\n", tangents.size());
+                            AZ_Info(s_builderName, "               %zu bitangents\n", bitangents.size());
+                            AZ_Info(s_builderName, "               %zu skinJointIndices\n", skinJointIndices.size());
+                            AZ_Info(s_builderName, "               %zu skinWeights\n", skinWeights.size());
+                        }
+
                         RPI::ModelAssetHelpers::AlignStreamBuffer(positions, vertexCount, PositionFormat, SkinnedMeshBufferAlignment);
                         RPI::ModelAssetHelpers::AlignStreamBuffer(normals, vertexCount, NormalFormat, SkinnedMeshBufferAlignment);
                         RPI::ModelAssetHelpers::AlignStreamBuffer(tangents, vertexCount, TangentFormat, SkinnedMeshBufferAlignment);
@@ -948,6 +1094,18 @@ namespace AZ
                             skinJointIndices, totalVertexInfluences, SkinIndicesFormat, SkinnedMeshBufferAlignment);
                         RPI::ModelAssetHelpers::AlignStreamBuffer(
                             skinWeights, totalVertexInfluences, SkinWeightFormat, SkinnedMeshBufferAlignment);
+
+                        if (AZ::SceneAPI::Utilities::IsDebugEnabled())
+                        {
+                            AZ_Info(s_builderName, "  Aligning Buffers for mesh '%s' with vertexCount %zu\n", productMesh.m_name.GetCStr(), vertexCount);
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "  After:       %zu positions\n", positions.size());
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "               %zu normals\n", normals.size());
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "               %zu tangents\n", tangents.size());
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "               %zu bitangents\n", bitangents.size());
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "               %zu skinJointIndices\n", skinJointIndices.size());
+                            AZ_Info(AZ::SceneAPI::Utilities::LogWindow, "               %zu skinWeights\n", skinWeights.size());
+                        }
+                        
                     }
 
                     // A morph target that only influenced one source mesh might be split over multiple product meshes
@@ -1055,43 +1213,73 @@ namespace AZ
             AZStd::vector<uint16_t>& skinJointIndices = productMesh.m_skinJointIndices;
             AZStd::vector<float>& skinWeights = productMesh.m_skinWeights;
 
-            size_t numInfluencesAdded = 0;
-            for (const auto& skinData : sourceMesh.m_skinData)
+            if (!sourceMesh.m_skinData.empty())
             {
-                const size_t numSkinInfluences = skinData->GetLinkCount(vertexIndex);
-
-                for (size_t influenceIndex = 0; influenceIndex < numSkinInfluences; ++influenceIndex)
+                size_t numInfluencesAdded = 0;
+                for (const auto& skinData : sourceMesh.m_skinData)
                 {
-                    const AZ::SceneAPI::DataTypes::ISkinWeightData::Link& link = skinData->GetLink(vertexIndex, influenceIndex);
+                    const size_t numSkinInfluences = skinData->GetLinkCount(vertexIndex);
 
-                    const float weight = link.weight;
-                    const AZStd::string& boneName = skinData->GetBoneName(link.boneId);
-
-                    // The bone id is a local bone id to the mesh. Since there could be multiple meshes, we store a global index to this asset,
-                    // which is guaranteed to be unique. Later we will translate those indices back using the skinmetadata.
-                    if (!jointNameToIndexMap.contains(boneName))
+                    for (size_t influenceIndex = 0; influenceIndex < numSkinInfluences; ++influenceIndex)
                     {
-                        jointNameToIndexMap[boneName] = aznumeric_caster(jointNameToIndexMap.size());
-                    }
-                    const AZ::u16 jointIndex = jointNameToIndexMap[boneName];
+                        const AZ::SceneAPI::DataTypes::ISkinWeightData::Link& link = skinData->GetLink(vertexIndex, influenceIndex);
 
-                    // Add skin influence
-                    if (weight > m_skinRuleSettings.m_weightThreshold)
-                    {
-                        if (numInfluencesAdded < productMesh.m_influencesPerVertex)
+                        const float weight = link.weight;
+                        const AZStd::string& boneName = skinData->GetBoneName(link.boneId);
+
+                        // The bone id is a local bone id to the mesh. Since there could be multiple meshes, we store a global index to this
+                        // asset, which is guaranteed to be unique. Later we will translate those indices back using the skinmetadata.
+                        if (!jointNameToIndexMap.contains(boneName))
                         {
-                            skinJointIndices.push_back(jointIndex);
-                            skinWeights.push_back(weight);
-                            numInfluencesAdded++;
+                            jointNameToIndexMap[boneName] = aznumeric_caster(jointNameToIndexMap.size());
+                        }
+                        const AZ::u16 jointIndex = jointNameToIndexMap[boneName];
+
+                        // Add skin influence
+                        if (weight > m_skinRuleSettings.m_weightThreshold)
+                        {
+                            if (numInfluencesAdded < productMesh.m_influencesPerVertex)
+                            {
+                                skinJointIndices.push_back(jointIndex);
+                                skinWeights.push_back(weight);
+                                numInfluencesAdded++;
+                            }
                         }
                     }
                 }
-            }
 
-            for (size_t influenceIndex = numInfluencesAdded; influenceIndex < productMesh.m_influencesPerVertex; ++influenceIndex)
+                for (size_t influenceIndex = numInfluencesAdded; influenceIndex < productMesh.m_influencesPerVertex; ++influenceIndex)
+                {
+                    skinJointIndices.push_back(0);
+                    skinWeights.push_back(0.0f);
+                }
+            }
+            else 
             {
-                skinJointIndices.push_back(0);
-                skinWeights.push_back(0.0f);
+                // if we trigger this 'else' it means that we have been asked to synthesize skinning data for a mesh that contains none,
+                // since this function is not going to get called except in the case where we're building a skinned object anyway.
+                if (!jointNameToIndexMap.contains(sourceMesh.m_parentName))
+                {
+                    jointNameToIndexMap[sourceMesh.m_parentName] = aznumeric_caster(jointNameToIndexMap.size());
+                }
+                const AZ::u16 jointIndex = jointNameToIndexMap[sourceMesh.m_parentName];
+
+                for (size_t influenceIndex = 0; influenceIndex < productMesh.m_influencesPerVertex; ++influenceIndex)
+                {
+                    // Handles the possible future case where more than 1 influence is requested per vertex, which would be required
+                    // if there is ever a desire to merge multiple skinned meshes that have different skinning influences per vertex
+                    // by normalizing them to the same number of influences per vertex so that their streams are compatible.
+                    if (influenceIndex == 0)
+                    {
+                        skinJointIndices.push_back(jointIndex);
+                        skinWeights.push_back(1.0f);
+                    }
+                    else
+                    {
+                        skinJointIndices.push_back(0);
+                        skinWeights.push_back(0.0f);
+                    }
+                }
             }
         }
 

+ 1 - 0
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.h

@@ -81,6 +81,7 @@ namespace AZ
             struct SourceMeshContent
             {
                 AZ::Name m_name;
+                AZStd::string m_parentName; // Used in case of skinned meshes that need autogenerated joint indices
                 AZStd::shared_ptr<const MeshData> m_meshData;
                 SceneAPI::DataTypes::MatrixType m_worldTransform = SceneAPI::DataTypes::MatrixType::CreateIdentity();
                 AZStd::shared_ptr<const TangentData> m_meshTangents;

+ 53 - 33
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp

@@ -265,51 +265,71 @@ namespace AZ::RPI
         metaData.m_numVertices = numMorphedVertices;
 
         const float morphedVerticesRatio = numMorphedVertices / static_cast<float>(numVertices);
-        AZ_Printf(ModelAssetBuilderComponent::s_builderName, "'%s' morphs %.1f%% of the vertices.", blendShapeName.c_str(), morphedVerticesRatio * 100.0f);
+        
 
-        // Calculate the minimum and maximum position compression values.
+        if (numMorphedVertices > 0)
         {
-            const AZ::Vector3& boxMin = deltaPositionAabb.GetMin();
-            const AZ::Vector3& boxMax = deltaPositionAabb.GetMax();
-            auto [minValue, maxValue] = AZStd::minmax({boxMin.GetX(), boxMin.GetY(), boxMin.GetZ(),
-                boxMax.GetX(), boxMax.GetY(), boxMax.GetZ()});
-
-            // Make sure the diff between min and max isn't too small.
-            if (maxValue - minValue < 1.0f)
+            AZ_Printf(
+                ModelAssetBuilderComponent::s_builderName,
+                "Blend shape named '%s' morphs %.1f%%  (%" PRIu32 " / % " PRIu32 ") of the vertices of mesh %s ",
+                blendShapeName.c_str(),
+                morphedVerticesRatio * 100.0f,
+                numMorphedVertices,
+                numVertices,
+                productMesh.m_name.GetCStr());
+
+            // Calculate the minimum and maximum position compression values.
             {
-                minValue -= 0.5f; // TODO: Value needs further consideration but is proven to work for EMotion FX.
-                maxValue += 0.5f;
+                const AZ::Vector3& boxMin = deltaPositionAabb.GetMin();
+                const AZ::Vector3& boxMax = deltaPositionAabb.GetMax();
+                auto [minValue, maxValue] =
+                    AZStd::minmax({ boxMin.GetX(), boxMin.GetY(), boxMin.GetZ(), boxMax.GetX(), boxMax.GetY(), boxMax.GetZ() });
+
+                // Make sure the diff between min and max isn't too small.
+                if (maxValue - minValue < 1.0f)
+                {
+                    minValue -= 0.5f; // TODO: Value needs further consideration but is proven to work for EMotion FX.
+                    maxValue += 0.5f;
+                }
+
+                metaData.m_minPositionDelta = minValue;
+                metaData.m_maxPositionDelta = maxValue;
             }
 
-            metaData.m_minPositionDelta = minValue;
-            metaData.m_maxPositionDelta = maxValue;
-        }
+            metaData.m_wrinkleMask = GetWrinkleMask(sourceSceneFilename, blendShapeName);
+
+            metaAssetCreator.AddMorphTarget(metaData);
 
-        metaData.m_wrinkleMask = GetWrinkleMask(sourceSceneFilename, blendShapeName);
+            AZ_Assert(uncompressedPositionDeltas.size() == compressedDeltas.size(), "Number of uncompressed (%d) and compressed position delta components (%d) do not match.",
+                uncompressedPositionDeltas.size(), compressedDeltas.size());
 
-        metaAssetCreator.AddMorphTarget(metaData);
+            // Compress the position deltas. (Only the newly added ones from this morph target)
+            for (size_t i = 0; i < uncompressedPositionDeltas.size(); i++)
+            {
+                compressedDeltas[i].m_positionX = Compress<uint16_t>(uncompressedPositionDeltas[i].GetX(), metaData.m_minPositionDelta, metaData.m_maxPositionDelta);
+                compressedDeltas[i].m_positionY = Compress<uint16_t>(uncompressedPositionDeltas[i].GetY(), metaData.m_minPositionDelta, metaData.m_maxPositionDelta);
+                compressedDeltas[i].m_positionZ = Compress<uint16_t>(uncompressedPositionDeltas[i].GetZ(), metaData.m_minPositionDelta, metaData.m_maxPositionDelta);
+            }
 
-        AZ_Assert(uncompressedPositionDeltas.size() == compressedDeltas.size(), "Number of uncompressed (%d) and compressed position delta components (%d) do not match.",
-            uncompressedPositionDeltas.size(), compressedDeltas.size());
+            // Now that we have all our compressed deltas, they need to be packed
+            // the way the shader expects to read them and added to the product mesh
+            packedCompressedMorphTargetVertexData.reserve(packedCompressedMorphTargetVertexData.size() + compressedDeltas.size());
+            for (size_t i = 0; i < compressedDeltas.size(); ++i)
+            {
+                packedCompressedMorphTargetVertexData.emplace_back(RPI::PackMorphTargetDelta(compressedDeltas[i]));
+            }
 
-        // Compress the position deltas. (Only the newly added ones from this morph target)
-        for (size_t i = 0; i < uncompressedPositionDeltas.size(); i++)
-        {
-            compressedDeltas[i].m_positionX = Compress<uint16_t>(uncompressedPositionDeltas[i].GetX(), metaData.m_minPositionDelta, metaData.m_maxPositionDelta);
-            compressedDeltas[i].m_positionY = Compress<uint16_t>(uncompressedPositionDeltas[i].GetY(), metaData.m_minPositionDelta, metaData.m_maxPositionDelta);
-            compressedDeltas[i].m_positionZ = Compress<uint16_t>(uncompressedPositionDeltas[i].GetZ(), metaData.m_minPositionDelta, metaData.m_maxPositionDelta);
+            AZ_Assert((packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex) == numMorphedVertices, "Vertex index range (%d) in morph target meta data does not match number of morphed vertices (%d).",
+                packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex, numMorphedVertices);
         }
-
-        // Now that we have all our compressed deltas, they need to be packed
-        // the way the shader expects to read them and added to the product mesh
-        packedCompressedMorphTargetVertexData.reserve(packedCompressedMorphTargetVertexData.size() + compressedDeltas.size());
-        for (size_t i = 0; i < compressedDeltas.size(); ++i)
+        else
         {
-            packedCompressedMorphTargetVertexData.emplace_back(RPI::PackMorphTargetDelta(compressedDeltas[i]));
+            AZ_Info(
+                ModelAssetBuilderComponent::s_builderName,
+                "Blend shape named '%s' does not affect mesh %s",
+                blendShapeName.c_str(),
+                productMesh.m_name.GetCStr());
         }
-
-        AZ_Assert((packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex) == numMorphedVertices, "Vertex index range (%d) in morph target meta data does not match number of morphed vertices (%d).",
-            packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex, numMorphedVertices);
     }
 
     Data::Asset<RPI::StreamingImageAsset> MorphTargetExporter::GetWrinkleMask(const AZStd::string& sourceSceneFullFilePath, const AZStd::string& blendShapeName) const

+ 6 - 7
Gems/SceneProcessing/Code/Source/SceneBuilder/SceneBuilderWorker.cpp

@@ -264,8 +264,9 @@ namespace SceneBuilder
             return;
         }
 
-        auto debugFlagItr = request.m_jobDescription.m_jobParameters.find(AZ_CRC_CE("DebugFlag"));
-        DebugOutputScope theDebugOutputScope(debugFlagItr != request.m_jobDescription.m_jobParameters.end() && debugFlagItr->second == "true");
+        auto itr = request.m_jobDescription.m_jobParameters.find(AZ_CRC_CE("DebugFlag"));
+        const bool isDebug = (itr != request.m_jobDescription.m_jobParameters.end() && itr->second == "true");
+        DebugOutputScope theDebugOutputScope(isDebug);
 
         AZStd::shared_ptr<Scene> scene;
         if (!LoadScene(scene, request, response))
@@ -412,6 +413,7 @@ namespace SceneBuilder
         using namespace AZ::SceneAPI;
         using namespace AZ::SceneAPI::Events;
         using namespace AZ::SceneAPI::SceneCore;
+        using AZ::SceneAPI::Utilities::IsDebugEnabled;
 
         AZ_Assert(scene, "Invalid scene passed for exporting.");
 
@@ -424,13 +426,10 @@ namespace SceneBuilder
         AZ_TracePrintf(Utilities::LogWindow, "Creating export entities.\n");
         EntityConstructor::EntityPointer exporter = EntityConstructor::BuildEntity("Scene Exporters", ExportingComponent::TYPEINFO_Uuid());
 
-        auto itr = request.m_jobDescription.m_jobParameters.find(AZ_CRC_CE("DebugFlag"));
-        const bool isDebug = (itr != request.m_jobDescription.m_jobParameters.end() && itr->second == "true");
-
         ExportProductList productList;
         ProcessingResultCombiner result;
         AZ_TracePrintf(Utilities::LogWindow, "Preparing for export.\n");
-        result += Process<PreExportEventContext>(productList, outputFolder, *scene, platformIdentifier, isDebug);
+        result += Process<PreExportEventContext>(productList, outputFolder, *scene, platformIdentifier, IsDebugEnabled());
         AZ_TracePrintf(Utilities::LogWindow, "Exporting...\n");
         result += Process<ExportEventContext>(productList, outputFolder, *scene, platformIdentifier);
         AZ_TracePrintf(Utilities::LogWindow, "Finalizing export process.\n");
@@ -459,7 +458,7 @@ namespace SceneBuilder
             AssetBuilderSDK::JobProduct jsonProduct(folder);
             response.m_outputProducts.emplace_back(jsonProduct);
         }
-        if (isDebug)
+        if (IsDebugEnabled())
         {
             AZStd::string productName;
             AzFramework::StringFunc::Path::GetFullFileName(scene->GetSourceFilename().c_str(), productName);