Forráskód Böngészése

Formalized the concept of an model's material slots

Formalized the concept of an model's material slots

Before, the ModelAsset and MaterialComponent code was conflating the idea of a material slot ID and a default material assignment. The default material asset's sub-ID was being used to uniquely identify the material slot as well. This blocks our ability to use other materials as the default assignment for individual meshes; we are forced to use whatever material that was generated from the source model file (like FBX). 

With these changes, we separate the concept of a material AssetId and a material slot ID, and store them separately. There is a new ModelMaterialSlot struct to describe each slot, including a unique "StableId". The ModelAsset stores a map of the slots, and each mesh refers to a slot by its StableId.

This is a precursor to another task that will optionally disable the auto-conversion of materials from source model files.

Also:
- These changes also enable material property overrides without having to make an editable material first, which I don't think was supported before.
- Removed unused Default.materialtype from the RPI Assets folder.
- Encapsulated members in EditorMaterialComponentExporter::ExportItem for better maintainability.

See also https://github.com/o3de/o3de-atom-sampleviewer/pull/175 

Testing:
- Took screenshots of several AtomTest levels with material overrides before making any changes. Compared these after the changes. Test levels included ActorTest_SingleActor, ActorTest_MultipleActors, and two custom levels that used shaderball and multi-mat_mesh-groups_1m_cubes.
- Lots of manual fiddling with material component.
- Created a white box component and saw that it rendered correctly.
- Cherry-picked these changes into Apocalypse's code base and verified with one of their levels.
- Ran AtomSampleViewer automated test suite. Some tests failed, but these were failing before my changes.
santorac 4 éve
szülő
commit
fa52124f9c
47 módosított fájl, 549 hozzáadás és 659 törlés
  1. 1 1
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignment.h
  2. 18 18
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignmentId.h
  3. 1 1
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/SkinnedMesh/SkinnedMeshInputBuffers.h
  4. 16 20
      Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp
  5. 56 27
      Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignmentId.cpp
  6. 8 26
      Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
  7. 2 1
      Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp
  8. 0 90
      Gems/Atom/RPI/Assets/Materials/Default.materialtype
  9. 0 127
      Gems/Atom/RPI/Assets/Materials/DefaultMaterial.azsl
  10. 0 26
      Gems/Atom/RPI/Assets/Materials/DefaultMaterial.shader
  11. 0 33
      Gems/Atom/RPI/Assets/Materials/DefaultMaterial_DepthPass.azsl
  12. 0 20
      Gems/Atom/RPI/Assets/Materials/DefaultMaterial_DepthPass.shader
  13. 0 5
      Gems/Atom/RPI/Assets/atom_rpi_asset_files.cmake
  14. 2 2
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h
  15. 6 3
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/ModelLod.h
  16. 13 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h
  17. 4 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAssetCreator.h
  18. 8 4
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h
  19. 3 2
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h
  20. 43 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelMaterialSlot.h
  21. 1 1
      Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MaterialAssetBuilderComponent.cpp
  22. 13 8
      Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp
  23. 2 0
      Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.h
  24. 6 6
      Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp
  25. 18 9
      Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLod.cpp
  26. 4 3
      Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelSystem.cpp
  27. 21 1
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp
  28. 27 0
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAssetCreator.cpp
  29. 6 6
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAsset.cpp
  30. 8 5
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp
  31. 34 0
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelMaterialSlot.cpp
  32. 41 21
      Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp
  33. 2 0
      Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake
  34. 4 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h
  35. 54 70
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp
  36. 25 65
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp
  37. 30 9
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.h
  38. 13 27
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp
  39. 3 1
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.h
  40. 1 1
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentConfig.cpp
  41. 13 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp
  42. 1 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h
  43. 3 4
      Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp
  44. 20 5
      Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp
  45. 1 0
      Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h
  46. 16 11
      Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.cpp
  47. 1 0
      Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.h

+ 1 - 1
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignment.h

@@ -66,6 +66,6 @@ namespace AZ
 
         //! Find an assignment id corresponding to the lod and label substring filters
         MaterialAssignmentId FindMaterialAssignmentIdInModel(
-            const Data::Instance<AZ::RPI::Model> model, const MaterialAssignmentLodIndex lodFilter, const AZStd::string& labelFilter);
+            const Data::Instance<AZ::RPI::Model>& model, const MaterialAssignmentLodIndex lodFilter, const AZStd::string& labelFilter);
     } // namespace Render
 } // namespace AZ

+ 18 - 18
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignmentId.h

@@ -15,6 +15,7 @@
 #include <AzCore/RTTI/RTTI.h>
 #include <AzCore/RTTI/ReflectContext.h>
 #include <AzCore/StringFunc/StringFunc.h>
+#include <Atom/RPI.Reflect/Model/ModelMaterialSlot.h>
 
 namespace AZ
 {
@@ -23,51 +24,50 @@ namespace AZ
         using MaterialAssignmentLodIndex = AZ::u64;
 
         //! MaterialAssignmentId is used to address available and overridable material slots on a model.
-        //! The LOD and one of the model's original material asset IDs are used as coordinates that identify
+        //! The LOD and one of the model's original material slot IDs are used as coordinates that identify
         //! a specific material slot or a set of slots matching either.
         struct MaterialAssignmentId final
         {
             AZ_RTTI(AZ::Render::MaterialAssignmentId, "{EB603581-4654-4C17-B6DE-AE61E79EDA97}");
             AZ_CLASS_ALLOCATOR(AZ::Render::MaterialAssignmentId, SystemAllocator, 0);
             static void Reflect(ReflectContext* context);
+            static bool ConvertVersion(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement);
 
             MaterialAssignmentId() = default;
 
-            MaterialAssignmentId(MaterialAssignmentLodIndex lodIndex, const AZ::Data::AssetId& materialAssetId);
+            MaterialAssignmentId(MaterialAssignmentLodIndex lodIndex, RPI::ModelMaterialSlot::StableId materialSlotStableId);
 
-            //! Create an ID that maps to all material slots, regardless of asset ID or LOD, effectively applying to an entire model.
+            //! Create an ID that maps to all material slots, regardless of slot ID or LOD, effectively applying to an entire model.
             static MaterialAssignmentId CreateDefault();
 
-            //! Create an ID that maps to all material slots with a corresponding asset ID, regardless of LOD.
-            static MaterialAssignmentId CreateFromAssetOnly(AZ::Data::AssetId materialAssetId);
+            //! Create an ID that maps to all material slots with a corresponding slot ID, regardless of LOD.
+            static MaterialAssignmentId CreateFromStableIdOnly(RPI::ModelMaterialSlot::StableId materialSlotStableId);
 
-            //! Create an ID that maps to a specific material slot with a corresponding asset ID and LOD.
-            static MaterialAssignmentId CreateFromLodAndAsset(MaterialAssignmentLodIndex lodIndex, AZ::Data::AssetId materialAssetId);
+            //! Create an ID that maps to a specific material slot with a corresponding stable ID and LOD.
+            static MaterialAssignmentId CreateFromLodAndStableId(MaterialAssignmentLodIndex lodIndex, RPI::ModelMaterialSlot::StableId materialSlotStableId);
 
-            //! Returns true if the asset ID and LOD are invalid
+            //! Returns true if the slot stable ID and LOD are invalid, meaning this assignment applies to the entire model.
             bool IsDefault() const;
 
-            //! Returns true if the asset ID is valid and LOD is invalid
-            bool IsAssetOnly() const;
+            //! Returns true if the slot stable ID is valid and LOD is invalid, meaning this assignment applies to every LOD.
+            bool IsSlotIdOnly() const;
 
-            //! Returns true if the asset ID and LOD are both valid
-            bool IsLodAndAsset() const;
+            //! Returns true if the slot stable ID and LOD are both valid, meaning this assignment applies to a single material slot on a specific LOD.
+            bool IsLodAndSlotId() const;
 
-            //! Creates a string composed of the asset path and LOD
+            //! Creates a string describing all the details of the assignment ID
             AZStd::string ToString() const;
 
-            //! Creates a hash composed of the asset ID sub ID and LOD
+            //! Creates a hash composed of all elements of the assignment ID
             size_t GetHash() const;
 
-            //! Returns true if both asset ID sub IDs and LODs match
             bool operator==(const MaterialAssignmentId& rhs) const;
-
-            //! Returns true if both asset ID sub IDs and LODs do not match
             bool operator!=(const MaterialAssignmentId& rhs) const;
 
             static constexpr MaterialAssignmentLodIndex NonLodIndex = -1;
+
             MaterialAssignmentLodIndex m_lodIndex = NonLodIndex;
-            AZ::Data::AssetId m_materialAssetId = AZ::Data::AssetId();
+            RPI::ModelMaterialSlot::StableId m_materialSlotStableId = RPI::ModelMaterialSlot::InvalidStableId;
         };
 
     } // namespace Render

+ 1 - 1
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/SkinnedMesh/SkinnedMeshInputBuffers.h

@@ -45,7 +45,7 @@ namespace AZ
             uint32_t m_vertexOffset = 0;
             uint32_t m_vertexCount = 0;
             Aabb m_aabb = Aabb::CreateNull();
-            Data::Asset<RPI::MaterialAsset> m_material;
+            AZ::RPI::ModelMaterialSlot m_materialSlot;
         };
 
         //! Buffer views for a specific sub-mesh that are not modified during skinning and thus are shared by all instances of the same skinned mesh

+ 16 - 20
Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp

@@ -33,8 +33,7 @@ namespace AZ
                 serializeContext->Class<MaterialAssignment>()
                     ->Version(1)
                     ->Field("MaterialAsset", &MaterialAssignment::m_materialAsset)
-                    ->Field("PropertyOverrides", &MaterialAssignment::m_propertyOverrides)
-                    ;
+                    ->Field("PropertyOverrides", &MaterialAssignment::m_propertyOverrides);
             }
 
             if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
@@ -50,8 +49,7 @@ namespace AZ
                     ->Constructor<const Data::Asset<RPI::MaterialAsset>&, const Data::Instance<RPI::Material>&>()
                     ->Method("ToString", &MaterialAssignment::ToString)
                     ->Property("materialAsset", BehaviorValueProperty(&MaterialAssignment::m_materialAsset))
-                    ->Property("propertyOverrides", BehaviorValueProperty(&MaterialAssignment::m_propertyOverrides))
-                    ;
+                    ->Property("propertyOverrides", BehaviorValueProperty(&MaterialAssignment::m_propertyOverrides));
 
                 behaviorContext->ConstantProperty("DefaultMaterialAssignment", BehaviorConstant(DefaultMaterialAssignment))
                     ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
@@ -67,7 +65,6 @@ namespace AZ
                     ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
                     ->Attribute(AZ::Script::Attributes::Category, "render")
                     ->Attribute(AZ::Script::Attributes::Module, "render");
-
             }
         }
 
@@ -123,7 +120,7 @@ namespace AZ
             }
 
             const MaterialAssignment& assetAssignment =
