Skip to content

Instantly share code, notes, and snippets.

@unitycoder
Created November 3, 2025 11:03
Show Gist options
  • Select an option

  • Save unitycoder/b9b8bea826944b8e3ae61d20c3ec3982 to your computer and use it in GitHub Desktop.

Select an option

Save unitycoder/b9b8bea826944b8e3ae61d20c3ec3982 to your computer and use it in GitHub Desktop.
DSShaderGUI from DigitalSalmon
// 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