Merge pull request #7228 from aws-lumberyard-dev/Atom/santorac/RemixableMaterialTypes3

Reorganized Material Type Properties and Groups
monroegm-disable-blank-issue-2
santorac 4 years ago committed by GitHub
commit 1e67b35a98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -44,8 +44,8 @@ namespace AZ
AZStd::string m_shaderInputName;
// The indices of photometric units in the dropdown list
uint32_t m_ev100Index;
uint32_t m_nitIndex;
uint32_t m_ev100Index = 0;
uint32_t m_nitIndex = 1;
// Minimum and Maximum value for different photometric units
AZ::Vector2 m_ev100MinMax;

@ -34,7 +34,8 @@ namespace AZ
explicit MaterialPropertyId(AZStd::string_view propertyName);
MaterialPropertyId(AZStd::string_view groupName, AZStd::string_view propertyName);
MaterialPropertyId(const Name& groupName, const Name& propertyName);
explicit MaterialPropertyId(const AZStd::span<const AZStd::string> names);
explicit MaterialPropertyId(const AZStd::span<AZStd::string> names);
MaterialPropertyId(const AZStd::span<AZStd::string> groupNames, AZStd::string_view propertyName);
AZ_DEFAULT_COPY_MOVE(MaterialPropertyId);

@ -22,9 +22,12 @@ namespace AZ
namespace RPI
{
class MaterialTypeAsset;
class MaterialTypeAssetCreator;
class MaterialFunctorSourceDataHolder;
class JsonMaterialPropertySerializer;
//! This is a simple data structure for serializing in/out material type source files.
//! Note that there may be a mixture of public and private members, as we are gradually introducing a proper API.
class MaterialTypeSourceData final
{
public:
@ -69,13 +72,22 @@ namespace AZ
struct PropertyDefinition
{
friend class JsonMaterialPropertySerializer;
AZ_CLASS_ALLOCATOR(PropertyDefinition, SystemAllocator, 0);
AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::PropertyDefinition, "{E0DB3C0D-75DB-4ADB-9E79-30DA63FA18B7}");
static const float DefaultMin;
static const float DefaultMax;
static const float DefaultStep;
AZStd::string m_name; //!< The name of the property within the property group. The full property ID will be groupName.propertyName.
PropertyDefinition() = default;
explicit PropertyDefinition(AZStd::string_view name) : m_name(name)
{
}
const AZStd::string& GetName() const { return m_name; }
MaterialPropertyVisibility m_visibility = MaterialPropertyVisibility::Default;
@ -97,6 +109,58 @@ namespace AZ
MaterialPropertyValue m_softMin;
MaterialPropertyValue m_softMax;
MaterialPropertyValue m_step;
private:
// We are gradually moving toward having a more proper API for MaterialTypeSourceData code, but we still some public members
// like above. However, it's important for m_name to be private because it is used as the key for lookups, collision validation, etc.
AZStd::string m_name; //!< The name of the property within the property group. The full property ID will be groupName.propertyName.
};
using PropertyList = AZStd::vector<AZStd::unique_ptr<PropertyDefinition>>;
struct PropertyGroup
{
friend class MaterialTypeSourceData;
AZ_CLASS_ALLOCATOR(PropertyGroup, SystemAllocator, 0);
AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::PropertyGroup, "{BA3AA0E4-C74D-4FD0-ADB2-00B060F06314}");
public:
PropertyGroup() = default;
AZ_DISABLE_COPY(PropertyGroup)
const AZStd::string& GetName() const { return m_name; }
const AZStd::string& GetDisplayName() const { return m_displayName; }
const AZStd::string& GetDescription() const { return m_description; }
const PropertyList& GetProperties() const { return m_properties; }
const AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& GetPropertyGroups() const { return m_propertyGroups; }
const AZStd::vector<Ptr<MaterialFunctorSourceDataHolder>>& GetFunctors() const { return m_materialFunctorSourceData; }
void SetDisplayName(AZStd::string_view displayName) { m_displayName = displayName; }
void SetDescription(AZStd::string_view description) { m_description = description; }
//! Add a new property to this PropertyGroup.
//! @param name a unique for the property. Must be a C-style identifier.
//! @return the new PropertyDefinition, or null if the name was not valid.
PropertyDefinition* AddProperty(AZStd::string_view name);
//! Add a new nested PropertyGroup to this PropertyGroup.
//! @param name a unique for the property group. Must be a C-style identifier.
//! @return the new PropertyGroup, or null if the name was not valid.
PropertyGroup* AddPropertyGroup(AZStd::string_view name);
private:
static PropertyGroup* AddPropertyGroup(AZStd::string_view name, AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& toPropertyGroupList);
AZStd::string m_name;
AZStd::string m_displayName;
AZStd::string m_description;
PropertyList m_properties;
AZStd::vector<AZStd::unique_ptr<PropertyGroup>> m_propertyGroups;
AZStd::vector<Ptr<MaterialFunctorSourceDataHolder>> m_materialFunctorSourceData;
};
struct ShaderVariantReferenceData
@ -118,8 +182,6 @@ namespace AZ
AZStd::unordered_map<Name/*shaderOption*/, Name/*value*/> m_shaderOptionValues;
};
using PropertyList = AZStd::vector<PropertyDefinition>;
struct VersionUpdatesRenameOperationDefinition
{
AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::VersionUpdatesRenameOperationDefinition, "{F2295489-E15A-46CC-929F-8D42DEDBCF14}");
@ -148,14 +210,21 @@ namespace AZ
{
AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::PropertyLayout, "{AE53CF3F-5C3B-44F5-B2FB-306F0EB06393}");
PropertyLayout() = default;
AZ_DISABLE_COPY(PropertyLayout)
//! This field is unused, and has been replaced by MaterialTypeSourceData::m_version below. It is kept for legacy file compatibility to suppress warnings and errors.
uint32_t m_versionOld = 0;
//! [Deprecated] Use m_propertyGroups instead
//! List of groups that will contain the available properties
AZStd::vector<GroupDefinition> m_groups;
AZStd::vector<GroupDefinition> m_groupsOld;
//! [Deprecated] Use m_propertyGroups instead
AZStd::map<AZStd::string /*group name*/, AZStd::vector<PropertyDefinition>> m_propertiesOld;
//! Collection of all available user-facing properties
AZStd::map<AZStd::string /*group name*/, PropertyList> m_properties;
AZStd::vector<AZStd::unique_ptr<PropertyGroup>> m_propertyGroups;
};
AZStd::string m_description;
@ -165,8 +234,6 @@ namespace AZ
VersionUpdates m_versionUpdates;
PropertyLayout m_propertyLayout;
//! A list of shader variants that are always used at runtime; they cannot be turned off
AZStd::vector<ShaderVariantReferenceData> m_shaderCollection;
@ -181,38 +248,94 @@ namespace AZ
//! Copy over UV custom names to the properties enum values.
void ResolveUvEnums();
const GroupDefinition* FindGroup(AZStd::string_view groupName) const;
//! Searches for a specific property.
//! Note this function can find properties using old versions of the property name; in that case,
//! the name in the returned PropertyDefinition* will not match the @propertyName that was searched for.
//! @return the requested property, or null if it could not be found
const PropertyDefinition* FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName) const;
//! Add a new PropertyGroup for containing properties or other PropertyGroups.
//! @param propertyGroupId The ID of the new property group. To add as a nested PropertyGroup, use a full path ID like "levelA.levelB.levelC"; in this case a property group "levelA.levelB" must already exist.
//! @return a pointer to the new PropertyGroup or null if there was a problem (an AZ_Error will be reported).
PropertyGroup* AddPropertyGroup(AZStd::string_view propertyGroupId);
//! Add a new property to a PropertyGroup.
//! @param propertyId The ID of the new property, like "layerBlend.factor" or "layer2.roughness.texture". The indicated property group must already exist.
//! @return a pointer to the new PropertyDefinition or null if there was a problem (an AZ_Error will be reported).
PropertyDefinition* AddProperty(AZStd::string_view propertyId);
//! Return the PropertyLayout containing the tree of property groups and property definitions.
const PropertyLayout& GetPropertyLayout() const { return m_propertyLayout; }
//! Find the PropertyGroup with the given ID.
//! @param propertyGroupId The full ID of a property group to find, like "levelA.levelB.levelC".
//! @return the found PropertyGroup or null if it doesn't exist.
const PropertyGroup* FindPropertyGroup(AZStd::string_view propertyGroupId) const;
PropertyGroup* FindPropertyGroup(AZStd::string_view propertyGroupId);
//! Find the definition for a property with the given ID.
//! @param propertyId The full ID of a property to find, like "baseColor.texture".
//! @return the found PropertyDefinition or null if it doesn't exist.
const PropertyDefinition* FindProperty(AZStd::string_view propertyId) const;
PropertyDefinition* FindProperty(AZStd::string_view propertyId);
//! Tokenizes an ID string like "itemA.itemB.itemC" into a vector like ["itemA", "itemB", "itemC"].
static AZStd::vector<AZStd::string_view> TokenizeId(AZStd::string_view id);
//! Splits an ID string like "itemA.itemB.itemC" into a vector like ["itemA.itemB", "itemC"].
static AZStd::vector<AZStd::string_view> SplitId(AZStd::string_view id);
//! Call back function type used with the enumeration functions.
//! Return false to terminate the traversal.
using EnumeratePropertyGroupsCallback = AZStd::function<bool(
const AZStd::string&, // The property ID context (i.e. "levelA.levelB.")
const PropertyGroup* // the next property group in the tree
)>;
//! Construct a complete list of group definitions, including implicit groups, arranged in the same order as the source data
//! Groups with the same name will be consolidated into a single entry
AZStd::vector<GroupDefinition> GetGroupDefinitionsInDisplayOrder() const;
//! Recursively traverses all of the property groups contained in the material type, executing a callback function for each.
//! @return false if the enumeration was terminated early by the callback returning false.
bool EnumeratePropertyGroups(const EnumeratePropertyGroupsCallback& callback) const;
//! Call back function type used with the numeration functions
//! Call back function type used with the numeration functions.
//! Return false to terminate the traversal.
using EnumeratePropertiesCallback = AZStd::function<bool(
const AZStd::string&, // The name of the group containing the property
const AZStd::string&, // The name of the property
const PropertyDefinition& // the property definition object that corresponds to the group and property names
const AZStd::string&, // The property ID context (i.e. "levelA.levelB."
const PropertyDefinition* // the property definition object
)>;
//! Traverse all of the properties contained in the source data executing a callback function
//! Traversal will occur in group alphabetical order and stop once all properties have been enumerated or the callback function returns false
void EnumerateProperties(const EnumeratePropertiesCallback& callback) const;
//! Traverse all of the properties in the source data in display/storage order executing a callback function
//! Traversal will stop once all properties have been enumerated or the callback function returns false
void EnumeratePropertiesInDisplayOrder(const EnumeratePropertiesCallback& callback) const;
//! Recursively traverses all of the properties contained in the material type, executing a callback function for each.
//! @return false if the enumeration was terminated early by the callback returning false.
bool EnumerateProperties(const EnumeratePropertiesCallback& callback) const;
Outcome<Data::Asset<MaterialTypeAsset>> CreateMaterialTypeAsset(Data::AssetId assetId, AZStd::string_view materialTypeSourceFilePath = "", bool elevateWarnings = true) const;
//! Possibly renames @propertyId based on the material version update steps.
//! @return true if the property was renamed
bool ApplyPropertyRenames(MaterialPropertyId& propertyId) const;
//! If the data was loaded from an old format file (i.e. where "groups" and "properties" were separate sections),
//! this converts to the new format where properties are listed inside property groups.
bool ConvertToNewDataFormat();
private:
const PropertyGroup* FindPropertyGroup(AZStd::span<const AZStd::string_view> parsedPropertyGroupId, AZStd::span<const AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList) const;
PropertyGroup* FindPropertyGroup(AZStd::span<AZStd::string_view> parsedPropertyGroupId, AZStd::span<AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList);
const PropertyDefinition* FindProperty(AZStd::span<const AZStd::string_view> parsedPropertyId, AZStd::span<const AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList) const;
PropertyDefinition* FindProperty(AZStd::span<AZStd::string_view> parsedPropertyId, AZStd::span<AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList);
// Function overloads for recursion, returns false to indicate that recursion should end.
bool EnumeratePropertyGroups(const EnumeratePropertyGroupsCallback& callback, AZStd::string propertyIdContext, const AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& inPropertyGroupList) const;
bool EnumerateProperties(const EnumeratePropertiesCallback& callback, AZStd::string propertyIdContext, const AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& inPropertyGroupList) const;
//! Recursively populates a material asset with properties from the tree of material property groups.
//! @param materialTypeSourceFilePath path to the material type file that is being processed, used to look up relative paths
//! @param propertyNameContext the accumulated prefix that should be applied to any property names encountered in the current @propertyGroup
//! @param propertyGroup the current PropertyGroup that is being processed
//! @return false if errors are detected and processing should abort
bool BuildPropertyList(
const AZStd::string& materialTypeSourceFilePath,
MaterialTypeAssetCreator& materialTypeAssetCreator,
AZStd::vector<AZStd::string>& propertyNameContext,
const MaterialTypeSourceData::PropertyGroup* propertyGroup) const;
//! Construct a complete list of group definitions, including implicit groups, arranged in the same order as the source data.
//! Groups with the same name will be consolidated into a single entry.
//! Operates on the old format PropertyLayout::m_groups, used for conversion to the new format.
AZStd::vector<GroupDefinition> GetOldFormatGroupDefinitionsInDisplayOrder() const;
PropertyLayout m_propertyLayout;
};
//! The wrapper class for derived material functors.
@ -241,7 +364,7 @@ namespace AZ
return m_actualSourceData ? m_actualSourceData->CreateFunctor(editorContext) : Failure();
}
const Ptr<MaterialFunctorSourceData> GetActualSourceData() const { return m_actualSourceData; }
Ptr<MaterialFunctorSourceData> GetActualSourceData() const { return m_actualSourceData; }
private:
Ptr<MaterialFunctorSourceData> m_actualSourceData = nullptr; // The derived material functor instance.
};