-                GetMaterialAssignmentFromMap(materials, MaterialAssignmentId::CreateFromAssetOnly(id.m_materialAssetId));
+                GetMaterialAssignmentFromMap(materials, MaterialAssignmentId::CreateFromStableIdOnly(id.m_materialSlotStableId));
             if (assetAssignment.m_materialInstance.get())
             {
                 return assetAssignment;
@@ -152,11 +149,12 @@ namespace AZ
                     {
                         if (mesh.m_material)
                         {
-                            const MaterialAssignmentId generalId = MaterialAssignmentId::CreateFromAssetOnly(mesh.m_material->GetAssetId());
+                            const MaterialAssignmentId generalId =
+                                MaterialAssignmentId::CreateFromStableIdOnly(mesh.m_materialSlotStableId);
                             materials[generalId] = MaterialAssignment(mesh.m_material->GetAsset(), mesh.m_material);
 
                             const MaterialAssignmentId specificId =
-                                MaterialAssignmentId::CreateFromLodAndAsset(lodIndex, mesh.m_material->GetAssetId());
+                                MaterialAssignmentId::CreateFromLodAndStableId(lodIndex, mesh.m_materialSlotStableId);
                             materials[specificId] = MaterialAssignment(mesh.m_material->GetAsset(), mesh.m_material);
                         }
                     }
@@ -168,38 +166,36 @@ namespace AZ
         }
 
         MaterialAssignmentId FindMaterialAssignmentIdInLod(
-            const Data::Instance<AZ::RPI::ModelLod>& lod, const MaterialAssignmentLodIndex lodIndex, const AZStd::string& labelFilter)
+            const Data::Instance<AZ::RPI::Model>& model,
+            const Data::Instance<AZ::RPI::ModelLod>& lod,
+            const MaterialAssignmentLodIndex lodIndex,
+            const AZStd::string& labelFilter)
         {
             for (const AZ::RPI::ModelLod::Mesh& mesh : lod->GetMeshes())
             {
-                if (mesh.m_material && mesh.m_material->GetAssetId().IsValid())
+                const AZ::RPI::ModelMaterialSlot& slot = model->GetModelAsset()->FindMaterialSlot(mesh.m_materialSlotStableId);
+                if (AZ::StringFunc::Contains(slot.m_displayName.GetCStr(), labelFilter, true))
                 {
-                    AZ::Data::AssetInfo assetInfo;
-                    AZ::Data::AssetCatalogRequestBus::BroadcastResult(
-                        assetInfo, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, mesh.m_material->GetAssetId());
-                    if (assetInfo.m_assetId.IsValid() && AZ::StringFunc::Contains(assetInfo.m_relativePath, labelFilter, true))
-                    {
-                        return MaterialAssignmentId::CreateFromLodAndAsset(lodIndex, mesh.m_material->GetAssetId());
-                    }
+                    return MaterialAssignmentId::CreateFromLodAndStableId(lodIndex, mesh.m_materialSlotStableId);
                 }
             }
             return MaterialAssignmentId();
         }
 
         MaterialAssignmentId FindMaterialAssignmentIdInModel(
-            const Data::Instance<AZ::RPI::Model> model, const MaterialAssignmentLodIndex lodFilter, const AZStd::string& labelFilter)
+            const Data::Instance<AZ::RPI::Model>& model, const MaterialAssignmentLodIndex lodFilter, const AZStd::string& labelFilter)
         {
             if (model && !labelFilter.empty())
             {
                 if (lodFilter < model->GetLodCount())
                 {
-                    return FindMaterialAssignmentIdInLod(model->GetLods()[lodFilter], lodFilter, labelFilter);
+                    return FindMaterialAssignmentIdInLod(model, model->GetLods()[lodFilter], lodFilter, labelFilter);
                 }
 
                 for (size_t lodIndex = 0; lodIndex < model->GetLodCount(); ++lodIndex)
                 {
                     const MaterialAssignmentId result =
-                        FindMaterialAssignmentIdInLod(model->GetLods()[lodIndex], MaterialAssignmentId::NonLodIndex, labelFilter);
+                        FindMaterialAssignmentIdInLod(model, model->GetLods()[lodIndex], MaterialAssignmentId::NonLodIndex, labelFilter);
                     if (!result.IsDefault())
                     {
                         return result;

+ 56 - 27
Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignmentId.cpp

@@ -14,14 +14,46 @@ namespace AZ
 {
     namespace Render
     {
+        bool MaterialAssignmentId::ConvertVersion(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement)
+        {
+            if (classElement.GetVersion() < 2)
+            {
+                constexpr AZ::u32 materialAssetIdCrc = AZ_CRC("materialAssetId");
+
+                AZ::Data::AssetId materialAssetId;
+                if (!classElement.GetChildData(materialAssetIdCrc, materialAssetId))
+                {
+                    AZ_Error("AZ::Render::MaterialAssignmentId::ConvertVersion", false, "Failed to get AssetId element");
+                    return false;
+                }
+
+                if (!classElement.RemoveElementByName(materialAssetIdCrc))
+                {
+                    AZ_Error("AZ::Render::MaterialAssignmentId::ConvertVersion", false, "Failed to remove deprecated element materialAssetId");
+                    // No need to early-return, the object will still load successfully, it will just report more errors about the unrecognized element.
+                }
+
+                if (materialAssetId.IsValid())
+                {
+                    classElement.AddElementWithData(context, "materialSlotStableId", materialAssetId.m_subId);
+                }
+                else
+                {
+                    classElement.AddElementWithData(context, "materialSlotStableId", RPI::ModelMaterialSlot::InvalidStableId);
+                }
+            }
+
+            return true;
+        }
+
         void MaterialAssignmentId::Reflect(ReflectContext* context)
         {
             if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
             {
                 serializeContext->Class<MaterialAssignmentId>()
-                    ->Version(1)
+                    ->Version(2, &MaterialAssignmentId::ConvertVersion)
                     ->Field("lodIndex", &MaterialAssignmentId::m_lodIndex)
-                    ->Field("materialAssetId", &MaterialAssignmentId::m_materialAssetId)
+                    ->Field("materialSlotStableId", &MaterialAssignmentId::m_materialSlotStableId)
                     ;
             }
 
@@ -33,75 +65,72 @@ namespace AZ
                     ->Attribute(AZ::Script::Attributes::Module, "render")
                     ->Constructor()
                     ->Constructor<const MaterialAssignmentId&>()
-                    ->Constructor<MaterialAssignmentLodIndex, AZ::Data::AssetId>()
+                    ->Constructor<MaterialAssignmentLodIndex, RPI::ModelMaterialSlot::StableId>()
                     ->Method("IsDefault", &MaterialAssignmentId::IsDefault)
-                    ->Method("IsAssetOnly", &MaterialAssignmentId::IsAssetOnly)
-                    ->Method("IsLodAndAsset", &MaterialAssignmentId::IsLodAndAsset)
+                    ->Method("IsAssetOnly", &MaterialAssignmentId::IsSlotIdOnly)     // Included for compatibility. Use "IsSlotIdOnly" instead.
+                    ->Method("IsLodAndAsset", &MaterialAssignmentId::IsLodAndSlotId) // Included for compatibility. Use "IsLodAndSlotId" instead.
+                    ->Method("IsSlotIdOnly", &MaterialAssignmentId::IsSlotIdOnly)
+                    ->Method("IsLodAndSlotId", &MaterialAssignmentId::IsLodAndSlotId)
                     ->Method("ToString", &MaterialAssignmentId::ToString)
                     ->Property("lodIndex", BehaviorValueProperty(&MaterialAssignmentId::m_lodIndex))
-                    ->Property("materialAssetId", BehaviorValueProperty(&MaterialAssignmentId::m_materialAssetId))
+                    ->Property("materialSlotStableId", BehaviorValueProperty(&MaterialAssignmentId::m_materialSlotStableId))
                     ;
             }
         }
 
-        MaterialAssignmentId::MaterialAssignmentId(MaterialAssignmentLodIndex lodIndex, const AZ::Data::AssetId& materialAssetId)
+        MaterialAssignmentId::MaterialAssignmentId(MaterialAssignmentLodIndex lodIndex, RPI::ModelMaterialSlot::StableId materialSlotStableId)
             : m_lodIndex(lodIndex)
-            , m_materialAssetId(materialAssetId)
+            , m_materialSlotStableId(materialSlotStableId)
         {
         }
 
         MaterialAssignmentId MaterialAssignmentId::CreateDefault()
         {
-            return MaterialAssignmentId(NonLodIndex, AZ::Data::AssetId());
+            return MaterialAssignmentId(NonLodIndex, RPI::ModelMaterialSlot::InvalidStableId);
         }
 
-        MaterialAssignmentId MaterialAssignmentId::CreateFromAssetOnly(AZ::Data::AssetId materialAssetId)
+        MaterialAssignmentId MaterialAssignmentId::CreateFromStableIdOnly(RPI::ModelMaterialSlot::StableId materialSlotStableId)
         {
-            return MaterialAssignmentId(NonLodIndex, materialAssetId);
+            return MaterialAssignmentId(NonLodIndex, materialSlotStableId);
         }
 
-        MaterialAssignmentId MaterialAssignmentId::CreateFromLodAndAsset(
-            MaterialAssignmentLodIndex lodIndex, AZ::Data::AssetId materialAssetId)
+        MaterialAssignmentId MaterialAssignmentId::CreateFromLodAndStableId(
+            MaterialAssignmentLodIndex lodIndex, RPI::ModelMaterialSlot::StableId materialSlotStableId)
         {
-            return MaterialAssignmentId(lodIndex, materialAssetId);
+            return MaterialAssignmentId(lodIndex, materialSlotStableId);
         }
 
         bool MaterialAssignmentId::IsDefault() const
         {
-            return m_lodIndex == NonLodIndex && !m_materialAssetId.IsValid();
+            return m_lodIndex == NonLodIndex && m_materialSlotStableId == RPI::ModelMaterialSlot::InvalidStableId;
         }
 
-        bool MaterialAssignmentId::IsAssetOnly() const
+        bool MaterialAssignmentId::IsSlotIdOnly() const
         {
-            return m_lodIndex == NonLodIndex && m_materialAssetId.IsValid();
+            return m_lodIndex == NonLodIndex && m_materialSlotStableId != RPI::ModelMaterialSlot::InvalidStableId;
         }
 
-        bool MaterialAssignmentId::IsLodAndAsset() const
+        bool MaterialAssignmentId::IsLodAndSlotId() const
         {
-            return m_lodIndex != NonLodIndex && m_materialAssetId.IsValid();
+            return m_lodIndex != NonLodIndex && m_materialSlotStableId != RPI::ModelMaterialSlot::InvalidStableId;
         }
 
         AZStd::string MaterialAssignmentId::ToString() const
         {
-            AZStd::string assetPathString;
-            AZ::Data::AssetCatalogRequestBus::BroadcastResult(
-                assetPathString, &AZ::Data::AssetCatalogRequests::GetAssetPathById, m_materialAssetId);
-            AZ::StringFunc::Path::StripPath(assetPathString);
-            AZ::StringFunc::Path::StripExtension(assetPathString);
-            return AZStd::string::format("%s:%llu", assetPathString.c_str(), m_lodIndex);
+            return AZStd::string::format("%u:%llu", m_materialSlotStableId, m_lodIndex);
         }
 
         size_t MaterialAssignmentId::GetHash() const
         {
             size_t seed = 0;
             AZStd::hash_combine(seed, m_lodIndex);
-            AZStd::hash_combine(seed, m_materialAssetId.m_subId);
+            AZStd::hash_combine(seed, m_materialSlotStableId);
             return seed;
         }
 
         bool MaterialAssignmentId::operator==(const MaterialAssignmentId& rhs) const
         {
-            return m_lodIndex == rhs.m_lodIndex && m_materialAssetId.m_subId == rhs.m_materialAssetId.m_subId;
+            return m_lodIndex == rhs.m_lodIndex && m_materialSlotStableId == rhs.m_materialSlotStableId;
         }
 
         bool MaterialAssignmentId::operator!=(const MaterialAssignmentId& rhs) const

+ 8 - 26
Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp

@@ -485,20 +485,12 @@ namespace AZ
                 AZ_Error("MeshDataInstance::MeshLoader", false, "Invalid model asset Id.");
                 return;
             }
-
-            // Check if the model is in the instance database and skip the loading process in this case.
-            // The model asset id is used as instance id to indicate that it is a static and shared.
-            Data::Instance<RPI::Model> model = Data::InstanceDatabase<RPI::Model>::Instance().Find(Data::InstanceId::CreateFromAssetId(m_modelAsset.GetId()));
-            if (model)
+            
+            if (!m_modelAsset.IsReady())
             {
-                // In case the mesh asset requires instancing (e.g. when containing a cloth buffer), the model will always be cloned and there will not be a
-                // model instance with the asset id as instance id as searched above.
-                m_parent->Init(model);
-                m_modelChangedEvent.Signal(AZStd::move(model));
-                return;
+                m_modelAsset.QueueLoad();
             }
 
-            m_modelAsset.QueueLoad();
             Data::AssetBus::Handler::BusConnect(modelAsset.GetId());
         }
 
@@ -589,18 +581,6 @@ namespace AZ
         {
             AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
 
-            auto modelAsset = model->GetModelAsset();
-            for (const auto& modelLodAsset : modelAsset->GetLodAssets())
-            {
-                for (const auto& mesh : modelLodAsset->GetMeshes())
-                {
-                    if (mesh.GetMaterialAsset().GetStatus() != Data::AssetData::AssetStatus::Ready)
-                    {
-
-                    }
-                }
-            }
-
             m_model = model;
             const size_t modelLodCount = m_model->GetLodCount();
             m_drawPacketListsByLod.resize(modelLodCount);
@@ -644,10 +624,12 @@ namespace AZ
 
             for (size_t meshIndex = 0; meshIndex < meshCount; ++meshIndex)
             {
-                Data::Instance<RPI::Material> material = modelLod.GetMeshes()[meshIndex].m_material;
+                const RPI::ModelLod::Mesh& mesh = modelLod.GetMeshes()[meshIndex];
+
+                Data::Instance<RPI::Material> material = mesh.m_material;
 
                 // Determine if there is a material override specified for this sub mesh
-                const MaterialAssignmentId materialAssignmentId(modelLodIndex, material ? material->GetAssetId() : AZ::Data::AssetId());
+                const MaterialAssignmentId materialAssignmentId(modelLodIndex, mesh.m_materialSlotStableId);
                 const MaterialAssignment& materialAssignment = GetMaterialAssignmentFromMapWithFallback(m_materialAssignments, materialAssignmentId);
                 if (materialAssignment.m_materialInstance.get())
                 {
@@ -790,7 +772,7 @@ namespace AZ
                 // retrieve the material
                 Data::Instance<RPI::Material> material = mesh.m_material;
 
-                const MaterialAssignmentId materialAssignmentId(rayTracingLod, material ? material->GetAssetId() : AZ::Data::AssetId());
+                const MaterialAssignmentId materialAssignmentId(rayTracingLod, mesh.m_materialSlotStableId);
                 const MaterialAssignment& materialAssignment = GetMaterialAssignmentFromMapWithFallback(m_materialAssignments, materialAssignmentId);
                 if (materialAssignment.m_materialInstance.get())
                 {

+ 2 - 1
Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp

@@ -640,7 +640,8 @@ namespace AZ
                     Aabb localAabb = lod.m_subMeshProperties[i].m_aabb;
                     modelLodCreator.SetMeshAabb(AZStd::move(localAabb));
 
-                    modelLodCreator.SetMeshMaterialAsset(lod.m_subMeshProperties[i].m_material);
+                    modelCreator.AddMaterialSlot(lod.m_subMeshProperties[i].m_materialSlot);
+                    modelLodCreator.SetMeshMaterialSlot(lod.m_subMeshProperties[i].m_materialSlot.m_stableId);
 
                     modelLodCreator.EndMesh();
                 }

+ 0 - 90
Gems/Atom/RPI/Assets/Materials/Default.materialtype

@@ -1,90 +0,0 @@
-{
-    "description": "A simple default base material used primarily for imported model files like FBX.",
-    "propertyLayout": {
-        "version": 1,
-        "properties": {
-            "general": [
-                {
-                    "id": "DiffuseColor",
-                    "type": "color",
-                    "defaultValue": [ 1.0, 1.0, 1.0 ],
-                    "connection": {
-                        "type": "shaderInput",
-                        "id": "m_diffuseColor"
-                    }
-                },
-                {
-                    "id": "DiffuseMap",
-                    "type": "image",
-                    "defaultValue": "",
-                    "connection": {
-                        "type": "shaderInput",
-                        "id": "m_diffuseMap"
-                    }
-                },
-                {
-                    "id": "UseDiffuseMap",
-                    "type": "bool",
-                    "defaultValue": false,
-                    "connection": {
-                        "type": "shaderOption",
-                        "id": "o_useDiffuseMap"
-                    }
-                },
-                {
-                    "id": "SpecularColor",
-                    "type": "color",
-                    "defaultValue": [ 0.0, 0.0, 0.0 ],
-                    "connection": {
-                        "type": "shaderInput",
-                        "id": "m_specularColor"
-                    }
-                },
-                {
-                    "id": "SpecularMap",
-                    "type": "image",
-                    "defaultValue": "",
-                    "connection": {
-                        "type": "shaderInput",
-                        "id": "m_specularMap"
-                    }
-                },
-                {
-                    "id": "UseSpecularMap",
-                    "type": "bool",
-                    "defaultValue": false,
-                    "connection": {
-                        "type": "shaderOption",
-                        "id": "o_useSpecularMap"
-                    }
-                },
-                {
-                    "id": "NormalMap",
-                    "type": "image",
-                    "defaultValue": "",
-                    "connection": {
-                        "type": "shaderInput",
-                        "id": "m_normalMap"
-                    }
-                },
-                {
-                    "id": "UseNormalMap",
-                    "type": "bool",
-                    "defaultValue": false,
-                    "connection": {
-                        "type": "shaderOption",
-                        "id": "o_useNormalMap"
-                    }
-                }
-            ]
-        }
-    },
-    "shaders": [
-        {
-            "file": "DefaultMaterial.shader"
-        },
-        {
-            "file": "DefaultMaterial_DepthPass.shader"
-        }            
-    ]
-}

+ 0 - 127
Gems/Atom/RPI/Assets/Materials/DefaultMaterial.azsl

@@ -1,127 +0,0 @@
-
-/*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
-
-#include <scenesrg.srgi>
-#include <viewsrg.srgi>
-
-#include <Atom/RPI/ShaderResourceGroups/DefaultDrawSrg.azsli>
-#include <Atom/RPI/ShaderResourceGroups/DefaultObjectSrg.azsli>
-#include <Atom/RPI/TangentSpace.azsli>
-
-ShaderResourceGroup MaterialSrg : SRG_PerMaterial
-{
-    float4 m_diffuseColor;
-    float3 m_specularColor;
- 
-    Texture2D m_diffuseMap;
-    Texture2D m_normalMap;
-    Texture2D m_specularMap;
-
-    Sampler m_sampler
-    {
-        MaxAnisotropy = 16;
-        AddressU = Wrap;
-        AddressV = Wrap;
-        AddressW = Wrap;
-    };
-}
-
-option bool o_useDiffuseMap = false;
-option bool o_useSpecularMap = false;
-option bool o_useNormalMap = false;
-
-struct VertexInput
-{
-    float3 m_position : POSITION;
-    float3 m_normal : NORMAL;
-    float4 m_tangent : TANGENT;
-    float3 m_bitangent : BITANGENT;
-    float2 m_uv : UV0;
-};
-
-struct VertexOutput
-{
-    float4 m_position : SV_Position;
-    float3 m_normal : NORMAL;
-    float3 m_tangent : TANGENT;
-    float3 m_bitangent : BITANGENT;
-    float2 m_uv : UV0;
-    float3 m_positionToCamera : VIEW;
-};
-
-VertexOutput MainVS(VertexInput input)
-{
-    const float4x4 objectToWorldMatrix = ObjectSrg::GetWorldMatrix();
-
-    VertexOutput output;
-    float3 worldPosition = mul(objectToWorldMatrix, float4(input.m_position,1)).xyz;
-    output.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(worldPosition, 1.0));
-
-    output.m_uv = input.m_uv;
-        
-    output.m_positionToCamera = ViewSrg::m_worldPosition - worldPosition;
-
-    float3x3 objectToWorldMatrixIT = ObjectSrg::GetWorldMatrixInverseTranspose();
-    
-    ConstructTBN(input.m_normal, input.m_tangent, input.m_bitangent, objectToWorldMatrix, objectToWorldMatrixIT, output.m_normal, output.m_tangent, output.m_bitangent);
-
-    return output;
-}
-
-struct PixelOutput
-{
-    float4 m_color : SV_Target0;
-};
-
-PixelOutput MainPS(VertexOutput input)
-{
-    PixelOutput output;
-    
-    // Very rough placeholder lighting
-    static const float3 lightDir = normalize(float3(1,1,1));
-
-    float4 baseColor = MaterialSrg::m_diffuseColor;
-    if (o_useDiffuseMap)
-    {
-        baseColor *= MaterialSrg::m_diffuseMap.Sample(MaterialSrg::m_sampler, input.m_uv);
-    }
-    
-    float3 specular = MaterialSrg::m_specularColor;
-    if (o_useSpecularMap)
-    {
-        specular *= MaterialSrg::m_specularMap.Sample(MaterialSrg::m_sampler, input.m_uv).rgb;
-    }
-
-    float3 normal;
-    if (o_useNormalMap)
-    {
-        float4 sampledValue = MaterialSrg::m_normalMap.Sample(MaterialSrg::m_sampler, input.m_uv);
-        normal = GetWorldSpaceNormal(sampledValue.xy, input.m_normal, input.m_tangent, input.m_bitangent);
-    }
-    else
-    {
-        normal = normalize(input.m_normal);
-    }
-
-    float3 viewDir = normalize(input.m_positionToCamera);
-    float3 H = normalize(lightDir + viewDir);
-    float NdotH  = max(0.001, dot(normal, H));
-    float NdotL = saturate(dot(normal, lightDir));
-    
-    float3 diffuse = NdotL * baseColor.xyz;
-    
-    specular = pow(NdotH, 5.0) * specular;
-    
-    // Combined
-    float3 result = diffuse + specular + float3(0.1, 0.1, 0.1) * baseColor.xyz;
-    
-    output.m_color = float4(result.xyz, baseColor.a);
-    
-    return output;
-}

+ 0 - 26
Gems/Atom/RPI/Assets/Materials/DefaultMaterial.shader

@@ -1,26 +0,0 @@
-{
-    "Source" : "DefaultMaterial.azsl",
-
-    "DepthStencilState" : { 
-        "Depth" : { "Enable" : true, "CompareFunc" : "Equal" }
-    },
-
-    "DrawList" : "forward",
-
-    "ProgramSettings":
-    {
-      "EntryPoints":
-      [
-        {
-          "name": "MainVS",
-          "type": "Vertex"
-        },
-        {
-          "name": "MainPS",
-          "type": "Fragment"
-        }
-      ]
-    }
-}
-
-

+ 0 - 33
Gems/Atom/RPI/Assets/Materials/DefaultMaterial_DepthPass.azsl

@@ -1,33 +0,0 @@
-
-/*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
-
-#include <scenesrg.srgi>
-#include <viewsrg.srgi>
-
-#include <Atom/RPI/ShaderResourceGroups/DefaultObjectSrg.azsli>
-
-struct VertexInput
-{
-    float3 m_position : POSITION;
-};
-
-struct VertexOutput
-{
-    float4 m_position : SV_Position;
-};
-
-VertexOutput MainVS(VertexInput input)
-{
-    const float4x4 objectToWorldMatrix = ObjectSrg::GetWorldMatrix();
-
-    VertexOutput output;
-    float3 worldPosition = mul(objectToWorldMatrix, float4(input.m_position,1)).xyz;
-    output.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(worldPosition, 1.0));
-    return output;
-}

+ 0 - 20
Gems/Atom/RPI/Assets/Materials/DefaultMaterial_DepthPass.shader

@@ -1,20 +0,0 @@
-{
-    "Source" : "DefaultMaterial_DepthPass.azsl",
-
-    "DepthStencilState" : { 
-        "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" }
-    }, 
-
-    "DrawList" : "depth",
-
-    "ProgramSettings":
-    {
-      "EntryPoints":
-      [
-        {
-          "name": "MainVS",
-          "type": "Vertex"
-        }
-      ]
-    }
-}

+ 0 - 5
Gems/Atom/RPI/Assets/atom_rpi_asset_files.cmake

@@ -7,11 +7,6 @@
 # 
 
 set(FILES
-    Materials/Default.materialtype
-    Materials/DefaultMaterial.azsl
-    Materials/DefaultMaterial.shader
-    Materials/DefaultMaterial_DepthPass.azsl
-    Materials/DefaultMaterial_DepthPass.shader
     Shader/DecomposeMsImage.azsl
     Shader/DecomposeMsImage.shader
     Shader/ImagePreview.azsl

+ 2 - 2
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h

@@ -90,8 +90,8 @@ namespace AZ
         private:
             Model() = default;
 
-            static Data::Instance<Model> CreateInternal(ModelAsset& modelAsset);
-            RHI::ResultCode Init(ModelAsset& modelAsset);
+            static Data::Instance<Model> CreateInternal(const Data::Asset<ModelAsset>& modelAsset);
+            RHI::ResultCode Init(const Data::Asset<ModelAsset>& modelAsset);
 
             AZStd::fixed_vector<Data::Instance<ModelLod>, ModelLodAsset::LodCountMax> m_lods;
             Data::Asset<ModelAsset> m_modelAsset;

+ 6 - 3
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/ModelLod.h

@@ -18,6 +18,7 @@
 #include <Atom/RHI.Reflect/Limits.h>
 
 #include <Atom/RPI.Reflect/Model/ModelLodAsset.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
 
 #include <AtomCore/std/containers/array_view.h>
 #include <AtomCore/std/containers/vector_set.h>
@@ -72,6 +73,8 @@ namespace AZ
                 RHI::IndexBufferView m_indexBufferView;
 
                 StreamInfoList m_streamInfo;
+
+                ModelMaterialSlot::StableId m_materialSlotStableId = ModelMaterialSlot::InvalidStableId;
                 
                 //! The default material assigned to the mesh by the asset.
                 Data::Instance<Material> m_material;
@@ -82,7 +85,7 @@ namespace AZ
             AZ_INSTANCE_DATA(ModelLod, "{3C796FC9-2067-4E0F-A660-269F8254D1D5}");
             AZ_CLASS_ALLOCATOR(ModelLod, AZ::SystemAllocator, 0);
 
-            static Data::Instance<ModelLod> FindOrCreate(const Data::Asset<ModelLodAsset>& lodAsset);
+            static Data::Instance<ModelLod> FindOrCreate(const Data::Asset<ModelLodAsset>& lodAsset, const Data::Asset<ModelAsset>& modelAsset);
 
             ~ModelLod() = default;
 
@@ -122,8 +125,8 @@ namespace AZ
         private:
             ModelLod() = default;
 
-            static Data::Instance<ModelLod> CreateInternal(ModelLodAsset& lodAsset);
-            RHI::ResultCode Init(ModelLodAsset& lodAsset);
+            static Data::Instance<ModelLod> CreateInternal(const Data::Asset<ModelLodAsset>& lodAsset, const AZStd::any* modelAssetAny);
+            RHI::ResultCode Init(const Data::Asset<ModelLodAsset>& lodAsset, const Data::Asset<ModelAsset>& modelAsset);
 
             bool SetMeshInstanceData(
                 const ModelLodAsset::Mesh::StreamBufferInfo& streamBufferInfo,

+ 13 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h

@@ -49,6 +49,12 @@ namespace AZ
 
             //! Returns the model-space axis aligned bounding box
             const AZ::Aabb& GetAabb() const;
+            
+            //! Returns the list of all ModelMaterialSlot's for the model, across all LODs.
+            const ModelMaterialSlotMap& GetMaterialSlots() const;
+            
+            //! Find a material slot with the given stableId, or returns an invalid slot if it isn't found.
+            const ModelMaterialSlot& FindMaterialSlot(uint32_t stableId) const;
 
             //! Returns the number of Lods in the model
             size_t GetLodCount() const;
@@ -97,6 +103,13 @@ namespace AZ
             volatile mutable bool m_isKdTreeCalculationRunning = false;
             mutable AZStd::mutex m_kdTreeLock;
             mutable AZStd::optional<AZStd::size_t> m_modelTriangleCount;
+            
+            // Lists all of the material slots that are used by this LOD.
+            // Note the same slot can appear in multiple LODs in the model, so that LODs don't have to refer back to the model asset.
+            ModelMaterialSlotMap m_materialSlots;
+
+            // A default ModelMaterialSlot to be returned upon error conditions.
+            ModelMaterialSlot m_fallbackSlot;
 
             AZStd::size_t CalculateTriangleCount() const;
         };

+ 4 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAssetCreator.h

@@ -29,6 +29,10 @@ namespace AZ
 
             //! Assigns a name to the model
             void SetName(AZStd::string_view name);
+            
+            //! Adds a new material slot to the asset.
+            //! If a slot with the same stable ID already exists, it will be replaced.
+            void AddMaterialSlot(const ModelMaterialSlot& materialSlot);
 
             //! Adds a Lod to the model.
             void AddLodAsset(Data::Asset<ModelLodAsset>&& lodAsset);

+ 8 - 4
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h

@@ -16,6 +16,7 @@
 #include <Atom/RPI.Reflect/Buffer/BufferAssetView.h>
 #include <Atom/RPI.Reflect/Buffer/BufferAsset.h>
 #include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Model/ModelMaterialSlot.h>
 
 #include <AzCore/Asset/AssetCommon.h>
 #include <AzCore/Math/Aabb.h>
@@ -84,8 +85,9 @@ namespace AZ
                 //! Returns the number of indices in this mesh
                 uint32_t GetIndexCount() const;
 
-                //! Returns the reference to material asset used by this mesh
-                const Data::Asset <MaterialAsset>& GetMaterialAsset() const;
+                //! Returns the ID of the material slot used by this mesh.
+                //! This maps into the ModelAsset's material slot list.
+                ModelMaterialSlot::StableId GetMaterialSlotId() const;
 
                 //! Returns the name of this mesh
                 const AZ::Name& GetName() const;
@@ -124,7 +126,9 @@ namespace AZ
                 AZ::Name m_name;
                 AZ::Aabb m_aabb = AZ::Aabb::CreateNull();
 
-                Data::Asset<MaterialAsset> m_materialAsset{ Data::AssetLoadBehavior::PreLoad };
+                // Identifies the material slot that is used by this mesh.
+                // References material slot in the ModelAsset that owns this mesh; see ModelAsset::FindMaterialSlot().
+                ModelMaterialSlot::StableId m_materialSlotId = ModelMaterialSlot::InvalidStableId;
 
                 // Both the buffer in m_indexBufferAssetView and the buffers in m_streamBufferInfo 
                 // may point to either unique buffers for the mesh or to consolidated 
@@ -147,7 +151,7 @@ namespace AZ
         private:
             AZStd::vector<Mesh> m_meshes;
             AZ::Aabb m_aabb = AZ::Aabb::CreateNull();
-
+            
             // These buffers owned by the lod are the consolidated super buffers. 
             // Meshes may either have views into these buffers or they may own 
             // their own buffers.

+ 3 - 2
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h

@@ -8,6 +8,7 @@
 
 #pragma once
 
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
 #include <Atom/RPI.Reflect/Model/ModelLodAsset.h>
 #include <Atom/RPI.Reflect/AssetCreator.h>
 
@@ -45,9 +46,9 @@ namespace AZ
             //! Begin and BeginMesh must be called first.
             void SetMeshAabb(AZ::Aabb&& aabb);
 
-            //! Sets the material asset for the current SubMesh.
+            //! Sets the ID of the model's material slot that this mesh uses.
             //! Begin and BeginMesh must be called first
-            void SetMeshMaterialAsset(const Data::Asset<MaterialAsset>& materialAsset);
+            void SetMeshMaterialSlot(ModelMaterialSlot::StableId id);
 
             //! Sets the given BufferAssetView to the current SubMesh as the index buffer.
             //! Begin and BeginMesh must be called first

+ 43 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelMaterialSlot.h

@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ * 
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+namespace AZ
+{
+    class ReflectContext;
+
+    namespace RPI
+    {
+        //! Use by model assets to identify a logical material slot.
+        //! Each slot has a unique ID, a name, and a default material. Each mesh in model will reference a single ModelMaterialSlot.
+        //! Other classes like MeshFeatureProcessor and MaterialComponent can override the material associated with individual slots
+        //! to alter the default appearance of the mesh.
+        struct ModelMaterialSlot
+        {
+            AZ_TYPE_INFO(ModelMaterialSlot, "{0E88A62A-D83D-4C1B-8DE7-CE972B8124B5}");
+                
+            static void Reflect(AZ::ReflectContext* context);
+
+            using StableId = uint32_t;
+            static const StableId InvalidStableId;
+
+            //! This ID must have a consistent value when the asset is reprocessed by the asset pipeline, and must be unique within the ModelLodAsset.
+            //! In practice, this set using the MaterialUid from SceneAPI. See ModelAssetBuilderComponent::CreateMesh.
+            StableId m_stableId = InvalidStableId; 
+
+            Name m_displayName; //!< The name of the slot as displayed to the user in UI. (Using Name instead of string for fast copies)
+
+            Data::Asset<MaterialAsset> m_defaultMaterialAsset{ Data::AssetLoadBehavior::PreLoad }; //!< The material that will be applied to this slot by default.
+        };
+
+        using ModelMaterialSlotMap = AZStd::unordered_map<ModelMaterialSlot::StableId, ModelMaterialSlot>;
+
+    } //namespace RPI
+} // namespace AZ

+ 1 - 1
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MaterialAssetBuilderComponent.cpp

@@ -91,7 +91,7 @@ namespace AZ
             if (auto* serialize = azrtti_cast<SerializeContext*>(context))
             {
                 serialize->Class<MaterialAssetBuilderComponent, SceneAPI::SceneCore::ExportingComponent>()
-                    ->Version(14);  // [ATOM-13410]
+                    ->Version(16);  // Optional material conversion
             }
         }
 

+ 13 - 8
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp

@@ -109,7 +109,7 @@ namespace AZ
             if (auto* serialize = azrtti_cast<SerializeContext*>(context))
             {
                 serialize->Class<ModelAssetBuilderComponent, SceneAPI::SceneCore::ExportingComponent>()
-                    ->Version(27);  // [ATOM-15658]
+                    ->Version(29);  // (updated to separate material slot ID from default material asset)
             }
         }
 
@@ -367,6 +367,9 @@ namespace AZ
 
             MorphTargetMetaAssetCreator morphTargetMetaCreator;
             morphTargetMetaCreator.Begin(MorphTargetMetaAsset::ConstructAssetId(modelAssetId, modelAssetName));
+            
+            ModelAssetCreator modelAssetCreator;
+            modelAssetCreator.Begin(modelAssetId);
 
             uint32_t lodIndex = 0;
             for (const SourceMeshContentList& sourceMeshContentList : sourceMeshContentListsByLod)
@@ -429,7 +432,7 @@ namespace AZ
 
                     for (const ProductMeshView& meshView : lodMeshViews)
                     {
-                        if (!CreateMesh(meshView, indexBuffer, streamBuffers, lodAssetCreator, context.m_materialsByUid))
+                        if (!CreateMesh(meshView, indexBuffer, streamBuffers, modelAssetCreator, lodAssetCreator, context.m_materialsByUid))
                         {
                             return AZ::SceneAPI::Events::ProcessingResult::Failure;
                         }
@@ -469,10 +472,6 @@ namespace AZ
             }
             sourceMeshContentListsByLod.clear();
 
-            // Build the final asset structure
-            ModelAssetCreator modelAssetCreator;
-            modelAssetCreator.Begin(modelAssetId);
-
             // Finalize all LOD assets
             for (auto& lodAsset : lodAssets)
             {
@@ -1796,6 +1795,7 @@ namespace AZ
             const ProductMeshView& meshView,
             const BufferAssetView& lodIndexBuffer,
             const AZStd::vector<ModelLodAsset::Mesh::StreamBufferInfo>& lodStreamBuffers,
+            ModelAssetCreator& modelAssetCreator,
             ModelLodAssetCreator& lodAssetCreator,
             const MaterialAssetsByUid& materialAssetsByUid)
         {
@@ -1806,8 +1806,13 @@ namespace AZ
                 auto iter = materialAssetsByUid.find(meshView.m_materialUid);
                 if (iter != materialAssetsByUid.end())
                 {
-                    const Data::Asset<MaterialAsset>& materialAsset = iter->second.m_asset;
-                    lodAssetCreator.SetMeshMaterialAsset(materialAsset);
+                    ModelMaterialSlot materialSlot;
+                    materialSlot.m_stableId = meshView.m_materialUid;
+                    materialSlot.m_displayName = iter->second.m_name;
+                    materialSlot.m_defaultMaterialAsset = iter->second.m_asset;
+
+                    modelAssetCreator.AddMaterialSlot(materialSlot);
+                    lodAssetCreator.SetMeshMaterialSlot(materialSlot.m_stableId);
                 }
             }
 

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

@@ -42,6 +42,7 @@ namespace AZ
         using SkinData = AZ::SceneAPI::DataTypes::ISkinWeightData;
 
         class Stream;
+        class ModelAssetCreator;
         class ModelLodAssetCreator;
         class BufferAssetCreator;
         struct PackedCompressedMorphTargetDelta;
@@ -294,6 +295,7 @@ namespace AZ
                 const ProductMeshView& meshView,
                 const BufferAssetView& lodIndexBuffer,
                 const AZStd::vector<ModelLodAsset::Mesh::StreamBufferInfo>& lodStreamBuffers,
+                ModelAssetCreator& modelAssetCreator,
                 ModelLodAssetCreator& lodAssetCreator,
                 const MaterialAssetsByUid& materialAssetsByUid);
 

+ 6 - 6
Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp

@@ -40,7 +40,7 @@ namespace AZ
             return m_lods;
         }
 
-        Data::Instance<Model> Model::CreateInternal(ModelAsset& modelAsset)
+        Data::Instance<Model> Model::CreateInternal(const Data::Asset<ModelAsset>& modelAsset)
         {
             AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
             Data::Instance<Model> model = aznew Model();
@@ -54,15 +54,15 @@ namespace AZ
             return nullptr;
         }
 
-        RHI::ResultCode Model::Init(ModelAsset& modelAsset)
+        RHI::ResultCode Model::Init(const Data::Asset<ModelAsset>& modelAsset)
         {
             AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
 
-            m_lods.resize(modelAsset.GetLodAssets().size());
+            m_lods.resize(modelAsset->GetLodAssets().size());
 
             for (size_t lodIndex = 0; lodIndex < m_lods.size(); ++lodIndex)
             {
-                const Data::Asset<ModelLodAsset>& lodAsset = modelAsset.GetLodAssets()[lodIndex];
+                const Data::Asset<ModelLodAsset>& lodAsset = modelAsset->GetLodAssets()[lodIndex];
 
                 if (!lodAsset)
                 {
@@ -70,7 +70,7 @@ namespace AZ
                     return RHI::ResultCode::Fail;
                 }
 
-                Data::Instance<ModelLod> lodInstance = ModelLod::FindOrCreate(lodAsset);
+                Data::Instance<ModelLod> lodInstance = ModelLod::FindOrCreate(lodAsset, modelAsset);
                 if (lodInstance == nullptr)
                 {
                     return RHI::ResultCode::Fail;
@@ -98,7 +98,7 @@ namespace AZ
                 m_lods[lodIndex] = AZStd::move(lodInstance);
             }
 
-            m_modelAsset = { &modelAsset, AZ::Data::AssetLoadBehavior::PreLoad };
+            m_modelAsset = modelAsset;
             m_isUploadPending = true;
             return RHI::ResultCode::Success;
         }

+ 18 - 9
Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLod.cpp

@@ -19,11 +19,14 @@ namespace AZ
 {
     namespace RPI
     {
-        Data::Instance<ModelLod> ModelLod::FindOrCreate(const Data::Asset<ModelLodAsset>& lodAsset)
+        Data::Instance<ModelLod> ModelLod::FindOrCreate(const Data::Asset<ModelLodAsset>& lodAsset, const Data::Asset<ModelAsset>& modelAsset)
         {
+            AZStd::any modelAssetAny{&modelAsset};
+
             return Data::InstanceDatabase<ModelLod>::Instance().FindOrCreate(
                 Data::InstanceId::CreateFromAssetId(lodAsset.GetId()),
-                lodAsset);
+                lodAsset,
+                &modelAssetAny);
         }
 
         AZStd::array_view<ModelLod::Mesh> ModelLod::GetMeshes() const
@@ -31,10 +34,13 @@ namespace AZ
             return m_meshes;
         }
 
-        Data::Instance<ModelLod> ModelLod::CreateInternal(ModelLodAsset& lodAsset)
+        Data::Instance<ModelLod> ModelLod::CreateInternal(const Data::Asset<ModelLodAsset>& lodAsset, const AZStd::any* modelAssetAny)
         {
+            AZ_Assert(modelAssetAny != nullptr, "Invalid model asset param");
+            auto modelAsset = AZStd::any_cast<Data::Asset<ModelAsset>*>(*modelAssetAny);
+
             Data::Instance<ModelLod> lod = aznew ModelLod();
-            const RHI::ResultCode resultCode = lod->Init(lodAsset);
+            const RHI::ResultCode resultCode = lod->Init(lodAsset, *modelAsset);
 
             if (resultCode == RHI::ResultCode::Success)
             {
@@ -44,11 +50,11 @@ namespace AZ
             return nullptr;
         }
 
-        RHI::ResultCode ModelLod::Init(ModelLodAsset& lodAsset)
+        RHI::ResultCode ModelLod::Init(const Data::Asset<ModelLodAsset>& lodAsset, const Data::Asset<ModelAsset>& modelAsset)
         {
             AZ_TRACE_METHOD();
 
-            for (const ModelLodAsset::Mesh& mesh : lodAsset.GetMeshes())
+            for (const ModelLodAsset::Mesh& mesh : lodAsset->GetMeshes())
             {
                 Mesh meshInstance;
 
@@ -100,10 +106,13 @@ namespace AZ
                     }
                 }
 
-                auto& materialAsset = mesh.GetMaterialAsset();
-                if (materialAsset.IsReady())
+                const ModelMaterialSlot& materialSlot = modelAsset->FindMaterialSlot(mesh.GetMaterialSlotId());
+
+                meshInstance.m_materialSlotStableId = materialSlot.m_stableId;
+
+                if (materialSlot.m_defaultMaterialAsset.IsReady())
                 {
-                    meshInstance.m_material = Material::FindOrCreate(materialAsset);
+                    meshInstance.m_material = Material::FindOrCreate(materialSlot.m_defaultMaterialAsset);
                 }
 
                 m_meshes.emplace_back(AZStd::move(meshInstance));

+ 4 - 3
Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelSystem.cpp

@@ -24,6 +24,7 @@ namespace AZ
         {
             ModelLodAsset::Reflect(context);
             ModelAsset::Reflect(context);
+            ModelMaterialSlot::Reflect(context);
             MorphTargetMetaAsset::Reflect(context);
             SkinMetaAsset::Reflect(context);
         }
@@ -40,9 +41,9 @@ namespace AZ
         {
             //Create Lod Database
             AZ::Data::InstanceHandler<ModelLod> lodInstanceHandler;
-            lodInstanceHandler.m_createFunction = [](Data::AssetData* modelLodAsset)
+            lodInstanceHandler.m_createFunctionWithParam = [](Data::AssetData* modelLodAsset, const AZStd::any* modelAsset)
             {
-                return ModelLod::CreateInternal(*(azrtti_cast<ModelLodAsset*>(modelLodAsset)));
+                return ModelLod::CreateInternal(Data::Asset<ModelLodAsset>{modelLodAsset, AZ::Data::AssetLoadBehavior::PreLoad}, modelAsset);
             };
             Data::InstanceDatabase<ModelLod>::Create(azrtti_typeid<ModelLodAsset>(), lodInstanceHandler);
 
@@ -50,7 +51,7 @@ namespace AZ
             AZ::Data::InstanceHandler<Model> modelInstanceHandler;
             modelInstanceHandler.m_createFunction = [](Data::AssetData* modelAsset)
             {
-                return Model::CreateInternal(*(azrtti_cast<ModelAsset*>(modelAsset)));
+                return Model::CreateInternal(Data::Asset<ModelAsset>{modelAsset, AZ::Data::AssetLoadBehavior::PreLoad});
             };
             Data::InstanceDatabase<Model>::Create(azrtti_typeid<ModelAsset>(), modelInstanceHandler);
         }

+ 21 - 1
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp

@@ -29,9 +29,10 @@ namespace AZ
             if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
             {
                 serializeContext->Class<ModelAsset, Data::AssetData>()
-                    ->Version(0)
+                    ->Version(1)
                     ->Field("Name", &ModelAsset::m_name)
                     ->Field("Aabb", &ModelAsset::m_aabb)
+                    ->Field("MaterialSlots", &ModelAsset::m_materialSlots)
                     ->Field("LodAssets", &ModelAsset::m_lodAssets)
                     ;
             }
@@ -56,6 +57,25 @@ namespace AZ
         {
             return m_aabb;
         }
+        
+        const ModelMaterialSlotMap& ModelAsset::GetMaterialSlots() const
+        {
+            return m_materialSlots;
+        }
+            
+        const ModelMaterialSlot& ModelAsset::FindMaterialSlot(uint32_t stableId) const
+        {
+            auto iter = m_materialSlots.find(stableId);
+
+            if (iter == m_materialSlots.end())
+            {
+                return m_fallbackSlot;
+            }
+            else
+            {
+                return iter->second;
+            }
+        }
 
         size_t ModelAsset::GetLodCount() const
         {

+ 27 - 0
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAssetCreator.cpp

@@ -29,6 +29,33 @@ namespace AZ
                 m_asset->m_name = name;
             }
         }
+        
+        void ModelAssetCreator::AddMaterialSlot(const ModelMaterialSlot& materialSlot)
+        {
+            if (ValidateIsReady())
+            {
+                auto iter = m_asset->m_materialSlots.find(materialSlot.m_stableId);
+
+                if (iter == m_asset->m_materialSlots.end())
+                {
+                    m_asset->m_materialSlots[materialSlot.m_stableId] = materialSlot;
+                }
+                else
+                {
+                    if (materialSlot.m_displayName != iter->second.m_displayName)
+                    {
+                        ReportWarning("Material slot %u was already added with a different name.", materialSlot.m_stableId);
+                    }
+
+                    if (materialSlot.m_defaultMaterialAsset != iter->second.m_defaultMaterialAsset)
+                    {
+                        ReportWarning("Material slot %u was already added with a different default MaterialAsset.", materialSlot.m_stableId);
+                    }
+
+                    iter->second = materialSlot;
+                }
+            }
+        }
 
         void ModelAssetCreator::AddLodAsset(Data::Asset<ModelLodAsset>&& lodAsset)
         {

+ 6 - 6
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAsset.cpp

@@ -31,16 +31,16 @@ namespace AZ
 
             Mesh::Reflect(context);
         }
-
+        
         void ModelLodAsset::Mesh::Reflect(AZ::ReflectContext* context)
         {
             if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
             {
                 serializeContext->Class<ModelLodAsset::Mesh>()
-                    ->Version(0)
-                    ->Field("Material", &ModelLodAsset::Mesh::m_materialAsset)
+                    ->Version(1)
                     ->Field("Name", &ModelLodAsset::Mesh::m_name)
                     ->Field("AABB", &ModelLodAsset::Mesh::m_aabb)
+                    ->Field("MaterialSlotId", &ModelLodAsset::Mesh::m_materialSlotId)
                     ->Field("IndexBufferAssetView", &ModelLodAsset::Mesh::m_indexBufferAssetView)
                     ->Field("StreamBufferInfo", &ModelLodAsset::Mesh::m_streamBufferInfo)
                     ;
@@ -75,9 +75,9 @@ namespace AZ
             return m_indexBufferAssetView.GetBufferViewDescriptor().m_elementCount;
         }
 
-        const Data::Asset <MaterialAsset>& ModelLodAsset::Mesh::GetMaterialAsset() const
+        ModelMaterialSlot::StableId ModelLodAsset::Mesh::GetMaterialSlotId() const
         {
-            return m_materialAsset;
+            return m_materialSlotId;
         }
 
         const AZ::Name& ModelLodAsset::Mesh::GetName() const
@@ -118,7 +118,7 @@ namespace AZ
         {
             return m_aabb;
         }
-
+        
         const BufferAssetView* ModelLodAsset::Mesh::GetSemanticBufferAssetView(const AZ::Name& semantic) const
         {
             const AZStd::array_view<ModelLodAsset::Mesh::StreamBufferInfo>& streamBufferList = GetStreamBufferInfoList();

+ 8 - 5
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp

@@ -60,13 +60,15 @@ namespace AZ
                 m_currentMesh.m_aabb = AZStd::move(aabb);
             }
         }
-
-        void ModelLodAssetCreator::SetMeshMaterialAsset(const Data::Asset<MaterialAsset>& materialAsset)
+        
+        void ModelLodAssetCreator::SetMeshMaterialSlot(ModelMaterialSlot::StableId id)
         {
-            if (ValidateIsMeshReady())
+            if (!ValidateIsMeshReady())
             {
-                m_currentMesh.m_materialAsset = materialAsset;
+                return;
             }
+
+            m_currentMesh.m_materialSlotId = id;
         }
 
         void ModelLodAssetCreator::SetMeshIndexBuffer(const BufferAssetView& bufferAssetView)
@@ -288,7 +290,8 @@ namespace AZ
                 creator.SetMeshName(sourceMesh.GetName());
                 AZ::Aabb aabb = sourceMesh.GetAabb();
                 creator.SetMeshAabb(AZStd::move(aabb));
-                creator.SetMeshMaterialAsset(sourceMesh.GetMaterialAsset());
+
+                creator.SetMeshMaterialSlot(sourceMesh.GetMaterialSlotId());
 
                 // Mesh index buffer view
                 const BufferAssetView& sourceIndexBufferView = sourceMesh.GetIndexBufferAssetView();

+ 34 - 0
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelMaterialSlot.cpp

@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ * 
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RPI.Reflect/Model/ModelMaterialSlot.h>
+#include <AzCore/RTTI/ReflectContext.h>
+#include <AzCore/Serialization/SerializeContext.h>
+
+namespace AZ
+{
+    namespace RPI
+    {
+        // Normally this would be defined in the header file and substituted by the compiler, but for
+        // some reason clang doesn't accept it.
+        const ModelMaterialSlot::StableId ModelMaterialSlot::InvalidStableId = -1;
+
+        void ModelMaterialSlot::Reflect(AZ::ReflectContext* context)
+        {
+            if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+            {
+                serializeContext->Class<ModelMaterialSlot>()
+                    ->Version(0)
+                    ->Field("StableId", &ModelMaterialSlot::m_stableId)
+                    ->Field("DisplayName", &ModelMaterialSlot::m_displayName)
+                    ->Field("DefaultMaterialAsset", &ModelMaterialSlot::m_defaultMaterialAsset)
+                    ;
+            }
+        }
+
+    } // namespace RPI
+} // namespace AZ

+ 41 - 21
Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp

@@ -17,6 +17,7 @@
 
 #include <AzCore/std/limits.h>
 #include <AzCore/Component/Entity.h>
+#include <AzCore/Math/Sfmt.h>
 
 #include <AZTestShared/Math/MathTestHelpers.h>
 #include <AzTest/AzTest.h>
@@ -91,7 +92,7 @@ namespace UnitTest
             AZ::Aabb m_aabb = AZ::Aabb::CreateNull();
             uint32_t m_indexCount = 0;
             uint32_t m_vertexCount = 0;
-            AZ::Data::Asset<AZ::RPI::MaterialAsset> m_material;
+            AZ::RPI::ModelMaterialSlot::StableId m_materialSlotId = AZ::RPI::ModelMaterialSlot::InvalidStableId;
         };
 
         struct ExpectedLod
@@ -106,6 +107,18 @@ namespace UnitTest
             AZStd::vector<ExpectedLod> m_lods;
         };
 
+        void SetUp() override
+        {
+            RPITestFixture::SetUp();
+
+            auto assetId = AZ::Data::AssetId(AZ::Uuid::CreateRandom(), 0);
+            auto typeId = AZ::AzTypeInfo<AZ::RPI::MaterialAsset>::Uuid();
+            m_materialAsset = AZ::Data::Asset<AZ::RPI::MaterialAsset>(assetId, typeId, "");
+
+            // Some tests attempt to serialize-in the model asset, which should not attempt to actually load this dummy asset reference.
+            m_materialAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehaviorNamespace::NoLoad); 
+        }
+
         AZ::RHI::ShaderSemantic GetPositionSemantic() const
         {
             return AZ::RHI::ShaderSemantic(AZ::Name("POSITION"));
@@ -136,6 +149,7 @@ namespace UnitTest
             return true;
         }
 
+        //! This function assumes the model has "sharedMeshCount + separateMeshCount" unique material slots, with incremental IDs starting at 0.
         AZ::Data::Asset<AZ::RPI::ModelLodAsset> BuildTestLod(const uint32_t sharedMeshCount, const uint32_t separateMeshCount, ExpectedLod& expectedLod)
         {
             using namespace AZ;
@@ -148,6 +162,8 @@ namespace UnitTest
             const uint32_t indexCount = 36;
             const uint32_t vertexCount = 36;
 
+            RPI::ModelMaterialSlot::StableId materialSlotId = 0;
+
             if(sharedMeshCount > 0)
             {
                 const uint32_t sharedIndexCount = indexCount * sharedMeshCount;
@@ -164,7 +180,7 @@ namespace UnitTest
                     ExpectedMesh expectedMesh;
                     expectedMesh.m_indexCount = indexCount;
                     expectedMesh.m_vertexCount = vertexCount;
-                    expectedMesh.m_material = m_materialAsset;
+                    expectedMesh.m_materialSlotId = i;
 
                     RHI::BufferViewDescriptor indexBufferViewDescriptor =
                         RHI::BufferViewDescriptor::CreateStructured(i * indexCount, indexCount, sizeof(uint32_t));
@@ -180,7 +196,7 @@ namespace UnitTest
                     creator.BeginMesh();
                     Aabb aabb = expectedMesh.m_aabb;
                     creator.SetMeshAabb(AZStd::move(aabb));
-                    creator.SetMeshMaterialAsset(m_materialAsset);
+                    creator.SetMeshMaterialSlot(materialSlotId++);
                     creator.SetMeshIndexBuffer({ sharedIndexBuffer, indexBufferViewDescriptor });
                     creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { sharedPositionBuffer, vertexBufferViewDescriptor });
                     creator.EndMesh();
@@ -195,7 +211,7 @@ namespace UnitTest
                 ExpectedMesh expectedMesh;
                 expectedMesh.m_indexCount = indexCount;
                 expectedMesh.m_vertexCount = vertexCount;
-                expectedMesh.m_material = m_materialAsset;
+                expectedMesh.m_materialSlotId = sharedMeshCount + i;
 
                 RHI::BufferViewDescriptor indexBufferViewDescriptor =
                     RHI::BufferViewDescriptor::CreateStructured(0, indexCount, sizeof(uint32_t));
@@ -213,7 +229,7 @@ namespace UnitTest
                 creator.BeginMesh();
                 Aabb aabb = expectedMesh.m_aabb;
                 creator.SetMeshAabb(AZStd::move(aabb));
-                creator.SetMeshMaterialAsset(m_materialAsset);
+                creator.SetMeshMaterialSlot(materialSlotId++);
                 creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor });
                 creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { positonBuffer, positionBufferViewDescriptor });
 
@@ -239,6 +255,15 @@ namespace UnitTest
 
             creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom()));
             creator.SetName("TestModel");
+            
+            for (RPI::ModelMaterialSlot::StableId materialSlotId = 0; materialSlotId < sharedMeshCount + separateMeshCount; ++materialSlotId)
+            {
+                RPI::ModelMaterialSlot slot;
+                slot.m_defaultMaterialAsset = m_materialAsset;
+                slot.m_displayName = AZStd::string::format("Slot%d", materialSlotId);
+                slot.m_stableId = materialSlotId;
+                creator.AddMaterialSlot(slot);
+            }
 
             for (uint32_t i = 0; i < lodCount; ++i)
             {
@@ -263,7 +288,7 @@ namespace UnitTest
             EXPECT_TRUE(mesh.GetAabb() == expectedMesh.m_aabb);
             EXPECT_TRUE(mesh.GetIndexCount() == expectedMesh.m_indexCount);
             EXPECT_TRUE(mesh.GetVertexCount() == expectedMesh.m_vertexCount);
-            EXPECT_TRUE(mesh.GetMaterialAsset() == expectedMesh.m_material);
+            EXPECT_TRUE(mesh.GetMaterialSlotId() == expectedMesh.m_materialSlotId);
         }
 
         void ValidateLodAsset(const AZ::RPI::ModelLodAsset* lodAsset, const ExpectedLod& expectedLod)
@@ -299,9 +324,7 @@ namespace UnitTest
         }
 
         const uint32_t m_manyMesh = 100; // Not too much to hold up the tests but enough to stress them
-        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_materialAsset =
-            AZ::Data::Asset<AZ::RPI::MaterialAsset>(AZ::Data::AssetId(AZ::Uuid::CreateRandom(), 0),
-                AZ::AzTypeInfo<AZ::RPI::MaterialAsset>::Uuid(), "");
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_materialAsset;
 
     };
 
@@ -687,11 +710,11 @@ namespace UnitTest
         }
     }
 
-    // Tests that if we try to set the material id on a mesh
+    // Tests that if we try to set the material slot on a mesh
     // without calling Begin or BeginMesh that it fails
     // as expected. Also tests the case that Begin *is*
     // called but BeginMesh is not.
-    TEST_F(ModelTests, SetMaterialIdNoBeginNoBeginMesh)
+    TEST_F(ModelTests, SetMaterialSlotNoBeginNoBeginMesh)
     {
         using namespace AZ;
 
@@ -699,7 +722,7 @@ namespace UnitTest
 
         {
             ErrorMessageFinder messageFinder("Begin() was not called");
-            creator.SetMeshMaterialAsset(m_materialAsset);
+            creator.SetMeshMaterialSlot(0);
         }
 
         creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom()));
@@ -707,7 +730,7 @@ namespace UnitTest
         //This should still fail even if we call Begin but not BeginMesh
         {
             ErrorMessageFinder messageFinder("BeginMesh() was not called");
-            creator.SetMeshMaterialAsset(m_materialAsset);
+            creator.SetMeshMaterialSlot(0);
         }
     }
 
@@ -827,7 +850,7 @@ namespace UnitTest
 
             creator.BeginMesh();
             creator.SetMeshAabb(AZStd::move(aabb));
-            creator.SetMeshMaterialAsset(m_materialAsset);
+            creator.SetMeshMaterialSlot(0);
             creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor });
             creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor });
 
@@ -842,7 +865,7 @@ namespace UnitTest
             ErrorMessageFinder messageFinder("BeginMesh() was not called", 5);
 
             creator.SetMeshAabb(AZStd::move(aabb));
-            creator.SetMeshMaterialAsset(m_materialAsset);
+            creator.SetMeshMaterialSlot(0);
             creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor });
             creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor });
 
@@ -885,7 +908,7 @@ namespace UnitTest
 
             creator.BeginMesh();
             creator.SetMeshAabb(AZStd::move(aabb));
-            creator.SetMeshMaterialAsset(m_materialAsset);
+            creator.SetMeshMaterialSlot(0);
             creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor });
             creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor });
 
@@ -907,7 +930,7 @@ namespace UnitTest
 
             creator.BeginMesh();
             creator.SetMeshAabb(AZStd::move(aabb));
-            creator.SetMeshMaterialAsset(m_materialAsset);
+            creator.SetMeshMaterialSlot(0);
             creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor });
             creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor });
 
@@ -1019,10 +1042,7 @@ namespace UnitTest
 
             lodCreator.BeginMesh();
             lodCreator.SetMeshAabb(AZ::Aabb::CreateFromMinMax({-1.0f, -1.0f, -0.5f}, {1.0f, 1.0f, 0.5f}));
-            lodCreator.SetMeshMaterialAsset(
-                AZ::Data::Asset<AZ::RPI::MaterialAsset>(AZ::Data::AssetId(AZ::Uuid::CreateRandom(), 0),
-                    AZ::AzTypeInfo<AZ::RPI::MaterialAsset>::Uuid(), "")
-            );
+            lodCreator.SetMeshMaterialSlot(AZ::Sfmt::GetInstance().Rand32());
 
             {
                 AZ::Data::Asset<AZ::RPI::BufferAsset> indexBuffer = BuildTestBuffer(indicesCount, sizeof(uint32_t));

+ 2 - 0
Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake

@@ -22,6 +22,7 @@ set(FILES
     Include/Atom/RPI.Reflect/Model/ModelKdTree.h
     Include/Atom/RPI.Reflect/Model/ModelLodAsset.h
     Include/Atom/RPI.Reflect/Model/ModelLodIndex.h
+    Include/Atom/RPI.Reflect/Model/ModelMaterialSlot.h
     Include/Atom/RPI.Reflect/Model/ModelAssetCreator.h
     Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h
     Include/Atom/RPI.Reflect/Model/MorphTargetDelta.h
@@ -106,6 +107,7 @@ set(FILES
     Source/RPI.Reflect/Model/ModelLodAsset.cpp
     Source/RPI.Reflect/Model/ModelAssetCreator.cpp
     Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp
+    Source/RPI.Reflect/Model/ModelMaterialSlot.cpp
     Source/RPI.Reflect/Model/MorphTargetDelta.cpp
     Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp
     Source/RPI.Reflect/Model/MorphTargetMetaAssetCreator.cpp

+ 4 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h

@@ -74,6 +74,10 @@ namespace AZ
             //! Get material assignment id matching lod and label substring
             virtual MaterialAssignmentId FindMaterialAssignmentId(
                 const MaterialAssignmentLodIndex lod, const AZStd::string& label) const = 0;
+                
+            //! Returns the list of all ModelMaterialSlot's for the model, across all LODs.
+            virtual RPI::ModelMaterialSlotMap GetModelMaterialSlots() const = 0;
+
             virtual MaterialAssignmentMap GetMaterialAssignments() const = 0;
             virtual AZStd::unordered_set<AZ::Name> GetModelUvNames() const = 0;
         };

+ 54 - 70
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp

@@ -17,6 +17,7 @@
 #include <Atom/RPI.Public/Image/StreamingImage.h>
 #include <Atom/RPI.Reflect/Material/MaterialAsset.h>
 #include <Atom/RPI.Reflect/Material/MaterialTypeAsset.h>
+#include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h>
 
 AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
 #include <QMenu>
@@ -44,64 +45,15 @@ namespace AZ
 
             if (classElement.GetVersion() < 3)
             {
-                // The default material was changed from an asset to an EditorMaterialComponentSlot and old data must be converted
-                constexpr AZ::u32 defaultMaterialAssetDataCrc = AZ_CRC("defaultMaterialAsset", 0x736fc071);
-
-                Data::Asset<RPI::MaterialAsset> oldDefaultMaterialData;
-                if (!classElement.GetChildData(defaultMaterialAssetDataCrc, oldDefaultMaterialData))
-                {
-                    AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to get defaultMaterialAsset element");
-                    return false;
-                }
-
-                if (!classElement.RemoveElementByName(defaultMaterialAssetDataCrc))
-                {
-                    AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to remove defaultMaterialAsset element");
-                    return false;
-                }
-
-                EditorMaterialComponentSlot newDefaultMaterialData;
-                newDefaultMaterialData.m_id = DefaultMaterialAssignmentId;
-                newDefaultMaterialData.m_materialAsset = oldDefaultMaterialData;
-                classElement.AddElementWithData(context, "defaultMaterialSlot", newDefaultMaterialData);
-
-                // Slots now support and display the default material asset when empty
-                // The old placeholder assignments are irrelevant and must be cleared
-                constexpr AZ::u32 materialSlotsByLodDataCrc = AZ_CRC("materialSlotsByLod", 0xb1498db6);
-
-                EditorMaterialComponentSlotsByLodContainer lodSlotData;
-                if (!classElement.GetChildData(materialSlotsByLodDataCrc, lodSlotData))
-                {
-                    AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to get materialSlotsByLod element");
-                    return false;
-                }
-
-                if (!classElement.RemoveElementByName(materialSlotsByLodDataCrc))
-                {
-                    AZ_Error("AZ::Render::EditorMaterialComponent::ConvertVersion", false, "Failed to remove materialSlotsByLod element");
-                    return false;
-                }
-
-                // Find and clear all slots that are assigned to the slot's default value
-                for (auto& lodSlots : lodSlotData)
-                {
-                    for (auto& slot : lodSlots)
-                    {
-                        if (slot.m_materialAsset.GetId() == slot.m_id.m_materialAssetId)
-                        {
-                            slot.m_materialAsset = {};
-                        }
-                    }
-                }
-
-                classElement.AddElementWithData(context, "materialSlotsByLod", lodSlotData);
+                AZ_Error("EditorMaterialComponent", false, "Material Component version < 3 is no longer supported");
+                return false;
             }
 
             if (classElement.GetVersion() < 4)
             {
                 classElement.AddElementWithData(context, "materialSlotsByLodEnabled", true);
             }
-
+            
             return true;
         }
 
@@ -238,7 +190,7 @@ namespace AZ
                 for (auto& materialSlotPair : GetMaterialSlots())
                 {
                     EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
-                    if (materialSlot->m_id.IsAssetOnly())
+                    if (materialSlot->m_id.IsSlotIdOnly())
                     {
                         materialSlot->Clear();
                     }
@@ -251,7 +203,7 @@ namespace AZ
                 for (auto& materialSlotPair : GetMaterialSlots())
                 {
                     EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
-                    if (materialSlot->m_id.IsLodAndAsset())
+                    if (materialSlot->m_id.IsLodAndSlotId())
                     {
                         materialSlot->Clear();
                     }
@@ -318,7 +270,7 @@ namespace AZ
             // Build the controller configuration from the editor configuration
             MaterialComponentConfig config = m_controller.GetConfiguration();
             config.m_materials.clear();
-
+            
             for (const auto& materialSlotPair : GetMaterialSlots())
             {
                 const EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
@@ -341,7 +293,7 @@ namespace AZ
                 else if (!materialSlot->m_propertyOverrides.empty() || !materialSlot->m_matModUvOverrides.empty())
                 {
                     MaterialAssignment& materialAssignment = config.m_materials[materialSlot->m_id];
-                    materialAssignment.m_materialAsset.Create(materialSlot->m_id.m_materialAssetId);
+                    materialAssignment.m_materialAsset = materialSlot->m_defaultMaterialAsset;
                     materialAssignment.m_propertyOverrides = materialSlot->m_propertyOverrides;
                     materialAssignment.m_matModUvOverrides = materialSlot->m_matModUvOverrides;
                 }
@@ -362,6 +314,9 @@ namespace AZ
             // Get the known material assignment slots from the associated model or other source
             MaterialAssignmentMap materialsFromSource;
             MaterialReceiverRequestBus::EventResult(materialsFromSource, GetEntityId(), &MaterialReceiverRequestBus::Events::GetMaterialAssignments);
+                        
+            RPI::ModelMaterialSlotMap modelMaterialSlots;
+            MaterialReceiverRequestBus::EventResult(modelMaterialSlots, GetEntityId(), &MaterialReceiverRequestBus::Events::GetModelMaterialSlots);
 
             // Generate the table of editable materials using the source data to define number of groups, elements, and initial values
             for (const auto& materialPair : materialsFromSource)
@@ -385,9 +340,33 @@ namespace AZ
                     OnConfigurationChanged();
                 };
 
+                const char* UnknownSlotName = "<unknown>";
+
+                // If this is the default material assignment ID then it represents the default slot which is not contained in any other group
+                if (slot.m_id == DefaultMaterialAssignmentId)
+                {
+                    slot.m_label = "Default Material";
+                }
+                else
+                {
+                    auto slotIter = modelMaterialSlots.find(slot.m_id.m_materialSlotStableId);
+                    if (slotIter != modelMaterialSlots.end())
+                    {
+                        const Name& displayName = slotIter->second.m_displayName;
+                        slot.m_label = !displayName.IsEmpty() ? displayName.GetStringView() : UnknownSlotName;
+
+                        slot.m_defaultMaterialAsset = slotIter->second.m_defaultMaterialAsset;
+                    }
+                    else
+                    {
+                        slot.m_label = UnknownSlotName;
+                    }
+                }
+
                 // if material is present in controller configuration, assign its data
                 const MaterialAssignment& materialFromController = GetMaterialAssignmentFromMap(config.m_materials, slot.m_id);
                 slot.m_materialAsset = materialFromController.m_materialAsset;
+
                 slot.m_propertyOverrides = materialFromController.m_propertyOverrides;
                 slot.m_matModUvOverrides = materialFromController.m_matModUvOverrides;
 
@@ -400,13 +379,13 @@ namespace AZ
                     continue;
                 }
 
-                if (slot.m_id.IsAssetOnly())
+                if (slot.m_id.IsSlotIdOnly())
                 {
                     m_materialSlots.push_back(slot);
                     continue;
                 }
 
-                if (slot.m_id.IsLodAndAsset())
+                if (slot.m_id.IsLodAndSlotId())
                 {
                     // Resize the containers to fit all elements
                     m_materialSlotsByLod.resize(AZ::GetMax<size_t>(m_materialSlotsByLod.size(), aznumeric_cast<size_t>(slot.m_id.m_lodIndex + 1)));
@@ -452,27 +431,26 @@ namespace AZ
         {
             AzToolsFramework::ScopedUndoBatch undoBatch("Generating materials.");
             SetDirty();
-
+            
             // First generating a unique set of all material asset IDs that will be used for source data generation
-            AZStd::unordered_set<AZ::Data::AssetId> assetIds;
+            AZStd::unordered_map<AZ::Data::AssetId, AZStd::string /*slot name*/> assetIdMap;
 
             auto materialSlots = GetMaterialSlots();
             for (auto& materialSlotPair : materialSlots)
             {
-                EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
-                if (materialSlot->m_id.m_materialAssetId.IsValid())
+                Data::AssetId defaultMaterialAssetId = materialSlotPair.second->m_defaultMaterialAsset.GetId();
+                if (defaultMaterialAssetId.IsValid())
                 {
-                    assetIds.insert(materialSlot->m_id.m_materialAssetId);
+                    assetIdMap[defaultMaterialAssetId] = materialSlotPair.second->GetLabel();
                 }
             }
 
             // Convert the unique set of asset IDs into export items that can be configured in the dialog 
             // The order should not matter because the table in the dialog can sort itself for a specific row
             EditorMaterialComponentExporter::ExportItemsContainer exportItems;
-            for (const AZ::Data::AssetId& assetId : assetIds)
+            for (auto assetIdInfo : assetIdMap)
             {
-                EditorMaterialComponentExporter::ExportItem exportItem;
-                exportItem.m_assetId = assetId;
+                EditorMaterialComponentExporter::ExportItem exportItem{assetIdInfo.first, assetIdInfo.second};
                 exportItems.push_back(exportItem);
             }
 
@@ -486,15 +464,20 @@ namespace AZ
                         continue;
                     }
 
-                    const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.m_exportPath, 0);
+                    const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.GetExportPath(), 0);
                     if (assetIdOutcome)
                     {
                         for (auto& materialSlotPair : materialSlots)
                         {
-                            EditorMaterialComponentSlot* materialSlot = materialSlotPair.second;
-                            if (materialSlot && materialSlot->m_id.m_materialAssetId == exportItem.m_assetId)
+                            EditorMaterialComponentSlot* editorMaterialSlot = materialSlotPair.second;
+
+                            if (editorMaterialSlot)
                             {
-                                materialSlot->m_materialAsset.Create(assetIdOutcome.GetValue());
+                                // We need to check whether replaced material corresponds to this slot's default material.
+                                if (editorMaterialSlot->m_defaultMaterialAsset.GetId() == exportItem.GetOriginalAssetId())  
+                                {
+                                    editorMaterialSlot->m_materialAsset.Create(assetIdOutcome.GetValue());
+                                }
                             }
                         }
                     }
@@ -582,3 +565,4 @@ namespace AZ
         }
     } // namespace Render
 } // namespace AZ
