Files
o3de/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp
T
santorac c5b128bec4 First pass at reworking and formalizing the way deferred material asset baking works. The feature basically works but needs more testing.
Before, the material builder was loading the MaterialTypeAsset and doing some processing with it, but was avoiding declaring job dependencies that would cause reprocessing lots of assets when a shader or .materialtype file changes. Reading the asset data isn't safe when not declaring a job dependency (or when declaring a weak job dependency like OrderOnce which is the case here). This caused to several known bugs.

The main change here is it no longer loads the MaterialTypeAsset at all; all other changes flow from there.

The biggest changes (when deferred material processing is enabled) are ...
1) MaterialSourceData no longer loads MaterialTypeAsset. All it really needs is to determine whether a string is an image file reference or an enum value, which is easy to do by just looking for the "." for the extension.
2) MaterialAssetCreator no longer produces a finalized material asset. It no longer uses MaterialAssetCreatorCommon because that only produces a non-finalized MaterialAsset, which has very different needs for the SetPropertyValue function. (We could consider merging MaterialAssetCreatorCommon into MaterialTypeAssetCreator since that's the only subclass at this point). And it doesn't do any validation against the properties layout since that can be done at runtime.
3) Moved processing of enum property values from MaterialSourceData to MaterialAsset::Finalize (this was the only thing being done in the builder that actually needed to read the material type asset data).

Also...
- Updated the MaterialAsset class mostly to clarify and formalize the two different modes it can be in: whether it is finalized or not.
- Merged the separate "IncludeMaterialPropertyNames" registry settings from MaterialConverterSystemComponent and MaterialBuilder into one "FinalizeMaterialAssets" setting used for both.
- Removed MaterialSourceData::ApplyVersionUpdates. Now the flow of data is the same regardless of whether the materials are finalized by the AP or at runtime. Version updates are always applied on the MaterialAsset.
- Added a validation check to MaterialTypeAssetCreator ensuring that once a property is renamed, the old name can never be used again for a new property. This assumption was already made previously, but not formalized, in that Material::FindPropertyIndex does not expect every caller to provide a version number for the material property name, also the material asset's list of raw property names was never versioned. The only way for this to be a safe assumption is to prevent reuse of old names.

Signed-off-by: santorac <55155825+santorac@users.noreply.github.com>
2022-01-18 10:29:36 -08:00

330 lines
15 KiB
C++

