ProjectDDD/Assets/Plugins/Animate UI Materials/Editor/GraphicMaterialOverrideEditor.cs

644 lines
23 KiB
C#

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
{
/// <summary>
/// The scroll position in the modifiers ScrollView
/// Usually not needed, but good to have
/// </summary>
Vector2 _scrollPosition;
/// <summary>
/// A fake material used to create an inspector
/// </summary>
Material _editorMaterial;
Object[] _editorMaterialArray;
/// <summary>
/// The editor of the fake material
/// </summary>
MaterialEditor _editorMaterialEditor;
/// <summary>
/// Override the reset context menu to implement the reset function
/// Needed instead of "MonoBehavior.Reset" on GraphicMaterialOverride because we need to record an Undo
/// </summary>
[MenuItem("CONTEXT/GraphicMaterialOverride/Reset")]
static void ResetMaterialModifiers(MenuCommand b)
{
GraphicMaterialOverride materialOverride = (GraphicMaterialOverride)b.context;
if (!materialOverride) return;
List<IMaterialPropertyModifier> 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);
}
}
/// <summary>
/// Ask the Graphic component to reload the modified material
/// </summary>
[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;
}
/// <summary>
/// Ask the Graphic component to reload the modified material
/// </summary>
[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<Material>(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<Graphic>() == 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<IMaterialPropertyModifier> 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);
}
/// <summary>
/// Display all added modifiers and their value
/// </summary>
/// <param name="modifiers"></param>
void DisplayModifiers(List<IMaterialPropertyModifier> 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);
}
/// <summary>
/// Returns the heights of each modifiers as a list
/// </summary>
/// <param name="modifiers"> The modifiers to get the heights of</param>
/// <returns></returns>
List<float> GetModifiersHeights(List<IMaterialPropertyModifier> modifiers)
{
Object[] targetMat = _editorMaterialArray;
return modifiers.Select(m => m.PropertyName)
.Select(n => MaterialEditor.GetMaterialProperty(targetMat, n))
.Select(_editorMaterialEditor.GetPropertyHeight)
.ToList();
}
/// <summary>
/// Begin a vertical group, and call a draw function on each modifier
/// </summary>
/// <param name="modifiers">The modifiers to draw</param>
/// <param name="action">The draw function for a modifier property</param>
static void ForEachParameterVertical(
List<IMaterialPropertyModifier> modifiers,
Action<IMaterialPropertyModifier> 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;
/// <summary>
/// Draw a button that activate the context menu
/// </summary>
/// <param name="modifier"></param>
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);
}
/// <summary>
/// Draw a toggle to enable or disable the target modifier component
/// </summary>
/// <param name="modifier">The modifier component</param>
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();
}
/// <summary>
/// Draw the value field from the property modifier
/// </summary>
/// <param name="modifier">The target IMaterialPropertyModifier</param>
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);
}
}
/// <summary>
/// Draw the context menu for one modifier
/// </summary>
/// <param name="modifier"></param>
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();
}
/// <summary>
/// Reset a modifier object to the default material value and record an undo
/// </summary>
/// <param name="modifier"></param>
void ResetModifier(IMaterialPropertyModifier modifier)
{
Undo.RecordObject(modifier as Object, "Reset modifier component");
modifier.ResetPropertyToDefault();
PrefabUtility.RecordPrefabInstancePropertyModifications(modifier as Object);
}
/// <summary>
/// Delete the GameObject of a modifier and record an Undo
/// </summary>
/// <param name="modifier"></param>
void DeleteModifier(IMaterialPropertyModifier modifier)
{
Undo.DestroyObjectImmediate(modifier.gameObject);
}
/// <summary>
/// Set the active state of a modifier and its GameObject
/// Records an undo
/// </summary>
/// <param name="modifier"></param>
/// <param name="isActive"></param>
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);
}
/// <summary>
/// Try to draw a range field for a shader property
/// If the information cannot be found, draw a float property
/// </summary>
/// <param name="material">The material holding the shader property</param>
/// <param name="propertyIndex">The index of the shader property</param>
/// <param name="property">The serialized property in the modifier</param>
/// <param name="label">The label of the property</param>
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;
}
/// <summary>
/// Draw a color field for a shader property, hdr if required
/// If the information cannot be found, draw a color property
/// </summary>
/// <param name="material">The material holding the shader property</param>
/// <param name="property">The serialized property in the modifier</param>
/// <param name="isHdr">If the property should be drawn as HDR</param>
/// <param name="label">The label of the property</param>
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;
}
/// <summary>
/// Display a dropdown to select a modifier
/// Filters out modifiers that are already added
/// </summary>
/// <returns></returns>
PropertyEntry? DisplayCreationPopup(List<IMaterialPropertyModifier> modifiers, List<ShaderPropertyInfo> properties)
{
// Create a set to filter out modifiers that are already added
HashSet<string> namesAlreadyUsed = modifiers
.Select(p => p.DisplayName)
.ToHashSet();
List<PropertyEntry> 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;
}
/// <summary>
/// Create a new GraphicPropertyOverride in a new child GameObject
/// </summary>
/// <param name="parent">The transform of the GraphicMaterialOverride</param>
/// <param name="propertyInfo">The property to override</param>
/// <exception cref="ArgumentOutOfRangeException">thrown when ShaderPropertyType is invalid</exception>
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}");
}
/// <summary>
/// Try to get the material from the Graphic component
/// </summary>
/// <returns></returns>
Material GetTargetMaterial()
{
GraphicMaterialOverride graphicMaterialOverride = (GraphicMaterialOverride)target;
return graphicMaterialOverride.TryGetComponent(out Graphic graphic) ? graphic.material : null;
}
}
}