+

+ 25 - 65
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp

@@ -37,47 +37,7 @@ namespace AZ
     {
         namespace EditorMaterialComponentExporter
         {
-            AZStd::string GetLabelByAssetId(const AZ::Data::AssetId& assetId)
-            {
-                AZStd::string label;
-                if (assetId.IsValid())
-                {
-                    // Material assets that are exported through the scene pipeline have their filenames generated by adding
-                    // the DCC material name as a prefix and a unique number to the end of the source file name.
-                    // Rather than storing the DCC material name inside of the material asset we can reproduce it by removing
-                    // the prefix and suffix from the product file name.
-
-                    // We need the material product path as the initial string that will be stripped down
-                    const AZStd::string& productPath = AZ::RPI::AssetUtils::GetProductPathByAssetId(assetId);
-                    if (!productPath.empty() && AzFramework::StringFunc::Path::GetFileName(productPath.c_str(), label))
-                    {
-                        // If there is a source file, typically an FBX or other model file, we must get its filename to remove the prefix from the label
-                        AZStd::string prefix;
-                        const AZStd::string& sourcePath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(assetId);
-                        if (!sourcePath.empty() && AZ::StringFunc::Path::GetFileName(sourcePath.c_str(), prefix))
-                        {
-                            if (!prefix.empty() && prefix.size() < label.size())
-                            {
-                                if (AZ::StringFunc::StartsWith(label, prefix, false))
-                                {
-                                    // All of the product filename's tokens are separated by underscores so we must also remove the first underscore after the prefix
-                                    label = label.substr(prefix.size() + 1);
-                                }
-                            }
-                        }
-
-                        // We can remove the numeric suffix by stripping the label of everything after the last underscore
-                        const auto iter = label.find_last_of("_");
-                        if (iter != AZStd::string::npos)
-                        {
-                            label = label.substr(0, iter);
-                        }
-                    }
-                }
-                return label;
-            }
-
-            AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId)
+            AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId, const AZStd::string& materialSlotName)
             {
                 AZStd::string exportPath;
                 if (assetId.IsValid())
@@ -85,7 +45,7 @@ namespace AZ
                     exportPath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(assetId);
                     AZ::StringFunc::Path::StripExtension(exportPath);
                     exportPath += "_";
-                    exportPath += GetLabelByAssetId(assetId);
+                    exportPath += materialSlotName;
                     exportPath += ".";
                     exportPath += AZ::RPI::MaterialSourceData::Extension;
                     AZ::StringFunc::Path::Normalize(exportPath);
@@ -132,12 +92,12 @@ namespace AZ
                 int row = 0;
                 for (ExportItem& exportItem : exportItems)
                 {
-                    QFileInfo fileInfo(GetExportPathByAssetId(exportItem.m_assetId).c_str());
+                    QFileInfo fileInfo(GetExportPathByAssetId(exportItem.GetOriginalAssetId(), exportItem.GetMaterialSlotName()).c_str());
 
                     // Configuring initial settings based on whether or not the target file already exists
-                    exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData();
-                    exportItem.m_exists = fileInfo.exists();
-                    exportItem.m_overwrite = false;
+                    exportItem.SetExportPath(fileInfo.absoluteFilePath().toUtf8().constData());
+                    exportItem.SetExists(fileInfo.exists());
+                    exportItem.SetOverwrite(false);
 
                     // Populate the table with data for every column
                     tableWidget->setItem(row, MaterialSlotColumn, new QTableWidgetItem());
@@ -146,23 +106,23 @@ namespace AZ
 
                     // Create a check box for toggling the enabled state of this item
                     QCheckBox* materialSlotCheckBox = new QCheckBox(tableWidget);
-                    materialSlotCheckBox->setChecked(exportItem.m_enabled);
-                    materialSlotCheckBox->setText(GetLabelByAssetId(exportItem.m_assetId).c_str());
+                    materialSlotCheckBox->setChecked(exportItem.GetEnabled());
+                    materialSlotCheckBox->setText(exportItem.GetMaterialSlotName().c_str());
                     tableWidget->setCellWidget(row, MaterialSlotColumn, materialSlotCheckBox);
 
                     // Create a file picker widget for selecting the save path for the exported material
                     AzQtComponents::BrowseEdit* materialFileWidget = new AzQtComponents::BrowseEdit(tableWidget);
                     materialFileWidget->setLineEditReadOnly(true);
                     materialFileWidget->setClearButtonEnabled(false);
-                    materialFileWidget->setEnabled(exportItem.m_enabled);
+                    materialFileWidget->setEnabled(exportItem.GetEnabled());
                     materialFileWidget->setText(fileInfo.fileName());
                     tableWidget->setCellWidget(row, MaterialFileColumn, materialFileWidget);
 
                     // Create a check box for toggling the overwrite state of this item
                     QWidget* overwriteCheckBoxContainer = new QWidget(tableWidget);
                     QCheckBox* overwriteCheckBox = new QCheckBox(overwriteCheckBoxContainer);
-                    overwriteCheckBox->setChecked(exportItem.m_overwrite);
-                    overwriteCheckBox->setEnabled(exportItem.m_enabled && exportItem.m_exists);
+                    overwriteCheckBox->setChecked(exportItem.GetOverwrite());
+                    overwriteCheckBox->setEnabled(exportItem.GetEnabled() && exportItem.GetExists());
 
                     overwriteCheckBoxContainer->setLayout(new QHBoxLayout(overwriteCheckBoxContainer));
                     overwriteCheckBoxContainer->layout()->addWidget(overwriteCheckBox);
@@ -173,21 +133,21 @@ namespace AZ
 
                     // Whenever the selection is updated, automatically apply the change to the export item
                     QObject::connect(materialSlotCheckBox, &QCheckBox::stateChanged, materialSlotCheckBox, [&exportItem, materialFileWidget, materialSlotCheckBox, overwriteCheckBox]([[maybe_unused]] int state) {
-                        exportItem.m_enabled = materialSlotCheckBox->isChecked();
-                        materialFileWidget->setEnabled(exportItem.m_enabled);
-                        overwriteCheckBox->setEnabled(exportItem.m_enabled && exportItem.m_exists);
+                        exportItem.SetEnabled(materialSlotCheckBox->isChecked());
+                        materialFileWidget->setEnabled(exportItem.GetEnabled());
+                        overwriteCheckBox->setEnabled(exportItem.GetEnabled() && exportItem.GetExists());
                     });
 
                     // Whenever the overwrite check box is updated, automatically apply the change to the export item
                     QObject::connect(overwriteCheckBox, &QCheckBox::stateChanged, overwriteCheckBox, [&exportItem, overwriteCheckBox]([[maybe_unused]] int state) {
-                        exportItem.m_overwrite = overwriteCheckBox->isChecked();
+                        exportItem.SetOverwrite(overwriteCheckBox->isChecked());
                     });
 
                     // Whenever the browse button is clicked, open a save file dialog in the same location as the current export file setting
                     QObject::connect(materialFileWidget, &AzQtComponents::BrowseEdit::attachedButtonTriggered, materialFileWidget, [&dialog, &exportItem, materialFileWidget, overwriteCheckBox]() {
                         QFileInfo fileInfo = QFileDialog::getSaveFileName(&dialog,
                             QString("Select Material Filename"),
-                            exportItem.m_exportPath.c_str(),
+                            exportItem.GetExportPath().c_str(),
                             QString("Material (*.material)"),
                             nullptr,
                             QFileDialog::DontConfirmOverwrite);
@@ -195,14 +155,14 @@ namespace AZ
                         // Only update the export data if a valid path and filename was selected
                         if (!fileInfo.absoluteFilePath().isEmpty())
                         {
-                            exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData();
-                            exportItem.m_exists = fileInfo.exists();
-                            exportItem.m_overwrite = fileInfo.exists();
+                            exportItem.SetExportPath(fileInfo.absoluteFilePath().toUtf8().constData());
+                            exportItem.SetExists(fileInfo.exists());
+                            exportItem.SetOverwrite(fileInfo.exists());
 
                             // Update the controls to display the new state
                             materialFileWidget->setText(fileInfo.fileName());
-                            overwriteCheckBox->setChecked(exportItem.m_overwrite);
-                            overwriteCheckBox->setEnabled(exportItem.m_enabled && exportItem.m_exists);
+                            overwriteCheckBox->setChecked(exportItem.GetOverwrite());
+                            overwriteCheckBox->setEnabled(exportItem.GetEnabled() && exportItem.GetExists());
                         }
                     });
 
@@ -245,24 +205,24 @@ namespace AZ
 
             bool ExportMaterialSourceData(const ExportItem& exportItem)
             {
-                if (!exportItem.m_enabled || exportItem.m_exportPath.empty())
+                if (!exportItem.GetEnabled() || exportItem.GetExportPath().empty())
                 {
                     return false;
                 }
 
-                if (exportItem.m_exists && !exportItem.m_overwrite)
+                if (exportItem.GetExists() && !exportItem.GetOverwrite())
                 {
                     return true;
                 }
 
                 EditorMaterialComponentUtil::MaterialEditData editData;
-                if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.m_assetId, editData))
+                if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.GetOriginalAssetId(), editData))
                 {
                     AZ_Warning("AZ::Render::EditorMaterialComponentExporter", false, "Failed to load material data.");
                     return false;
                 }
 
