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.
monroegm-disable-blank-issue-2
santorac 4 years ago committed by GitHub
commit fa52124f9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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

@ -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

@ -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;

@ -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

@ -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())
{

@ -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();
}

@ -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"
}
]
}

@ -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;
}

@ -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"
}
]
}
}

@ -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;
}

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

@ -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

@ -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;

@ -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,

@ -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;
};

@ -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);

@ -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.

@ -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

@ -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

@ -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
}
}

@ -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);
}
}

@ -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);

@ -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;
}

@ -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));

@ -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);
}

@ -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
{

@ -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)
{

@ -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();

@ -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();

@ -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

@ -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));

@ -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

@ -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;
};

@ -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

@ -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;

@ -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

@ -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();

@ -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;

@ -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;
}

@ -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

@ -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;

@ -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

@ -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());
}
}
}

@ -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;

@ -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);
}

@ -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";

Loading…
Cancel
Save