@ -52,7 +52,7 @@ namespace AZ
{
AssetBuilderSDK::AssetBuilderDesc materialBuilderDescriptor;
materialBuilderDescriptor.m_name = JobKey;
materialBuilderDescriptor.m_version = 116; // more material dependency improvements
materialBuilderDescriptor.m_version = 117; // new material type file format
materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.material", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.materialtype", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
materialBuilderDescriptor.m_busId = azrtti_typeid<MaterialBuilder>();

@ -88,7 +88,37 @@ namespace AZ
{
}
MaterialPropertyId::MaterialPropertyId(const AZStd::span<const AZStd::string> names)
MaterialPropertyId::MaterialPropertyId(const AZStd::span<AZStd::string> groupNames, AZStd::string_view propertyName)
{
for (const auto& name : groupNames)
{
if (!IsValidName(name))
{
AZ_Error("MaterialPropertyId", false, "'%s' is not a valid identifier.", name.c_str());
return;
}
}
if (!IsValidName(propertyName))
{
AZ_Error("MaterialPropertyId", false, "'%.*s' is not a valid identifier.", AZ_STRING_ARG(propertyName));
return;
}
if (groupNames.empty())
{
m_fullName = propertyName;
}
else
{
AZStd::string fullName;
AzFramework::StringFunc::Join(fullName, groupNames.begin(), groupNames.end(), ".");
fullName = AZStd::string::format("%s.%.*s", fullName.c_str(), AZ_STRING_ARG(propertyName));
m_fullName = fullName;
}
}
MaterialPropertyId::MaterialPropertyId(const AZStd::span<AZStd::string> names)
{
for (const auto& name : names)
{

@ -9,6 +9,7 @@
#include <Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h>
#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialPropertySerializer.h>
#include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
#include <AzCore/Math/Color.h>
#include <AzCore/Math/Vector2.h>

@ -217,14 +217,14 @@ namespace AZ
return Failure();
}
MaterialTypeSourceData materialTypeSourceData;
if (!AZ::RPI::JsonUtils::LoadObjectFromFile(materialTypeSourcePath, materialTypeSourceData))
auto materialTypeLoadOutcome = MaterialUtils::LoadMaterialTypeSourceData(materialTypeSourcePath);
if (!materialTypeLoadOutcome)
{
AZ_Error("MaterialSourceData", false, "Failed to load MaterialTypeSourceData: '%s'.", materialTypeSourcePath.c_str());
return Failure();
}
materialTypeSourceData.ResolveUvEnums();
MaterialTypeSourceData materialTypeSourceData = materialTypeLoadOutcome.TakeValue();
const auto materialTypeAsset =
materialTypeSourceData.CreateMaterialTypeAsset(materialTypeAssetId.GetValue(), materialTypeSourcePath, elevateWarnings);

@ -58,6 +58,10 @@ namespace AZ
serializeContext->Class<GroupDefinition>()->Version(4);
serializeContext->Class<PropertyDefinition>()->Version(1);
serializeContext->RegisterGenericType<AZStd::unique_ptr<PropertyGroup>>();
serializeContext->RegisterGenericType<AZStd::unique_ptr<PropertyDefinition>>();
serializeContext->RegisterGenericType<AZStd::vector<AZStd::unique_ptr<PropertyGroup>>>();
serializeContext->RegisterGenericType<AZStd::vector<AZStd::unique_ptr<PropertyDefinition>>>();
serializeContext->RegisterGenericType<PropertyConnectionList>();
serializeContext->Class<VersionUpdatesRenameOperationDefinition>()
@ -84,11 +88,22 @@ namespace AZ
->Field("options", &ShaderVariantReferenceData::m_shaderOptionValues)
;
serializeContext->Class<PropertyGroup>()
->Version(1)
->Field("name", &PropertyGroup::m_name)
->Field("displayName", &PropertyGroup::m_displayName)
->Field("description", &PropertyGroup::m_description)
->Field("properties", &PropertyGroup::m_properties)
->Field("propertyGroups", &PropertyGroup::m_propertyGroups)
->Field("functors", &PropertyGroup::m_materialFunctorSourceData)
;
serializeContext->Class<PropertyLayout>()
->Version(2) // Material Version Update
->Field("version", &PropertyLayout::m_versionOld)
->Field("groups", &PropertyLayout::m_groups)
->Field("properties", &PropertyLayout::m_properties)
->Version(3) // Added propertyGroups
->Field("version", &PropertyLayout::m_versionOld) //< Deprecated, preserved for backward compatibility, replaced by MaterialTypeSourceData::version
->Field("groups", &PropertyLayout::m_groupsOld) //< Deprecated, preserved for backward compatibility, replaced by propertyGroups
->Field("properties", &PropertyLayout::m_propertiesOld) //< Deprecated, preserved for backward compatibility, replaced by propertyGroups
->Field("propertyGroups", &PropertyLayout::m_propertyGroups)
;
serializeContext->RegisterGenericType<UvNameMap>();
@ -117,78 +132,194 @@ namespace AZ
const float MaterialTypeSourceData::PropertyDefinition::DefaultMax = std::numeric_limits<float>::max();
const float MaterialTypeSourceData::PropertyDefinition::DefaultStep = 0.1f;
const MaterialTypeSourceData::GroupDefinition* MaterialTypeSourceData::FindGroup(AZStd::string_view groupName) const
/*static*/ MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::PropertyGroup::AddPropertyGroup(AZStd::string_view name, AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& toPropertyGroupList)
{
for (const GroupDefinition& group : m_propertyLayout.m_groups)
auto iter = AZStd::find_if(toPropertyGroupList.begin(), toPropertyGroupList.end(), [name](const AZStd::unique_ptr<PropertyGroup>& existingPropertyGroup)
{
if (group.m_name == groupName)
return existingPropertyGroup->m_name == name;
});
if (iter != toPropertyGroupList.end())
{
return &group;
AZ_Error("Material source data", false, "PropertyGroup named '%.*s' already exists", AZ_STRING_ARG(name));
return nullptr;
}
if (!MaterialPropertyId::IsValidName(name))
{
AZ_Error("Material source data", false, "'%.*s' is not a valid identifier", AZ_STRING_ARG(name));
return nullptr;
}
toPropertyGroupList.push_back(AZStd::make_unique<PropertyGroup>());
toPropertyGroupList.back()->m_name = name;
return toPropertyGroupList.back().get();
}
MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::PropertyGroup::AddProperty(AZStd::string_view name)
{
auto propertyIter = AZStd::find_if(m_properties.begin(), m_properties.end(), [name](const AZStd::unique_ptr<PropertyDefinition>& existingProperty)
{
return existingProperty->GetName() == name;
});
if (propertyIter != m_properties.end())
{
AZ_Error("Material source data", false, "PropertyGroup '%s' already contains a property named '%.*s'", m_name.c_str(), AZ_STRING_ARG(name));
return nullptr;
}
bool MaterialTypeSourceData::ApplyPropertyRenames(MaterialPropertyId& propertyId) const
auto propertyGroupIter = AZStd::find_if(m_propertyGroups.begin(), m_propertyGroups.end(), [name](const AZStd::unique_ptr<PropertyGroup>& existingPropertyGroup)
{
bool renamed = false;
return existingPropertyGroup->m_name == name;
});
for (const VersionUpdateDefinition& versionUpdate : m_versionUpdates)
if (propertyGroupIter != m_propertyGroups.end())
{
for (const VersionUpdatesRenameOperationDefinition& action : versionUpdate.m_actions)
AZ_Error("Material source data", false, "Property name '%.*s' collides with a PropertyGroup of the same name", AZ_STRING_ARG(name));
return nullptr;
}
if (!MaterialPropertyId::IsValidName(name))
{
AZ_Error("Material source data", false, "'%.*s' is not a valid identifier", AZ_STRING_ARG(name));
return nullptr;
}
m_properties.emplace_back(AZStd::make_unique<PropertyDefinition>(name));
return m_properties.back().get();
}
MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::PropertyGroup::AddPropertyGroup(AZStd::string_view name)
{
if (action.m_operation == "rename")
auto iter = AZStd::find_if(m_properties.begin(), m_properties.end(), [name](const AZStd::unique_ptr<PropertyDefinition>& existingProperty)
{
if (action.m_renameFrom == propertyId.GetStringView())
return existingProperty->GetName() == name;
});
if (iter != m_properties.end())
{
propertyId = MaterialPropertyId::Parse(action.m_renameTo);
renamed = true;
AZ_Error("Material source data", false, "PropertyGroup name '%.*s' collides with a Property of the same name", AZ_STRING_ARG(name));
return nullptr;
}
return AddPropertyGroup(name, m_propertyGroups);
}
else
MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::AddPropertyGroup(AZStd::string_view propertyGroupId)
{
AZStd::vector<AZStd::string_view> splitPropertyGroupId = SplitId(propertyGroupId);
if (splitPropertyGroupId.size() == 1)
{
return PropertyGroup::AddPropertyGroup(propertyGroupId, m_propertyLayout.m_propertyGroups);
}
PropertyGroup* parentPropertyGroup = FindPropertyGroup(splitPropertyGroupId[0]);
if (!parentPropertyGroup)
{
AZ_Warning("Material source data", false, "Unsupported material version update operation '%s'", action.m_operation.c_str());
AZ_Error("Material source data", false, "PropertyGroup '%.*s' does not exists", AZ_STRING_ARG(splitPropertyGroupId[0]));
return nullptr;
}
return parentPropertyGroup->AddPropertyGroup(splitPropertyGroupId[1]);
}
MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::AddProperty(AZStd::string_view propertyId)
{
AZStd::vector<AZStd::string_view> splitPropertyId = SplitId(propertyId);
if (splitPropertyId.size() == 1)
{
AZ_Error("Material source data", false, "Property id '%.*s' is invalid. Properties must be added to a PropertyGroup (i.e. \"general.%.*s\").", AZ_STRING_ARG(propertyId), AZ_STRING_ARG(propertyId));
return nullptr;
}
PropertyGroup* parentPropertyGroup = FindPropertyGroup(splitPropertyId[0]);
if (!parentPropertyGroup)
{
AZ_Error("Material source data", false, "PropertyGroup '%.*s' does not exists", AZ_STRING_ARG(splitPropertyId[0]));
return nullptr;
}
return renamed;
return parentPropertyGroup->AddProperty(splitPropertyId[1]);
}
const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName) const
const MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::FindPropertyGroup(AZStd::span<const AZStd::string_view> parsedPropertyGroupId, AZStd::span<const AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList) const
{
for (const auto& propertyGroup : inPropertyGroupList)
{
if (propertyGroup->m_name != parsedPropertyGroupId[0])
{
continue;
}
else if (parsedPropertyGroupId.size() == 1)
{
auto groupIter = m_propertyLayout.m_properties.find(groupName);
if (groupIter != m_propertyLayout.m_properties.end())
return propertyGroup.get();
}
else
{
for (const PropertyDefinition& property : groupIter->second)
AZStd::span<const AZStd::string_view> subPath{parsedPropertyGroupId.begin() + 1, parsedPropertyGroupId.end()};
if (!subPath.empty())
{
if (property.m_name == propertyName)
const MaterialTypeSourceData::PropertyGroup* propertySubset = FindPropertyGroup(subPath, propertyGroup->m_propertyGroups);
if (propertySubset)
{
return &property;
return propertySubset;
}
}
}
}
// Property has not been found, try looking for renames in the version history
return nullptr;
}
MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::FindPropertyGroup(AZStd::span<AZStd::string_view> parsedPropertyGroupId, AZStd::span<AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList)
{
return const_cast<PropertyGroup*>(const_cast<const MaterialTypeSourceData*>(this)->FindPropertyGroup(parsedPropertyGroupId, inPropertyGroupList));
}
const MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::FindPropertyGroup(AZStd::string_view propertyGroupId) const
{
AZStd::vector<AZStd::string_view> tokens = TokenizeId(propertyGroupId);
return FindPropertyGroup(tokens, m_propertyLayout.m_propertyGroups);
}
MaterialPropertyId propertyId = MaterialPropertyId{groupName, propertyName};
ApplyPropertyRenames(propertyId);
MaterialTypeSourceData::PropertyGroup* MaterialTypeSourceData::FindPropertyGroup(AZStd::string_view propertyGroupId)
{
AZStd::vector<AZStd::string_view> tokens = TokenizeId(propertyGroupId);
return FindPropertyGroup(tokens, m_propertyLayout.m_propertyGroups);
}
// Do the search again with the new names
const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(
AZStd::span<const AZStd::string_view> parsedPropertyId,
AZStd::span<const AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList) const
{
for (const auto& propertyGroup : inPropertyGroupList)
{
if (propertyGroup->m_name == parsedPropertyId[0])
{
AZStd::span<const AZStd::string_view> subPath {parsedPropertyId.begin() + 1, parsedPropertyId.end()};
AZStd::vector<AZStd::string> tokens;
AZ::StringFunc::Tokenize(propertyId.GetStringView(), tokens, ".", true, true);
if (tokens.size() == 2)
if (subPath.size() == 1)
{
groupIter = m_propertyLayout.m_properties.find(tokens[0]);
if (groupIter != m_propertyLayout.m_properties.end())
for (AZStd::unique_ptr<PropertyDefinition>& property : propertyGroup->m_properties)
{
for (const PropertyDefinition& property : groupIter->second)
if (property->GetName() == subPath[0])
{
if (property.m_name == tokens[1])
return property.get();
}
}
}
else if(subPath.size() > 1)
{
return &property;
const MaterialTypeSourceData::PropertyDefinition* property = FindProperty(subPath, propertyGroup->m_propertyGroups);
if (property)
{
return property;
}
}
}
@ -197,6 +328,152 @@ namespace AZ
return nullptr;
}
MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::span<AZStd::string_view> parsedPropertyId, AZStd::span<AZStd::unique_ptr<PropertyGroup>> inPropertyGroupList)
{
return const_cast<MaterialTypeSourceData::PropertyDefinition*>(const_cast<const MaterialTypeSourceData*>(this)->FindProperty(parsedPropertyId, inPropertyGroupList));
}
const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view propertyId) const
{
AZStd::vector<AZStd::string_view> tokens = TokenizeId(propertyId);
return FindProperty(tokens, m_propertyLayout.m_propertyGroups);
}
MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view propertyId)
{
AZStd::vector<AZStd::string_view> tokens = TokenizeId(propertyId);
return FindProperty(tokens, m_propertyLayout.m_propertyGroups);
}
AZStd::vector<AZStd::string_view> MaterialTypeSourceData::TokenizeId(AZStd::string_view id)
{
AZStd::vector<AZStd::string_view> tokens;
AzFramework::StringFunc::TokenizeVisitor(id, [&tokens](AZStd::string_view t)
{
tokens.push_back(t);
},
"./", true, true);
return tokens;
}
AZStd::vector<AZStd::string_view> MaterialTypeSourceData::SplitId(AZStd::string_view id)
{
AZStd::vector<AZStd::string_view> parts;
parts.reserve(2);
size_t lastDelim = id.rfind('.', id.size()-1);
if (lastDelim == AZStd::string::npos)
{
parts.push_back(id);
}
else
{
parts.push_back(AZStd::string_view{id.begin(), id.begin()+lastDelim});
parts.push_back(AZStd::string_view{id.begin()+lastDelim+1, id.end()});
}
return parts;
}
bool MaterialTypeSourceData::EnumeratePropertyGroups(const EnumeratePropertyGroupsCallback& callback, AZStd::string propertyNameContext, const AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& inPropertyGroupList) const
{
for (auto& propertyGroup : inPropertyGroupList)
{
if (!callback(propertyNameContext, propertyGroup.get()))
{
return false; // Stop processing
}
const AZStd::string propertyNameContext2 = propertyNameContext + propertyGroup->m_name + ".";
if (!EnumeratePropertyGroups(callback, propertyNameContext2, propertyGroup->m_propertyGroups))
{
return false; // Stop processing
}
}
return true;
}
bool MaterialTypeSourceData::EnumeratePropertyGroups(const EnumeratePropertyGroupsCallback& callback) const
{
if (!callback)
{
return false;
}
return EnumeratePropertyGroups(callback, {}, m_propertyLayout.m_propertyGroups);
}
bool MaterialTypeSourceData::EnumerateProperties(const EnumeratePropertiesCallback& callback, AZStd::string propertyNameContext, const AZStd::vector<AZStd::unique_ptr<PropertyGroup>>& inPropertyGroupList) const
{
for (auto& propertyGroup : inPropertyGroupList)
{
const AZStd::string propertyNameContext2 = propertyNameContext + propertyGroup->m_name + ".";
for (auto& property : propertyGroup->m_properties)
{
if (!callback(propertyNameContext2, property.get()))
{
return false; // Stop processing
}
}
if (!EnumerateProperties(callback, propertyNameContext2, propertyGroup->m_propertyGroups))
{
return false; // Stop processing
}
}
return true;
}
bool MaterialTypeSourceData::EnumerateProperties(const EnumeratePropertiesCallback& callback) const
{
if (!callback)
{
return false;
}
return EnumerateProperties(callback, {}, m_propertyLayout.m_propertyGroups);
}
bool MaterialTypeSourceData::ConvertToNewDataFormat()
{
for (const auto& group : GetOldFormatGroupDefinitionsInDisplayOrder())
{
auto propertyListItr = m_propertyLayout.m_propertiesOld.find(group.m_name);
if (propertyListItr != m_propertyLayout.m_propertiesOld.end())
{
const auto& propertyList = propertyListItr->second;
for (auto& propertyDefinition : propertyList)
{
PropertyGroup* propertyGroup = FindPropertyGroup(group.m_name);
if (!propertyGroup)
{
m_propertyLayout.m_propertyGroups.emplace_back(AZStd::make_unique<PropertyGroup>());
m_propertyLayout.m_propertyGroups.back()->m_name = group.m_name;
m_propertyLayout.m_propertyGroups.back()->m_displayName = group.m_displayName;
m_propertyLayout.m_propertyGroups.back()->m_description = group.m_description;
propertyGroup = m_propertyLayout.m_propertyGroups.back().get();
}
PropertyDefinition* newProperty = propertyGroup->AddProperty(propertyDefinition.GetName());
*newProperty = propertyDefinition;
}
}
}
m_propertyLayout.m_groupsOld.clear();
m_propertyLayout.m_propertiesOld.clear();
return true;
}
void MaterialTypeSourceData::ResolveUvEnums()
{
AZStd::vector<AZStd::string> enumValues;
@ -206,26 +483,26 @@ namespace AZ
enumValues.push_back(uvNamePair.second);
}
for (auto& group : m_propertyLayout.m_properties)
{
for (PropertyDefinition& property : group.second)
EnumerateProperties([&enumValues](const AZStd::string&, const MaterialTypeSourceData::PropertyDefinition* property)
{
if (property.m_dataType == AZ::RPI::MaterialPropertyDataType::Enum && property.m_enumIsUv)
if (property->m_dataType == AZ::RPI::MaterialPropertyDataType::Enum && property->m_enumIsUv)
{
property.m_enumValues = enumValues;
}
}
// const_cast is safe because this is internal to the MaterialTypeSourceData. It isn't worth complicating things
// by adding another version of EnumerateProperties.
const_cast<MaterialTypeSourceData::PropertyDefinition*>(property)->m_enumValues = enumValues;
}
return true;
});
}
AZStd::vector<MaterialTypeSourceData::GroupDefinition> MaterialTypeSourceData::GetGroupDefinitionsInDisplayOrder() const
AZStd::vector<MaterialTypeSourceData::GroupDefinition> MaterialTypeSourceData::GetOldFormatGroupDefinitionsInDisplayOrder() const
{
AZStd::vector<MaterialTypeSourceData::GroupDefinition> groupDefinitions;
groupDefinitions.reserve(m_propertyLayout.m_properties.size());
groupDefinitions.reserve(m_propertyLayout.m_propertiesOld.size());
// Some groups are defined explicitly in the .materialtype file's "groups" section. This is the primary way groups are sorted in the UI.
AZStd::unordered_set<AZStd::string> foundGroups;
for (const auto& groupDefinition : m_propertyLayout.m_groups)
for (const auto& groupDefinition : m_propertyLayout.m_groupsOld)
{
if (foundGroups.insert(groupDefinition.m_name).second)
{
@ -238,7 +515,7 @@ namespace AZ
}
// Some groups are defined implicitly, in the "properties" section where a group name is used but not explicitly defined in the "groups" section.
for (const auto& propertyListPair : m_propertyLayout.m_properties)
for (const auto& propertyListPair : m_propertyLayout.m_propertiesOld)
{
const AZStd::string& groupName = propertyListPair.first;
if (foundGroups.insert(groupName).second)
@ -252,54 +529,183 @@ namespace AZ
return groupDefinitions;
}
void MaterialTypeSourceData::EnumerateProperties(const EnumeratePropertiesCallback& callback) const
bool MaterialTypeSourceData::BuildPropertyList(
const AZStd::string& materialTypeSourceFilePath,
MaterialTypeAssetCreator& materialTypeAssetCreator,
AZStd::vector<AZStd::string>& propertyNameContext,
const MaterialTypeSourceData::PropertyGroup* propertyGroup) const
{
if (!callback)
for (const AZStd::unique_ptr<PropertyDefinition>& property : propertyGroup->m_properties)
{
// Register the property...
MaterialPropertyId propertyId{propertyNameContext, property->GetName()};
if (!propertyId.IsValid())
{
return;
// MaterialPropertyId reports an error message
return false;
}
for (const auto& propertyListPair : m_propertyLayout.m_properties)
auto propertyGroupIter = AZStd::find_if(propertyGroup->GetPropertyGroups().begin(), propertyGroup->GetPropertyGroups().end(),
[&property](const AZStd::unique_ptr<PropertyGroup>& existingPropertyGroup)
{
const AZStd::string& groupName = propertyListPair.first;
const auto& propertyList = propertyListPair.second;
for (const auto& propertyDefinition : propertyList)
return existingPropertyGroup->GetName() == property->GetName();
});
if (propertyGroupIter != propertyGroup->GetPropertyGroups().end())
{
AZ_Error("Material source data", false, "Material property '%s' collides with a PropertyGroup with the same ID.", propertyId.GetCStr());
return false;
}
materialTypeAssetCreator.BeginMaterialProperty(propertyId, property->m_dataType);
if (property->m_dataType == MaterialPropertyDataType::Enum)
{
materialTypeAssetCreator.SetMaterialPropertyEnumNames(property->m_enumValues);
}
for (auto& output : property->m_outputConnections)
{
switch (output.m_type)
{
case MaterialPropertyOutputType::ShaderInput:
{
materialTypeAssetCreator.ConnectMaterialPropertyToShaderInput(Name{output.m_fieldName});
break;
}
case MaterialPropertyOutputType::ShaderOption:
{
if (output.m_shaderIndex >= 0)
{
const AZStd::string& propertyName = propertyDefinition.m_name;
if (!callback(groupName, propertyName, propertyDefinition))
materialTypeAssetCreator.ConnectMaterialPropertyToShaderOption(Name{output.m_fieldName}, output.m_shaderIndex);
}
else
{
return;
materialTypeAssetCreator.ConnectMaterialPropertyToShaderOptions(Name{output.m_fieldName});
}
break;
}
case MaterialPropertyOutputType::Invalid:
// Don't add any output mappings, this is the case when material functors are expected to process the property
break;
default:
AZ_Assert(false, "Unsupported MaterialPropertyOutputType");
return false;
}
}
void MaterialTypeSourceData::EnumeratePropertiesInDisplayOrder(const EnumeratePropertiesCallback& callback) const
materialTypeAssetCreator.EndMaterialProperty();
// Parse and set the property's value...
if (!property->m_value.IsValid())
{
AZ_Warning("Material source data", false, "Source data for material property value is invalid.");
}
else
{
switch (property->m_dataType)
{
case MaterialPropertyDataType::Image:
{
Data::Asset<ImageAsset> imageAsset;
MaterialUtils::GetImageAssetResult result = MaterialUtils::GetImageAssetReference(
imageAsset, materialTypeSourceFilePath, property->m_value.GetValue<AZStd::string>());
if (result == MaterialUtils::GetImageAssetResult::Missing)
{
if (!callback)
materialTypeAssetCreator.ReportError(
"Material property '%s': Could not find the image '%s'", propertyId.GetCStr(),
property->m_value.GetValue<AZStd::string>().data());
}
else
{
materialTypeAssetCreator.SetPropertyValue(propertyId, imageAsset);
}
}
break;
case MaterialPropertyDataType::Enum:
{
MaterialPropertyIndex propertyIndex = materialTypeAssetCreator.GetMaterialPropertiesLayout()->FindPropertyIndex(propertyId);
const MaterialPropertyDescriptor* propertyDescriptor = materialTypeAssetCreator.GetMaterialPropertiesLayout()->GetPropertyDescriptor(propertyIndex);
AZ::Name enumName = AZ::Name(property->m_value.GetValue<AZStd::string>());
uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName);
if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
{
materialTypeAssetCreator.ReportError("Enum value '%s' couldn't be found in the 'enumValues' list", enumName.GetCStr());
}
else
{
materialTypeAssetCreator.SetPropertyValue(propertyId, enumValue);
}
}
break;
default:
materialTypeAssetCreator.SetPropertyValue(propertyId, property->m_value);
break;
}
}
}
for (const AZStd::unique_ptr<PropertyGroup>& propertySubset : propertyGroup->m_propertyGroups)
{
propertyNameContext.push_back(propertySubset->m_name);
bool success = BuildPropertyList(
materialTypeSourceFilePath,
materialTypeAssetCreator,
propertyNameContext,
propertySubset.get());
propertyNameContext.pop_back();
if (!success)
{
return;
return false;
}
}
for (const auto& groupDefinition : GetGroupDefinitionsInDisplayOrder())
// We cannot create the MaterialFunctor until after all the properties are added because
// CreateFunctor() may need to look up properties in the MaterialPropertiesLayout
for (auto& functorData : propertyGroup->m_materialFunctorSourceData)
{
const AZStd::string& groupName = groupDefinition.m_name;
const auto propertyListItr = m_propertyLayout.m_properties.find(groupName);
if (propertyListItr != m_propertyLayout.m_properties.end())
MaterialFunctorSourceData::FunctorResult result = functorData->CreateFunctor(
MaterialFunctorSourceData::RuntimeContext(
materialTypeSourceFilePath,
materialTypeAssetCreator.GetMaterialPropertiesLayout(),
materialTypeAssetCreator.GetMaterialShaderResourceGroupLayout(),
materialTypeAssetCreator.GetShaderCollection()
)
);
if (result.IsSuccess())
{
const auto& propertyList = propertyListItr->second;
for (const auto& propertyDefinition : propertyList)
Ptr<MaterialFunctor>& functor = result.GetValue();
if (functor != nullptr)
{
const AZStd::string& propertyName = propertyDefinition.m_name;
if (!callback(groupName, propertyName, propertyDefinition))
materialTypeAssetCreator.AddMaterialFunctor(functor);
for (const AZ::Name& optionName : functorData->GetActualSourceData()->GetShaderOptionDependencies())
{
return;
materialTypeAssetCreator.ClaimShaderOptionOwnership(Name{optionName.GetCStr()});
}
}
}
else
{
materialTypeAssetCreator.ReportError("Failed to create MaterialFunctor");
return false;
}
}
return true;
}
Outcome<Data::Asset<MaterialTypeAsset>> MaterialTypeSourceData::CreateMaterialTypeAsset(Data::AssetId assetId, AZStd::string_view materialTypeSourceFilePath, bool elevateWarnings) const
{
MaterialTypeAssetCreator materialTypeAssetCreator;
@ -389,110 +795,18 @@ namespace AZ
}
}
for (auto& groupIter : m_propertyLayout.m_properties)
{
const AZStd::string& groupName = groupIter.first;
for (const PropertyDefinition& property : groupIter.second)
{
// Register the property...
MaterialPropertyId propertyId{ groupName, property.m_name };
if (!propertyId.IsValid())
{
materialTypeAssetCreator.ReportWarning("Cannot create material property with invalid ID '%s'.", propertyId.GetCStr());
continue;
}
materialTypeAssetCreator.BeginMaterialProperty(propertyId, property.m_dataType);
if (property.m_dataType == MaterialPropertyDataType::Enum)
for (const AZStd::unique_ptr<PropertyGroup>& propertyGroup : m_propertyLayout.m_propertyGroups)
{
materialTypeAssetCreator.SetMaterialPropertyEnumNames(property.m_enumValues);
}
AZStd::vector<AZStd::string> propertyNameContext;
propertyNameContext.push_back(propertyGroup->m_name);
bool success = BuildPropertyList(materialTypeSourceFilePath, materialTypeAssetCreator, propertyNameContext, propertyGroup.get());
for (auto& output : property.m_outputConnections)
{
switch (output.m_type)
{
case MaterialPropertyOutputType::ShaderInput:
materialTypeAssetCreator.ConnectMaterialPropertyToShaderInput(Name{ output.m_fieldName.data() });
break;
case MaterialPropertyOutputType::ShaderOption:
if (output.m_shaderIndex >= 0)
{
materialTypeAssetCreator.ConnectMaterialPropertyToShaderOption(Name{ output.m_fieldName.data() }, output.m_shaderIndex);
}
else
if (!success)
{
materialTypeAssetCreator.ConnectMaterialPropertyToShaderOptions(Name{ output.m_fieldName.data() });
}
break;
case MaterialPropertyOutputType::Invalid:
// Don't add any output mappings, this is the case when material functors are expected to process the property
break;
default:
AZ_Assert(false, "Unsupported MaterialPropertyOutputType");
return Failure();
}
}
materialTypeAssetCreator.EndMaterialProperty();
// Parse and set the property's value...
if (!property.m_value.IsValid())
{
AZ_Warning("Material source data", false, "Source data for material property value is invalid.");
}
else
{
switch (property.m_dataType)
{
case MaterialPropertyDataType::Image:
{
Data::Asset<ImageAsset> imageAsset;
MaterialUtils::GetImageAssetResult result = MaterialUtils::GetImageAssetReference(
imageAsset, materialTypeSourceFilePath, property.m_value.GetValue<AZStd::string>());
if (result == MaterialUtils::GetImageAssetResult::Missing)
{
materialTypeAssetCreator.ReportError(
"Material property '%s': Could not find the image '%s'", propertyId.GetCStr(),
property.m_value.GetValue<AZStd::string>().data());
}
else
{
materialTypeAssetCreator.SetPropertyValue(propertyId, imageAsset);
}
}
break;
case MaterialPropertyDataType::Enum:
{
MaterialPropertyIndex propertyIndex = materialTypeAssetCreator.GetMaterialPropertiesLayout()->FindPropertyIndex(propertyId);
const MaterialPropertyDescriptor* propertyDescriptor = materialTypeAssetCreator.GetMaterialPropertiesLayout()->GetPropertyDescriptor(propertyIndex);
AZ::Name enumName = AZ::Name(property.m_value.GetValue<AZStd::string>());
uint32_t enumValue = propertyDescriptor ? propertyDescriptor->GetEnumValue(enumName) : MaterialPropertyDescriptor::InvalidEnumValue;
if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
{
materialTypeAssetCreator.ReportError("Enum value '%s' couldn't be found in the 'enumValues' list", enumName.GetCStr());
}
else
{
materialTypeAssetCreator.SetPropertyValue(propertyId, enumValue);
}
}
break;
default:
materialTypeAssetCreator.SetPropertyValue(propertyId, property.m_value);
break;
}
}
}
}
// We cannot create the MaterialFunctor until after all the properties are added because
// CreateFunctor() may need to look up properties in the MaterialPropertiesLayout
for (auto& functorData : m_materialFunctorSourceData)