-                if (!EditorMaterialComponentUtil::SaveSourceMaterialFromEditData(exportItem.m_exportPath, editData))
+                if (!EditorMaterialComponentUtil::SaveSourceMaterialFromEditData(exportItem.GetExportPath(), editData))
                 {
                     AZ_Warning("AZ::Render::EditorMaterialComponentExporter", false, "Failed to save material data.");
                     return false;

+ 30 - 9
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.h

@@ -19,27 +19,48 @@ namespace AZ
     {
         namespace EditorMaterialComponentExporter
         {
-            // Attemts to generate a display label for a material slot by parsing its file name
-            AZStd::string GetLabelByAssetId(const AZ::Data::AssetId& assetId);
+            //! Generates a destination file path for exporting material source data
+            AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId, const AZStd::string& materialSlotName);
 
-            // Generates a destination file path for exporting material source data
-            AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId);
-
-            struct ExportItem
+            class ExportItem
             {
+            public:
+                //! @param originalAssetId   AssetId of the original built-in material, which will be exported.
+                //! @param materialSlotName  The name of the material slot will be used as part of the exported file name.
+                ExportItem(AZ::Data::AssetId originalAssetId, const AZStd::string& materialSlotName)
+                    : m_originalAssetId(originalAssetId)
+                    , m_materialSlotName(materialSlotName)
+                {}
+
+                void SetEnabled(bool enabled) { m_enabled = enabled; }
+                void SetExists(bool exists) { m_exists = exists; }
+                void SetOverwrite(bool overwrite) { m_overwrite = overwrite; }
+                void SetExportPath(const AZStd::string& exportPath) { m_exportPath = exportPath; }
+                
+                bool GetEnabled() const   { return m_enabled; }
+                bool GetExists() const    { return m_exists; }
+                bool GetOverwrite() const { return m_overwrite; }
+                const AZStd::string& GetExportPath() const { return m_exportPath; }
+
+                AZ::Data::AssetId GetOriginalAssetId() const { return m_originalAssetId; }
+                const AZStd::string& GetMaterialSlotName() const { return m_materialSlotName; }
+
+            private:
                 bool m_enabled = true;
                 bool m_exists = false;
                 bool m_overwrite = false;
-                AZ::Data::AssetId m_assetId;
                 AZStd::string m_exportPath;
+                AZ::Data::AssetId m_originalAssetId; //!< AssetId of the original built-in material, which will be exported.
+                AZStd::string m_materialSlotName;
             };
 
             using ExportItemsContainer = AZStd::vector<ExportItem>;
 
-            // Generates and opens a dialog for configuring material data export paths and actions
+            //! Generates and opens a dialog for configuring material data export paths and actions.
+            //! Note this will not modify the m_originalAssetId field in each ExportItem.
             bool OpenExportDialog(ExportItemsContainer& exportItems);
 
-            // Attemts to construct and save material source data from a product asset
+            //! Attemts to construct and save material source data from a product asset
             bool ExportMaterialSourceData(const ExportItem& exportItem);
         } // namespace EditorMaterialComponentExporter
     } // namespace Render

