/* * 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 #include #include #include #include #include #include #include namespace AZ { namespace RPI { const char* MaterialAsset::s_debugTraceName = "MaterialAsset"; const char* MaterialAsset::DisplayName = "MaterialAsset"; const char* MaterialAsset::Group = "Material"; const char* MaterialAsset::Extension = "azmaterial"; void MaterialAsset::Reflect(ReflectContext* context) { if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(14) // added m_rawPropertyValues ->Field("materialTypeAsset", &MaterialAsset::m_materialTypeAsset) ->Field("materialTypeVersion", &MaterialAsset::m_materialTypeVersion) ->Field("propertyValues", &MaterialAsset::m_propertyValues) ->Field("rawPropertyValues", &MaterialAsset::m_rawPropertyValues) ->Field("finalized", &MaterialAsset::m_wasPreFinalized) ; } } MaterialAsset::MaterialAsset() { } MaterialAsset::~MaterialAsset() { MaterialReloadNotificationBus::Handler::BusDisconnect(); Data::AssetBus::Handler::BusDisconnect(); AssetInitBus::Handler::BusDisconnect(); } const Data::Asset& MaterialAsset::GetMaterialTypeAsset() const { return m_materialTypeAsset; } const ShaderCollection& MaterialAsset::GetShaderCollection() const { return m_materialTypeAsset->GetShaderCollection(); } const MaterialFunctorList& MaterialAsset::GetMaterialFunctors() const { return m_materialTypeAsset->GetMaterialFunctors(); } const RHI::Ptr& MaterialAsset::GetMaterialSrgLayout(const SupervariantIndex& supervariantIndex) const { return m_materialTypeAsset->GetMaterialSrgLayout(supervariantIndex); } const RHI::Ptr& MaterialAsset::GetMaterialSrgLayout(const AZ::Name& supervariantName) const { return m_materialTypeAsset->GetMaterialSrgLayout(supervariantName); } const RHI::Ptr& MaterialAsset::GetMaterialSrgLayout() const { return m_materialTypeAsset->GetMaterialSrgLayout(); } const RHI::Ptr& MaterialAsset::GetObjectSrgLayout(const SupervariantIndex& supervariantIndex) const { return m_materialTypeAsset->GetObjectSrgLayout(supervariantIndex); } const RHI::Ptr& MaterialAsset::GetObjectSrgLayout(const AZ::Name& supervariantName) const { return m_materialTypeAsset->GetObjectSrgLayout(supervariantName); } const RHI::Ptr& MaterialAsset::GetObjectSrgLayout() const { return m_materialTypeAsset->GetObjectSrgLayout(); } const MaterialPropertiesLayout* MaterialAsset::GetMaterialPropertiesLayout() const { return m_materialTypeAsset->GetMaterialPropertiesLayout(); } bool MaterialAsset::WasPreFinalized() const { return m_wasPreFinalized; } //! Attempts to convert a numeric MaterialPropertyValue to another numeric type @T, //! since MaterialPropertyValue itself does not support any kind of casting. //! If the original MaterialPropertyValue is not a numeric type, the original value is returned. template MaterialPropertyValue CastNumericMaterialPropertyValue(const MaterialPropertyValue& value) { TypeId typeId = value.GetTypeId(); if (typeId == azrtti_typeid()) { return aznumeric_cast(value.GetValue()); } else if (typeId == azrtti_typeid()) { return aznumeric_cast(value.GetValue()); } else if (typeId == azrtti_typeid()) { return aznumeric_cast(value.GetValue()); } else if (typeId == azrtti_typeid()) { return aznumeric_cast(value.GetValue()); } else { return value; } } //! Attempts to convert an AZ::Vector[2-4] MaterialPropertyValue to another AZ::Vector[2-4] type @T. //! Any extra elements will be dropped or set to 0.0 as needed. //! If the original MaterialPropertyValue is not a Vector type, the original value is returned. template MaterialPropertyValue CastVectorMaterialPropertyValue(const MaterialPropertyValue& value) { float values[4] = {}; TypeId typeId = value.GetTypeId(); if (typeId == azrtti_typeid()) { value.GetValue().StoreToFloat2(values); } else if (typeId == azrtti_typeid()) { value.GetValue().StoreToFloat3(values); } else if (typeId == azrtti_typeid()) { value.GetValue().StoreToFloat4(values); } else { return value; } typeId = azrtti_typeid(); if (typeId == azrtti_typeid()) { return Vector2::CreateFromFloat2(values); } else if (typeId == azrtti_typeid()) { return Vector3::CreateFromFloat3(values); } else if (typeId == azrtti_typeid()) { return Vector4::CreateFromFloat4(values); } else { return value; } } void MaterialAsset::Finalize(AZStd::function reportWarning, AZStd::function reportError) { if (m_wasPreFinalized) { m_isFinalized = true; } if (m_isFinalized) { return; } if (!reportWarning) { reportWarning = []([[maybe_unused]] const char* message) { AZ_Warning(s_debugTraceName, false, "%s", message); }; } if (!reportError) { reportError = []([[maybe_unused]] const char* message) { AZ_Error(s_debugTraceName, false, "%s", message); }; } const uint32_t materialTypeVersion = m_materialTypeAsset->GetVersion(); if (m_materialTypeVersion < materialTypeVersion) { // It is possible that the material type has had some properties renamed or otherwise updated. If that's the case, // and this material is still referencing the old property layout, we need to apply any auto updates to rename those // properties before using them to realign the property values. ApplyVersionUpdates(); } const MaterialPropertiesLayout* propertyLayout = GetMaterialPropertiesLayout(); AZStd::vector finalizedPropertyValues(m_materialTypeAsset->GetDefaultPropertyValues().begin(), m_materialTypeAsset->GetDefaultPropertyValues().end()); for (const auto& [name, value] : m_rawPropertyValues) { const MaterialPropertyIndex propertyIndex = propertyLayout->FindPropertyIndex(name); if (propertyIndex.IsValid()) { const MaterialPropertyDescriptor* propertyDescriptor = propertyLayout->GetPropertyDescriptor(propertyIndex); if (value.Is() && propertyDescriptor->GetDataType() == MaterialPropertyDataType::Enum) { AZ::Name enumName = AZ::Name(value.GetValue()); uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName); if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue) { reportWarning(AZStd::string::format("Material property name \"%s\" has invalid enum value \"%s\".", name.GetCStr(), enumName.GetCStr()).c_str()); } else { finalizedPropertyValues[propertyIndex.GetIndex()] = enumValue; } } else if (value.Is() && propertyDescriptor->GetDataType() == MaterialPropertyDataType::Image) { // Here we assume that the material asset builder resolved any image source file paths to an ImageAsset reference. // So the only way a string could be present is if it's an empty image path reference, meaning no image should be bound. AZ_Assert(value.GetValue().empty(), "Material property '%s' references in image '%s'. Image file paths must be resolved by the material asset builder."); finalizedPropertyValues[propertyIndex.GetIndex()] = Data::Asset{}; } else { // The material asset could be finalized sometime after the original JSON is loaded, and the material type might not have been available // at that time, so the data type would not be known for each property. So each raw property's type was based on what appeared in the JSON // and here we have the first opportunity to resolve that value with the actual type. For example, a float property could have been specified in // the JSON as 7 instead of 7.0, which is valid. Similarly, a Color and a Vector3 can both be specified as "[0.0,0.0,0.0]" in the JSON file. MaterialPropertyValue finalValue = value; switch (propertyDescriptor->GetDataType()) { case MaterialPropertyDataType::Bool: finalValue = CastNumericMaterialPropertyValue(value); break; case MaterialPropertyDataType::Int: finalValue = CastNumericMaterialPropertyValue(value); break; case MaterialPropertyDataType::UInt: finalValue = CastNumericMaterialPropertyValue(value); break; case MaterialPropertyDataType::Float: finalValue = CastNumericMaterialPropertyValue(value); break; case MaterialPropertyDataType::Color: if (value.GetTypeId() == azrtti_typeid()) { finalValue = Color::CreateFromVector3(value.GetValue()); } else if (value.GetTypeId() == azrtti_typeid()) { Vector4 vector4 = value.GetValue(); finalValue = Color::CreateFromVector3AndFloat(vector4.GetAsVector3(), vector4.GetW()); } break; case MaterialPropertyDataType::Vector2: finalValue = CastVectorMaterialPropertyValue(value); break; case MaterialPropertyDataType::Vector3: if (value.GetTypeId() == azrtti_typeid()) { finalValue = value.GetValue().GetAsVector3(); } else { finalValue = CastVectorMaterialPropertyValue(value); } break; case MaterialPropertyDataType::Vector4: if (value.GetTypeId() == azrtti_typeid()) { finalValue = value.GetValue().GetAsVector4(); } else { finalValue = CastVectorMaterialPropertyValue(value); } break; } if (ValidateMaterialPropertyDataType(finalValue.GetTypeId(), name, propertyDescriptor, reportError)) { finalizedPropertyValues[propertyIndex.GetIndex()] = finalValue; } } } else { reportWarning(AZStd::string::format("Material property name \"%s\" is not found in the material properties layout and will not be used.", name.GetCStr()).c_str()); } } m_propertyValues.swap(finalizedPropertyValues); m_isFinalized = true; } const AZStd::vector& MaterialAsset::GetPropertyValues() { // This can't be done in MaterialAssetHandler::LoadAssetData because the MaterialTypeAsset isn't necessarily loaded at that point. // And it can't be done in PostLoadInit() because that happens on the next frame which might be too late. // And overriding AssetHandler::InitAsset in MaterialAssetHandler didn't work, because there seems to be non-determinism on the order // of InitAsset calls when a ModelAsset references a MaterialAsset, the model gets initialized first and then fails to use the material. // So we finalize just-in-time when properties are accessed. // If we could solve the problem with InitAsset, that would be the ideal place to call Finalize() and we could make GetPropertyValues() const again. Finalize(); AZ_Assert(GetMaterialPropertiesLayout() && m_propertyValues.size() == GetMaterialPropertiesLayout()->GetPropertyCount(), "MaterialAsset should be finalized but does not have the right number of property values."); return m_propertyValues; } const AZStd::vector>& MaterialAsset::GetRawPropertyValues() const { return m_rawPropertyValues; } void MaterialAsset::SetReady() { m_status = AssetStatus::Ready; // If this was created dynamically using MaterialAssetCreator (which is what calls SetReady()), // we need to connect to the AssetBus for reloads. PostLoadInit(); } bool MaterialAsset::PostLoadInit() { if (!m_materialTypeAsset.Get()) { AssetInitBus::Handler::BusDisconnect(); // Any MaterialAsset with invalid MaterialTypeAsset is not a successfully-loaded asset. return false; } else { Data::AssetBus::Handler::BusConnect(m_materialTypeAsset.GetId()); MaterialReloadNotificationBus::Handler::BusConnect(m_materialTypeAsset.GetId()); AssetInitBus::Handler::BusDisconnect(); return true; } } void MaterialAsset::OnMaterialTypeAssetReinitialized(const Data::Asset& materialTypeAsset) { // When reloads occur, it's possible for old Asset objects to hang around and report reinitialization, // so we can reduce unnecessary reinitialization in that case. if (materialTypeAsset.Get() == m_materialTypeAsset.Get()) { ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnMaterialTypeAssetReinitialized %s", this, materialTypeAsset.GetHint().c_str()); // MaterialAsset doesn't need to reinitialize any of its own data when MaterialTypeAsset reinitializes, // because all it depends on is the MaterialTypeAsset reference, rather than the data inside it. // Ultimately it's the Material that cares about these changes, so we just forward any signal we get. MaterialReloadNotificationBus::Event(GetId(), &MaterialReloadNotifications::OnMaterialAssetReinitialized, Data::Asset{this, AZ::Data::AssetLoadBehavior::PreLoad}); } } void MaterialAsset::ApplyVersionUpdates() { if (m_materialTypeVersion == m_materialTypeAsset->GetVersion()) { return; } [[maybe_unused]] const uint32_t originalVersion = m_materialTypeVersion; bool changesWereApplied = false; for (const MaterialVersionUpdate& versionUpdate : m_materialTypeAsset->GetMaterialVersionUpdateList()) { if (m_materialTypeVersion < versionUpdate.GetVersion()) { if (versionUpdate.ApplyVersionUpdates(*this)) { changesWereApplied = true; m_materialTypeVersion = versionUpdate.GetVersion(); } } } if (changesWereApplied) { AZ_Warning( "MaterialAsset", false, "This material is based on version '%u' of %s, but the material type is now at version '%u'. " "Automatic updates are available. Consider updating the .material source file for '%s'.", originalVersion, m_materialTypeAsset.ToString().c_str(), m_materialTypeAsset->GetVersion(), GetId().ToString().c_str()); } m_materialTypeVersion = m_materialTypeAsset->GetVersion(); } void MaterialAsset::ReinitializeMaterialTypeAsset(Data::Asset asset) { Data::Asset newMaterialTypeAsset = Data::static_pointer_cast(asset); if (newMaterialTypeAsset) { // The order of asset reloads is non-deterministic. If the MaterialAsset reloads before the // MaterialTypeAsset, this will make sure the MaterialAsset gets update with latest one. // This also covers the case where just the MaterialTypeAsset is reloaded and not the MaterialAsset. m_materialTypeAsset = newMaterialTypeAsset; // If the material asset was not finalized on disk, then we clear the previously finalized property values to force re-finalize. // This is necessary in case the property layout changed in some way. if (!m_wasPreFinalized) { m_isFinalized = false; m_propertyValues.clear(); } // Notify interested parties that this MaterialAsset is changed and may require other data to reinitialize as well MaterialReloadNotificationBus::Event(GetId(), &MaterialReloadNotifications::OnMaterialAssetReinitialized, Data::Asset{this, AZ::Data::AssetLoadBehavior::PreLoad}); } } void MaterialAsset::OnAssetReloaded(Data::Asset asset) { ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReloaded %s", this, asset.GetHint().c_str()); ReinitializeMaterialTypeAsset(asset); } void MaterialAsset::OnAssetReady(Data::Asset asset) { // Regarding why we listen to both OnAssetReloaded and OnAssetReady, see explanation in ShaderAsset::OnAssetReady. ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReady %s", this, asset.GetHint().c_str()); ReinitializeMaterialTypeAsset(asset); } Data::AssetHandler::LoadResult MaterialAssetHandler::LoadAssetData( const AZ::Data::Asset& asset, AZStd::shared_ptr stream, const AZ::Data::AssetFilterCB& assetLoadFilterCB) { if (Base::LoadAssetData(asset, stream, assetLoadFilterCB) == Data::AssetHandler::LoadResult::LoadComplete) { asset.GetAs()->AssetInitBus::Handler::BusConnect(); return Data::AssetHandler::LoadResult::LoadComplete; } return Data::AssetHandler::LoadResult::Error; } } // namespace RPI } // namespace AZ