@ -102,6 +102,7 @@ namespace AZ
settings.m_metadata.Add(fileLoadContext);
JsonSerialization::Load(materialType, *document, settings);
materialType.ConvertToNewDataFormat();
materialType.ResolveUvEnums();
if (reportingHelper.ErrorsReported())

@ -94,6 +94,40 @@ namespace UnitTest
errorMessageFinder.CheckExpectedErrorsFound();
}
TEST_F(MaterialPropertyIdTests, TestConstructWithMultipleParentNamesSeparateFromPropertyName)
{
AZStd::vector<AZStd::string> names{"layer1", "clearCoat", "normal"};
MaterialPropertyId id{names, "factor"};
EXPECT_TRUE(id.IsValid());
EXPECT_STREQ(id.GetCStr(), "layer1.clearCoat.normal.factor");
AZ::Name idCastedToName = id;
EXPECT_EQ(idCastedToName, AZ::Name{"layer1.clearCoat.normal.factor"});
}
TEST_F(MaterialPropertyIdTests, TestConstructWithMultipleParentNamesSeparateFromPropertyName_BadParentName)
{
ErrorMessageFinder errorMessageFinder;
errorMessageFinder.AddExpectedErrorMessage("not a valid identifier");
AZStd::vector<AZStd::string> names{"layer1", "clear-coat", "normal"};
MaterialPropertyId id{names, "factor"};
EXPECT_FALSE(id.IsValid());
errorMessageFinder.CheckExpectedErrorsFound();
}
TEST_F(MaterialPropertyIdTests, TestConstructWithMultipleParentNamesSeparateFromPropertyName_BadPropertyName)
{
ErrorMessageFinder errorMessageFinder;
errorMessageFinder.AddExpectedErrorMessage("not a valid identifier");
AZStd::vector<AZStd::string> names{"layer1", "clearCoat", "normal"};
MaterialPropertyId id{names, "#factor"};
EXPECT_FALSE(id.IsValid());
errorMessageFinder.CheckExpectedErrorsFound();
}
TEST_F(MaterialPropertyIdTests, TestParse)
{
MaterialPropertyId id = MaterialPropertyId::Parse("layer1.clearCoat.normal.factor");

@ -45,8 +45,7 @@ namespace JsonSerializationTests
AZStd::shared_ptr<AZ::RPI::MaterialTypeSourceData::PropertyDefinition> CreatePartialDefaultInstance() override
{
auto result = AZStd::make_shared<AZ::RPI::MaterialTypeSourceData::PropertyDefinition>();
result->m_name = "testProperty";
auto result = AZStd::make_shared<AZ::RPI::MaterialTypeSourceData::PropertyDefinition>("testProperty");
result->m_dataType = AZ::RPI::MaterialPropertyDataType::Float;
result->m_step = 1.0f;
result->m_value = 0.0f;
@ -65,8 +64,7 @@ namespace JsonSerializationTests
AZStd::shared_ptr<AZ::RPI::MaterialTypeSourceData::PropertyDefinition> CreateFullySetInstance() override
{
auto result = AZStd::make_shared<AZ::RPI::MaterialTypeSourceData::PropertyDefinition>();
result->m_name = "testProperty";
auto result = AZStd::make_shared<AZ::RPI::MaterialTypeSourceData::PropertyDefinition>("testProperty");
result->m_description = "description";
result->m_displayName = "display_name";
result->m_dataType = AZ::RPI::MaterialPropertyDataType::Float;
@ -135,7 +133,7 @@ namespace JsonSerializationTests
const AZ::RPI::MaterialTypeSourceData::PropertyDefinition& lhs,
const AZ::RPI::MaterialTypeSourceData::PropertyDefinition& rhs) override
{
if (lhs.m_name != rhs.m_name) { return false; }
if (lhs.GetName() != rhs.GetName()) { return false; }
if (lhs.m_description != rhs.m_description) { return false; }
if (lhs.m_displayName != rhs.m_displayName) { return false; }
if (lhs.m_dataType != rhs.m_dataType) { return false; }
@ -216,7 +214,7 @@ namespace UnitTest
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing());
EXPECT_EQ(AZ::JsonSerializationResult::Outcomes::PartialDefaults, loadResult.m_jsonResultCode.GetOutcome());
EXPECT_EQ("testProperty", propertyData.m_name);
EXPECT_EQ("testProperty", propertyData.GetName());
EXPECT_EQ("Test Property", propertyData.m_displayName);
EXPECT_EQ("This is a property description", propertyData.m_description);
EXPECT_EQ(MaterialPropertyDataType::Float, propertyData.m_dataType);
@ -851,7 +849,7 @@ namespace UnitTest
EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask());
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing());
EXPECT_EQ("testProperty", propertyData.m_name);
EXPECT_EQ("testProperty", propertyData.GetName());
EXPECT_EQ(1, propertyData.m_outputConnections.size());
EXPECT_EQ(MaterialPropertyOutputType::ShaderOption, propertyData.m_outputConnections[0].m_type);
@ -934,7 +932,7 @@ namespace UnitTest
EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask());
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing());
EXPECT_EQ(propertyData.m_name, "testProperty");
EXPECT_EQ(propertyData.GetName(), "testProperty");
EXPECT_EQ(propertyData.m_dataType, MaterialPropertyDataType::Float);
EXPECT_EQ(propertyData.m_outputConnections.size(), 0);
@ -964,7 +962,7 @@ namespace UnitTest
EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask());
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing());
EXPECT_EQ(propertyData.m_name, "testProperty");
EXPECT_EQ(propertyData.GetName(), "testProperty");
EXPECT_EQ(propertyData.m_dataType, MaterialPropertyDataType::Float);
EXPECT_EQ(propertyData.m_outputConnections.size(), 1);
EXPECT_EQ(propertyData.m_outputConnections[0].m_fieldName, "o_foo");

