You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInsp...

524 lines
28 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <Material/EditorMaterialComponentInspector.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <Atom/RPI.Edit/Common/AssetUtils.h>
#include <Atom/RPI.Edit/Common/JsonUtils.h>
#include <Atom/RPI.Edit/Material/MaterialFunctorSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
#include <Atom/RPI.Edit/Material/MaterialSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialUtils.h>
#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
#include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
#include <Atom/RPI.Reflect/Material/MaterialTypeAsset.h>
#include <Atom/RPI.Reflect/Material/MaterialFunctor.h>
#include <AtomToolsFramework/Inspector/InspectorPropertyGroupWidget.h>
#include <AtomToolsFramework/Util/MaterialPropertyUtil.h>
#include <AzToolsFramework/API/EditorWindowRequestBus.h>
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
#include <QApplication>
#include <QDialog>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
AZ_POP_DISABLE_WARNING
namespace AZ
{
namespace Render
{
namespace EditorMaterialComponentInspector
{
MaterialPropertyInspector::MaterialPropertyInspector(const AZ::Data::AssetId& assetId, PropertyChangedCallback propertyChangedCallback, QWidget* parent)
: AtomToolsFramework::InspectorWidget(parent)
, m_materialAssetId(assetId)
, m_propertyChangedCallback(propertyChangedCallback)
{
}
MaterialPropertyInspector::~MaterialPropertyInspector()
{
AtomToolsFramework::InspectorRequestBus::Handler::BusDisconnect();
}
bool MaterialPropertyInspector::LoadMaterial()
{
if (!m_materialAssetId.IsValid())
{
AZ_Warning("AZ::Render::EditorMaterialComponentInspector", false, "Attempted to load material data for invalid asset id.");
return false;
}
// Load the originating product asset from which the new source has set will be generated
auto materialAssetOutcome = AZ::RPI::AssetUtils::LoadAsset<AZ::RPI::MaterialAsset>(m_materialAssetId);
if (!materialAssetOutcome)
{
AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Failed to load material asset: %s", m_materialAssetId.ToString<AZStd::string>().c_str());
return false;
}
m_materialAsset = materialAssetOutcome.GetValue();
m_materialTypeAsset = m_materialAsset->GetMaterialTypeAsset();
m_parentMaterialAsset = {};
// The material instance is still needed for functor execution
m_materialInstance = AZ::RPI::Material::Create(m_materialAsset);
if (!m_materialInstance)
{
AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Material instance could not be created.");
return false;
}
// We need a valid path to the material type source data because it's required for to get the property layout and assign to the new material
const AZStd::string& materialTypePath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(m_materialTypeAsset.GetId());
if (materialTypePath.empty())
{
AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Failed to locate source material type asset: %s", m_materialAssetId.ToString<AZStd::string>().c_str());
return false;
}
// Getting the source info for the material type file to make sure that it exists
// We also need to watch folder to generate a relative asset path for the material type
bool result = false;
AZ::Data::AssetInfo info;
AZStd::string watchFolder;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(result, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath, materialTypePath.c_str(), info, watchFolder);
if (!result)
{
AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Failed to get source file info and asset path: %s", materialTypePath.c_str());
return false;
}
// At this point, we should be ready to attempt to load the material type data
auto materialTypeOutcome = AZ::RPI::MaterialUtils::LoadMaterialTypeSourceData(materialTypePath);
if (!materialTypeOutcome.IsSuccess())
{
AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Failed to load material type source data: %s", materialTypePath.c_str());
return false;
}
m_materialTypeSourceData = materialTypeOutcome.GetValue();
// Get a list of all the editor functors to be used for property editor states
auto propertyLayout = m_materialAsset->GetMaterialPropertiesLayout();
const AZ::RPI::MaterialFunctorSourceData::EditorContext editorContext = AZ::RPI::MaterialFunctorSourceData::EditorContext(materialTypePath, propertyLayout);
for (AZ::RPI::Ptr<AZ::RPI::MaterialFunctorSourceDataHolder> functorData : m_materialTypeSourceData.m_materialFunctorSourceData)
{
AZ::RPI::MaterialFunctorSourceData::FunctorResult createResult = functorData->CreateFunctor(editorContext);
if (createResult.IsSuccess())
{
AZ::RPI::Ptr<AZ::RPI::MaterialFunctor>& functor = createResult.GetValue();
if (functor != nullptr)
{
m_editorFunctors.push_back(functor);
}
}
else
{
AZ_Error("AZ::Render::EditorMaterialComponentInspector", false, "Material functors were not created: '%s'.", materialTypePath.c_str());
}
}
return true;
}
void MaterialPropertyInspector::Reset()
{
m_activeProperty = {};
m_groups = {};
m_dirtyPropertyFlags.set();
AtomToolsFramework::InspectorRequestBus::Handler::BusDisconnect();
AtomToolsFramework::InspectorWidget::Reset();
}
void MaterialPropertyInspector::AddDetailsGroup()
{
const AZStd::string& groupNameId = "Details";
const AZStd::string& groupDisplayName = "Details";
const AZStd::string& groupDescription = "";
auto& group = m_groups[groupNameId];
AtomToolsFramework::DynamicPropertyConfig propertyConfig;
propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::Asset;
propertyConfig.m_id = "details.materialType";
propertyConfig.m_nameId = "materialType";
propertyConfig.m_displayName = "Material Type";
propertyConfig.m_description = propertyConfig.m_displayName;
propertyConfig.m_defaultValue = AZStd::any(m_materialAsset->GetMaterialTypeAsset());
propertyConfig.m_originalValue = propertyConfig.m_defaultValue;
propertyConfig.m_parentValue = propertyConfig.m_defaultValue;
propertyConfig.m_readOnly = true;
group.m_properties.emplace_back(propertyConfig);
propertyConfig = {};
propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::Asset;
propertyConfig.m_id = "details.parentMaterial";
propertyConfig.m_nameId = "parentMaterial";
propertyConfig.m_displayName = "Parent Material";
propertyConfig.m_description = propertyConfig.m_displayName;
propertyConfig.m_defaultValue = AZStd::any(m_parentMaterialAsset);
propertyConfig.m_originalValue = propertyConfig.m_defaultValue;
propertyConfig.m_parentValue = propertyConfig.m_defaultValue;
propertyConfig.m_readOnly = true;
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(&group, &group, group.TYPEINFO_Uuid(), this, this,
[this](const AzToolsFramework::InstanceDataNode* source, const AzToolsFramework::InstanceDataNode* target) {
AZ_UNUSED(source);
const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(target);
return property && AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_parentValue);
});
AddGroup(groupNameId, groupDisplayName, groupDescription, propertyGroupWidget);
}
void MaterialPropertyInspector::AddUvNamesGroup()
{
const AZStd::string groupNameId = AZ::RPI::UvGroupName;
const AZStd::string groupDisplayName = "UV Names";
const AZStd::string groupDescription = "UV set names in this material, which can be renamed to match those in the model.";
auto& group = m_groups[groupNameId];
const RPI::MaterialUvNameMap& uvNameMap = m_materialAsset->GetMaterialTypeAsset()->GetUvNameMap();
group.m_properties.reserve(uvNameMap.size());
for (const RPI::UvNamePair& uvNamePair : uvNameMap)
{
AtomToolsFramework::DynamicPropertyConfig propertyConfig;
const AZStd::string shaderInputStr = uvNamePair.m_shaderInput.ToString();
const AZStd::string uvName = uvNamePair.m_uvName.GetStringView();
propertyConfig = {};
propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::String;
propertyConfig.m_id = AZ::RPI::MaterialPropertyId(groupNameId, shaderInputStr).GetCStr();
propertyConfig.m_nameId = shaderInputStr;
propertyConfig.m_displayName = shaderInputStr;
propertyConfig.m_description = shaderInputStr;
propertyConfig.m_defaultValue = uvName;
propertyConfig.m_originalValue = uvName;
propertyConfig.m_parentValue = uvName;
propertyConfig.m_readOnly = true;
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(&group, &group, group.TYPEINFO_Uuid(), this, this,
[this](const AzToolsFramework::InstanceDataNode* source, const AzToolsFramework::InstanceDataNode* target) {
AZ_UNUSED(source);
const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(target);
return property && AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_parentValue);
});
AddGroup(groupNameId, groupDisplayName, groupDescription, propertyGroupWidget);
}
void MaterialPropertyInspector::Populate()
{
AddGroupsBegin();
AddDetailsGroup();
AddUvNamesGroup();
// Copy all of the properties from the material asset to the source data that will be exported
for (const auto& groupDefinition : m_materialTypeSourceData.GetGroupDefinitionsInDisplayOrder())
{
const AZStd::string& groupNameId = groupDefinition.m_nameId;
const AZStd::string& groupDisplayName = !groupDefinition.m_displayName.empty() ? groupDefinition.m_displayName : groupNameId;
const AZStd::string& groupDescription = !groupDefinition.m_description.empty() ? groupDefinition.m_description : groupDisplayName;
auto& group = m_groups[groupNameId];
const auto& propertyLayout = m_materialTypeSourceData.m_propertyLayout;
const auto& propertyListItr = propertyLayout.m_properties.find(groupNameId);
if (propertyListItr != propertyLayout.m_properties.end())
{
group.m_properties.reserve(propertyListItr->second.size());
for (const auto& propertyDefinition : propertyListItr->second)
{
AtomToolsFramework::DynamicPropertyConfig propertyConfig;
AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDefinition);
propertyConfig.m_id = AZ::RPI::MaterialPropertyId(groupNameId, propertyDefinition.m_nameId).GetFullName();
const auto& propertyIndex = m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id);
propertyConfig.m_defaultValue = AtomToolsFramework::ConvertToEditableType(m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]);
propertyConfig.m_parentValue = AtomToolsFramework::ConvertToEditableType(m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]);
propertyConfig.m_originalValue = AtomToolsFramework::ConvertToEditableType(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(&group, &group, group.TYPEINFO_Uuid(), this, this,
[this](const AzToolsFramework::InstanceDataNode* source, const AzToolsFramework::InstanceDataNode* target) {
AZ_UNUSED(source);
const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(target);
return property && AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_parentValue);
});
AddGroup(groupNameId, groupDisplayName, groupDescription, propertyGroupWidget);
}
AddGroupsEnd();
m_dirtyPropertyFlags.set();
RunEditorMaterialFunctors();
}
void MaterialPropertyInspector::RunPropertyChangedCallback()
{
if (m_propertyChangedCallback)
{
m_propertyChangedCallback(m_materialPropertyOverrideMap);
}
}
void MaterialPropertyInspector::RunEditorMaterialFunctors()
{
AZStd::unordered_set<AZ::Name> changedPropertyNames;
// Convert editor property configuration data into material property meta data so that it can be used to execute functors
AZStd::unordered_map<AZ::Name, AZ::RPI::MaterialPropertyDynamicMetadata> propertyDynamicMetadata;
for (auto& group : m_groups)
{
for (auto& property : group.second.m_properties)
{
AtomToolsFramework::ConvertToPropertyMetaData(propertyDynamicMetadata[property.GetId()], property.GetConfig());
}
}
for (AZ::RPI::Ptr<AZ::RPI::MaterialFunctor>& functor : m_editorFunctors)
{
const AZ::RPI::MaterialPropertyFlags& materialPropertyDependencies = functor->GetMaterialPropertyDependencies();
// None also covers case that the client code doesn't register material properties to dependencies,
// which will later get caught in Process() when trying to access a property.
if (materialPropertyDependencies.none() || functor->NeedsProcess(m_dirtyPropertyFlags))
{
AZ::RPI::MaterialFunctor::EditorContext context = AZ::RPI::MaterialFunctor::EditorContext(
m_materialInstance->GetPropertyValues(),
m_materialInstance->GetMaterialPropertiesLayout(),
propertyDynamicMetadata,
changedPropertyNames,
&materialPropertyDependencies
);
functor->Process(context);
}
}
m_dirtyPropertyFlags.reset();
// Apply any changes to material property meta data back to the editor property configurations
for (auto& group : m_groups)
{
for (auto& property : group.second.m_properties)
{
AtomToolsFramework::DynamicPropertyConfig propertyConfig = property.GetConfig();
const auto oldVisible = propertyConfig.m_visible;
const auto oldReadOnly = propertyConfig.m_readOnly;
AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDynamicMetadata[property.GetId()]);
property.SetConfig(propertyConfig);
if (oldReadOnly != propertyConfig.m_readOnly)
{
RefreshAll();
}
if (oldVisible != propertyConfig.m_visible)
{
RebuildAll();
}
}
}
}
void MaterialPropertyInspector::UpdateMaterialInstanceProperty(const AtomToolsFramework::DynamicProperty& property)
{
if (m_materialInstance)
{
const auto propertyIndex = m_materialInstance->FindPropertyIndex(property.GetId());
if (!propertyIndex.IsNull())
{
m_dirtyPropertyFlags.set(propertyIndex.GetIndex());
const auto runtimeValue = AtomToolsFramework::ConvertToRuntimeType(property.GetValue());
if (runtimeValue.IsValid())
{
m_materialInstance->SetPropertyValue(propertyIndex, runtimeValue);
}
}
}
}
void MaterialPropertyInspector::SetOverrides(const MaterialPropertyOverrideMap& propertyOverrideMap)
{
m_materialPropertyOverrideMap = propertyOverrideMap;
for (auto& group : m_groups)
{
for (auto& property : group.second.m_properties)
{
const AtomToolsFramework::DynamicPropertyConfig& propertyConfig = property.GetConfig();
const auto overrideItr = m_materialPropertyOverrideMap.find(propertyConfig.m_id);
const auto& editValue = overrideItr != m_materialPropertyOverrideMap.end() ? overrideItr->second : propertyConfig.m_originalValue;
// This first converts to an acceptable runtime type in case the value came from script
const auto propertyIndex = m_materialInstance->FindPropertyIndex(property.GetId());
if (!propertyIndex.IsNull())
{
const auto runtimeValue = AtomToolsFramework::ConvertToRuntimeType(editValue);
if (runtimeValue.IsValid())
{
property.SetValue(AtomToolsFramework::ConvertToEditableType(runtimeValue));
}
}
else
{
property.SetValue(editValue);
}
UpdateMaterialInstanceProperty(property);
}
}
m_dirtyPropertyFlags.set();
RunPropertyChangedCallback();
RunEditorMaterialFunctors();
RebuildAll();
}
void MaterialPropertyInspector::BeforePropertyModified(AzToolsFramework::InstanceDataNode* pNode)
{
// For some reason the reflected property editor notifications are not symmetrical
// This function is called continuously anytime a property changes until the edit has completed
// Because of that, we have to track whether or not we are continuing to edit the same property to know when editing has started and ended
const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(pNode);
if (property)
{
if (m_activeProperty != property)
{
m_activeProperty = property;
}
}
}
void MaterialPropertyInspector::AfterPropertyModified(AzToolsFramework::InstanceDataNode* pNode)
{
const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(pNode);
if (property)
{
if (m_activeProperty == property)
{
m_materialPropertyOverrideMap[m_activeProperty->GetId()] = m_activeProperty->GetValue();
UpdateMaterialInstanceProperty(*m_activeProperty);
RunPropertyChangedCallback();
}
}
}
void MaterialPropertyInspector::SetPropertyEditingComplete(AzToolsFramework::InstanceDataNode* pNode)
{
// As above, there are symmetrical functions on the notification interface for when editing begins and ends and has been completed but they are not being called following that pattern.
// when this function executes the changes to the property are ready to be committed or reverted
const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(pNode);
if (property)
{
if (m_activeProperty == property)
{
m_materialPropertyOverrideMap[m_activeProperty->GetId()] = m_activeProperty->GetValue();
UpdateMaterialInstanceProperty(*m_activeProperty);
RunPropertyChangedCallback();
RunEditorMaterialFunctors();
m_activeProperty = nullptr;
}
}
}
bool OpenInspectorDialog(const AZ::Data::AssetId& assetId, MaterialPropertyOverrideMap propertyOverrideMap, PropertyChangedCallback propertyChangedCallback)
{
QWidget* activeWindow = nullptr;
AzToolsFramework::EditorWindowRequestBus::BroadcastResult(activeWindow, &AzToolsFramework::EditorWindowRequests::GetAppMainWindow);
// Constructing a dialog with a table to display all configurable material export items
QDialog dialog(activeWindow);
dialog.setWindowTitle("Material Inspector");
MaterialPropertyInspector* inspector = new MaterialPropertyInspector(assetId, propertyChangedCallback, &dialog);
if (!inspector->LoadMaterial())
{
return false;
}
inspector->Populate();
inspector->SetOverrides(propertyOverrideMap);
// Create the bottom row of the dialog with action buttons for exporting or canceling the operation
QWidget* buttonRow = new QWidget(&dialog);
buttonRow->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
QPushButton* revertButton = new QPushButton("Revert", buttonRow);
QObject::connect(revertButton, &QPushButton::clicked, revertButton, [inspector, propertyOverrideMap] {
inspector->SetOverrides(propertyOverrideMap);
});
QPushButton* clearButton = new QPushButton("Clear", buttonRow);
QObject::connect(clearButton, &QPushButton::clicked, clearButton, [inspector] {
inspector->SetOverrides(MaterialPropertyOverrideMap());
});
QPushButton* confirmButton = new QPushButton("Confirm", buttonRow);
QObject::connect(confirmButton, &QPushButton::clicked, confirmButton, [&dialog] {
dialog.accept();
});
QPushButton* cancelButton = new QPushButton("Cancel", buttonRow);
QObject::connect(cancelButton, &QPushButton::clicked, cancelButton, [inspector, propertyOverrideMap, &dialog] {
inspector->SetOverrides(propertyOverrideMap);
dialog.reject();
});
QHBoxLayout* buttonLayout = new QHBoxLayout(buttonRow);
buttonLayout->addStretch();
buttonLayout->addWidget(revertButton);
buttonLayout->addWidget(clearButton);
buttonLayout->addWidget(confirmButton);
buttonLayout->addWidget(cancelButton);
QVBoxLayout* dialogLayout = new QVBoxLayout(&dialog);
dialogLayout->addWidget(inspector);
dialogLayout->addWidget(buttonRow);
dialog.setLayout(dialogLayout);
// Forcing the initial dialog size to accomodate typical content.
// Temporarily settng fixed size because dialog.show/exec invokes WindowDecorationWrapper::showEvent.
// This forces the dialog to be centered and sized based on the layout of content.
// Resizing the dialog after show will not be centered and moving the dialog programatically doesn't m0ve the custmk frame.
dialog.setFixedSize(300, 600);
dialog.show();
// Removing fixed size to allow drag resizing
dialog.setMinimumSize(0, 0);
dialog.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
// Return true if the user press the export button
return dialog.exec() == QDialog::Accepted;
}
} // namespace EditorMaterialComponentInspector
} // namespace Render
} // namespace AZ
//#include <AtomLyIntegration/CommonFeatures/moc_EditorMaterialComponentInspector.cpp>