/*
* 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/Material/MaterialAsset.h>
#include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
#include <Atom/RPI.Reflect/Material/MaterialFunctor.h>
#include <Atom/RPI.Reflect/Material/MaterialVersionUpdate.h>
#include <Atom/RPI.Reflect/Asset/AssetHandler.h>
#include <Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h>
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/TickBus.h>
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<SerializeContext*>(context))
{
serializeContext->Class<MaterialAsset, AZ::Data::AssetData>()
->Version(13) // added m_rawPropertyValues
->Field("materialTypeAsset", &MaterialAsset::m_materialTypeAsset)
->Field("materialTypeVersion", &MaterialAsset::m_materialTypeVersion)
->Field("propertyValues", &MaterialAsset::m_propertyValues)
->Field("rawPropertyValues", &MaterialAsset::m_rawPropertyValues)
->Field("isFinalized", &MaterialAsset::m_isFinalized)
;
}
}
MaterialAsset::MaterialAsset()
{
}
MaterialAsset::~MaterialAsset()
{
MaterialReloadNotificationBus::Handler::BusDisconnect();
Data::AssetBus::Handler::BusDisconnect();
AssetInitBus::Handler::BusDisconnect();
}
const Data::Asset<MaterialTypeAsset>& 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<RHI::ShaderResourceGroupLayout>& MaterialAsset::GetMaterialSrgLayout(const SupervariantIndex& supervariantIndex) const
{
return m_materialTypeAsset->GetMaterialSrgLayout(supervariantIndex);
}
const RHI::Ptr<RHI::ShaderResourceGroupLayout>& MaterialAsset::GetMaterialSrgLayout(const AZ::Name& supervariantName) const
{
return m_materialTypeAsset->GetMaterialSrgLayout(supervariantName);
}
const RHI::Ptr<RHI::ShaderResourceGroupLayout>& MaterialAsset::GetMaterialSrgLayout() const
{
return m_materialTypeAsset->GetMaterialSrgLayout();
}
const RHI::Ptr<RHI::ShaderResourceGroupLayout>& MaterialAsset::GetObjectSrgLayout(const SupervariantIndex& supervariantIndex) const
{
return m_materialTypeAsset->GetObjectSrgLayout(supervariantIndex);
}
const RHI::Ptr<RHI::ShaderResourceGroupLayout>& MaterialAsset::GetObjectSrgLayout(const AZ::Name& supervariantName) const
{
return m_materialTypeAsset->GetObjectSrgLayout(supervariantName);
}
const RHI::Ptr<RHI::ShaderResourceGroupLayout>& MaterialAsset::GetObjectSrgLayout() const
{
return m_materialTypeAsset->GetObjectSrgLayout();
}
const MaterialPropertiesLayout* MaterialAsset::GetMaterialPropertiesLayout() const
{
return m_materialTypeAsset->GetMaterialPropertiesLayout();
}
bool MaterialAsset::IsFinalized() const
{
if (m_isFinalized)
{
AZ_Assert(GetMaterialPropertiesLayout() && m_propertyValues.size() == GetMaterialPropertiesLayout()->GetPropertyCount(), "MaterialAsset is marked as Finalized but does not have the right number of property values.");
}
return m_isFinalized;
}
void MaterialAsset::Finalize()
{
if (IsFinalized())
{
return;
}
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<MaterialPropertyValue> 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<AZStd::string>() && propertyDescriptor->GetDataType() == MaterialPropertyDataType::Enum)
{
AZ::Name enumName = AZ::Name(value.GetValue<AZStd::string>());
uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName);
if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
{
AZ_Error(s_debugTraceName, false, "Material property name \"%s\" has invalid enum value \"%s\".", name.GetCStr(), enumName.GetCStr());
}
else
{
finalizedPropertyValues[propertyIndex.GetIndex()] = enumValue;
}
}
else if (value.Is<AZStd::string>() && 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<AZStd::string>().empty(), "Material property '%s' references in image '%s'. Image file paths must be resolved by the material asset builder.");
finalizedPropertyValues[propertyIndex.GetIndex()] = Data::Asset<ImageAsset>{};
}
else
{
finalizedPropertyValues[propertyIndex.GetIndex()] = value;
}
}
else
{
AZ_Warning(s_debugTraceName, false, "Material property name \"%s\" is not found in the material properties layout and will not be used.", name.GetCStr());
}
}
m_propertyValues.swap(finalizedPropertyValues);
m_isFinalized = true;
}
const AZStd::vector<MaterialPropertyValue>& MaterialAsset::GetPropertyValues() const
{
// 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. So we finalize just-in-time
// when properties are accessed.
const_cast<MaterialAsset*>(this)->Finalize();
return m_propertyValues;
}
const AZStd::vector<AZStd::pair<Name, MaterialPropertyValue>>& 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>& 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<MaterialAsset>{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<AZStd::string>().c_str(), m_materialTypeAsset->GetVersion(),
GetId().ToString<AZStd::string>().c_str());
}
m_materialTypeVersion = m_materialTypeAsset->GetVersion();
}
void MaterialAsset::ReinitializeMaterialTypeAsset(Data::Asset<Data::AssetData> asset)
{
Data::Asset<MaterialTypeAsset> newMaterialTypeAsset = Data::static_pointer_cast<MaterialTypeAsset>(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
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<MaterialAsset>{this, AZ::Data::AssetLoadBehavior::PreLoad});
}
}
void MaterialAsset::OnAssetReloaded(Data::Asset<Data::AssetData> asset)
{
ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReloaded %s", this, asset.GetHint().c_str());
ReinitializeMaterialTypeAsset(asset);
}
void MaterialAsset::OnAssetReady(Data::Asset<Data::AssetData> 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<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
if (Base::LoadAssetData(asset, stream, assetLoadFilterCB) == Data::AssetHandler::LoadResult::LoadComplete)
{
asset.GetAs<MaterialAsset>()->AssetInitBus::Handler::BusConnect();
asset.GetAs<MaterialAsset>()->m_wasPreFinalized = asset.GetAs<MaterialAsset>()->m_isFinalized;
return Data::AssetHandler::LoadResult::LoadComplete;
}
return Data::AssetHandler::LoadResult::Error;
}
} // namespace RPI
} // namespace AZ