@ -93,8 +93,10 @@ namespace UnitTest
{
"version": 10,
"propertyLayout": {
"properties": {
"general": [
"propertyGroups": [
{
"name": "general",
"properties": [
{"name": "MyBool", "type": "bool"},
{"name": "MyInt", "type": "Int"},
{"name": "MyUInt", "type": "UInt"},
@ -107,6 +109,7 @@ namespace UnitTest
{"name": "MyEnum", "type": "Enum", "enumValues": ["Enum0", "Enum1", "Enum2"], "defaultValue": "Enum0"}
]
}
]
},
"shaders": [
{
@ -468,14 +471,18 @@ namespace UnitTest
const AZStd::string simpleMaterialTypeJson = R"(
{
"propertyLayout": {
"properties": {
"general": [
"propertyGroups":
[
{
"name": "general",
"properties": [
{
"name": "testValue",
"type": "Float"
}
]
}
]
}
}
)";

@ -2,14 +2,11 @@
"description": "Base Material with properties used to define Standard PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model.",
"version": 3,
"propertyLayout": {
"groups": [
"propertyGroups": [
{
"name": "settings",
"displayName": "Settings"
}
],
"properties": {
"settings": [
"displayName": "Settings",
"properties": [
{
"name": "color",
"displayName": "Color",
@ -46,6 +43,7 @@
}
]
}
]
},
"shaders": [
{

@ -78,7 +78,7 @@ namespace AtomToolsFramework
void ConvertToPropertyConfig(AtomToolsFramework::DynamicPropertyConfig& propertyConfig, const AZ::RPI::MaterialTypeSourceData::PropertyDefinition& propertyDefinition)
{
propertyConfig.m_dataType = ConvertToEditableType(propertyDefinition.m_dataType);
propertyConfig.m_name = propertyDefinition.m_name;
propertyConfig.m_name = propertyDefinition.GetName();
propertyConfig.m_displayName = propertyDefinition.m_displayName;
propertyConfig.m_description = propertyDefinition.m_description;
propertyConfig.m_defaultValue = ConvertToEditableType(propertyDefinition.m_value);

@ -585,9 +585,9 @@ namespace MaterialEditor
bool result = true;
// populate sourceData with properties that meet the filter
m_materialTypeSourceData.EnumerateProperties([&](const AZStd::string& groupName, const AZStd::string& propertyName, const auto& propertyDefinition) {
m_materialTypeSourceData.EnumerateProperties([&](const AZStd::string& propertyIdContext, const auto& propertyDefinition) {
const MaterialPropertyId propertyId(groupName, propertyName);
Name propertyId{propertyIdContext + propertyDefinition->GetName()};
const auto it = m_properties.find(propertyId);
if (it != m_properties.end() && propertyFilter(it->second))
@ -595,14 +595,16 @@ namespace MaterialEditor
MaterialPropertyValue propertyValue = AtomToolsFramework::ConvertToRuntimeType(it->second.GetValue());
if (propertyValue.IsValid())
{
if (!AtomToolsFramework::ConvertToExportFormat(exportPath, propertyId, propertyDefinition, propertyValue))
if (!AtomToolsFramework::ConvertToExportFormat(exportPath, propertyId, *propertyDefinition, propertyValue))
{
AZ_Error("MaterialDocument", false, "Material document property could not be converted: '%s' in '%s'.", propertyId.GetCStr(), m_absolutePath.c_str());
result = false;
return false;
}
sourceData.m_properties[groupName][propertyName].m_value = propertyValue;
// TODO: Support populating the Material Editor with nested property groups, not just the top level.
const AZStd::string groupName = propertyId.GetStringView().substr(0, propertyId.GetStringView().size() - propertyDefinition->GetName().size() - 1);
sourceData.m_properties[groupName][propertyDefinition->GetName()].m_value = propertyValue;
}
}
return true;
@ -679,7 +681,7 @@ namespace MaterialEditor
AZ_Error("MaterialDocument", false, "Material type source data could not be loaded: '%s'.", m_materialSourceData.m_materialType.c_str());
return false;
}
m_materialTypeSourceData = materialTypeOutcome.GetValue();
m_materialTypeSourceData = materialTypeOutcome.TakeValue();
}
else if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), MaterialTypeSourceData::Extension))
{
@ -692,7 +694,7 @@ namespace MaterialEditor
AZ_Error("MaterialDocument", false, "Material type source data could not be loaded: '%s'.", m_absolutePath.c_str());
return false;
}
m_materialTypeSourceData = materialTypeOutcome.GetValue();
m_materialTypeSourceData = materialTypeOutcome.TakeValue();
// We are storing absolute paths in the loaded version of the source data so that the files can be resolved at all times.
m_materialSourceData.m_materialType = m_absolutePath;
@ -779,11 +781,14 @@ namespace MaterialEditor
// Populate the property map from a combination of source data and assets
// Assets must still be used for now because they contain the final accumulated value after all other materials
// in the hierarchy are applied
m_materialTypeSourceData.EnumerateProperties([this, &parentPropertyValues](const AZStd::string& groupName, const AZStd::string& propertyName, const auto& propertyDefinition) {
m_materialTypeSourceData.EnumeratePropertyGroups([this, &parentPropertyValues](const AZStd::string& propertyIdContext, const MaterialTypeSourceData::PropertyGroup* propertyGroup)
{
AtomToolsFramework::DynamicPropertyConfig propertyConfig;
for (const auto& propertyDefinition : propertyGroup->GetProperties())
{
// Assign id before conversion so it can be used in dynamic description
propertyConfig.m_id = MaterialPropertyId(groupName, propertyName);
propertyConfig.m_id = propertyIdContext + propertyGroup->GetName() + "." + propertyDefinition->GetName();
const auto& propertyIndex = m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id);
const bool propertyIndexInBounds = propertyIndex.IsValid() && propertyIndex.GetIndex() < m_materialAsset->GetPropertyValues().size();
@ -791,21 +796,26 @@ namespace MaterialEditor
if (propertyIndexInBounds)
{
AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDefinition);
AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, *propertyDefinition);
propertyConfig.m_showThumbnail = true;
propertyConfig.m_originalValue = AtomToolsFramework::ConvertToEditableType(m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]);
propertyConfig.m_parentValue = AtomToolsFramework::ConvertToEditableType(parentPropertyValues[propertyIndex.GetIndex()]);
auto groupDefinition = m_materialTypeSourceData.FindGroup(groupName);
propertyConfig.m_groupName = groupDefinition ? groupDefinition->m_displayName : groupName;
// TODO: Support populating the Material Editor with nested property groups, not just the top level.
// (Does DynamicPropertyConfig really even need m_groupName?)
propertyConfig.m_groupName = propertyGroup->GetDisplayName();
m_properties[propertyConfig.m_id] = AtomToolsFramework::DynamicProperty(propertyConfig);
}
}
return true;
});
// Populate the property group visibility map
for (MaterialTypeSourceData::GroupDefinition& group : m_materialTypeSourceData.GetGroupDefinitionsInDisplayOrder())
// TODO: Support populating the Material Editor with nested property groups, not just the top level.
for (const AZStd::unique_ptr<MaterialTypeSourceData::PropertyGroup>& propertyGroup : m_materialTypeSourceData.GetPropertyLayout().m_propertyGroups)
{
m_propertyGroupVisibility[AZ::Name{group.m_name}] = true;
m_propertyGroupVisibility[AZ::Name{propertyGroup->GetName()}] = true;
}
// Adding properties for material type and parent as part of making dynamic
@ -867,6 +877,7 @@ namespace MaterialEditor
m_properties[propertyConfig.m_id] = AtomToolsFramework::DynamicProperty(propertyConfig);
}
// Add material functors that are in the top-level functors list.
const MaterialFunctorSourceData::EditorContext editorContext =
MaterialFunctorSourceData::EditorContext(m_materialSourceData.m_materialType, m_materialAsset->GetMaterialPropertiesLayout());
for (Ptr<MaterialFunctorSourceDataHolder> functorData : m_materialTypeSourceData.m_materialFunctorSourceData)
@ -888,6 +899,40 @@ namespace MaterialEditor
}
}
// Add any material functors that are located inside each property group.
bool enumerateResult = m_materialTypeSourceData.EnumeratePropertyGroups(
[this](const AZStd::string&, const MaterialTypeSourceData::PropertyGroup* propertyGroup)
{
const MaterialFunctorSourceData::EditorContext editorContext = MaterialFunctorSourceData::EditorContext(
m_materialSourceData.m_materialType, m_materialAsset->GetMaterialPropertiesLayout());
for (Ptr<MaterialFunctorSourceDataHolder> functorData : propertyGroup->GetFunctors())
{
MaterialFunctorSourceData::FunctorResult result = functorData->CreateFunctor(editorContext);
if (result.IsSuccess())
{
Ptr<MaterialFunctor>& functor = result.GetValue();
if (functor != nullptr)
{
m_editorFunctors.push_back(functor);
}
}
else
{
AZ_Error("MaterialDocument", false, "Material functors were not created: '%s'.", m_absolutePath.c_str());
return false;
}
}
return true;
});
if (!enumerateResult)
{
return false;
}
AZ::RPI::MaterialPropertyFlags dirtyFlags;
dirtyFlags.set(); // Mark all properties as dirty since we just loaded the material and need to initialize property visibility
RunEditorMaterialFunctors(dirtyFlags);

@ -171,28 +171,23 @@ namespace MaterialEditor
MaterialDocumentRequestBus::EventResult(
materialTypeSourceData, m_documentId, &MaterialDocumentRequestBus::Events::GetMaterialTypeSourceData);
for (const auto& groupDefinition : materialTypeSourceData->GetGroupDefinitionsInDisplayOrder())
// TODO: Support populating the Material Editor with nested property groups, not just the top level.
for (const AZStd::unique_ptr<AZ::RPI::MaterialTypeSourceData::PropertyGroup>& propertyGroup : materialTypeSourceData->GetPropertyLayout().m_propertyGroups)
{
const AZStd::string& groupName = groupDefinition.m_name;
const AZStd::string& groupDisplayName = !groupDefinition.m_displayName.empty() ? groupDefinition.m_displayName : groupName;
const AZStd::string& groupDescription =
!groupDefinition.m_description.empty() ? groupDefinition.m_description : groupDisplayName;
const AZStd::string& groupName = propertyGroup->GetName();
const AZStd::string& groupDisplayName = !propertyGroup->GetDisplayName().empty() ? propertyGroup->GetDisplayName() : groupName;
const AZStd::string& groupDescription = !propertyGroup->GetDescription().empty() ? propertyGroup->GetDescription() : groupDisplayName;
auto& group = m_groups[groupName];
const auto& propertyLayout = materialTypeSourceData->m_propertyLayout;
const auto& propertyListItr = propertyLayout.m_properties.find(groupName);
if (propertyListItr != propertyLayout.m_properties.end())
{
group.m_properties.reserve(propertyListItr->second.size());
for (const auto& propertyDefinition : propertyListItr->second)
group.m_properties.reserve(propertyGroup->GetProperties().size());
for (const auto& propertyDefinition : propertyGroup->GetProperties())
{
AtomToolsFramework::DynamicProperty property;
AtomToolsFramework::AtomToolsDocumentRequestBus::EventResult(
property, m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::GetProperty,
AZ::RPI::MaterialPropertyId(groupName, propertyDefinition.m_name));
AZ::RPI::MaterialPropertyId(groupName, propertyDefinition->GetName()));
group.m_properties.push_back(property);
}
}
// Passing in same group as main and comparison instance to enable custom value comparison for highlighting modified properties
auto propertyGroupWidget = new AtomToolsFramework::InspectorPropertyGroupWidget(

@ -293,45 +293,40 @@ namespace AZ
void MaterialPropertyInspector::AddPropertiesGroup()
{
// Copy all of the properties from the material asset to the source data that will be exported
for (const auto& groupDefinition : m_editData.m_materialTypeSourceData.GetGroupDefinitionsInDisplayOrder())
// TODO: Support populating the Material Editor with nested property groups, not just the top level.
for (const AZStd::unique_ptr<AZ::RPI::MaterialTypeSourceData::PropertyGroup>& propertyGroup : m_editData.m_materialTypeSourceData.GetPropertyLayout().m_propertyGroups)
{
const AZStd::string& groupName = groupDefinition.m_name;
const AZStd::string& groupDisplayName = !groupDefinition.m_displayName.empty() ? groupDefinition.m_displayName : groupName;
const AZStd::string& groupDescription = !groupDefinition.m_description.empty() ? groupDefinition.m_description : groupDisplayName;
const AZStd::string& groupName = propertyGroup->GetName();
const AZStd::string& groupDisplayName = !propertyGroup->GetDisplayName().empty() ? propertyGroup->GetDisplayName() : groupName;
const AZStd::string& groupDescription = !propertyGroup->GetDescription().empty() ? propertyGroup->GetDescription() : groupDisplayName;
auto& group = m_groups[groupName];
const auto& propertyLayout = m_editData.m_materialTypeSourceData.m_propertyLayout;
const auto& propertyListItr = propertyLayout.m_properties.find(groupName);
if (propertyListItr != propertyLayout.m_properties.end())
{
group.m_properties.reserve(propertyListItr->second.size());
for (const auto& propertyDefinition : propertyListItr->second)
group.m_properties.reserve(propertyGroup->GetProperties().size());
for (const auto& propertyDefinition : propertyGroup->GetProperties())
{
AtomToolsFramework::DynamicPropertyConfig propertyConfig;
// Assign id before conversion so it can be used in dynamic description
propertyConfig.m_id = AZ::RPI::MaterialPropertyId(groupName, propertyDefinition.m_name);
propertyConfig.m_id = AZ::RPI::MaterialPropertyId(groupName, propertyDefinition->GetName());
AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDefinition);
AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, *propertyDefinition.get());
const auto& propertyIndex =
m_editData.m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id);
propertyConfig.m_groupName = groupDisplayName;
propertyConfig.m_showThumbnail = true;
propertyConfig.m_defaultValue = AtomToolsFramework::ConvertToEditableType(
m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]);
// There is no explicit parent material here. Material instance property overrides replace the values from the
// assigned material asset. Its values should be treated as parent, for comparison, in this case.
propertyConfig.m_parentValue = AtomToolsFramework::ConvertToEditableType(
m_editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]);
m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]);
propertyConfig.m_originalValue = AtomToolsFramework::ConvertToEditableType(
m_editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]);
group.m_properties.emplace_back(propertyConfig);
}
}
// Passing in same group as main and comparison instance to enable custom value comparison for highlighting modified properties
auto propertyGroupWidget = new AtomToolsFramework::InspectorPropertyGroupWidget(

@ -92,7 +92,7 @@ namespace AZ
AZ_Error("AZ::Render::EditorMaterialComponentUtil", false, "Failed to load material type source data: %s", editData.m_materialTypeSourcePath.c_str());
return false;
}
editData.m_materialTypeSourceData = materialTypeOutcome.GetValue();
editData.m_materialTypeSourceData = materialTypeOutcome.TakeValue();
return true;
}
@ -113,15 +113,16 @@ namespace AZ
// Copy all of the properties from the material asset to the source data that will be exported
bool result = true;
editData.m_materialTypeSourceData.EnumerateProperties([&](const AZStd::string& groupName, const AZStd::string& propertyName, const auto& propertyDefinition){
const AZ::RPI::MaterialPropertyId propertyId(groupName, propertyName);
editData.m_materialTypeSourceData.EnumerateProperties([&](const AZStd::string& propertyIdContext, const AZ::RPI::MaterialTypeSourceData::PropertyDefinition* propertyDefinition)
{
AZ::Name propertyId(propertyIdContext + propertyDefinition->GetName());
const AZ::RPI::MaterialPropertyIndex propertyIndex =
editData.m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyId);
AZ::RPI::MaterialPropertyValue propertyValue =
editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()];
AZ::RPI::MaterialPropertyValue propertyValueDefault = propertyDefinition.m_value;
AZ::RPI::MaterialPropertyValue propertyValueDefault = propertyDefinition->m_value;
if (editData.m_materialParentAsset.IsReady())
{
propertyValueDefault = editData.m_materialParentAsset->GetPropertyValues()[propertyIndex.GetIndex()];
@ -134,7 +135,7 @@ namespace AZ
propertyValue = AZ::RPI::MaterialPropertyValue::FromAny(propertyOverrideItr->second);
}
if (!AtomToolsFramework::ConvertToExportFormat(path, propertyId, propertyDefinition, propertyValue))
if (!AtomToolsFramework::ConvertToExportFormat(path, propertyId, *propertyDefinition, propertyValue))
{
AZ_Error("AZ::Render::EditorMaterialComponentUtil", false, "Failed to export: %s", path.c_str());
result = false;
@ -147,7 +148,9 @@ namespace AZ
return true;
}
exportData.m_properties[groupName][propertyDefinition.m_name].m_value = propertyValue;
// TODO: Support populating the Material Editor with nested property groups, not just the top level.
const AZStd::string groupName = propertyId.GetStringView().substr(0, propertyId.GetStringView().size() - propertyDefinition->GetName().size() - 1);
exportData.m_properties[groupName][propertyDefinition->GetName()].m_value = propertyValue;
return true;
});

Loading…
Cancel
Save