// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Xml; using FlaxEditor.Content; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Surface; using FlaxEditor.Viewport.Previews; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; namespace FlaxEditor.Windows.Assets { /// /// Material window allows to view and edit asset. /// Note: it uses actual asset to modify so changes are visible live in the game/editor preview. /// /// /// public sealed class MaterialInstanceWindow : AssetEditorWindowBase { private sealed class EditParamOverrideAction : IUndoAction { public MaterialInstanceWindow Window; public string Name; public bool Before; /// public string ActionString => "Edit Override"; private void Set(bool value) { Window.Asset.GetParameter(Name).IsOverride = value; } /// public void Do() { Set(!Before); } /// public void Undo() { Set(Before); } /// public void Dispose() { Window = null; } } /// /// The material properties proxy object. /// [CustomEditor(typeof(ParametersEditor))] private sealed class PropertiesProxy { private MaterialBase _restoreBase; private Dictionary _restoreParams; [EditorDisplay("General"), Tooltip("The base material used to override it's properties")] public MaterialBase BaseMaterial { get => Window?.Asset != null ? Window?.Asset.BaseMaterial : null; set { var asset = Window?.Asset; if (asset) { if (value == asset) { MessageBox.Show("Cannot use material itself as instance base.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } asset.BaseMaterial = value; Window._editor.BuildLayoutOnUpdate(); } } } /// /// The window reference. Used to handle some special logic. /// [NoSerialize, HideInEditor] public MaterialInstanceWindow Window; /// /// The material parameter values collection. Used to record undo changes. /// /// /// Contains only items with raw values excluding Flax Objects. /// [HideInEditor] public object[] Values { get => Window?.Asset != null ? Window?.Asset.Parameters.Select(x => x.Value).ToArray() : null; set { if (Window?.Asset == null) return; var parameters = Window?.Asset.Parameters; if (value != null && parameters != null) { if (value.Length != parameters.Length) return; for (int i = 0; i < value.Length; i++) { var p = parameters[i].Value; if (p is FlaxEngine.Object || p == null) continue; parameters[i].Value = value[i]; } } } } /// /// The material parameter values collection. Used to record undo changes. /// /// /// Contains only items with references to Flax Objects identified by ID. /// [HideInEditor] public FlaxEngine.Object[] ValuesRef { get => Window?.Asset != null ? Window?.Asset.Parameters.Select(x => x.Value as FlaxEngine.Object).ToArray() : null; set { if (Window?.Asset == null) return; var parameters = Window?.Asset?.Parameters; if (value != null && parameters != null) { if (value.Length != parameters.Length) return; for (int i = 0; i < value.Length; i++) { var p = parameters[i].Value; if (!(p is FlaxEngine.Object || p == null)) continue; parameters[i].Value = value[i]; } } } } /// /// Gathers parameters from the specified material. /// /// The material window. public void OnLoad(MaterialInstanceWindow materialWin) { // Link Window = materialWin; // Prepare restore data PeekState(); } /// /// Records the current state to restore it on DiscardChanges. /// public void PeekState() { if (Window == null) return; var material = Window.Asset; _restoreBase = material.BaseMaterial; var parameters = material.Parameters; _restoreParams = new Dictionary(); for (int i = 0; i < parameters.Length; i++) _restoreParams[parameters[i].Name] = parameters[i].Value; } /// /// On discard changes /// public void DiscardChanges() { if (Window == null) return; var material = Window.Asset; material.BaseMaterial = _restoreBase; var parameters = material.Parameters; for (int i = 0; i < parameters.Length; i++) { var p = parameters[i]; if (p.IsPublic && _restoreParams.TryGetValue(p.Name, out var value)) { p.Value = value; } } } /// /// Clears temporary data. /// public void OnClean() { // Unlink Window = null; } } /// /// Custom editor for editing material parameters collection. /// /// public class ParametersEditor : GenericEditor { private SurfaceUtils.GraphParameterData[] _graphParameters; /// public override void Initialize(LayoutElementsContainer layout) { // Prepare var proxy = (PropertiesProxy)Values[0]; var materialInstance = proxy.Window?.Asset; if (materialInstance == null) { layout.Label("No parameters"); return; } if (!materialInstance.IsLoaded || (materialInstance.BaseMaterial && !materialInstance.BaseMaterial.IsLoaded)) { layout.Label("Loading...", TextAlignment.Center); return; } var parameters = materialInstance.Parameters; base.Initialize(layout); if (parameters.Length == 0) return; var parametersGroup = layout.Group("Parameters"); var settingButton = parametersGroup.AddSettingsButton(); settingButton.Clicked += (image, button) => OnSettingsButtonClicked(image, button, proxy.Window); var baseMaterial = materialInstance.BaseMaterial; var material = baseMaterial; if (material) { while (material is MaterialInstance instance) material = instance.BaseMaterial; } _graphParameters = SurfaceUtils.InitGraphParameters(parameters, (Material)material); SurfaceUtils.DisplayGraphParameters(parametersGroup, _graphParameters, (instance, parameter, tag) => { // Get material parameter var p = (MaterialParameter)tag; var proxyEx = (PropertiesProxy)instance; var array = proxyEx.Window.Asset.Parameters; if (array == null || !array.Contains(p)) throw new TargetException("Material parameters collection has been changed."); return p.Value; }, (instance, value, parameter, tag) => { // Set material parameter and surface parameter var p = (MaterialParameter)tag; var proxyEx = (PropertiesProxy)instance; p.Value = value; proxyEx.Window._paramValueChange = true; }, Values, null, (LayoutElementsContainer itemLayout, ValueContainer valueContainer, ref SurfaceUtils.GraphParameterData e) => { var p = (MaterialParameter)e.Tag; // Try to get default value (from the base material) var pBase = baseMaterial != null ? baseMaterial.GetParameter(p.Name) : null; if (pBase != null && pBase.ParameterType == p.ParameterType) { valueContainer.SetDefaultValue(pBase.Value); } // Add label with checkbox for parameter value override var label = new CheckablePropertyNameLabel(e.DisplayName); label.CheckBox.Checked = p.IsOverride; label.CheckBox.Tag = new KeyValuePair(proxy.Window._properties, p); label.CheckChanged += nameLabel => { var pair = (KeyValuePair)nameLabel.CheckBox.Tag; var proxyEx = pair.Key; var pEx = pair.Value; pEx.IsOverride = nameLabel.CheckBox.Checked; proxyEx.Window._undo.AddAction(new EditParamOverrideAction { Window = proxyEx.Window, Name = pEx.Name, Before = !nameLabel.CheckBox.Checked, }); }; itemLayout.Property(label, valueContainer, null, e.Tooltip?.Text); }); } private void OnSettingsButtonClicked(Image image, MouseButton mouseButton, MaterialInstanceWindow window) { if (mouseButton != MouseButton.Left) return; var cm = new ContextMenu(); if (window != null) cm.AddButton("Revert All Parameters", window.OnRevertAllParameters).TooltipText = "Reverts all the overridden parameters to the default values."; cm.AddButton("Override All Parameters", OnOverrideAll).TooltipText = "Checks all parameters overrides."; cm.AddButton("Remove Parameter Overrides", OnRemoveOverrides).TooltipText = "Unchecks all overrides for parameters."; cm.Show(image, image.Size); } private void OnRemoveOverrides() { OnSetOverrides(false); } private void OnOverrideAll() { OnSetOverrides(true); } private void OnSetOverrides(bool isOverride) { var proxy = (PropertiesProxy)Values[0]; var undoActions = new List(); foreach (var graphParameter in _graphParameters) { var p = (MaterialParameter)graphParameter.Tag; if (!p.IsPublic || p.IsOverride == isOverride) continue; p.IsOverride = isOverride; undoActions.Add(new EditParamOverrideAction { Window = proxy.Window, Name = p.Name, Before = !isOverride, }); } if (undoActions.Count == 0) return; proxy.Window._undo.AddAction(new MultiUndoAction(undoActions)); proxy.Window.MarkAsEdited(); Presenter.BuildLayoutOnUpdate(); } } private readonly SplitPanel _split; private readonly MaterialPreview _preview; private readonly ToolStripButton _saveButton; private readonly ToolStripButton _undoButton; private readonly ToolStripButton _redoButton; private readonly CustomEditorPresenter _editor; private readonly Undo _undo; private readonly PropertiesProxy _properties; private bool _isWaitingForLoad; internal bool _paramValueChange; /// public MaterialInstanceWindow(Editor editor, AssetItem item) : base(editor, item) { var inputOptions = Editor.Options.Options.Input; // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; _undo.RedoDone += OnUndoRedo; _undo.ActionDone += OnAction; // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/graphics/materials/instanced-materials/index.html")).LinkTooltip("See documentation to learn more"); // Split Panel _split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolstrip.Bottom, 0), SplitterValue = 0.5f, Parent = this }; // Material preview _preview = new MaterialPreview(true) { Parent = _split.Panel1 }; // Material properties editor _editor = new CustomEditorPresenter(_undo, "Loading..."); _editor.Panel.Parent = _split.Panel2; _properties = new PropertiesProxy(); _editor.Modified += OnMaterialPropertyEdited; // Setup input actions InputActions.Add(options => options.Undo, _undo.PerformUndo); InputActions.Add(options => options.Redo, _undo.PerformRedo); } private void OnRevertAllParameters() { var baseMaterial = Asset.BaseMaterial; if (!baseMaterial) return; var parameters = Asset.Parameters; var actions = new List(); for (var i = 0; i < parameters.Length; i++) { var p = parameters[i]; if (p.IsOverride) { p.IsOverride = false; actions.Add(new EditParamOverrideAction { Window = this, Name = p.Name, Before = true, }); } } using (new UndoMultiBlock(_undo, _editor.Selection, "Revert all the parameters to the default values", new MultiUndoAction(actions))) { for (var i = 0; i < parameters.Length; i++) { var p = parameters[i]; var pBase = baseMaterial.GetParameter(p.Name); if (pBase != null && pBase.ParameterType == p.ParameterType) p.Value = pBase.Value; } } _editor.BuildLayoutOnUpdate(); } private void OnAction(IUndoAction action) { _paramValueChange = false; MarkAsEdited(); UpdateToolstrip(); } private void OnUndoRedo(IUndoAction action) { _paramValueChange = false; MarkAsEdited(); UpdateToolstrip(); _editor.BuildLayoutOnUpdate(); } private void OnMaterialPropertyEdited() { _paramValueChange = false; //MarkAsEdited(); } /// public override void Save() { if (!IsEdited) return; if (Asset.Save()) { Editor.LogError("Cannot save asset."); return; } _properties.PeekState(); ClearEditedFlag(); _item.RefreshThumbnail(); } /// protected override void UpdateToolstrip() { _saveButton.Enabled = IsEdited; _undoButton.Enabled = _undo.CanUndo; _redoButton.Enabled = _undo.CanRedo; base.UpdateToolstrip(); } /// protected override void UnlinkItem() { _properties.OnClean(); _preview.Material = null; _isWaitingForLoad = false; base.UnlinkItem(); } /// protected override void OnAssetLinked() { _preview.Material = _asset; _isWaitingForLoad = true; base.OnAssetLinked(); } /// protected override void OnClose() { if (Asset) { // Discard unsaved changes _properties.DiscardChanges(); } // Cleanup _undo.Clear(); base.OnClose(); } /// public override void Update(float deltaTime) { if (_isWaitingForLoad && _asset.IsLoaded && (_asset.BaseMaterial == null || _asset.BaseMaterial.IsLoaded)) { _isWaitingForLoad = false; _properties.OnLoad(this); _editor.Select(_properties); ClearEditedFlag(); _undo.Clear(); _editor.BuildLayout(); } base.Update(deltaTime); } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { LayoutSerializeSplitter(writer, "Split", _split); } /// public override void OnLayoutDeserialize(XmlElement node) { LayoutDeserializeSplitter(node, "Split", _split); } /// public override void OnLayoutDeserialize() { _split.SplitterValue = 0.5f; } } }