+ 13 - 27
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp

@@ -20,7 +20,7 @@
 
 AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
 #include <QMenu>
-#include <QAction>
+#include <QAction> 
 #include <QCursor>
 AZ_POP_DISABLE_WARNING
 
@@ -48,7 +48,7 @@ namespace AZ
                     return false;
                 }
 
-                const MaterialAssignmentId newId(oldId.first, oldId.second);
+                const MaterialAssignmentId newId(oldId.first, oldId.second.m_subId);
                 classElement.AddElementWithData(context, "id", newId);
             }
 
@@ -80,9 +80,10 @@ namespace AZ
             if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
             {
                 serializeContext->Class<EditorMaterialComponentSlot>()
-                    ->Version(5, &EditorMaterialComponentSlot::ConvertVersion)
+                    ->Version(6, &EditorMaterialComponentSlot::ConvertVersion)
                     ->Field("id", &EditorMaterialComponentSlot::m_id)
                     ->Field("materialAsset", &EditorMaterialComponentSlot::m_materialAsset)
+                    ->Field("defaultMaterialAsset", &EditorMaterialComponentSlot::m_defaultMaterialAsset)
                     ;
 
                 if (AZ::EditContext* editContext = serializeContext->GetEditContext())
@@ -121,21 +122,12 @@ namespace AZ
 
         AZ::Data::AssetId EditorMaterialComponentSlot::GetDefaultAssetId() const
         {
-            return m_id.m_materialAssetId;
+            return m_defaultMaterialAsset.GetId();
         }
 
         AZStd::string EditorMaterialComponentSlot::GetLabel() const
         {
-            // Generate the label for the material slot based on the assignment ID
-            // If this is the default material assignment ID then it represents the default slot which is not contained in any other group
-            if (m_id == DefaultMaterialAssignmentId)
-            {
-                return "Default Material";
-            }
-
-            // Otherwise the label can be generated by parsing the source file name associated with the asset ID
-            const AZStd::string& label = EditorMaterialComponentExporter::GetLabelByAssetId(m_id.m_materialAssetId);
-            return !label.empty() ? label : "<unknown>";
+            return m_label;
         }
 
         bool EditorMaterialComponentSlot::HasSourceData() const
@@ -183,27 +175,21 @@ namespace AZ
             OnMaterialChanged();
         }
 
-        void EditorMaterialComponentSlot::SetDefaultAsset()
+        void EditorMaterialComponentSlot::ResetToDefaultAsset()
         {
-            m_materialAsset = {};
+            m_materialAsset = m_defaultMaterialAsset;
             m_propertyOverrides = {};
             m_matModUvOverrides = {};
-            if (m_id.m_materialAssetId.IsValid())
-            {
-                // If no material is assigned to this slot, assign the default material from the slot id to edit its properties
-                m_materialAsset.Create(m_id.m_materialAssetId);
-            }
             OnMaterialChanged();
         }
 
         void EditorMaterialComponentSlot::OpenMaterialExporter()
         {
             // Because we are generating a source material from this specific slot there is only one entry
-            // But we still need to allow the user to reconfigure it using the dialogue
+            // But we still need to allow the user to reconfigure it using the dialog
             EditorMaterialComponentExporter::ExportItemsContainer exportItems;
             {
-                EditorMaterialComponentExporter::ExportItem exportItem;
-                exportItem.m_assetId = m_id.m_materialAssetId;
+                EditorMaterialComponentExporter::ExportItem exportItem{m_defaultMaterialAsset.GetId(), m_label};
                 exportItems.push_back(exportItem);
             }
 
@@ -218,7 +204,7 @@ namespace AZ
                     }
 
                     // Generate a new asset ID utilizing the export file path so that we can update this material slot to reference the new asset
