Files
o3de/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.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

347 lines
16 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.Edit/Material/MaterialSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h>
#include <Atom/RPI.Edit/Material/MaterialSourceDataSerializer.h>
#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
#include <Atom/RPI.Edit/Material/MaterialUtils.h>
#include <Atom/RPI.Edit/Material/MaterialConverterBus.h>
#include <Atom/RPI.Edit/Common/AssetUtils.h>
#include <Atom/RPI.Edit/Common/JsonFileLoadContext.h>
#include <Atom/RPI.Edit/Common/JsonReportingHelper.h>
#include <Atom/RPI.Edit/Common/JsonUtils.h>
#include <Atom/RPI.Reflect/Material/MaterialAssetCreator.h>
#include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
#include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
#include <Atom/RPI.Public/Image/StreamingImage.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/IO/TextStreamWriters.h>
#include <AzCore/IO/GenericStreams.h>
#include <AzCore/IO/IOUtils.h>
#include <AzCore/JSON/prettywriter.h>
#include <AzCore/std/algorithm.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
namespace AZ
{
namespace RPI
{
void MaterialSourceData::Reflect(ReflectContext* context)
{
if (JsonRegistrationContext* jsonContext = azrtti_cast<JsonRegistrationContext*>(context))
{
jsonContext->Serializer<JsonMaterialSourceDataSerializer>()->HandlesType<MaterialSourceData>();
jsonContext->Serializer<JsonMaterialPropertyValueSerializer>()->HandlesType<MaterialSourceData::Property>();
}
else if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
{
serializeContext->Class<MaterialSourceData>()
->Version(1)
;
serializeContext->RegisterGenericType<PropertyMap>();
serializeContext->RegisterGenericType<PropertyGroupMap>();
serializeContext->Class<MaterialSourceData::Property>()
->Version(1)
;
}
}
// Helper function for CreateMaterialAsset, for applying basic material property values
template<typename T>
void ApplyMaterialValues(MaterialAssetCreator& materialAssetCreator, const AZStd::map<Name, T>& values)
{
for (auto& entry : values)
{
const Name& propertyId = entry.first;
materialAssetCreator.SetPropertyValue(propertyId, entry.second);
}
}
Outcome<Data::Asset<MaterialAsset>> MaterialSourceData::CreateMaterialAsset(
Data::AssetId assetId, AZStd::string_view materialSourceFilePath, MaterialAssetProcessingMode processingMode, bool elevateWarnings) const
{
MaterialAssetCreator materialAssetCreator;
materialAssetCreator.SetElevateWarnings(elevateWarnings);
Outcome<Data::AssetId> materialTypeAssetId = AssetUtils::MakeAssetId(materialSourceFilePath, m_materialType, 0);
if (!materialTypeAssetId)
{
return Failure();
}
Data::Asset<MaterialTypeAsset> materialTypeAsset;
switch (processingMode)
{
case MaterialAssetProcessingMode::DeferredBake:
{
// Don't load the material type data, just create a reference to it
materialTypeAsset = Data::Asset<MaterialTypeAsset>{ materialTypeAssetId.GetValue(), azrtti_typeid<MaterialTypeAsset>(), m_materialType };
break;
}
case MaterialAssetProcessingMode::PreBake:
{
// In this case we need to load the material type data in preparation for the material->Finalize() step below.
auto materialTypeAssetOutcome = AssetUtils::LoadAsset<MaterialTypeAsset>(materialTypeAssetId.GetValue());
if (!materialTypeAssetOutcome)
{
return Failure();
}
materialTypeAsset = materialTypeAssetOutcome.GetValue();
break;
}
default:
{
AZ_Assert(false, "Unhandled MaterialAssetProcessingMode");
return Failure();
}
}
materialAssetCreator.Begin(assetId, materialTypeAsset);
if (!m_parentMaterial.empty())
{
auto parentMaterialAsset = AssetUtils::LoadAsset<MaterialAsset>(materialSourceFilePath, m_parentMaterial);
if (!parentMaterialAsset.IsSuccess())
{
return Failure();
}
// Make sure the parent material has the same material type
{
Data::AssetId parentsMaterialTypeId = parentMaterialAsset.GetValue()->GetMaterialTypeAsset().GetId();
if (materialTypeAssetId.GetValue() != parentsMaterialTypeId)
{
AZ_Error("MaterialSourceData", false, "This material and its parent material do not share the same material type.");
return Failure();
}
}
// Inherit the parent's property values...
switch (processingMode)
{
case MaterialAssetProcessingMode::DeferredBake:
{
for (auto& property : parentMaterialAsset.GetValue()->GetRawPropertyValues())
{
materialAssetCreator.SetPropertyValue(property.first, property.second);
}
break;
}
case MaterialAssetProcessingMode::PreBake:
{
const MaterialPropertiesLayout* propertiesLayout = parentMaterialAsset.GetValue()->GetMaterialPropertiesLayout();
if (parentMaterialAsset.GetValue()->GetPropertyValues().size() != propertiesLayout->GetPropertyCount())
{
AZ_Assert(false, "The parent material should have been finalized with %zu properties but it has %zu. Something is out of sync.",
propertiesLayout->GetPropertyCount(), parentMaterialAsset.GetValue()->GetPropertyValues().size());
return Failure();
}
for (size_t propertyIndex = 0; propertyIndex < propertiesLayout->GetPropertyCount(); ++propertyIndex)
{
materialAssetCreator.SetPropertyValue(
propertiesLayout->GetPropertyDescriptor(MaterialPropertyIndex{propertyIndex})->GetName(),
parentMaterialAsset.GetValue()->GetPropertyValues()[propertyIndex]);
}
break;
}
default:
{
AZ_Assert(false, "Unhandled MaterialAssetProcessingMode");
return Failure();
}
}
}
ApplyPropertiesToAssetCreator(materialAssetCreator, materialSourceFilePath);
Data::Asset<MaterialAsset> material;
if (materialAssetCreator.End(material))
{
if (processingMode == MaterialAssetProcessingMode::PreBake)
{
material->Finalize();
}
return Success(material);
}
else
{
return Failure();
}
}
Outcome<Data::Asset<MaterialAsset>> MaterialSourceData::CreateMaterialAssetFromSourceData(
Data::AssetId assetId,
AZStd::string_view materialSourceFilePath,
bool elevateWarnings,
AZStd::unordered_set<AZStd::string>* sourceDependencies) const
{
const auto materialTypeSourcePath = AssetUtils::ResolvePathReference(materialSourceFilePath, m_materialType);
const auto materialTypeAssetId = AssetUtils::MakeAssetId(materialTypeSourcePath, 0);
if (!materialTypeAssetId.IsSuccess())
{
AZ_Error("MaterialSourceData", false, "Failed to create material type asset ID: '%s'.", materialTypeSourcePath.c_str());
return Failure();
}
MaterialTypeSourceData materialTypeSourceData;
if (!AZ::RPI::JsonUtils::LoadObjectFromFile(materialTypeSourcePath, materialTypeSourceData))
{
AZ_Error("MaterialSourceData", false, "Failed to load MaterialTypeSourceData: '%s'.", materialTypeSourcePath.c_str());
return Failure();
}
materialTypeSourceData.ResolveUvEnums();
const auto materialTypeAsset =
materialTypeSourceData.CreateMaterialTypeAsset(materialTypeAssetId.GetValue(), materialTypeSourcePath, elevateWarnings);
if (!materialTypeAsset.IsSuccess())
{
AZ_Error("MaterialSourceData", false, "Failed to create material type asset from source data: '%s'.", materialTypeSourcePath.c_str());
return Failure();
}
// Track all of the material and material type assets loaded while trying to create a material asset from source data. This will
// be used for evaluating circular dependencies and returned for external monitoring or other use.
AZStd::unordered_set<AZStd::string> dependencies;
dependencies.insert(materialSourceFilePath);
dependencies.insert(materialTypeSourcePath);
// Load and build a stack of MaterialSourceData from all of the parent materials in the hierarchy. Properties from the source
// data will be applied in reverse to the asset creator.
AZStd::vector<MaterialSourceData> parentSourceDataStack;
AZStd::string parentSourceRelPath = m_parentMaterial;
AZStd::string parentSourceAbsPath = AssetUtils::ResolvePathReference(materialSourceFilePath, parentSourceRelPath);
while (!parentSourceRelPath.empty())
{
if (!dependencies.insert(parentSourceAbsPath).second)
{
AZ_Error("MaterialSourceData", false, "Detected circular dependency between materials: '%s' and '%s'.", materialSourceFilePath.data(), parentSourceAbsPath.c_str());
return Failure();
}
MaterialSourceData parentSourceData;
if (!AZ::RPI::JsonUtils::LoadObjectFromFile(parentSourceAbsPath, parentSourceData))
{
AZ_Error("MaterialSourceData", false, "Failed to load MaterialSourceData for parent material: '%s'.", parentSourceAbsPath.c_str());
return Failure();
}
// Make sure that all materials in the hierarchy share the same material type
const auto parentTypeAssetId = AssetUtils::MakeAssetId(parentSourceAbsPath, parentSourceData.m_materialType, 0);
if (!parentTypeAssetId)
{
AZ_Error("MaterialSourceData", false, "Parent material asset ID wasn't found: '%s'.", parentSourceAbsPath.c_str());
return Failure();
}
if (parentTypeAssetId.GetValue() != materialTypeAssetId.GetValue())
{
AZ_Error("MaterialSourceData", false, "This material and its parent material do not share the same material type.");
return Failure();
}
// Get the location of the next parent material and push the source data onto the stack
parentSourceRelPath = parentSourceData.m_parentMaterial;
parentSourceAbsPath = AssetUtils::ResolvePathReference(parentSourceAbsPath, parentSourceRelPath);
parentSourceDataStack.emplace_back(AZStd::move(parentSourceData));
}
// Create the material asset from all the previously loaded source data
MaterialAssetCreator materialAssetCreator;
materialAssetCreator.SetElevateWarnings(elevateWarnings);
materialAssetCreator.Begin(assetId, materialTypeAsset.GetValue());
while (!parentSourceDataStack.empty())
{
parentSourceDataStack.back().ApplyPropertiesToAssetCreator(materialAssetCreator, materialSourceFilePath);
parentSourceDataStack.pop_back();
}
ApplyPropertiesToAssetCreator(materialAssetCreator, materialSourceFilePath);
Data::Asset<MaterialAsset> material;
if (materialAssetCreator.End(material))
{
// Unlike CreateMaterialAsset(), we can always finalize the material here because we loaded created the MaterialTypeAsset from
// the source .materialtype file, so the necessary data is always available.
// (In case you are wondering why we don't use CreateMaterialAssetFromSourceData in MaterialBuilder: that would require a
// source dependency between the .materialtype and .material file, which would cause all .material files to rebuild when you
// edit the .materialtype; it's faster to not read the material type data at all ... until it's needed at runtime)
material->Finalize();
if (sourceDependencies)
{
sourceDependencies->insert(dependencies.begin(), dependencies.end());
}
return Success(material);
}
return Failure();
}
void MaterialSourceData::ApplyPropertiesToAssetCreator(
AZ::RPI::MaterialAssetCreator& materialAssetCreator, const AZStd::string_view& materialSourceFilePath) const
{
for (auto& group : m_properties)
{
for (auto& property : group.second)
{
MaterialPropertyId propertyId{ group.first, property.first };
if (!property.second.m_value.IsValid())
{
materialAssetCreator.ReportWarning("Source data for material property value is invalid.");
}
else if (property.second.m_value.Is<AZStd::string>() && AzFramework::StringFunc::Contains(property.second.m_value.GetValue<AZStd::string>(), "."))
{
Data::Asset<ImageAsset> imageAsset;
MaterialUtils::GetImageAssetResult result = MaterialUtils::GetImageAssetReference(
imageAsset, materialSourceFilePath, property.second.m_value.GetValue<AZStd::string>());
if (result == MaterialUtils::GetImageAssetResult::Missing)
{
materialAssetCreator.ReportWarning(
"Material property '%s': Could not find the image '%s'", propertyId.GetFullName().GetCStr(),
property.second.m_value.GetValue<AZStd::string>().data());
}
imageAsset.SetAutoLoadBehavior(Data::AssetLoadBehavior::PreLoad);
materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), imageAsset);
}
else
{
materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), property.second.m_value);
}
}
}
}
} // namespace RPI
} // namespace AZ