From 14d2e38b90c07bf2bd4fc0afc7d2e3409d88e84f Mon Sep 17 00:00:00 2001 From: Chris Santora Date: Sat, 17 Jul 2021 00:07:23 -0700 Subject: [PATCH] Refactored how model material slots work in preparation to support more flexible material conversion options for the scene asset pipeline. The material slot IDs are based on the MaterialUid that come from SceneAPI. Since these IDs are also used as the AssetId sub-ID for the converted material assets, the system was just checking the material asset sub-ID to determine the material slot ID. But in order to support certain FBX material conversion options, we needed to break this tie, so the slot ID is separate from the AssetId of the material in that slot. This will allow some other material to be used in the slot, instead of being forced to use one that was generated from the FBX. Here we inttroduce a new struct ModelMaterialSlot which formalizes the concept of material slot, with an ID, display name, and default material assignment. The ID still comes from the MaterialUid like before. The display name is built-in, rather than being parsed out from the asset file name. And the default material assignment can be any material asset, it doesn't have to come from the FBX (or other scene file). This commit is just the preliminary set of changes. Cursory testing shows that it works pretty well but more testing is needed (and likely some fixes) before merging. Here is what's left to do... Add serialization version converters to preserve prior prefab data. See if we can get rid of GetLabelByAssetId function only rely on the display name inside ModelMaterialSlot. I'm not sure if the condition for enabling the "Edit Material Instance..." context menu item is correct. Test actors Lots more testing in general Signed-off-by: santorac <55155825+santorac@users.noreply.github.com> --- .../Feature/Material/MaterialAssignmentId.h | 35 +++-- .../Source/Material/MaterialAssignment.cpp | 6 +- .../Source/Material/MaterialAssignmentId.cpp | 51 ++++--- .../Code/Source/Mesh/MeshFeatureProcessor.cpp | 20 +-- .../SkinnedMesh/SkinnedMeshInputBuffers.cpp | 7 +- .../Include/Atom/RPI.Public/Model/ModelLod.h | 2 + .../Atom/RPI.Reflect/Model/ModelAsset.h | 3 + .../Atom/RPI.Reflect/Model/ModelLodAsset.h | 29 +++- .../RPI.Reflect/Model/ModelLodAssetCreator.h | 6 +- .../RPI.Reflect/Model/ModelMaterialSlot.h | 43 ++++++ .../Model/ModelAssetBuilderComponent.cpp | 10 +- .../Code/Source/RPI.Public/Model/ModelLod.cpp | 9 +- .../Source/RPI.Public/Model/ModelSystem.cpp | 1 + .../Source/RPI.Reflect/Model/ModelAsset.cpp | 24 ++++ .../RPI.Reflect/Model/ModelLodAsset.cpp | 48 ++++++- .../Model/ModelLodAssetCreator.cpp | 32 ++++- .../RPI.Reflect/Model/ModelMaterialSlot.cpp | 30 ++++ .../RPI/Code/atom_rpi_reflect_files.cmake | 2 + .../Material/MaterialComponentBus.h | 3 + .../Material/EditorMaterialComponent.cpp | 136 +++++++++--------- .../EditorMaterialComponentExporter.cpp | 6 +- .../EditorMaterialComponentExporter.h | 11 +- .../Material/EditorMaterialComponentSlot.cpp | 33 ++--- .../Material/EditorMaterialComponentSlot.h | 4 +- .../Material/MaterialComponentConfig.cpp | 2 +- .../Source/Mesh/MeshComponentController.cpp | 13 ++ .../Source/Mesh/MeshComponentController.h | 1 + .../EMotionFXAtom/Code/Source/ActorAsset.cpp | 8 +- .../Code/Source/AtomActorInstance.cpp | 13 ++ .../Code/Source/AtomActorInstance.h | 1 + .../Rendering/Atom/WhiteBoxAtomRenderMesh.cpp | 5 +- 31 files changed, 399 insertions(+), 195 deletions(-) create mode 100644 Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelMaterialSlot.h create mode 100644 Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelMaterialSlot.cpp diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignmentId.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignmentId.h index dd198a7179..d9ae8099da 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignmentId.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignmentId.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace AZ { @@ -23,7 +24,7 @@ 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 { @@ -33,41 +34,39 @@ namespace AZ 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 diff --git a/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp b/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp index ec48d57d1a..9ae4f65304 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp @@ -123,7 +123,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 +152,11 @@ 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); } } diff --git a/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignmentId.cpp b/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignmentId.cpp index 71dea0596f..8b6b0be237 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignmentId.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignmentId.cpp @@ -19,9 +19,9 @@ namespace AZ if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(1) + ->Version(2) ->Field("lodIndex", &MaterialAssignmentId::m_lodIndex) - ->Field("materialAssetId", &MaterialAssignmentId::m_materialAssetId) + ->Field("materialSlotStableId", &MaterialAssignmentId::m_materialSlotStableId) ; } @@ -33,75 +33,72 @@ namespace AZ ->Attribute(AZ::Script::Attributes::Module, "render") ->Constructor() ->Constructor() - ->Constructor() + ->Constructor() ->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 diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp index c6362f3a5d..6e031aa853 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp @@ -589,18 +589,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 +632,12 @@ namespace AZ for (size_t meshIndex = 0; meshIndex < meshCount; ++meshIndex) { - Data::Instance material = modelLod.GetMeshes()[meshIndex].m_material; + const RPI::ModelLod::Mesh& mesh = modelLod.GetMeshes()[meshIndex]; + + Data::Instance 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 +780,7 @@ namespace AZ // retrieve the material Data::Instance 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()) { diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp index 155b751272..640e30d0f6 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp @@ -639,8 +639,13 @@ namespace AZ Aabb localAabb = lod.m_subMeshProperties[i].m_aabb; modelLodCreator.SetMeshAabb(AZStd::move(localAabb)); + + // Create a separate material slot for each sub-mesh + AZ::RPI::ModelMaterialSlot materialSlot; + materialSlot.m_stableId = i; + materialSlot.m_defaultMaterialAsset = lod.m_subMeshProperties[i].m_material; - modelLodCreator.SetMeshMaterialAsset(lod.m_subMeshProperties[i].m_material); + modelLodCreator.SetMeshMaterialSlot(materialSlot); modelLodCreator.EndMesh(); } diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/ModelLod.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/ModelLod.h index dcc4813b3d..36892d0027 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/ModelLod.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/ModelLod.h @@ -72,6 +72,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 m_material; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h index b7414f73a2..891aec04b3 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h @@ -49,6 +49,9 @@ 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. + RPI::ModelMaterialSlotMap GetModelMaterialSlots() const; //! Returns the number of Lods in the model size_t GetLodCount() const; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h index 4e86b0e367..61e2ebeb05 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -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 & GetMaterialAsset() const; + //! Returns the index of the material slot used by this mesh. + //! This indexes into the ModelLodAsset's material slot list. + size_t GetMaterialSlotIndex() 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 m_materialAsset{ Data::AssetLoadBehavior::PreLoad }; + // Identifies the material that is used by this mesh. + // References material slot in the ModelLodAsset that owns this mesh; see ModelLodAsset::GetMaterialSlot(). + size_t m_materialSlotIndex = 0; // Both the buffer in m_indexBufferAssetView and the buffers in m_streamBufferInfo // may point to either unique buffers for the mesh or to consolidated @@ -143,11 +147,21 @@ namespace AZ //! Returns the model-space axis-aligned bounding box of all meshes in the lod const AZ::Aabb& GetAabb() const; + + //! Returns an array view into the collection of material slots available to this lod + AZStd::array_view GetMaterialSlots() const; + + //! Returns a specific material slot by index, with error checking. + //! The index can be retrieved from Mesh::GetMaterialSlotIndex(). + const ModelMaterialSlot& GetMaterialSlot(size_t slotIndex) const; + + //! Find a material slot with the given stableId, or returns null if it isn't found. + const ModelMaterialSlot* FindMaterialSlot(uint32_t stableId) const; private: AZStd::vector 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. @@ -155,6 +169,13 @@ namespace AZ Data::Asset m_indexBuffer; AZStd::vector> m_streamBuffers; + // 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. + AZStd::vector m_materialSlots; + + // A default ModelMaterialSlot to be returned upon error conditions. + ModelMaterialSlot m_fallbackSlot; + void AddMesh(const Mesh& mesh); void SetReady(); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h index c3291e5c73..5672b49f68 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAssetCreator.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include @@ -45,9 +46,10 @@ namespace AZ //! Begin and BeginMesh must be called first. void SetMeshAabb(AZ::Aabb&& aabb); - //! Sets the material asset for the current SubMesh. + //! Sets the material slot data for the current SubMesh. + //! Adds a new material slot to the ModelLodAsset if it doesn't already exist. //! Begin and BeginMesh must be called first - void SetMeshMaterialAsset(const Data::Asset& materialAsset); + void SetMeshMaterialSlot(const ModelMaterialSlot& materialSlot); //! Sets the given BufferAssetView to the current SubMesh as the index buffer. //! Begin and BeginMesh must be called first diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelMaterialSlot.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelMaterialSlot.h new file mode 100644 index 0000000000..27137459b7 --- /dev/null +++ b/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 + +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 = -1; + + //! 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 m_defaultMaterialAsset{ Data::AssetLoadBehavior::PreLoad }; //!< The material that will be applied to this slot by default. + }; + + using ModelMaterialSlotMap = AZStd::unordered_map; + + } //namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp index aa2ad37647..608ee17d95 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp @@ -109,7 +109,7 @@ namespace AZ if (auto* serialize = azrtti_cast(context)) { serialize->Class() - ->Version(27); // [ATOM-15658] + ->Version(29); // (updated to separate material slot ID from default material asset) } } @@ -1806,8 +1806,12 @@ namespace AZ auto iter = materialAssetsByUid.find(meshView.m_materialUid); if (iter != materialAssetsByUid.end()) { - const Data::Asset& 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; + + lodAssetCreator.SetMeshMaterialSlot(materialSlot); } } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLod.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLod.cpp index b68930e4a3..2dfee9e2f1 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLod.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLod.cpp @@ -100,10 +100,13 @@ namespace AZ } } - auto& materialAsset = mesh.GetMaterialAsset(); - if (materialAsset.IsReady()) + const ModelMaterialSlot& materialSlot = lodAsset.GetMaterialSlot(mesh.GetMaterialSlotIndex()); + + 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)); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelSystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelSystem.cpp index 610f183eb1..b9781843cf 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelSystem.cpp +++ b/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); } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp index 083de10d47..486faafbdb 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp @@ -56,6 +56,30 @@ namespace AZ { return m_aabb; } + + RPI::ModelMaterialSlotMap ModelAsset::GetModelMaterialSlots() const + { + RPI::ModelMaterialSlotMap slotMap; + + for (const Data::Asset& lod : GetLodAssets()) + { + for (const AZ::RPI::ModelMaterialSlot& materialSlot : lod->GetMaterialSlots()) + { + auto iter = slotMap.find(materialSlot.m_stableId); + if (iter == slotMap.end()) + { + slotMap.emplace(materialSlot.m_stableId, materialSlot); + } + else + { + AZ_Assert(materialSlot.m_displayName == iter->second.m_displayName && materialSlot.m_defaultMaterialAsset.GetId() == iter->second.m_defaultMaterialAsset.GetId(), + "Multiple LODs have mismatched data for the same material slot."); + } + } + } + + return slotMap; + } size_t ModelAsset::GetLodCount() const { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAsset.cpp index 190e4c5315..4811f6a1db 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAsset.cpp @@ -23,24 +23,25 @@ namespace AZ if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(0) + ->Version(1) ->Field("Meshes", &ModelLodAsset::m_meshes) ->Field("Aabb", &ModelLodAsset::m_aabb) + ->Field("MaterialSlots", &ModelLodAsset::m_materialSlots) ; } Mesh::Reflect(context); } - + void ModelLodAsset::Mesh::Reflect(AZ::ReflectContext* context) { if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(0) - ->Field("Material", &ModelLodAsset::Mesh::m_materialAsset) + ->Version(1) ->Field("Name", &ModelLodAsset::Mesh::m_name) ->Field("AABB", &ModelLodAsset::Mesh::m_aabb) + ->Field("MaterialSlotIndex", &ModelLodAsset::Mesh::m_materialSlotIndex) ->Field("IndexBufferAssetView", &ModelLodAsset::Mesh::m_indexBufferAssetView) ->Field("StreamBufferInfo", &ModelLodAsset::Mesh::m_streamBufferInfo) ; @@ -75,9 +76,9 @@ namespace AZ return m_indexBufferAssetView.GetBufferViewDescriptor().m_elementCount; } - const Data::Asset & ModelLodAsset::Mesh::GetMaterialAsset() const + size_t ModelLodAsset::Mesh::GetMaterialSlotIndex() const { - return m_materialAsset; + return m_materialSlotIndex; } const AZ::Name& ModelLodAsset::Mesh::GetName() const @@ -118,6 +119,41 @@ namespace AZ { return m_aabb; } + + AZStd::array_view ModelLodAsset::GetMaterialSlots() const + { + return m_materialSlots; + } + + const ModelMaterialSlot& ModelLodAsset::GetMaterialSlot(size_t slotIndex) const + { + if (slotIndex < m_materialSlots.size()) + { + return m_materialSlots[slotIndex]; + } + else + { + AZ_Error("ModelAsset", false, "Material slot index %zu out of range. ModelAsset has %zu slots.", slotIndex, m_materialSlots.size()); + return m_fallbackSlot; + } + } + + const ModelMaterialSlot* ModelLodAsset::FindMaterialSlot(uint32_t stableId) const + { + auto iter = AZStd::find_if(m_materialSlots.begin(), m_materialSlots.end(), [&stableId](const ModelMaterialSlot& existingMaterialSlot) + { + return existingMaterialSlot.m_stableId == stableId; + }); + + if (iter == m_materialSlots.end()) + { + return nullptr; + } + else + { + return iter; + } + } const BufferAssetView* ModelLodAsset::Mesh::GetSemanticBufferAssetView(const AZ::Name& semantic) const { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp index 6f17a5c590..5a066d2517 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelLodAssetCreator.cpp @@ -60,12 +60,32 @@ namespace AZ m_currentMesh.m_aabb = AZStd::move(aabb); } } - - void ModelLodAssetCreator::SetMeshMaterialAsset(const Data::Asset& materialAsset) + + void ModelLodAssetCreator::SetMeshMaterialSlot(const ModelMaterialSlot& materialSlot) { - if (ValidateIsMeshReady()) + auto iter = AZStd::find_if(m_asset->m_materialSlots.begin(), m_asset->m_materialSlots.end(), [&materialSlot](const ModelMaterialSlot& existingMaterialSlot) + { + return existingMaterialSlot.m_stableId == materialSlot.m_stableId; + }); + + if (iter == m_asset->m_materialSlots.end()) { - m_currentMesh.m_materialAsset = materialAsset; + m_currentMesh.m_materialSlotIndex = m_asset->m_materialSlots.size(); + m_asset->m_materialSlots.push_back(materialSlot); + } + else + { + if (materialSlot.m_displayName != iter->m_displayName) + { + ReportWarning("Material slot %u was already added with a different name.", materialSlot.m_stableId); + } + + if (materialSlot.m_defaultMaterialAsset != iter->m_defaultMaterialAsset) + { + ReportWarning("Material slot %u was already added with a different MaterialAsset.", materialSlot.m_stableId); + } + + *iter = materialSlot; } } @@ -288,7 +308,9 @@ namespace AZ creator.SetMeshName(sourceMesh.GetName()); AZ::Aabb aabb = sourceMesh.GetAabb(); creator.SetMeshAabb(AZStd::move(aabb)); - creator.SetMeshMaterialAsset(sourceMesh.GetMaterialAsset()); + + const ModelMaterialSlot& materialSlot = sourceAsset->GetMaterialSlot(sourceMesh.GetMaterialSlotIndex()); + creator.SetMeshMaterialSlot(materialSlot); // Mesh index buffer view const BufferAssetView& sourceIndexBufferView = sourceMesh.GetIndexBufferAssetView(); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelMaterialSlot.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelMaterialSlot.cpp new file mode 100644 index 0000000000..61f3ebbe3a --- /dev/null +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelMaterialSlot.cpp @@ -0,0 +1,30 @@ +/* + * 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 +#include +#include + +namespace AZ +{ + namespace RPI + { + void ModelMaterialSlot::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("StableId", &ModelMaterialSlot::m_stableId) + ->Field("DisplayName", &ModelMaterialSlot::m_displayName) + ->Field("DefaultMaterialAsset", &ModelMaterialSlot::m_defaultMaterialAsset) + ; + } + } + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake b/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake index c049837045..49c7231fed 100644 --- a/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake +++ b/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 diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h index 134bf6db47..4072684987 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h @@ -69,6 +69,9 @@ namespace AZ : public ComponentBus { public: + //! 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 GetModelUvNames() const = 0; }; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp index d7dc4335b3..f1767a8b1d 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp @@ -44,57 +44,8 @@ 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 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) @@ -238,7 +189,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 +202,7 @@ namespace AZ for (auto& materialSlotPair : GetMaterialSlots()) { EditorMaterialComponentSlot* materialSlot = materialSlotPair.second; - if (materialSlot->m_id.IsLodAndAsset()) + if (materialSlot->m_id.IsLodAndSlotId()) { materialSlot->Clear(); } @@ -318,6 +269,9 @@ namespace AZ // Build the controller configuration from the editor configuration MaterialComponentConfig config = m_controller.GetConfiguration(); config.m_materials.clear(); + + RPI::ModelMaterialSlotMap modelMaterialSlots; + MaterialReceiverRequestBus::EventResult(modelMaterialSlots, GetEntityId(), &MaterialReceiverRequestBus::Events::GetModelMaterialSlots); for (const auto& materialSlotPair : GetMaterialSlots()) { @@ -340,10 +294,15 @@ 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_propertyOverrides = materialSlot->m_propertyOverrides; - materialAssignment.m_matModUvOverrides = materialSlot->m_matModUvOverrides; + auto materialSlotIter = modelMaterialSlots.find(materialSlot->m_id.m_materialSlotStableId); + + if (materialSlotIter != modelMaterialSlots.end()) + { + MaterialAssignment& materialAssignment = config.m_materials[materialSlot->m_id]; + materialAssignment.m_materialAsset = materialSlotIter->second.m_defaultMaterialAsset; + materialAssignment.m_propertyOverrides = materialSlot->m_propertyOverrides; + materialAssignment.m_matModUvOverrides = materialSlot->m_matModUvOverrides; + } } } @@ -362,6 +321,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,6 +347,29 @@ namespace AZ OnConfigurationChanged(); }; + const char* UnknownSlotName = ""; + + // 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; @@ -400,13 +385,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(m_materialSlotsByLod.size(), aznumeric_cast(slot.m_id.m_lodIndex + 1))); @@ -452,17 +437,19 @@ namespace AZ { AzToolsFramework::ScopedUndoBatch undoBatch("Generating materials."); SetDirty(); + + RPI::ModelMaterialSlotMap modelMaterialSlots; + MaterialReceiverRequestBus::EventResult(modelMaterialSlots, GetEntityId(), &MaterialReceiverRequestBus::Events::GetModelMaterialSlots); // First generating a unique set of all material asset IDs that will be used for source data generation AZStd::unordered_set assetIds; - auto materialSlots = GetMaterialSlots(); - for (auto& materialSlotPair : materialSlots) + for (auto& materialSlot : modelMaterialSlots) { - EditorMaterialComponentSlot* materialSlot = materialSlotPair.second; - if (materialSlot->m_id.m_materialAssetId.IsValid()) + Data::AssetId defaultMaterialAssetId = materialSlot.second.m_defaultMaterialAsset.GetId(); + if (defaultMaterialAssetId.IsValid()) { - assetIds.insert(materialSlot->m_id.m_materialAssetId); + assetIds.insert(defaultMaterialAssetId); } } @@ -472,7 +459,7 @@ namespace AZ for (const AZ::Data::AssetId& assetId : assetIds) { EditorMaterialComponentExporter::ExportItem exportItem; - exportItem.m_assetId = assetId; + exportItem.m_originalAssetId = assetId; exportItems.push_back(exportItem); } @@ -489,12 +476,23 @@ namespace AZ const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.m_exportPath, 0); if (assetIdOutcome) { - for (auto& materialSlotPair : materialSlots) + for (auto& materialSlotPair : GetMaterialSlots()) { - 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()); + // Only update the slot of it was originally empty, having no override material. + // We need to check whether replaced material corresponds to this slot's default material. + if (!editorMaterialSlot->m_materialAsset.GetId().IsValid()) + { + auto materialSlot = modelMaterialSlots.find(editorMaterialSlot->m_id.m_materialSlotStableId); + if (materialSlot != modelMaterialSlots.end() && + materialSlot->second.m_defaultMaterialAsset.GetId() == exportItem.m_originalAssetId) + { + editorMaterialSlot->m_materialAsset.Create(assetIdOutcome.GetValue()); + } + } } } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp index bde500bdc5..1e6b19f616 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp @@ -132,7 +132,7 @@ namespace AZ int row = 0; for (ExportItem& exportItem : exportItems) { - QFileInfo fileInfo(GetExportPathByAssetId(exportItem.m_assetId).c_str()); + QFileInfo fileInfo(GetExportPathByAssetId(exportItem.m_originalAssetId).c_str()); // Configuring initial settings based on whether or not the target file already exists exportItem.m_exportPath = fileInfo.absoluteFilePath().toUtf8().constData(); @@ -147,7 +147,7 @@ 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->setText(GetLabelByAssetId(exportItem.m_originalAssetId).c_str()); tableWidget->setCellWidget(row, MaterialSlotColumn, materialSlotCheckBox); // Create a file picker widget for selecting the save path for the exported material @@ -256,7 +256,7 @@ namespace AZ } EditorMaterialComponentUtil::MaterialEditData editData; - if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.m_assetId, editData)) + if (!EditorMaterialComponentUtil::LoadMaterialEditDataFromAssetId(exportItem.m_originalAssetId, editData)) { AZ_Warning("AZ::Render::EditorMaterialComponentExporter", false, "Failed to load material data."); return false; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.h index 00bef9ee66..289bde0c75 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.h @@ -19,10 +19,10 @@ namespace AZ { namespace EditorMaterialComponentExporter { - // Attemts to generate a display label for a material slot by parsing its file name + //! 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 + //! Generates a destination file path for exporting material source data AZStd::string GetExportPathByAssetId(const AZ::Data::AssetId& assetId); struct ExportItem @@ -30,16 +30,17 @@ namespace AZ bool m_enabled = true; bool m_exists = false; bool m_overwrite = false; - AZ::Data::AssetId m_assetId; + AZ::Data::AssetId m_originalAssetId; //!< AssetId of the original built-in material, which will be exported. AZStd::string m_exportPath; }; using ExportItemsContainer = AZStd::vector; - // 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 diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp index d341bbb290..c8a2024b39 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp +++ b/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 -#include +#include #include 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); } @@ -83,6 +83,7 @@ namespace AZ ->Version(5, &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 : ""; + return m_label; } bool EditorMaterialComponentSlot::HasSourceData() const @@ -183,27 +175,22 @@ 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; + exportItem.m_originalAssetId = m_defaultMaterialAsset.GetId(); exportItems.push_back(exportItem); } @@ -275,7 +262,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(); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.h index e8b4f46854..79fddf2611 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.h +++ b/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 m_materialAsset; + Data::Asset m_defaultMaterialAsset; MaterialPropertyOverrideMap m_propertyOverrides; AZStd::function m_materialChangedCallback; AZStd::function m_propertyChangedCallback; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentConfig.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentConfig.cpp index a0adcd3187..f7b7177e29 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentConfig.cpp +++ b/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; } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp index 29bd9a839b..a2a3022e91 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp +++ b/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 modelAsset = GetModelAsset(); + if (modelAsset.IsReady()) + { + return modelAsset->GetModelMaterialSlots(); + } + else + { + return {}; + } + } MaterialAssignmentMap MeshComponentController::GetMaterialAssignments() const { diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h index 80b483452f..78c2e0797f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h @@ -111,6 +111,7 @@ namespace AZ void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; // MaterialReceiverRequestBus::Handler overrides ... + RPI::ModelMaterialSlotMap GetModelMaterialSlots() const override; MaterialAssignmentMap GetMaterialAssignments() const override; AZStd::unordered_set GetModelUvNames() const override; diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp index 3143191135..ae617f910f 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp @@ -96,12 +96,10 @@ namespace AZ skinnedSubMesh.m_vertexCount = aznumeric_cast(subMeshVertexCount); lodVertexCount += aznumeric_cast(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_material = lodAsset->GetMaterialSlot(modelMesh.GetMaterialSlotIndex()).m_defaultMaterialAsset; // Queue the material asset - the ModelLod seems to handle delayed material loads - skinnedSubMesh.m_material = Data::AssetManager::Instance().GetAsset(materialId, azrtti_typeid(), skinnedSubMesh.m_material.GetAutoLoadBehavior()); + skinnedSubMesh.m_material.QueueLoad(); + subMeshes.push_back(skinnedSubMesh); } else diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp index 4c6045d7ce..6697162c18 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp +++ b/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 modelAsset = GetModelAsset(); + if (modelAsset.IsReady()) + { + return modelAsset->GetModelMaterialSlots(); + } + else + { + return {}; + } + } MaterialAssignmentMap AtomActorInstance::GetMaterialAssignments() const { diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h index c854fed3c1..e7a4047012 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h @@ -120,6 +120,7 @@ namespace AZ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MaterialReceiverRequestBus::Handler overrides... + RPI::ModelMaterialSlotMap GetModelMaterialSlots() const override; MaterialAssignmentMap GetMaterialAssignments() const override; AZStd::unordered_set GetModelUvNames() const override; diff --git a/Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.cpp b/Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.cpp index 3f089a8639..5a9b8191ed 100644 --- a/Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.cpp +++ b/Gems/WhiteBox/Code/Source/Rendering/Atom/WhiteBoxAtomRenderMesh.cpp @@ -116,7 +116,10 @@ namespace WhiteBox // set the default material if (auto materialAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath(TexturedMaterialPath.data())) { - modelLodCreator.SetMeshMaterialAsset(materialAsset); + AZ::RPI::ModelMaterialSlot materialSlot; + materialSlot.m_stableId = 0; + materialSlot.m_defaultMaterialAsset = materialAsset; + modelLodCreator.SetMeshMaterialSlot(materialSlot); } else {