Created
November 3, 2025 11:03
-
-
Save unitycoder/b9b8bea826944b8e3ae61d20c3ec3982 to your computer and use it in GitHub Desktop.
DSShaderGUI from DigitalSalmon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // https://www.twitch.tv/digitalsalmon | |
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Text.RegularExpressions; | |
| using DigitalSalmon.Extensions; | |
| using UnityEditor; | |
| using UnityEditor.Graphs; | |
| using UnityEditor.Rendering; | |
| using UnityEngine; | |
| namespace DigitalSalmon { | |
| public class DSShaderGUI : ShaderGUI { | |
| private static readonly int PROP_ZTESTMODE = Shader.PropertyToID("_ZTestMode"); | |
| public class ShaderPropertyAttributeDetails { | |
| public string Name; | |
| public string Value; | |
| } | |
| public class ShaderPropertyDetails { | |
| public string Name; | |
| public string DisplayName; | |
| public string Type; | |
| public string DefaultValue; | |
| public ShaderPropertyAttributeDetails[] AttributeDetails; | |
| public void Log() { | |
| Debug.Log($"Property: {Name}, Display: {DisplayName}, Type: {Type}, Default: {DefaultValue}"); | |
| } | |
| } | |
| private Dictionary<MaterialProperty, ShaderPropertyDetails> materialPropertyToShaderPropertyDetails = new Dictionary<MaterialProperty, ShaderPropertyDetails>(); | |
| private List<ShaderPropertyDetails> shaderPropertyDetails = new List<ShaderPropertyDetails>(); | |
| List<string> categories = new List<string>(); | |
| private Dictionary<string, List<ShaderPropertyDetails>> categoryToProperties = new Dictionary<string, List<ShaderPropertyDetails>>(); | |
| private int propertyIndex = 0; | |
| private int propertySubIndex = 0; | |
| string activeCategory = null; | |
| public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) { | |
| //base.OnGUI(materialEditor, properties); | |
| materialEditor.SetDefaultGUIWidths(); | |
| ValidateShaderPropertyDetails(materialEditor, properties); | |
| propertyIndex = 0; | |
| propertySubIndex = 0; | |
| activeCategory = null; | |
| foreach (var property in properties) { | |
| ShaderPropertyDetails details = shaderPropertyDetails.FirstOrDefault(p => p.Name == property.name); | |
| if (details == null) { | |
| GUILayout.Label($"ERROR - {property.name}"); | |
| continue; | |
| } | |
| if (DrawCustomAttributesEarly(materialEditor, property, details)) { | |
| materialEditor.ShaderProperty(property, property.displayName); | |
| propertyIndex++; | |
| propertySubIndex++; | |
| } | |
| } | |
| BeginCategory("Advanced Options"); | |
| materialEditor.RenderQueueField(); | |
| Material material = materialEditor.target as Material; | |
| if (material) { | |
| GUILayout.Label($"Has: {material.HasFloat(PROP_ZTESTMODE)}"); | |
| if (material.HasFloat(PROP_ZTESTMODE)) { | |
| UnityEngine.Rendering.CompareFunction zTest = (UnityEngine.Rendering.CompareFunction)material.GetFloat(PROP_ZTESTMODE); | |
| int newZTest = (int)(UnityEngine.Rendering.CompareFunction)EditorGUILayout.EnumPopup("ZTest Mode", zTest); | |
| material.SetFloat(PROP_ZTESTMODE, newZTest); | |
| } | |
| } | |
| EndCategory(); | |
| } | |
| private Rect DrawCategoryEarly() { | |
| Rect r = EditorGUILayout.GetControlRect(true); | |
| Rect background = r; | |
| background.x = 0; | |
| background.width = 1000; | |
| Rect top = background; | |
| top.height = 1; | |
| GUI.color = Colours.BLACK_10; | |
| GUI.DrawTexture(background, Texture2D.whiteTexture); | |
| GUI.color = Colours.BLACK_40; | |
| GUI.DrawTexture(top, Texture2D.whiteTexture); | |
| GUI.color = Color.white; | |
| return r; | |
| } | |
| private void DrawCategoryLate() { | |
| GUILayout.Space(3); | |
| } | |
| private void BeginCategory(MaterialEditor materialEditor, ShaderPropertyAttributeDetails attribute) { | |
| Rect r = DrawCategoryEarly(); | |
| if (attribute.Name == ATTRIB_MCCATEGORY) { | |
| Rect toggleRect = r; | |
| //toggleRect.x += 16f; | |
| toggleRect.y += 2f; | |
| toggleRect.width = 13f; | |
| toggleRect.height = 13f; | |
| Rect labelRect = r; | |
| labelRect.xMin += 16f; | |
| labelRect.xMax -= 20f + 16 + 5; | |
| Material material = materialEditor.target as Material; | |
| bool isEnabled = material.IsKeywordEnabled(attribute.Value); | |
| bool newIsEnabled = GUI.Toggle(toggleRect, isEnabled, GUIContent.none, CoreEditorStyles.smallTickbox); | |
| if (newIsEnabled != isEnabled) { | |
| if (newIsEnabled) material.EnableKeyword(attribute.Value); | |
| else material.DisableKeyword(attribute.Value); | |
| } | |
| GUI.Label(labelRect, attribute.Value, EditorStyles.boldLabel); | |
| } | |
| else { | |
| GUI.Label(r, attribute.Value, EditorStyles.boldLabel); | |
| } | |
| DrawCategoryLate(); | |
| } | |
| private void BeginCategory(string label) { | |
| Rect r = DrawCategoryEarly(); | |
| GUI.Label(r, label, EditorStyles.boldLabel); | |
| DrawCategoryLate(); | |
| } | |
| private void EndCategory() { | |
| GUILayout.Space(9); | |
| propertySubIndex = 0; | |
| //EditorGUILayout.EndFoldoutHeaderGroup(); | |
| } | |
| private const string ATTRIB_TITLE = "Title"; | |
| private const string ATTRIB_SUBTITLE = "Subtitle"; | |
| private const string ATTRIB_NOTE = "Note"; | |
| private const string ATTRIB_CATEGORY = "PropCategory"; | |
| private const string ATTRIB_MCCATEGORY = "MCCategory"; | |
| private const string ATTRIB_IMCCATEGORY = "IMCCategory"; | |
| private const string CATEGORY_HIDDEN = "Hidden"; | |
| private const string ATTRIB_HIDDEN = "HideInInspector"; | |
| private bool DrawCustomAttributesEarly(MaterialEditor materialEditor, MaterialProperty property, ShaderPropertyDetails details) { | |
| ShaderPropertyAttributeDetails categoryAttrib = details.AttributeDetails.FirstOrDefault(a => a.Name == ATTRIB_CATEGORY || a.Name == ATTRIB_MCCATEGORY || a.Name == ATTRIB_IMCCATEGORY); | |
| if (categoryAttrib != null && categoryAttrib.Value == CATEGORY_HIDDEN) return false; | |
| if (details.AttributeDetails.FirstOrDefault(a => a.Name == ATTRIB_HIDDEN) != null) return false; | |
| bool hasNewCategoryAttrib = (categoryAttrib != null && categoryAttrib.Value != activeCategory); | |
| if (activeCategory != null && (categoryAttrib == null || hasNewCategoryAttrib)) { | |
| EndCategory(); | |
| EditorGUI.indentLevel--; | |
| } | |
| if (hasNewCategoryAttrib) { | |
| BeginCategory(materialEditor, categoryAttrib); | |
| EditorGUI.indentLevel++; | |
| activeCategory = categoryAttrib.Value; | |
| } | |
| if (categoryAttrib != null && categoryAttrib.Name == ATTRIB_MCCATEGORY) { | |
| string keyword = categoryAttrib.Value; | |
| Material material = (Material) materialEditor.target; | |
| if (!material.IsKeywordEnabled(keyword)) { | |
| return false; | |
| } | |
| } | |
| if (categoryAttrib != null && categoryAttrib.Name == ATTRIB_IMCCATEGORY) { | |
| string keyword = categoryAttrib.Value; | |
| Material material = (Material) materialEditor.target; | |
| if (material.IsKeywordEnabled(keyword)) { | |
| return false; | |
| } | |
| } | |
| if (categoryAttrib == null) { activeCategory = null; } | |
| foreach (var attrib in details.AttributeDetails) { | |
| if (attrib.Name == ATTRIB_TITLE) { | |
| if (propertySubIndex > 0) GUILayout.Space(10); | |
| EditorGUILayout.LabelField(attrib.Value, EditorStyles.boldLabel); | |
| } | |
| if (attrib.Name == ATTRIB_SUBTITLE) { | |
| if (propertySubIndex > 0) GUILayout.Space(10); | |
| EditorGUILayout.LabelField(attrib.Value, EditorStyles.miniBoldLabel); | |
| } | |
| if (attrib.Name == ATTRIB_NOTE) { | |
| if (propertySubIndex > 0) GUILayout.Space(2); | |
| Color c = GUI.color; | |
| GUI.color = c.WithAlpha(0.6f); | |
| EditorGUILayout.LabelField($"({attrib.Value})", EditorStyles.miniLabel); | |
| GUI.color = c; | |
| } | |
| } | |
| return true; | |
| } | |
| protected void ValidateShaderPropertyDetails(MaterialEditor materialEditor, MaterialProperty[] properties) { | |
| foreach (var property in properties) { | |
| ShaderPropertyDetails details = shaderPropertyDetails.FirstOrDefault(p => p.Name == property.name); | |
| if (details == null) { | |
| Revalidate(materialEditor, properties); | |
| return; | |
| } | |
| } | |
| } | |
| private void Revalidate(MaterialEditor materialEditor, MaterialProperty[] properties) { | |
| InitialiseShaderAttributes(materialEditor); | |
| BindMaterialProperties(materialEditor, properties); | |
| BuildCategoryLookup(); | |
| } | |
| protected void BindMaterialProperties(MaterialEditor materialEditor, MaterialProperty[] properties) { | |
| materialPropertyToShaderPropertyDetails.Clear(); | |
| foreach (var property in properties) { | |
| ShaderPropertyDetails details = shaderPropertyDetails.FirstOrDefault(p => p.Name == property.name); | |
| if (details != null) { materialPropertyToShaderPropertyDetails[property] = details; } | |
| } | |
| } | |
| protected void BuildCategoryLookup() { | |
| categories.Clear(); | |
| categoryToProperties.Clear(); | |
| foreach (var property in shaderPropertyDetails) { | |
| var categoryAttrib = property.AttributeDetails.FirstOrDefault(a => a.Name == "Category"); | |
| if (categoryAttrib == null) continue; | |
| if (!categories.Contains(categoryAttrib.Name)) categories.Add(categoryAttrib.Name); | |
| if (categoryToProperties[categoryAttrib.Name] == null) categoryToProperties[categoryAttrib.Name] = new List<ShaderPropertyDetails>(); | |
| categoryToProperties[categoryAttrib.Name].Add(property); | |
| } | |
| } | |
| protected void InitialiseShaderAttributes(MaterialEditor materialEditor) { | |
| shaderPropertyDetails.Clear(); | |
| // Get the material currently being edited | |
| Material material = materialEditor.target as Material; | |
| // From the material, you can get the Shader | |
| Shader shader = material.shader; | |
| string shaderAssetPath = AssetDatabase.GetAssetPath(shader); | |
| string shaderFileStr = File.ReadAllText(shaderAssetPath); | |
| string propertyPattern = "^\\s*((?:\\[[^\\]]*\\]\\s*)*)" + // attributes | |
| "(_\\w+)\\s*" + // property name | |
| "\\(\\s*\"([^\"]+)\"\\s*,\\s*" + // display name inside quotes | |
| "(.*?)\\s*\\)\\s*=\\s*" + // type: lazy match everything until closing ) and = | |
| "([^\\r\\n]+)"; // default value | |
| Regex propertyRegex = new Regex(propertyPattern, RegexOptions.Multiline); | |
| string attributePattern = @"\[(\w+)(?:\((.*?)\))?\]"; | |
| Regex attributeRegex = new Regex(attributePattern); | |
| foreach (Match match in propertyRegex.Matches(shaderFileStr)) { | |
| string attributesRaw = match.Groups[1].Value; // e.g. "[Toggle][HDR("thing")]" | |
| string propertyName = match.Groups[2].Value; | |
| string displayName = match.Groups[3].Value; | |
| string type = match.Groups[4].Value; | |
| string defaultValue = match.Groups[5].Value; | |
| var attributeList = ListPool<ShaderPropertyAttributeDetails>.New(); | |
| foreach (Match attrMatch in attributeRegex.Matches(attributesRaw)) { | |
| var attr = new ShaderPropertyAttributeDetails { | |
| Name = attrMatch.Groups[1].Value, | |
| Value = attrMatch.Groups[2].Success ? attrMatch.Groups[2].Value : null | |
| }; | |
| attributeList.Add(attr); | |
| } | |
| var propertyDetails = new ShaderPropertyDetails { | |
| Name = propertyName, | |
| DisplayName = displayName, | |
| Type = type, | |
| DefaultValue = defaultValue, | |
| AttributeDetails = attributeList.ToArray() | |
| }; | |
| ListPool<ShaderPropertyAttributeDetails>.Return(attributeList); | |
| shaderPropertyDetails.Add(propertyDetails); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment