using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Plugins.Animate_UI_Materials.EditorExtensions; using UnityEditor; using UnityEditorInternal; using UnityEngine; using UnityEngine.UI; using Object = UnityEngine.Object; namespace Plugins.Animate_UI_Materials.Editor { using PropertyType = ShaderUtil.ShaderPropertyType; [CustomEditor(typeof(GraphicMaterialOverride), true)] public class GraphicMaterialOverrideEditor : UnityEditor.Editor { /// /// The scroll position in the modifiers ScrollView /// Usually not needed, but good to have /// Vector2 _scrollPosition; /// /// A fake material used to create an inspector /// Material _editorMaterial; Object[] _editorMaterialArray; /// /// The editor of the fake material /// MaterialEditor _editorMaterialEditor; /// /// Override the reset context menu to implement the reset function /// Needed instead of "MonoBehavior.Reset" on GraphicMaterialOverride because we need to record an Undo /// [MenuItem("CONTEXT/GraphicMaterialOverride/Reset")] static void ResetMaterialModifiers(MenuCommand b) { GraphicMaterialOverride materialOverride = (GraphicMaterialOverride)b.context; if (!materialOverride) return; List modifiers = materialOverride.GetModifiers().ToList(); Object[] modifiersAsObjects = modifiers.Select(m => (Object)m).ToArray(); Undo.RecordObjects(modifiersAsObjects, "Reset material modifiers"); foreach (IMaterialPropertyModifier modifier in modifiers) { modifier.ResetPropertyToDefault(); PrefabUtility.RecordPrefabInstancePropertyModifications((Object)modifier); } } /// /// Ask the Graphic component to reload the modified material /// [MenuItem("CONTEXT/GraphicMaterialOverride/Reload Source Material")] static void ReloadSourceMaterial(MenuCommand b) { if (b.context is not GraphicMaterialOverride materialOverride) return; materialOverride.SetMaterialDirty(); EditorUtility.SetDirty(materialOverride); } [MenuItem("CONTEXT/MonoBehaviour/Bake Material Variant", true)] static bool BakeMaterialVariantValidator(MenuCommand b) { return b.context is IMaterialModifier; } /// /// Ask the Graphic component to reload the modified material /// [MenuItem("CONTEXT/MonoBehaviour/Bake Material Variant")] static void BakeMaterialVariant(MenuCommand b) { if (b.context is not IMaterialModifier) return; if (b.context is not Component materialModifier) return; if (materialModifier.TryGetComponent(out Graphic materialSource) == false) { Debug.LogWarning("Cannot find associated Graphic"); return; } Material original = materialSource.material; Material modified = materialSource.materialForRendering; Material asset = new(modified); #if UNITY_2022_1_OR_NEWER && UNITY_EDITOR asset.parent = original; #endif asset.hideFlags = HideFlags.None; string path = GetMaterialVariantPath(original); AssetDatabase.CreateAsset(asset, path); EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath(path)); } static string GetMaterialVariantPath(Material original) { string path = null; string name = original ? original.name : "Material"; #if UNITY_2022_1_OR_NEWER && UNITY_EDITOR { Material current = original; while (path == null && current) { path = AssetDatabase.GetAssetPath(current); current = current.parent; } } #else if (original != null) path = AssetDatabase.GetAssetPath(original); #endif path = Path.GetDirectoryName(path); path ??= Application.dataPath; path += $"/{name} Override.asset"; return AssetDatabase.GenerateUniqueAssetPath(path); } public override void OnInspectorGUI() { DrawDefaultInspector(); // Get the materialOverride component GraphicMaterialOverride materialOverride = (GraphicMaterialOverride)target; if (materialOverride.GetComponent() == null) EditorGUILayout.HelpBox( "Cannot find any sibling UI element. Add a UI element to use this component", MessageType.Warning); else if (GetTargetMaterial() == null) EditorGUILayout.HelpBox( "Cannot find any material. Add a material to the UI element to use this component", MessageType.Warning); Material baseMaterial = GetTargetMaterial(); if (baseMaterial == null) return; if (!_editorMaterial) { _editorMaterial = materialOverride.GetEditorMaterial(GetTargetMaterial()); _editorMaterialArray = new Object[] { _editorMaterial }; } InternalEditorUtility.SetIsInspectorExpanded(_editorMaterial, true); if (!_editorMaterialEditor || _editorMaterialEditor.target != _editorMaterial) _editorMaterialEditor = CreateEditor(_editorMaterial) as MaterialEditor; var properties = ShaderPropertyInfo.GetMaterialProperties(baseMaterial); var names = properties.Select(p => p.name).ToList(); // Get the active modifiers List modifiers = materialOverride .GetModifiers(true) .OrderBy(m => names.IndexOf(m.PropertyName)) .ToList(); // Display the current modifier values DisplayModifiers(modifiers); // Use a popup to create new modifiers if (DisplayCreationPopup(modifiers, properties) is { } toCreate) CreateNewModifier(materialOverride.transform, toCreate); } /// /// Display all added modifiers and their value /// /// void DisplayModifiers(List modifiers) { EditorGUILayout.LabelField("Modifiers"); if (modifiers.Count == 0) EditorGUILayout.HelpBox("Select a value from the dropdown to add a property modifier", MessageType.Info); using GUILayout.ScrollViewScope scrollViewScope = new(_scrollPosition); using GUILayout.HorizontalScope horizontalScope = new(); ForEachParameterVertical(modifiers, DrawModifier); } /// /// Returns the heights of each modifiers as a list /// /// The modifiers to get the heights of /// List GetModifiersHeights(List modifiers) { Object[] targetMat = _editorMaterialArray; return modifiers.Select(m => m.PropertyName) .Select(n => MaterialEditor.GetMaterialProperty(targetMat, n)) .Select(_editorMaterialEditor.GetPropertyHeight) .ToList(); } /// /// Begin a vertical group, and call a draw function on each modifier /// /// The modifiers to draw /// The draw function for a modifier property static void ForEachParameterVertical( List modifiers, Action action ) { using EditorGUILayout.VerticalScope scope = new(); foreach (IMaterialPropertyModifier modifier in modifiers) { using GUILayout.HorizontalScope hScope = new(); action(modifier); } } void DrawModifier(IMaterialPropertyModifier modifier) { DrawModifierToggle(modifier); DrawModifierKebabMenu(modifier); DrawModifierValue(modifier); } // The cached style of the kebab menu button GUIStyle _kebabMenuStyle; /// /// Draw a button that activate the context menu /// /// void DrawModifierKebabMenu(IMaterialPropertyModifier modifier) { if (_kebabMenuStyle == null) { _kebabMenuStyle = new GUIStyle(GUI.skin.GetStyle("PaneOptions")); // Force the height of the button _kebabMenuStyle.fixedHeight = EditorGUIUtility.singleLineHeight; _kebabMenuStyle.margin = new RectOffset(0, 0, 3, 0); } if (GUILayout.Button("", _kebabMenuStyle)) DrawModifierContextMenu(modifier); } /// /// Draw a toggle to enable or disable the target modifier component /// /// The modifier component void DrawModifierToggle(IMaterialPropertyModifier modifier) { GameObject targetObject = modifier.gameObject; SerializedObject targetSO = new(targetObject); SerializedProperty activeProp = targetSO.FindProperty("m_IsActive"); EditorGUI.ChangeCheckScope scope = new(); EditorGUILayout.PropertyField(activeProp, GUIContent.none, false, GUILayout.MaxWidth(16f)); if (scope.changed) targetSO.ApplyModifiedProperties(); } /// /// Draw the value field from the property modifier /// /// The target IMaterialPropertyModifier protected virtual void DrawModifierValue(IMaterialPropertyModifier modifier) { MonoBehaviour modifierComponent = (MonoBehaviour)modifier; // Add change checks to the property field EditorGUI.BeginChangeCheck(); // Create a serialized object on the modifier, to display it properly SerializedObject obj = new(modifierComponent); // Get the serialized property SerializedProperty property = obj.FindProperty("propertyValue"); try { EditorGUIUtility.fieldWidth = 64f; DrawMaterialProperty(modifier, property); EditorGUIUtility.fieldWidth = -1; } catch (ExitGUIException e) { throw; } // e is used for debugging purposes #pragma warning disable CS0168 // Variable is declared but never used catch (Exception e) #pragma warning restore CS0168 // Variable is declared but never used { // Put breakpoint here EditorGUIUtility.fieldWidth = -1; DrawFallbackProperty(modifier, property); } // If no change was applied, ignore storage if (!EditorGUI.EndChangeCheck()) return; // Set the serialized property from the current prop // Record an undo Undo.RecordObject(modifierComponent, $"Modified property override {modifier.PropertyName}"); // If we are in a prefab, ensure unity knows about the modification PrefabUtility.RecordPrefabInstancePropertyModifications(modifierComponent); // Apply the modified property obj.ApplyModifiedProperties(); } void DrawFallbackProperty(IMaterialPropertyModifier modifier, SerializedProperty property) { GUI.backgroundColor = new Color(1, 0.5f, 0); EditorGUILayout.PropertyField(property, new GUIContent(modifier.PropertyName)); GUI.backgroundColor = Color.white; } FieldInfo _materialPropertyFlagsField = typeof(MaterialProperty).GetField("m_Flags", BindingFlags.NonPublic | BindingFlags.Instance); void DrawMaterialProperty( IMaterialPropertyModifier modifier, SerializedProperty property ) { // Get the actual Shader Property MaterialProperty materialProperty = MaterialEditor.GetMaterialProperty(_editorMaterialArray, modifier.PropertyName); MaterialProperty.PropFlags oldFlags = materialProperty.flags; MaterialProperty.PropFlags flags = oldFlags; // Hide the scale offset in the texture property drawer if (modifier is GraphicPropertyOverrideTexture or GraphicPropertyOverrideScaleAndOffset) { bool wantsScaleOffset = modifier is GraphicPropertyOverrideScaleAndOffset; flags &= ~MaterialProperty.PropFlags.NoScaleOffset; if (!wantsScaleOffset) flags |= MaterialProperty.PropFlags.NoScaleOffset; } flags &= ~MaterialProperty.PropFlags.PerRendererData; if (oldFlags != flags) { _materialPropertyFlagsField.SetValue(materialProperty, (int)flags); } // Asset correct property type SerializedMaterialPropertyUtility.AssertTypeEqual(property, materialProperty); // Set the buffer shader property to our current value SerializedMaterialPropertyUtility.CopyProperty(materialProperty, property); // Get the height needed to render float height = _editorMaterialEditor.GetPropertyHeight(materialProperty); // Get the control rect Rect rect = EditorGUILayout.GetControlRect(true, height); using EditorGUI.PropertyScope scope = new(rect, new GUIContent(modifier.DisplayName), property); // Set the animator colored backgrounds if (GraphicMaterialOverrideHelper.OverridePropertyColor(materialProperty, (Object)modifier, out Color background)) { GUI.backgroundColor = background; } using EditorGUI.ChangeCheckScope changes = new(); // Draw the property using the hidden editor _editorMaterialEditor.ShaderProperty(rect, materialProperty, scope.content, 0); // Reset the background color GUI.backgroundColor = Color.white; if (changes.changed) { // Place the result in the SerializedProperty SerializedMaterialPropertyUtility.CopyProperty(property, materialProperty); } } /// /// Draw the context menu for one modifier /// /// void DrawModifierContextMenu(IMaterialPropertyModifier modifier) { MonoBehaviour modifierComponent = (MonoBehaviour)modifier; GenericMenu menu = new(); menu.AddItem(new GUIContent("Select"), false, () => Selection.activeGameObject = modifierComponent.gameObject); menu.AddItem(new GUIContent("Set Default"), false, () => ResetModifier(modifier)); if (modifierComponent.isActiveAndEnabled) menu.AddItem(new GUIContent("Disable"), false, () => ModifierSetActive(modifier, false)); else menu.AddItem(new GUIContent("Enable"), false, () => ModifierSetActive(modifier, true)); menu.AddItem(new GUIContent("Delete"), false, () => DeleteModifier(modifier)); menu.ShowAsContext(); } /// /// Reset a modifier object to the default material value and record an undo /// /// void ResetModifier(IMaterialPropertyModifier modifier) { Undo.RecordObject(modifier as Object, "Reset modifier component"); modifier.ResetPropertyToDefault(); PrefabUtility.RecordPrefabInstancePropertyModifications(modifier as Object); } /// /// Delete the GameObject of a modifier and record an Undo /// /// void DeleteModifier(IMaterialPropertyModifier modifier) { Undo.DestroyObjectImmediate(modifier.gameObject); } /// /// Set the active state of a modifier and its GameObject /// Records an undo /// /// /// void ModifierSetActive(IMaterialPropertyModifier modifier, bool isActive) { // Make sure any modifications are properly propagated to unity Undo.RecordObjects( new[] { modifier as Object, modifier.gameObject }, "Toggled modifier component"); // If enabling, set the component and GameObject as active if (isActive) { modifier.enabled = true; modifier.gameObject.SetActive(true); } // If disabling, disable the GameObject only else { modifier.gameObject.SetActive(false); } PrefabUtility.RecordPrefabInstancePropertyModifications(modifier as Object); PrefabUtility.RecordPrefabInstancePropertyModifications(modifier.gameObject); } /// /// Try to draw a range field for a shader property /// If the information cannot be found, draw a float property /// /// The material holding the shader property /// The index of the shader property /// The serialized property in the modifier /// The label of the property public static void DrawFloatPropertyAsRange( Material material, int propertyIndex, SerializedProperty property, GUIContent label ) { if (!material || propertyIndex < 0) { EditorGUILayout.PropertyField(property); return; } Rect rect = GUILayoutUtility.GetRect( EditorGUIUtility.fieldWidth, EditorGUIUtility.labelWidth + EditorGUIUtility.fieldWidth + 110f, 18f, 18f); Shader shader = material.shader; float min = ShaderUtil.GetRangeLimits(shader, propertyIndex, 1); float max = ShaderUtil.GetRangeLimits(shader, propertyIndex, 2); using EditorGUI.PropertyScope scope = new(rect, label, property); EditorGUI.BeginChangeCheck(); float newValue = EditorGUI.Slider(rect, "⇔", property.floatValue, min, max); // Only assign the value back if it was actually changed by the user. // Otherwise a single value will be assigned to all objects when multi-object editing, // even when the user didn't touch the control. if (EditorGUI.EndChangeCheck()) property.floatValue = newValue; } /// /// Draw a color field for a shader property, hdr if required /// If the information cannot be found, draw a color property /// /// The material holding the shader property /// The serialized property in the modifier /// If the property should be drawn as HDR /// The label of the property public static void DrawColorPropertyAsHdr( Material material, SerializedProperty property, bool isHdr, GUIContent label ) { if (!material) { EditorGUILayout.PropertyField(property); return; } property.colorValue = EditorGUILayout.ColorField(label, property.colorValue, true, true, isHdr); } static readonly (string, string)[] LowerPropertyStrings = { ("_StencilComp", "UI Hidden Properties/StencilComp"), ("_Stencil", "UI Hidden Properties/Stencil"), ("_StencilOp", "UI Hidden Properties/StencilOp"), ("_StencilWriteMask", "UI Hidden Properties/StencilWriteMask"), ("_StencilReadMask", "UI Hidden Properties/StencilReadMask"), ("_ColorMask", "UI Hidden Properties/ColorMask"), ("_UseUIAlphaClip", "UI Hidden Properties/UseUIAlphaClip"), }; static string GetPropertyName(ShaderPropertyInfo prop) { foreach (var (lower, upper) in LowerPropertyStrings) { if (prop.name == lower) return upper; } return prop.name; } struct PropertyEntry { public string Name; public string DisplayName; public Type ComponentType; } /// /// Display a dropdown to select a modifier /// Filters out modifiers that are already added /// /// PropertyEntry? DisplayCreationPopup(List modifiers, List properties) { // Create a set to filter out modifiers that are already added HashSet namesAlreadyUsed = modifiers .Select(p => p.DisplayName) .ToHashSet(); List entries = new(); foreach (ShaderPropertyInfo info in properties) { if (!namesAlreadyUsed.Contains(info.name)) { entries.Add( new PropertyEntry { Name = info.name, DisplayName = GetPropertyName(info), ComponentType = info.type switch { PropertyType.Color => typeof(GraphicPropertyOverrideColor), PropertyType.Float => typeof(GraphicPropertyOverrideFloat), PropertyType.Range => typeof(GraphicPropertyOverrideRange), PropertyType.Vector => typeof(GraphicPropertyOverrideVector), PropertyType.TexEnv => typeof(GraphicPropertyOverrideTexture), _ => throw new ArgumentOutOfRangeException(), }, }); } if (info.type == PropertyType.TexEnv) { string displayName = GetPropertyName(info) + " Scale Offset"; if (!namesAlreadyUsed.Contains(displayName)) { entries.Add( new PropertyEntry { Name = info.name, DisplayName = displayName, ComponentType = typeof(GraphicPropertyOverrideScaleAndOffset), }); } } } string[] propertyNames = entries.Select(e => e.DisplayName).ToArray(); int selectedIndex = EditorGUILayout.Popup(new GUIContent("Add Override"), -1, propertyNames); if (selectedIndex >= 0) return entries[selectedIndex]; return null; } /// /// Create a new GraphicPropertyOverride in a new child GameObject /// /// The transform of the GraphicMaterialOverride /// The property to override /// thrown when ShaderPropertyType is invalid void CreateNewModifier(Transform parent, PropertyEntry propertyInfo) { // Increment undo group Undo.IncrementCurrentGroup(); GameObject child = new($"{propertyInfo.DisplayName} Override"); Undo.RegisterCreatedObjectUndo(child, $"Added override GameObject"); child.layer = parent.gameObject.layer; Undo.SetTransformParent(child.transform, parent, false, "Moved override GameObject"); GraphicPropertyOverride propertyOverride = Undo.AddComponent(child, propertyInfo.ComponentType) as GraphicPropertyOverride; propertyOverride!.PropertyName = propertyInfo.Name; propertyOverride.ResetPropertyToDefault(); Undo.RegisterCompleteObjectUndo(child, "Added override component"); Undo.SetCurrentGroupName($"Override ${propertyInfo.DisplayName}"); } /// /// Try to get the material from the Graphic component /// /// Material GetTargetMaterial() { GraphicMaterialOverride graphicMaterialOverride = (GraphicMaterialOverride)target; return graphicMaterialOverride.TryGetComponent(out Graphic graphic) ? graphic.material : null; } } }