-                    const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.m_exportPath, 0);
+                    const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.GetExportPath(), 0);
                     if (assetIdOutcome)
                     {
                         m_materialAsset.Create(assetIdOutcome.GetValue());
@@ -258,7 +244,7 @@ namespace AZ
                 // Treated as a special property. It will be updated together with properties.
                 OnPropertyChanged();
             };
-
+            
             if (m_materialAsset.GetId().IsValid())
             {
                 if (EditorMaterialComponentInspector::OpenInspectorDialog(m_materialAsset.GetId(), m_matModUvOverrides, m_modelUvNames, applyMatModUvOverrideChangedCallback))
@@ -275,7 +261,7 @@ namespace AZ
             QAction* action = nullptr;
 
             action = menu.addAction("Generate/Manage Source Material...", [this]() { OpenMaterialExporter(); });
-            action->setEnabled(m_id.m_materialAssetId.IsValid());
+            action->setEnabled(m_defaultMaterialAsset.GetId().IsValid());
 
             menu.addSeparator();
 

+ 3 - 1
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.h

@@ -34,7 +34,7 @@ namespace AZ
             AZStd::string GetLabel() const;
             bool HasSourceData() const;
             void OpenMaterialEditor() const;
-            void SetDefaultAsset();
+            void ResetToDefaultAsset();
             void Clear();
             void ClearOverrides();
             void OpenMaterialExporter();
@@ -42,7 +42,9 @@ namespace AZ
             void OpenUvNameMapInspector();
 
             MaterialAssignmentId m_id;
+            AZStd::string m_label;
             Data::Asset<RPI::MaterialAsset> m_materialAsset;
+            Data::Asset<RPI::MaterialAsset> m_defaultMaterialAsset;
             MaterialPropertyOverrideMap m_propertyOverrides;
             AZStd::function<void()> m_materialChangedCallback;
             AZStd::function<void()> m_propertyChangedCallback;

+ 1 - 1
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentConfig.cpp

@@ -44,7 +44,7 @@ namespace AZ
                 for (const auto& oldPair : oldMaterials)
                 {
                     const DeprecatedMaterialAssignmentId& oldId = oldPair.first;
-                    const MaterialAssignmentId newId(oldId.first, oldId.second);
+                    const MaterialAssignmentId newId(oldId.first, oldId.second.m_subId);
                     newMaterials[newId] = oldPair.second;
 
                 }

+ 13 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp

@@ -251,6 +251,19 @@ namespace AZ
                 m_meshFeatureProcessor->SetTransform(m_meshHandle, m_transformInterface->GetWorldTM(), m_cachedNonUniformScale);
             }
         }
