// Copyright (c) Jason Ma using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; namespace LWGUI { public enum SearchMode { Auto = 0, // Search by group first, and search by property when there are no results Property = 1, // Search by property Group = 2, // Search by group Num = 3 } public enum LogicalOperator { And, Or } public struct DisplayModeData { public bool showAllAdvancedProperties; public bool showAllHiddenProperties; public bool showOnlyModifiedProperties; public int advancedCount; public int hiddenCount; public bool IsDefaultDisplayMode() { return !(showAllAdvancedProperties || showAllHiddenProperties || showOnlyModifiedProperties); } } public class ShowIfData { public LogicalOperator logicalOperator = LogicalOperator.And; public string targetPropertyName = string.Empty; public CompareFunction compareFunction = CompareFunction.Equal; public float value = 0; } /// /// All static metadata for a Property, determined after the Shader is compiled. /// public class PropertyStaticData { public string name = string.Empty; public string displayName = string.Empty; // Decoded displayName (Helpbox and Tooltip are encoded in displayName) // Structure public string groupName = string.Empty; // [Group(groupName)] / [Sub(groupName)] / [Advanced(groupName)] public bool isMain = false; // [Group] public bool isAdvanced = false; // [Advanced] public bool isAdvancedHeader = false; // the first [Advanced] in the same group public bool isAdvancedHeaderProperty = false; public string advancedHeaderString = string.Empty; public PropertyStaticData parent = null; public List children = new List(); // Visibility public string conditionalDisplayKeyword = string.Empty; // [Group(groupName_conditionalDisplayKeyword)] public bool isSearchMatched = true; // Draws when the search match is successful public bool isExpanding = false; // Draws when the group is expanding public bool isHidden = false; // [Hidden] public List showIfDatas = new List(); // [ShowIf()] // Metadata public List extraPropNames = new List(); // Other Props that have been associated public string helpboxMessages = string.Empty; public string tooltipMessages = string.Empty; public ShaderPropertyPreset propertyPresetAsset = null; // The Referenced Preset Asset public void AddExtraProperty(string propName) { if (!extraPropNames.Contains(propName)) extraPropNames.Add(propName); } } /// /// Consistent metadata across different material instances of the same Shader. /// public class PerShaderData { public Dictionary propertyDatas = new Dictionary(); public SearchMode searchMode = SearchMode.Auto; public string searchString = string.Empty; public List favoriteproperties = new List(); public DisplayModeData displayModeData = new DisplayModeData(); public void BuildPropertyStaticData(Shader shader, MaterialProperty[] props) { // Get Property Static Data foreach (var prop in props) { var propStaticData = new PropertyStaticData(){ name = prop.name }; propertyDatas[prop.name] = propStaticData; List decoratorDrawers; var drawer = ReflectionHelper.GetPropertyDrawer(shader, prop, out decoratorDrawers); if (decoratorDrawers != null && decoratorDrawers.Count > 0) { foreach (var decoratorDrawer in decoratorDrawers) { if (decoratorDrawer is IBaseDrawer) (decoratorDrawer as IBaseDrawer).BuildStaticMetaData(shader, prop, props, propStaticData); } } if (drawer != null) { if (drawer is IBaseDrawer) (drawer as IBaseDrawer).BuildStaticMetaData(shader, prop, props, propStaticData); } DecodeMetaDataFromDisplayName(prop, propStaticData); } // Check Data foreach (var prop in props) { var propStaticData = propertyDatas[prop.name]; propStaticData.extraPropNames.RemoveAll((extraPropName => string.IsNullOrEmpty(extraPropName) || !propertyDatas.ContainsKey(extraPropName))); } // Build Property Structure { var groupToMainPropertyDic = new Dictionary(); // Collection Groups foreach (var prop in props) { var propData = propertyDatas[prop.name]; if (propData.isMain && !string.IsNullOrEmpty(propData.groupName) && !groupToMainPropertyDic.ContainsKey(propData.groupName)) groupToMainPropertyDic.Add(propData.groupName, prop); } // Register SubProps foreach (var prop in props) { var propData = propertyDatas[prop.name]; if (!propData.isMain && !string.IsNullOrEmpty(propData.groupName)) { foreach (var groupName in groupToMainPropertyDic.Keys) { if (propData.groupName.StartsWith(groupName)) { // Update Structure var mainProp = groupToMainPropertyDic[groupName]; propData.parent = propertyDatas[mainProp.name]; propertyDatas[mainProp.name].children.Add(propData); // Split groupName and conditional display keyword if (propData.groupName.Length > groupName.Length) { propData.conditionalDisplayKeyword = propData.groupName.Substring(groupName.Length, propData.groupName.Length - groupName.Length).ToUpper(); propData.groupName = groupName; } break; } } } } } // Build Display Mode Data { PropertyStaticData lastPropData = null; PropertyStaticData lastHeaderPropData = null; for (int i = 0; i < props.Length; i++) { var prop = props[i]; var propStaticData = propertyDatas[prop.name]; // Counting if (propStaticData.isHidden || (propStaticData.parent != null && (propStaticData.parent.isHidden || (propStaticData.parent.parent != null && propStaticData.parent.parent.isHidden)))) displayModeData.hiddenCount++; if (propStaticData.isAdvanced || (propStaticData.parent != null && (propStaticData.parent.isAdvanced || (propStaticData.parent.parent != null && propStaticData.parent.parent.isAdvanced)))) displayModeData.advancedCount++; // Build Advanced Structure if (propStaticData.isAdvanced) { // If it is the first prop in a Advanced Block, set to Header if (lastPropData == null || !lastPropData.isAdvanced || propStaticData.isAdvancedHeaderProperty || (!string.IsNullOrEmpty(propStaticData.advancedHeaderString) && propStaticData.advancedHeaderString != lastPropData.advancedHeaderString)) { propStaticData.isAdvancedHeader = true; lastHeaderPropData = propStaticData; } // Else set to child else { propStaticData.parent = lastHeaderPropData; lastHeaderPropData.children.Add(propStaticData); } } lastPropData = propStaticData; } } } private static readonly string _tooltipSplitter = "#"; private static readonly string _helpboxSplitter = "%"; public void DecodeMetaDataFromDisplayName(MaterialProperty prop, PropertyStaticData propStaticData) { var tooltips = prop.displayName.Split(new String[] { _tooltipSplitter }, StringSplitOptions.None); if (tooltips.Length > 1) { for (int i = 1; i <= tooltips.Length - 1; i++) { var str = tooltips[i]; var helpboxIndex = tooltips[i].IndexOf(_helpboxSplitter, StringComparison.Ordinal); if (helpboxIndex > 0) str = tooltips[i].Substring(0, helpboxIndex); propStaticData.tooltipMessages += str + "\n"; } } var helpboxes = prop.displayName.Split(new String[] { _helpboxSplitter }, StringSplitOptions.None); if (helpboxes.Length > 1) { for (int i = 1; i <= helpboxes.Length - 1; i++) { var str = helpboxes[i]; var tooltipIndex = helpboxes[i].IndexOf(_tooltipSplitter, StringComparison.Ordinal); if (tooltipIndex > 0) str = tooltips[i].Substring(0, tooltipIndex); propStaticData.helpboxMessages += str + "\n"; } } if (propStaticData.helpboxMessages.EndsWith("\n")) propStaticData.helpboxMessages = propStaticData.helpboxMessages.Substring(0, propStaticData.helpboxMessages.Length - 1); propStaticData.displayName = prop.displayName.Split(new String[] { _tooltipSplitter, _helpboxSplitter }, StringSplitOptions.None)[0]; } public void UpdateSearchFilter() { var isSearchStringEmpty = string.IsNullOrEmpty(searchString); var searchStringLower = searchString.ToLower(); var searchKeywords = searchStringLower.Split(' ', ',', ';', '|', ',', ';'); // Some possible separators // The First Search foreach (var propertyData in propertyDatas) { propertyData.Value.isSearchMatched = isSearchStringEmpty ? true : IsWholeWordMatch(propertyData.Value.displayName, propertyData.Key, searchKeywords); } // Further adjust visibility if (!isSearchStringEmpty) { var searchModeTemp = searchMode; // Auto: search by group first, and search by property when there are no results if (searchModeTemp == SearchMode.Auto) { // if has no group if (!propertyDatas.Any((propertyData => propertyData.Value.isSearchMatched && propertyData.Value.isMain))) searchModeTemp = SearchMode.Property; else searchModeTemp = SearchMode.Group; } // search by property if (searchModeTemp == SearchMode.Property) { // when a SubProp is displayed, the MainProp is also displayed foreach (var propertyData in propertyDatas) { if (propertyData.Value.isMain && propertyData.Value.children.Any((childPropertyData => childPropertyData.isSearchMatched))) propertyData.Value.isSearchMatched = true; } } // search by group else if (searchModeTemp == SearchMode.Group) { // when search by group, all SubProps should display with MainProp foreach (var propertyData in propertyDatas) { if (propertyData.Value.isMain) foreach (var childPropertyData in propertyData.Value.children) childPropertyData.isSearchMatched = propertyData.Value.isSearchMatched; } } } } private static bool IsWholeWordMatch(string displayName, string propertyName, string[] searchingKeywords) { bool contains = true; displayName = displayName.ToLower(); var name = propertyName.ToLower(); foreach (var keyword in searchingKeywords) { var isMatch = false; isMatch |= displayName.Contains(keyword); isMatch |= name.Contains(keyword); contains &= isMatch; } return contains; } public void ToggleShowAllAdvancedProperties() { foreach (var propertyStaticDataPair in propertyDatas) { if (propertyStaticDataPair.Value.isAdvancedHeader) propertyStaticDataPair.Value.isExpanding = displayModeData.showAllAdvancedProperties; } } } /// /// Property metadata dynamically generated perframe /// public class PropertyDynamicData { public MaterialProperty property; public MaterialProperty defualtProperty; // Default values may be overridden by Preset public string defaultValueDescription = string.Empty; // Description of the default values used in Tooltip public bool hasModified = false; // Are properties modified in the material? public bool hasChildrenModified = false; // Are Children properties modified in the material? public bool hasRevertChanged = false; // Used to call property EndChangeCheck() public bool isShowing = true; // ShowIf() result } public class PersetDynamicData { public ShaderPropertyPreset.Preset preset; public MaterialProperty property; public PersetDynamicData(ShaderPropertyPreset.Preset preset, MaterialProperty property) { this.preset = preset; this.property = property; } } /// /// Each frame of each material may have different metadata. /// public class PerFrameData { public Dictionary propertyDatas = new Dictionary(); public List activePresets = new List(); public int modifiedCount = 0; public void BuildPerFrameData(Shader shader, Material material, MaterialProperty[] props, PerShaderData perShaderData) { // Get active presets foreach (var prop in props) { List decoratorDrawers; var drawer = ReflectionHelper.GetPropertyDrawer(shader, prop, out decoratorDrawers); // Get Presets if (drawer != null) { if (drawer is IBasePresetDrawer) { var activePreset = (drawer as IBasePresetDrawer).GetActivePreset(prop, perShaderData.propertyDatas[prop.name].propertyPresetAsset); if (activePreset != null) { activePresets.Add(new PersetDynamicData(activePreset, prop)); } } } } // Apply presets to default material { var defaultMaterial = #if UNITY_2022_1_OR_NEWER material.parent ? UnityEngine.Object.Instantiate(material.parent) : #endif new Material(shader); foreach (var activePreset in activePresets) activePreset.preset.ApplyToDefaultMaterial(defaultMaterial); var defaultProperties = MaterialEditor.GetMaterialProperties(new[] { defaultMaterial }); Debug.Assert(defaultProperties.Length == props.Length); for (int i = 0; i < props.Length; i++) { Debug.Assert(props[i].name == defaultProperties[i].name); Debug.Assert(!propertyDatas.ContainsKey(props[i].name)); var hasModified = !Helper.PropertyValueEquals(props[i], defaultProperties[i]); if (hasModified) modifiedCount++; propertyDatas.Add(props[i].name, new PropertyDynamicData() { property = props[i], defualtProperty = defaultProperties[i], hasModified = hasModified }); } } foreach (var prop in props) { var propStaticData = perShaderData.propertyDatas[prop.name]; var propDynamicData = propertyDatas[prop.name]; // Override parent hasModified if (propDynamicData.hasModified) { var parentPropData = propStaticData.parent; if (parentPropData != null) { propertyDatas[parentPropData.name].hasChildrenModified = true; if (parentPropData.parent != null) propertyDatas[parentPropData.parent.name].hasChildrenModified = true; } } // Get default value descriptions List decoratorDrawers; var drawer = ReflectionHelper.GetPropertyDrawer(shader, prop, out decoratorDrawers); { if (decoratorDrawers != null && decoratorDrawers.Count > 0) { foreach (var decoratorDrawer in decoratorDrawers) { if (decoratorDrawer is IBaseDrawer) (decoratorDrawer as IBaseDrawer).GetDefaultValueDescription(shader, prop, perShaderData, this); } } if (drawer != null) { if (drawer is IBaseDrawer) (drawer as IBaseDrawer).GetDefaultValueDescription(shader, prop, perShaderData, this); } if (string.IsNullOrEmpty(propDynamicData.defaultValueDescription)) propDynamicData.defaultValueDescription = RevertableHelper.GetPropertyDefaultValueText(propDynamicData.defualtProperty); } // Get ShowIf() results foreach (var showIfData in propStaticData.showIfDatas) { var propCurrentValue = propertyDatas[showIfData.targetPropertyName].property.floatValue; bool compareResult; switch (showIfData.compareFunction) { case CompareFunction.Less: compareResult = propCurrentValue < showIfData.value; break; case CompareFunction.LessEqual: compareResult = propCurrentValue <= showIfData.value; break; case CompareFunction.Greater: compareResult = propCurrentValue > showIfData.value; break; case CompareFunction.NotEqual: compareResult = propCurrentValue != showIfData.value; break; case CompareFunction.GreaterEqual: compareResult = propCurrentValue >= showIfData.value; break; default: compareResult = propCurrentValue == showIfData.value; break; } switch (showIfData.logicalOperator) { case LogicalOperator.And: propDynamicData.isShowing &= compareResult; break; case LogicalOperator.Or: propDynamicData.isShowing |= compareResult; break; } } } } public MaterialProperty GetProperty(string propName) { if (!string.IsNullOrEmpty(propName) && propertyDatas.ContainsKey(propName)) return propertyDatas[propName].property; else return null; } public MaterialProperty GetDefaultProperty(string propName) { if (!string.IsNullOrEmpty(propName) && propertyDatas.ContainsKey(propName)) return propertyDatas[propName].defualtProperty; else return null; } public bool EndChangeCheck(string propName = null) { var result = EditorGUI.EndChangeCheck(); if (!string.IsNullOrEmpty(propName)) { result |= propertyDatas[propName].hasRevertChanged; propertyDatas[propName].hasRevertChanged = false; } return result; } } public class MetaDataHelper { private static Dictionary _shaderDataDic = new Dictionary(); public static PerShaderData BuildPerShaderData(Shader shader, MaterialProperty[] props) { if (!_shaderDataDic.ContainsKey(shader)) { var perShaderData = new PerShaderData(); perShaderData.BuildPropertyStaticData(shader, props); _shaderDataDic.Add(shader, perShaderData); } return _shaderDataDic[shader]; } public static void ForceRebuildPerShaderData(Shader shader) { if (shader && _shaderDataDic.ContainsKey(shader)) _shaderDataDic.Remove(shader); } public static PerFrameData BuildPerFrameData(Shader shader, Material material, MaterialProperty[] props) { var perFrameData = new PerFrameData(); perFrameData.BuildPerFrameData(shader, material, props, _shaderDataDic[shader]); return perFrameData; } public static string GetPropertyTooltip(PropertyStaticData propertyStaticData, PropertyDynamicData propertyDynamicData) { var str = propertyStaticData.tooltipMessages; if (!string.IsNullOrEmpty(str)) str += "\n\n"; str += "Property Name: " + propertyDynamicData.property.name + "\n"; str += "Default Value: " + propertyDynamicData.defaultValueDescription; return str; } private static readonly string _tooltipString = "#"; private static readonly string _helpboxString = "%"; public static string GetPropertyDisplayName(Shader shader, MaterialProperty prop) { var tooltipIndex = prop.displayName.IndexOf(_tooltipString, StringComparison.Ordinal); var helpboxIndex = prop.displayName.IndexOf(_helpboxString, StringComparison.Ordinal); var minIndex = tooltipIndex == -1 ? helpboxIndex : tooltipIndex; if (tooltipIndex != -1 && helpboxIndex != -1) minIndex = Mathf.Min(minIndex, helpboxIndex); if (minIndex == -1) return prop.displayName; else if (minIndex == 0) return string.Empty; else return prop.displayName.Substring(0, minIndex); } public static bool GetPropertyVisibility(MaterialProperty prop, Material material, LWGUI lwgui) { bool result = true; var propertyStaticData = lwgui.perShaderData.propertyDatas[prop.name]; var propertyDynamicData = lwgui.perFrameData.propertyDatas[prop.name]; var displayModeData = lwgui.perShaderData.displayModeData; if ( // if HideInInspector Helper.IsPropertyHideInInspector(prop) // if Search Filtered || !propertyStaticData.isSearchMatched // if the Conditional Display Keyword is not active || (!string.IsNullOrEmpty(propertyStaticData.conditionalDisplayKeyword) && !material.shaderKeywords.Any((str => str == propertyStaticData.conditionalDisplayKeyword))) || (!displayModeData.showAllHiddenProperties && propertyStaticData.isHidden) // if show modified only || (displayModeData.showOnlyModifiedProperties && !(propertyDynamicData.hasModified || propertyDynamicData.hasChildrenModified)) // ShowIf() == false || !propertyDynamicData.isShowing ) { result = false; } return result; } public static bool GetParentPropertyVisibility(PropertyStaticData parentPropStaticData, Material material, LWGUI lwgui) { bool result = true; if (parentPropStaticData != null && (!lwgui.perShaderData.propertyDatas[parentPropStaticData.name].isExpanding || !MetaDataHelper.GetPropertyVisibility(lwgui.perFrameData.propertyDatas[parentPropStaticData.name].property, material, lwgui))) { result = false; } return result; } } }