/* * 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 MaterialEditor { MaterialDocument::MaterialDocument() : AtomToolsFramework::AtomToolsDocument() { MaterialDocumentRequestBus::Handler::BusConnect(m_id); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentCreated, m_id); } MaterialDocument::~MaterialDocument() { AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentDestroyed, m_id); MaterialDocumentRequestBus::Handler::BusDisconnect(); Clear(); } AZ::Data::Asset MaterialDocument::GetAsset() const { return m_materialAsset; } AZ::Data::Instance MaterialDocument::GetInstance() const { return m_materialInstance; } const AZ::RPI::MaterialSourceData* MaterialDocument::GetMaterialSourceData() const { return &m_materialSourceData; } const AZ::RPI::MaterialTypeSourceData* MaterialDocument::GetMaterialTypeSourceData() const { return &m_materialTypeSourceData; } const AZStd::any& MaterialDocument::GetPropertyValue(const AZ::Name& propertyId) const { using namespace AZ; using namespace RPI; if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open."); return m_invalidValue; } const auto it = m_properties.find(propertyId); if (it == m_properties.end()) { AZ_Error("MaterialDocument", false, "Material document property could not be found: '%s'.", propertyId.GetCStr()); return m_invalidValue; } const AtomToolsFramework::DynamicProperty& property = it->second; return property.GetValue(); } const AtomToolsFramework::DynamicProperty& MaterialDocument::GetProperty(const AZ::Name& propertyId) const { if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open."); return m_invalidProperty; } const auto it = m_properties.find(propertyId); if (it == m_properties.end()) { AZ_Error("MaterialDocument", false, "Material document property could not be found: '%s'.", propertyId.GetCStr()); return m_invalidProperty; } const AtomToolsFramework::DynamicProperty& property = it->second; return property; } bool MaterialDocument::IsPropertyGroupVisible(const AZ::Name& propertyGroupFullName) const { if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open."); return false; } const auto it = m_propertyGroupVisibility.find(propertyGroupFullName); if (it == m_propertyGroupVisibility.end()) { AZ_Error("MaterialDocument", false, "Material document property group could not be found: '%s'.", propertyGroupFullName.GetCStr()); return false; } return it->second; } void MaterialDocument::SetPropertyValue(const AZ::Name& propertyId, const AZStd::any& value) { using namespace AZ; using namespace RPI; if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open."); return; } const auto it = m_properties.find(propertyId); if (it == m_properties.end()) { AZ_Error("MaterialDocument", false, "Material document property could not be found: '%s'.", propertyId.GetCStr()); return; } // This first converts to an acceptable runtime type in case the value came from script const AZ::RPI::MaterialPropertyValue propertyValue = AtomToolsFramework::ConvertToRuntimeType(value); AtomToolsFramework::DynamicProperty& property = it->second; property.SetValue(AtomToolsFramework::ConvertToEditableType(propertyValue)); const auto propertyIndex = m_materialInstance->FindPropertyIndex(propertyId); if (!propertyIndex.IsNull()) { if (m_materialInstance->SetPropertyValue(propertyIndex, propertyValue)) { MaterialPropertyFlags dirtyFlags = m_materialInstance->GetPropertyDirtyFlags(); Recompile(); EditorMaterialFunctorResult result = RunEditorMaterialFunctors(dirtyFlags); for (const Name& changedPropertyGroupName : result.m_updatedPropertyGroups) { AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentPropertyGroupVisibilityChanged, m_id, changedPropertyGroupName, IsPropertyGroupVisible(changedPropertyGroupName)); } for (const Name& changedPropertyName : result.m_updatedProperties) { AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentPropertyConfigModified, m_id, GetProperty(changedPropertyName)); } } } AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentPropertyValueModified, m_id, property); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentModified, m_id); } bool MaterialDocument::Open(AZStd::string_view loadPath) { if (!OpenInternal(loadPath)) { Clear(); AZ_Error("MaterialDocument", false, "Material document could not be opened: '%s'.", loadPath.data()); return false; } AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentOpened, m_id); return true; } bool MaterialDocument::Reopen() { // Store history and property changes that should be reapplied after reload auto undoHistoryToRestore = m_undoHistory; auto undoHistoryIndexToRestore = m_undoHistoryIndex; PropertyValueMap propertyValuesToRestore; for (const auto& propertyPair : m_properties) { const AtomToolsFramework::DynamicProperty& property = propertyPair.second; if (!AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_parentValue)) { propertyValuesToRestore[property.GetId()] = property.GetValue(); } } // Reopen the same document const AZStd::string loadPath = m_absolutePath; if (!OpenInternal(loadPath)) { Clear(); return false; } RestorePropertyValues(propertyValuesToRestore); AZStd::swap(undoHistoryToRestore, m_undoHistory); AZStd::swap(undoHistoryIndexToRestore, m_undoHistoryIndex); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentOpened, m_id); return true; } bool MaterialDocument::Save() { using namespace AZ; using namespace RPI; if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open to be saved: '%s'.", m_absolutePath.c_str()); return false; } if (!IsSavable()) { AZ_Error("MaterialDocument", false, "Material types can only be saved as a child: '%s'.", m_absolutePath.c_str()); return false; } // create source data from properties MaterialSourceData sourceData; sourceData.m_materialType = m_materialSourceData.m_materialType; sourceData.m_parentMaterial = m_materialSourceData.m_parentMaterial; AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); AzFramework::StringFunc::Replace(sourceData.m_parentMaterial, "\\", "/"); // populate sourceData with modified or overwritten properties const bool savedProperties = SavePropertiesToSourceData(sourceData, [](const AtomToolsFramework::DynamicProperty& property) { return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_parentValue); }); if (!savedProperties) { return false; } // write sourceData to .material file if (!AZ::RPI::JsonUtils::SaveObjectToFile(m_absolutePath, sourceData)) { AZ_Error("MaterialDocument", false, "Material document could not be saved: '%s'.", m_absolutePath.c_str()); return false; } // after saving, reset to a clean state for (auto& propertyPair : m_properties) { AtomToolsFramework::DynamicProperty& property = propertyPair.second; auto propertyConfig = property.GetConfig(); propertyConfig.m_originalValue = property.GetValue(); property.SetConfig(propertyConfig); } // Auto add or checkout saved file AzToolsFramework::SourceControlCommandBus::Broadcast(&AzToolsFramework::SourceControlCommandBus::Events::RequestEdit, m_absolutePath.c_str(), true, [](bool, const AzToolsFramework::SourceControlFileInfo&) {}); AZ_TracePrintf("MaterialDocument", "Material document saved: '%s'.\n", m_absolutePath.data()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentSaved, m_id); m_saveTriggeredInternally = true; return true; } bool MaterialDocument::SaveAsCopy(AZStd::string_view savePath) { using namespace AZ; using namespace RPI; if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open to be saved: '%s'.", m_absolutePath.c_str()); return false; } if (!IsSavable()) { AZ_Error("MaterialDocument", false, "Material types can only be saved as a child: '%s'.", m_absolutePath.c_str()); return false; } AZStd::string normalizedSavePath = savePath; if (!AzFramework::StringFunc::Path::Normalize(normalizedSavePath)) { AZ_Error("MaterialDocument", false, "Material document save path could not be normalized: '%s'.", normalizedSavePath.c_str()); return false; } // create source data from properties MaterialSourceData sourceData; sourceData.m_materialType = m_materialSourceData.m_materialType; sourceData.m_parentMaterial = m_materialSourceData.m_parentMaterial; AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); AzFramework::StringFunc::Replace(sourceData.m_parentMaterial, "\\", "/"); // populate sourceData with modified or overwritten properties const bool savedProperties = SavePropertiesToSourceData(sourceData, [](const AtomToolsFramework::DynamicProperty& property) { return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_parentValue); }); if (!savedProperties) { return false; } // write sourceData to .material file if (!AZ::RPI::JsonUtils::SaveObjectToFile(normalizedSavePath, sourceData)) { AZ_Error("MaterialDocument", false, "Material document could not be saved: '%s'.", normalizedSavePath.c_str()); return false; } // Auto add or checkout saved file AzToolsFramework::SourceControlCommandBus::Broadcast(&AzToolsFramework::SourceControlCommandBus::Events::RequestEdit, normalizedSavePath.c_str(), true, [](bool, const AzToolsFramework::SourceControlFileInfo&) {}); AZ_TracePrintf("MaterialDocument", "Material document saved: '%s'.\n", normalizedSavePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentSaved, m_id); // If the document is saved to a new file we need to reopen the new document to update assets, paths, property deltas. if (!Open(normalizedSavePath)) { return false; } // Setting flag after reopening becausse it's cleared on open m_saveTriggeredInternally = true; return true; } bool MaterialDocument::SaveAsChild(AZStd::string_view savePath) { using namespace AZ; using namespace RPI; if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open to be saved: '%s'.", m_absolutePath.c_str()); return false; } AZStd::string normalizedSavePath = savePath; if (!AzFramework::StringFunc::Path::Normalize(normalizedSavePath)) { AZ_Error("MaterialDocument", false, "Material document save path could not be normalized: '%s'.", normalizedSavePath.c_str()); return false; } if (m_absolutePath == normalizedSavePath) { // ToDo: this should scan the entire hierarchy so we don't overwrite parent's parent, for example AZ_Error("MaterialDocument", false, "Can't overwrite parent material with a child that depends on it."); return false; } // create source data from properties MaterialSourceData sourceData; sourceData.m_materialType = m_materialSourceData.m_materialType; AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Only assign a parent path if the source was a .material if (AzFramework::StringFunc::Path::IsExtension(m_relativePath.c_str(), MaterialSourceData::Extension)) { sourceData.m_parentMaterial = m_relativePath; } // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); AzFramework::StringFunc::Replace(sourceData.m_parentMaterial, "\\", "/"); // populate sourceData with modified properties const bool savedProperties = SavePropertiesToSourceData(sourceData, [](const AtomToolsFramework::DynamicProperty& property) { return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_originalValue); }); if (!savedProperties) { return false; } // write sourceData to .material file if (!AZ::RPI::JsonUtils::SaveObjectToFile(normalizedSavePath, sourceData)) { AZ_Error("MaterialDocument", false, "Material document could not be saved: '%s'.", normalizedSavePath.c_str()); return false; } // Auto add or checkout saved file AzToolsFramework::SourceControlCommandBus::Broadcast(&AzToolsFramework::SourceControlCommandBus::Events::RequestEdit, normalizedSavePath.c_str(), true, [](bool, const AzToolsFramework::SourceControlFileInfo&) {}); AZ_TracePrintf("MaterialDocument", "Material document saved: '%s'.\n", normalizedSavePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentSaved, m_id); // If the document is saved to a new file we need to reopen the new document to update assets, paths, property deltas. if (!Open(normalizedSavePath)) { return false; } // Setting flag after reopening becausse it's cleared on open m_saveTriggeredInternally = true; return true; } bool MaterialDocument::Close() { using namespace AZ; using namespace RPI; if (!IsOpen()) { AZ_Error("MaterialDocument", false, "Material document is not open."); return false; } AZ_TracePrintf("MaterialDocument", "Material document closed: '%s'.\n", m_absolutePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentClosed, m_id); // Clearing after notification so paths are still available Clear(); return true; } bool MaterialDocument::IsOpen() const { return !m_absolutePath.empty() && !m_relativePath.empty() && m_materialAsset.IsReady() && m_materialInstance; } bool MaterialDocument::IsModified() const { return AZStd::any_of(m_properties.begin(), m_properties.end(), [](const auto& propertyPair) { const AtomToolsFramework::DynamicProperty& property = propertyPair.second; return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_originalValue); }); } bool MaterialDocument::IsSavable() const { return AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), AZ::RPI::MaterialSourceData::Extension); } bool MaterialDocument::CanUndo() const { // Undo will only be allowed if something has been recorded and we're not at the beginning of history return IsOpen() && !m_undoHistory.empty() && m_undoHistoryIndex > 0; } bool MaterialDocument::CanRedo() const { // Redo will only be allowed if something has been recorded and we're not at the end of history return IsOpen() && !m_undoHistory.empty() && m_undoHistoryIndex < m_undoHistory.size(); } bool MaterialDocument::Undo() { if (CanUndo()) { // The history index is one beyond the last executed command. Decrement the index then execute undo. m_undoHistory[--m_undoHistoryIndex].first(); AZ_TracePrintf("MaterialDocument", "Material document undo: '%s'.\n", m_absolutePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentUndoStateChanged, m_id); return true; } return false; } bool MaterialDocument::Redo() { if (CanRedo()) { // Execute the current redo command then move the history index to the next position. m_undoHistory[m_undoHistoryIndex++].second(); AZ_TracePrintf("MaterialDocument", "Material document redo: '%s'.\n", m_absolutePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentUndoStateChanged, m_id); return true; } return false; } bool MaterialDocument::BeginEdit() { // Save the current properties as a momento for undo before any changes are applied m_propertyValuesBeforeEdit.clear(); for (const auto& propertyPair : m_properties) { const AtomToolsFramework::DynamicProperty& property = propertyPair.second; m_propertyValuesBeforeEdit[property.GetId()] = property.GetValue(); } return true; } bool MaterialDocument::EndEdit() { PropertyValueMap propertyValuesForUndo; PropertyValueMap propertyValuesForRedo; // After editing has completed, check to see if properties have changed so the deltas can be recorded in the history for (const auto& propertyBeforeEditPair : m_propertyValuesBeforeEdit) { const auto& propertyName = propertyBeforeEditPair.first; const auto& propertyValueForUndo = propertyBeforeEditPair.second; const auto& propertyValueForRedo = GetPropertyValue(propertyName); if (!AtomToolsFramework::ArePropertyValuesEqual(propertyValueForUndo, propertyValueForRedo)) { propertyValuesForUndo[propertyName] = propertyValueForUndo; propertyValuesForRedo[propertyName] = propertyValueForRedo; } } if (!propertyValuesForUndo.empty() && !propertyValuesForRedo.empty()) { // Wipe any state beyond the current history index m_undoHistory.erase(m_undoHistory.begin() + m_undoHistoryIndex, m_undoHistory.end()); // Add undo and redo operations using lambdas that will capture property state and restore it when executed m_undoHistory.emplace_back( [this, propertyValuesForUndo]() { RestorePropertyValues(propertyValuesForUndo); }, [this, propertyValuesForRedo]() { RestorePropertyValues(propertyValuesForRedo); }); // Assign the index to the end of history m_undoHistoryIndex = aznumeric_cast(m_undoHistory.size()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentUndoStateChanged, m_id); } m_propertyValuesBeforeEdit.clear(); return true; } void MaterialDocument::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) { if (m_compilePending) { if (m_materialInstance->Compile()) { m_compilePending = false; AZ::TickBus::Handler::BusDisconnect(); } } } void MaterialDocument::SourceFileChanged(AZStd::string relativePath, AZStd::string scanFolder, [[maybe_unused]] AZ::Uuid sourceUUID) { auto sourcePath = AZ::RPI::AssetUtils::ResolvePathReference(scanFolder, relativePath); if (m_absolutePath == sourcePath) { // ignore notifications caused by saving the open document if (!m_saveTriggeredInternally) { AZ_TracePrintf("MaterialDocument", "Material document changed externally: '%s'.\n", m_absolutePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast( &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentExternallyModified, m_id); } m_saveTriggeredInternally = false; } else if (m_sourceDependencies.find(sourcePath) != m_sourceDependencies.end()) { AZ_TracePrintf("MaterialDocument", "Material document dependency changed: '%s'.\n", m_absolutePath.c_str()); AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast( &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentDependencyModified, m_id); } } bool MaterialDocument::SavePropertiesToSourceData(AZ::RPI::MaterialSourceData& sourceData, PropertyFilterFunction propertyFilter) const { using namespace AZ; using namespace RPI; bool result = true; // populate sourceData with properties that meet the filter m_materialTypeSourceData.EnumerateProperties([this, &sourceData, &propertyFilter, &result](const AZStd::string& groupName, const AZStd::string& propertyName, const auto& propertyDefinition) { const MaterialPropertyId propertyId(groupName, propertyName); const auto it = m_properties.find(propertyId.GetFullName()); if (it != m_properties.end() && propertyFilter(it->second)) { MaterialPropertyValue propertyValue = AtomToolsFramework::ConvertToRuntimeType(it->second.GetValue()); if (propertyValue.IsValid()) { if (!m_materialTypeSourceData.ConvertPropertyValueToSourceDataFormat(propertyDefinition, propertyValue)) { AZ_Error("MaterialDocument", false, "Material document property could not be converted: '%s' in '%s'.", propertyId.GetFullName().GetCStr(), m_absolutePath.c_str()); result = false; return false; } sourceData.m_properties[groupName][propertyName].m_value = propertyValue; } } return true; }); return result; } bool MaterialDocument::OpenInternal(AZStd::string_view loadPath) { using namespace AZ; using namespace RPI; Clear(); m_absolutePath = loadPath; if (!AzFramework::StringFunc::Path::Normalize(m_absolutePath)) { AZ_Error("MaterialDocument", false, "Material document path could not be normalized: '%s'.", m_absolutePath.c_str()); return false; } if (AzFramework::StringFunc::Path::IsRelative(m_absolutePath.c_str())) { AZ_Error("MaterialDocument", false, "Material document path must be absolute: '%s'.", m_absolutePath.c_str()); return false; } bool result = false; Data::AssetInfo sourceAssetInfo; AZStd::string watchFolder; AzToolsFramework::AssetSystemRequestBus::BroadcastResult(result, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourcePath, m_absolutePath.c_str(), sourceAssetInfo, watchFolder); if (!result) { AZ_Error("MaterialDocument", false, "Could not find source material: '%s'.", m_absolutePath.c_str()); return false; } m_relativePath = sourceAssetInfo.m_relativePath; if (!AzFramework::StringFunc::Path::Normalize(m_relativePath)) { AZ_Error("MaterialDocument", false, "Material document path could not be normalized: '%s'.", m_relativePath.c_str()); return false; } AZStd::string materialTypeSourceFilePath; // The material document and inspector are constructed from source data if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), MaterialSourceData::Extension)) { // Load the material source data so that we can check properties and create a material asset from it if (!AZ::RPI::JsonUtils::LoadObjectFromFile(m_absolutePath, m_materialSourceData)) { AZ_Error("MaterialDocument", false, "Material source data could not be loaded: '%s'.", m_absolutePath.c_str()); return false; } // We must also always load the material type data for a complete, ordered set of the // groups and properties that will be needed for comparison and building the inspector materialTypeSourceFilePath = AssetUtils::ResolvePathReference(m_absolutePath, m_materialSourceData.m_materialType); auto materialTypeOutcome = MaterialUtils::LoadMaterialTypeSourceData(materialTypeSourceFilePath); if (!materialTypeOutcome.IsSuccess()) { AZ_Error("MaterialDocument", false, "Material type source data could not be loaded: '%s'.", materialTypeSourceFilePath.c_str()); return false; } m_materialTypeSourceData = materialTypeOutcome.GetValue(); if (MaterialSourceData::ApplyVersionUpdatesResult::Failed == m_materialSourceData.ApplyVersionUpdates(m_absolutePath)) { AZ_Error("MaterialDocument", false, "Material source data could not be auto updated to the latest version of the material type: '%s'.", m_materialSourceData.m_materialType.c_str()); return false; } } else if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), MaterialTypeSourceData::Extension)) { materialTypeSourceFilePath = m_absolutePath; // Load the material type source data, which will be used for enumerating properties and building material source data auto materialTypeOutcome = MaterialUtils::LoadMaterialTypeSourceData(materialTypeSourceFilePath); if (!materialTypeOutcome.IsSuccess()) { AZ_Error("MaterialDocument", false, "Material type source data could not be loaded: '%s'.", m_absolutePath.c_str()); return false; } m_materialTypeSourceData = materialTypeOutcome.GetValue(); // The document represents a material, not a material type. // If the input data is a material type file we have to generate the material source data by referencing it. m_materialSourceData.m_materialType = m_relativePath; m_materialSourceData.m_parentMaterial.clear(); } else { AZ_Error("MaterialDocument", false, "Material document extension not supported: '%s'.", m_absolutePath.c_str()); return false; } // In order to support automation, general usability, and 'save as' functionality, the user must not have to wait // for their JSON file to be cooked by the asset processor before opening or editing it. // We need to reduce or remove dependency on the asset processor. In order to get around the bottleneck for now, // we can create the asset dynamically from the source data. // Long term, the material document should not be concerned with assets at all. The viewport window should be the // only thing concerned with assets or instances. auto materialAssetResult = m_materialSourceData.CreateMaterialAssetFromSourceData(Uuid::CreateRandom(), m_absolutePath, true, true, &m_sourceDependencies); if (!materialAssetResult) { AZ_Error("MaterialDocument", false, "Material asset could not be created from source data: '%s'.", m_absolutePath.c_str()); return false; } m_materialAsset = materialAssetResult.GetValue(); if (!m_materialAsset.IsReady()) { AZ_Error("MaterialDocument", false, "Material asset is not ready: '%s'.", m_absolutePath.c_str()); return false; } const auto& materialTypeAsset = m_materialAsset->GetMaterialTypeAsset(); if (!materialTypeAsset.IsReady()) { AZ_Error("MaterialDocument", false, "Material type asset is not ready: '%s'.", m_absolutePath.c_str()); return false; } AZStd::array_view parentPropertyValues = materialTypeAsset->GetDefaultPropertyValues(); AZ::Data::Asset parentMaterialAsset; if (!m_materialSourceData.m_parentMaterial.empty()) { AZ::RPI::MaterialSourceData parentMaterialSourceData; const auto parentMaterialFilePath = AssetUtils::ResolvePathReference(m_absolutePath, m_materialSourceData.m_parentMaterial); if (!AZ::RPI::JsonUtils::LoadObjectFromFile(parentMaterialFilePath, parentMaterialSourceData)) { AZ_Error("MaterialDocument", false, "Material parent source data could not be loaded for: '%s'.", parentMaterialFilePath.c_str()); return false; } const auto parentMaterialAssetIdResult = AssetUtils::MakeAssetId(parentMaterialFilePath, 0); if (!parentMaterialAssetIdResult) { AZ_Error("MaterialDocument", false, "Material parent asset ID could not be created: '%s'.", parentMaterialFilePath.c_str()); return false; } auto parentMaterialAssetResult = m_materialSourceData.CreateMaterialAssetFromSourceData( parentMaterialAssetIdResult.GetValue(), parentMaterialFilePath, true, true, &m_sourceDependencies); if (!parentMaterialAssetResult) { AZ_Error("MaterialDocument", false, "Material parent asset could not be created from source data: '%s'.", parentMaterialFilePath.c_str()); return false; } parentMaterialAsset = parentMaterialAssetResult.GetValue(); parentPropertyValues = parentMaterialAsset->GetPropertyValues(); } // Creating a material from a material asset will fail if a texture is referenced but not loaded m_materialInstance = Material::Create(m_materialAsset); if (!m_materialInstance) { AZ_Error("MaterialDocument", false, "Material instance could not be created: '%s'.", m_absolutePath.c_str()); return false; } // Pipeline State Object changes are always allowed in the material editor because it only runs on developer systems // where such changes are supported at runtime. m_materialInstance->SetPsoHandlingOverride(AZ::RPI::MaterialPropertyPsoHandling::Allowed); // 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) { AtomToolsFramework::DynamicPropertyConfig propertyConfig; // Assign id before conversion so it can be used in dynamic description propertyConfig.m_id = MaterialPropertyId(groupName, propertyName).GetCStr(); const auto& propertyIndex = m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id); const bool propertyIndexInBounds = propertyIndex.IsValid() && propertyIndex.GetIndex() < m_materialAsset->GetPropertyValues().size(); AZ_Warning("MaterialDocument", propertyIndexInBounds, "Failed to add material property '%s' to document '%s'.", propertyConfig.m_id.GetCStr(), m_absolutePath.c_str()); if (propertyIndexInBounds) { 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; m_properties[propertyConfig.m_id] = AtomToolsFramework::DynamicProperty(propertyConfig); } return true; }); // Populate the property group visibility map for (MaterialTypeSourceData::GroupDefinition& group : m_materialTypeSourceData.GetGroupDefinitionsInDisplayOrder()) { m_propertyGroupVisibility[AZ::Name{group.m_name}] = true; } // Adding properties for material type and parent as part of making dynamic // properties and the inspector more general purpose. // This allows the read only properties to appear in the inspector like any // other property. // This may change or be removed once support for changing the material parent // is implemented. AtomToolsFramework::DynamicPropertyConfig propertyConfig; propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::Asset; propertyConfig.m_id = "overview.materialType"; propertyConfig.m_name = "materialType"; propertyConfig.m_displayName = "Material Type"; propertyConfig.m_groupName = "Overview"; propertyConfig.m_description = "The material type defines the layout, properties, default values, shader connections, and other " "data needed to create and edit a derived material."; propertyConfig.m_defaultValue = AZStd::any(materialTypeAsset); propertyConfig.m_originalValue = propertyConfig.m_defaultValue; propertyConfig.m_parentValue = propertyConfig.m_defaultValue; propertyConfig.m_readOnly = true; m_properties[propertyConfig.m_id] = AtomToolsFramework::DynamicProperty(propertyConfig); propertyConfig = {}; propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::Asset; propertyConfig.m_id = "overview.parentMaterial"; propertyConfig.m_name = "parentMaterial"; propertyConfig.m_displayName = "Parent Material"; propertyConfig.m_groupName = "Overview"; propertyConfig.m_description = "The parent material provides an initial configuration whose properties are inherited and overriden by a derived material."; propertyConfig.m_defaultValue = AZStd::any(parentMaterialAsset); propertyConfig.m_originalValue = propertyConfig.m_defaultValue; propertyConfig.m_parentValue = propertyConfig.m_defaultValue; propertyConfig.m_readOnly = true; propertyConfig.m_showThumbnail = true; m_properties[propertyConfig.m_id] = AtomToolsFramework::DynamicProperty(propertyConfig); //Add UV name customization properties const RPI::MaterialUvNameMap& uvNameMap = materialTypeAsset->GetUvNameMap(); for (const RPI::UvNamePair& uvNamePair : uvNameMap) { const AZStd::string shaderInput = uvNamePair.m_shaderInput.ToString(); const AZStd::string uvName = uvNamePair.m_uvName.GetStringView(); propertyConfig = {}; propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::String; propertyConfig.m_id = MaterialPropertyId(UvGroupName, shaderInput).GetCStr(); propertyConfig.m_name = shaderInput; propertyConfig.m_displayName = shaderInput; propertyConfig.m_groupName = "UV Sets"; propertyConfig.m_description = shaderInput; propertyConfig.m_defaultValue = uvName; propertyConfig.m_originalValue = uvName; propertyConfig.m_parentValue = uvName; propertyConfig.m_readOnly = true; m_properties[propertyConfig.m_id] = AtomToolsFramework::DynamicProperty(propertyConfig); } const MaterialFunctorSourceData::EditorContext editorContext = MaterialFunctorSourceData::EditorContext(materialTypeSourceFilePath, m_materialAsset->GetMaterialPropertiesLayout()); for (Ptr functorData : m_materialTypeSourceData.m_materialFunctorSourceData) { MaterialFunctorSourceData::FunctorResult result2 = functorData->CreateFunctor(editorContext); if (result2.IsSuccess()) { Ptr& functor = result2.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; } } 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); // Connecting to bus to monitor external changes AzToolsFramework::AssetSystemBus::Handler::BusConnect(); AZ_TracePrintf("MaterialDocument", "Material document opened: '%s'.\n", m_absolutePath.c_str()); return true; } void MaterialDocument::Recompile() { if (!m_compilePending) { AZ::TickBus::Handler::BusConnect(); m_compilePending = true; } } void MaterialDocument::Clear() { AZ::TickBus::Handler::BusDisconnect(); AzToolsFramework::AssetSystemBus::Handler::BusDisconnect(); m_materialAsset = {}; m_materialInstance = {}; m_absolutePath.clear(); m_relativePath.clear(); m_sourceDependencies.clear(); m_saveTriggeredInternally = {}; m_compilePending = {}; m_properties.clear(); m_editorFunctors.clear(); m_materialTypeSourceData = AZ::RPI::MaterialTypeSourceData(); m_materialSourceData = AZ::RPI::MaterialSourceData(); m_propertyValuesBeforeEdit.clear(); m_undoHistory.clear(); m_undoHistoryIndex = {}; } void MaterialDocument::RestorePropertyValues(const PropertyValueMap& propertyValues) { for (const auto& propertyValuePair : propertyValues) { const auto& propertyName = propertyValuePair.first; const auto& propertyValue = propertyValuePair.second; SetPropertyValue(propertyName, propertyValue); } } MaterialDocument::EditorMaterialFunctorResult MaterialDocument::RunEditorMaterialFunctors(AZ::RPI::MaterialPropertyFlags dirtyFlags) { EditorMaterialFunctorResult result; AZStd::unordered_map propertyDynamicMetadata; AZStd::unordered_map propertyGroupDynamicMetadata; for (auto& propertyPair : m_properties) { AtomToolsFramework::DynamicProperty& property = propertyPair.second; AtomToolsFramework::ConvertToPropertyMetaData(propertyDynamicMetadata[property.GetId()], property.GetConfig()); } for (auto& groupPair : m_propertyGroupVisibility) { AZ::RPI::MaterialPropertyGroupDynamicMetadata& metadata = propertyGroupDynamicMetadata[AZ::Name{groupPair.first}]; bool visible = groupPair.second; metadata.m_visibility = visible ? AZ::RPI::MaterialPropertyGroupVisibility::Enabled : AZ::RPI::MaterialPropertyGroupVisibility::Hidden; } for (AZ::RPI::Ptr& 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(dirtyFlags)) { AZ::RPI::MaterialFunctor::EditorContext context = AZ::RPI::MaterialFunctor::EditorContext( m_materialInstance->GetPropertyValues(), m_materialInstance->GetMaterialPropertiesLayout(), propertyDynamicMetadata, propertyGroupDynamicMetadata, result.m_updatedProperties, result.m_updatedPropertyGroups, &materialPropertyDependencies ); functor->Process(context); } } for (auto& propertyPair : m_properties) { AtomToolsFramework::DynamicProperty& property = propertyPair.second; AtomToolsFramework::DynamicPropertyConfig propertyConfig = property.GetConfig(); AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDynamicMetadata[property.GetId()]); property.SetConfig(propertyConfig); } for (auto& updatedPropertyGroup : result.m_updatedPropertyGroups) { bool visible = propertyGroupDynamicMetadata[updatedPropertyGroup].m_visibility == AZ::RPI::MaterialPropertyGroupVisibility::Enabled; m_propertyGroupVisibility[updatedPropertyGroup] = visible; } return result; } } // namespace MaterialEditor