+        
+        RPI::ModelMaterialSlotMap MeshComponentController::GetModelMaterialSlots() const
+        {
+            Data::Asset<const RPI::ModelAsset> modelAsset = GetModelAsset();
+            if (modelAsset.IsReady())
+            {
+                return modelAsset->GetMaterialSlots();
+            }
+            else
+            {
+                return {};
+            }
+        }
 
         MaterialAssignmentId MeshComponentController::FindMaterialAssignmentId(
             const MaterialAssignmentLodIndex lod, const AZStd::string& label) const

+ 1 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h

@@ -113,6 +113,7 @@ namespace AZ
             // MaterialReceiverRequestBus::Handler overrides ...
             virtual MaterialAssignmentId FindMaterialAssignmentId(
                 const MaterialAssignmentLodIndex lod, const AZStd::string& label) const override;
+            RPI::ModelMaterialSlotMap GetModelMaterialSlots() const override;
             MaterialAssignmentMap GetMaterialAssignments() const override;
             AZStd::unordered_set<AZ::Name> GetModelUvNames() const override;
 

+ 3 - 4
Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp

@@ -96,12 +96,11 @@ namespace AZ
                         skinnedSubMesh.m_vertexCount = aznumeric_cast<uint32_t>(subMeshVertexCount);
                         lodVertexCount += aznumeric_cast<uint32_t>(subMeshVertexCount);
 
-                        // The default material id used by a sub-mesh is the guid of the source scene file plus the subId which is a unique material ID from the scene API
-                        AZ::u32 subId = modelMesh.GetMaterialAsset().GetId().m_subId;
-                        AZ::Data::AssetId materialId{ actorAssetId.m_guid, subId };
+                        skinnedSubMesh.m_materialSlot = actor->GetMeshAsset()->FindMaterialSlot(modelMesh.GetMaterialSlotId());
 
                         // Queue the material asset - the ModelLod seems to handle delayed material loads
-                        skinnedSubMesh.m_material = Data::AssetManager::Instance().GetAsset(materialId, azrtti_typeid<RPI::MaterialAsset>(), skinnedSubMesh.m_material.GetAutoLoadBehavior());
+                        skinnedSubMesh.m_materialSlot.m_defaultMaterialAsset.QueueLoad();
+
                         subMeshes.push_back(skinnedSubMesh);
                     }
                     else

+ 20 - 5
Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp

@@ -307,6 +307,19 @@ namespace AZ
             m_meshFeatureProcessor = nullptr;
             m_skinnedMeshFeatureProcessor = nullptr;
         }
+        
+        RPI::ModelMaterialSlotMap AtomActorInstance::GetModelMaterialSlots() const
+        {
+            Data::Asset<const RPI::ModelAsset> modelAsset = GetModelAsset();
+            if (modelAsset.IsReady())
+            {
+                return modelAsset->GetMaterialSlots();
+            }
+            else
+            {
+                return {};
+            }
+        }
 
         MaterialAssignmentId AtomActorInstance::FindMaterialAssignmentId(
             const MaterialAssignmentLodIndex lod, const AZStd::string& label) const
@@ -501,16 +514,18 @@ namespace AZ
                     const AZStd::vector< SkinnedSubMeshProperties>& subMeshProperties = inputLod.GetSubMeshProperties();
                     for (const SkinnedSubMeshProperties& submesh : subMeshProperties)
                     {
-                        AZ_Error("AtomActorInstance", submesh.m_material, "Actor does not have a valid default material in lod %d", lodIndex);
-                        if (submesh.m_material)
+                        Data::Asset<RPI::MaterialAsset> materialAsset = submesh.m_materialSlot.m_defaultMaterialAsset;
+                        AZ_Error("AtomActorInstance", materialAsset, "Actor does not have a valid default material in lod %d", lodIndex);
+
+                        if (materialAsset)
                         {
-                            if (!submesh.m_material->IsReady())
+                            if (!materialAsset->IsReady())
                             {
                                 // Start listening for the material's OnAssetReady event.
                                 // AtomActorInstance::Create is called on the main thread, so there should be no need to synchronize with the OnAssetReady event handler
                                 // since those events will also come from the main thread
-                                m_waitForMaterialLoadIds.insert(submesh.m_material->GetId());
-                                Data::AssetBus::MultiHandler::BusConnect(submesh.m_material->GetId());
+                                m_waitForMaterialLoadIds.insert(materialAsset->GetId());
+                                Data::AssetBus::MultiHandler::BusConnect(materialAsset->GetId());
                             }
                         }
                     }

+ 1 - 0
Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h

@@ -122,6 +122,7 @@ namespace AZ
             // MaterialReceiverRequestBus::Handler overrides...
             virtual MaterialAssignmentId FindMaterialAssignmentId(
                 const MaterialAssignmentLodIndex lod, const AZStd::string& label) const override;
+            RPI::ModelMaterialSlotMap GetModelMaterialSlots() const override;
             MaterialAssignmentMap GetMaterialAssignments() const override;
             AZStd::unordered_set<AZ::Name> GetModelUvNames() const override;
 

+ 16 - 11
Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.cpp

@@ -112,17 +112,8 @@ namespace WhiteBox
         AddLodBuffers(modelLodCreator);
         modelLodCreator.BeginMesh();
         modelLodCreator.SetMeshAabb(meshData.GetAabb());
-
-        // set the default material
-        if (auto materialAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::RPI::MaterialAsset>(TexturedMaterialPath.data()))
-        {
-            modelLodCreator.SetMeshMaterialAsset(materialAsset);
-        }
-        else
-        {
-            AZ_Error("CreateLodAsset", false, "Could not load material.");
-            return false;
-        }
+        
+        modelLodCreator.SetMeshMaterialSlot(OneMaterialSlotId);
 
         AddMeshBuffers(modelLodCreator);
         modelLodCreator.EndMesh();
@@ -154,6 +145,20 @@ namespace WhiteBox
         modelCreator.Begin(AZ::Data::AssetId(AZ::Uuid::CreateRandom()));
         modelCreator.SetName(ModelName);
         modelCreator.AddLodAsset(AZStd::move(m_lodAsset));
+        
+        if (auto materialAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::RPI::MaterialAsset>(TexturedMaterialPath.data()))
+        {
+            AZ::RPI::ModelMaterialSlot materialSlot;
+            materialSlot.m_stableId = OneMaterialSlotId;
+            materialSlot.m_defaultMaterialAsset = materialAsset;
+            modelCreator.AddMaterialSlot(materialSlot);
+        }
+        else
+        {
+            AZ_Error("CreateLodAsset", false, "Could not load material.");
+            return;
+        }
+
         modelCreator.End(m_modelAsset);
     }
 

+ 1 - 0
Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.h

@@ -91,6 +91,7 @@ namespace WhiteBox
         // TODO: LYN-784
         static constexpr AZStd::string_view TexturedMaterialPath = "materials/defaultpbr.azmaterial";
         static constexpr AZStd::string_view SolidMaterialPath = "materials/defaultpbr.azmaterial";
+        static constexpr AZ::RPI::ModelMaterialSlot::StableId OneMaterialSlotId = 0;
 
         //! White box model name.
         static constexpr AZStd::string_view ModelName = "WhiteBoxMesh";