/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace AZ { namespace RPI { bool MaterialPropertyCollection::Init( RHI::ConstPtr layout, const AZStd::vector& defaultValues) { m_layout = layout; if (!m_layout) { AZ_Error("MaterialPropertyCollection", false, "MaterialPropertiesLayout is invalid"); return false; } // If this Init() is actually a re-initialize, we need to re-apply any overridden property values // after loading the default property values, so we save that data here. MaterialPropertyFlags prevOverrideFlags = m_propertyOverrideFlags; AZStd::vector prevPropertyValues = m_propertyValues; // The property values are cleared to their default state to ensure that SetPropertyValue() does not early-return // when called below. This is important when Init() is actually a re-initialize. m_propertyValues.clear(); // Initialize the shader runtime data like shader constant buffers and shader variants by applying the // material's property values. This will feed through the normal runtime material value-change data flow, which may // include custom property change handlers provided by the material type. // // This baking process could be more efficient by doing it at build-time rather than run-time. However, the // architectural complexity of supporting separate asset/runtime paths for assigning buffers/images is prohibitive. m_propertyValues.resize(defaultValues.size()); AZ_Assert(defaultValues.size() == m_layout->GetPropertyCount(), "The number of properties in this material doesn't match the property layout"); AZ_Assert(defaultValues.size() <= Limits::Material::PropertyCountMax, "Too many material properties. Max is %d.", Limits::Material::PropertyCountMax); for (size_t i = 0; i < defaultValues.size(); ++i) { const MaterialPropertyValue& value = defaultValues[i]; MaterialPropertyIndex propertyIndex{i}; if (!SetPropertyValue(propertyIndex, value)) { return false; } } // Clear all override flags because we just loaded properties from the asset m_propertyOverrideFlags.reset(); // Now apply any properties that were overridden before for (size_t i = 0; i < prevPropertyValues.size(); ++i) { if (prevOverrideFlags[i]) { SetPropertyValue(MaterialPropertyIndex{i}, prevPropertyValues[i]); } } return true; } const MaterialPropertyValue& MaterialPropertyCollection::GetPropertyValue(MaterialPropertyIndex index) const { static const MaterialPropertyValue emptyValue; if (m_propertyValues.size() <= index.GetIndex()) { AZ_Error("MaterialPropertyCollection", false, "Property index out of range."); return emptyValue; } return m_propertyValues[index.GetIndex()]; } const AZStd::vector& MaterialPropertyCollection::GetPropertyValues() const { return m_propertyValues; } void MaterialPropertyCollection::SetAllPropertyDirtyFlags() { m_propertyDirtyFlags.set(); } void MaterialPropertyCollection::ClearAllPropertyDirtyFlags() { m_propertyDirtyFlags.reset(); } template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const Type& value) { if (!index.IsValid()) { AZ_Assert(false, "SetPropertyValue: Invalid MaterialPropertyIndex"); return false; } const MaterialPropertyDescriptor* propertyDescriptor = m_layout->GetPropertyDescriptor(index); if (!ValidatePropertyAccess(propertyDescriptor)) { return false; } MaterialPropertyValue& savedPropertyValue = m_propertyValues[index.GetIndex()]; // If the property value didn't actually change, don't waste time running functors and compiling the changes. if (savedPropertyValue == value) { return false; } savedPropertyValue = value; m_propertyDirtyFlags.set(index.GetIndex()); m_propertyOverrideFlags.set(index.GetIndex()); return true; } template<> bool MaterialPropertyCollection::SetPropertyValue>(MaterialPropertyIndex index, const Data::Asset& value) { Data::Asset imageAsset = value; if (!imageAsset.GetId().IsValid()) { // The image asset reference is null so set the property to an empty Image instance so the AZStd::any will not be empty. return SetPropertyValue(index, Data::Instance()); } else { AZ::Data::AssetType assetType = imageAsset.GetType(); if (assetType != azrtti_typeid() && assetType != azrtti_typeid()) { AZ::Data::AssetInfo assetInfo; AZ::Data::AssetCatalogRequestBus::BroadcastResult( assetInfo, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, imageAsset.GetId()); assetType = assetInfo.m_assetType; } // There is an issue in the Asset(Asset) copy constructor which is used with the FindOrCreate() calls below. // If the AssetData is valid, then it will get the actual asset type ID from the AssetData. However, if it is null // then it will continue using the original type ID. The InstanceDatabase will end up asking the AssetManager for // the asset using the wrong type (ImageAsset) and will lead to various error messages and in the end the asset // will never be loaded. So we work around this issue by forcing the asset type ID to the correct value first. // See https://github.com/o3de/o3de/issues/12224 if (!imageAsset.Get()) { imageAsset = Data::Asset{imageAsset.GetId(), assetType, imageAsset.GetHint()}; } Data::Instance image = nullptr; if (assetType == azrtti_typeid()) { Data::Asset streamingImageAsset = imageAsset; image = StreamingImage::FindOrCreate(streamingImageAsset); } else if (assetType == azrtti_typeid()) { Data::Asset attachmentImageAsset = imageAsset; image = AttachmentImage::FindOrCreate(attachmentImageAsset); } else { AZ_Error("MaterialPropertyCollection", false, "Unsupported image asset type: %s", assetType.ToString().c_str()); return false; } if (!image) { AZ_Error("MaterialPropertyCollection", false, "Image asset could not be loaded"); return false; } return SetPropertyValue(index, image); } } template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const bool& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const int32_t& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const uint32_t& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const float& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const Vector2& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const Vector3& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const Vector4& value); template bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex index, const Color& value); template bool MaterialPropertyCollection::SetPropertyValue>(MaterialPropertyIndex index, const Data::Instance& value); bool MaterialPropertyCollection::SetPropertyValue(MaterialPropertyIndex propertyIndex, const MaterialPropertyValue& value) { if (!value.IsValid()) { auto descriptor = m_layout->GetPropertyDescriptor(propertyIndex); if (descriptor) { AZ_Assert(false, "Empty value found for material property '%s'", descriptor->GetName().GetCStr()); } else { AZ_Assert(false, "Empty value found for material property [%d], and this property does not have a descriptor."); } return false; } if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is()) { return SetPropertyValue(propertyIndex, value.GetValue()); } else if (value.Is>()) { return SetPropertyValue(propertyIndex, value.GetValue>()); } else if (value.Is>()) { return SetPropertyValue(propertyIndex, value.GetValue>()); } else { AZ_Assert(false, "Unhandled material property value type"); return false; } } template const Type& MaterialPropertyCollection::GetPropertyValue(MaterialPropertyIndex index) const { static const Type defaultValue{}; const MaterialPropertyDescriptor* propertyDescriptor = nullptr; if (Validation::IsEnabled()) { if (!index.IsValid()) { AZ_Assert(false, "GetPropertyValue: Invalid MaterialPropertyIndex"); return defaultValue; } propertyDescriptor = m_layout->GetPropertyDescriptor(index); if (!ValidatePropertyAccess(propertyDescriptor)) { return defaultValue; } } const MaterialPropertyValue& value = m_propertyValues[index.GetIndex()]; if (value.Is()) { return value.GetValue(); } else { if (Validation::IsEnabled()) { AZ_Assert(false, "Material property '%s': Stored property value has the wrong data type. Expected %s but is %s.", propertyDescriptor->GetName().GetCStr(), azrtti_typeid().template ToString().data(), // 'template' because clang says "error: use 'template' keyword to treat 'ToString' as a dependent template name" value.GetTypeId().ToString().data()); } return defaultValue; } } // Using explicit instantiation to restrict GetPropertyValue to the set of types that we support template const bool& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const int32_t& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const uint32_t& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const float& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const Vector2& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const Vector3& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const Vector4& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const Color& MaterialPropertyCollection::GetPropertyValue (MaterialPropertyIndex index) const; template const Data::Instance& MaterialPropertyCollection::GetPropertyValue>(MaterialPropertyIndex index) const; const MaterialPropertyFlags& MaterialPropertyCollection::GetPropertyDirtyFlags() const { return m_propertyDirtyFlags; } RHI::ConstPtr MaterialPropertyCollection::GetMaterialPropertiesLayout() const { return m_layout; } template bool MaterialPropertyCollection::ValidatePropertyAccess(const MaterialPropertyDescriptor* propertyDescriptor) const { // Note that we have warnings here instead of errors because this can happen while materials are hot reloading // after a material property layout changes in the MaterialTypeAsset, as there's a brief time when the data // might be out of sync between MaterialAssets and MaterialTypeAssets. if (!propertyDescriptor) { AZ_Warning("MaterialPropertyCollection", false, "MaterialPropertyDescriptor is null"); return false; } AZ::TypeId accessDataType = azrtti_typeid(); // Must align with the order in MaterialPropertyDataType static const AZStd::array types = {{ AZ::TypeId{}, // Invalid azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), azrtti_typeid(), azrtti_typeid>(), azrtti_typeid() }}; AZ::TypeId actualDataType = types[static_cast(propertyDescriptor->GetDataType())]; if (accessDataType != actualDataType) { AZ_Warning("MaterialPropertyCollection", false, "Material property '%s': Accessed as type %s but is type %s", propertyDescriptor->GetName().GetCStr(), GetMaterialPropertyDataTypeString(accessDataType).c_str(), ToString(propertyDescriptor->GetDataType())); return false; } return true; } } // namespace RPI } // namespace AZ