You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
592 lines
19 KiB
592 lines
19 KiB
//-------------------------------------------------------------------------------------------------------------------------------- |
|
// Cartoon FX |
|
// (c) 2012-2020 Jean Moreno |
|
//-------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
using UnityEngine; |
|
using UnityEditor; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Text.RegularExpressions; |
|
|
|
// Custom material inspector for Stylized FX shaders |
|
// - organize UI using comments in the shader code |
|
// - more flexibility than the material property drawers |
|
// version 2 (dec 2017) |
|
|
|
namespace CartoonFX |
|
{ |
|
public class MaterialInspector : ShaderGUI |
|
{ |
|
//Set by PropertyDrawers to defined if the next properties should be visible |
|
static private Stack<bool> ShowStack = new Stack<bool>(); |
|
|
|
static public bool ShowNextProperty { get; private set; } |
|
static public void PushShowProperty(bool value) |
|
{ |
|
ShowStack.Push(ShowNextProperty); |
|
ShowNextProperty &= value; |
|
} |
|
static public void PopShowProperty() |
|
{ |
|
ShowNextProperty = ShowStack.Pop(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
const string kGuiCommandPrefix = "//#"; |
|
const string kGC_IfKeyword = "IF_KEYWORD"; |
|
const string kGC_IfProperty = "IF_PROPERTY"; |
|
const string kGC_EndIf = "END_IF"; |
|
const string kGC_HelpBox = "HELP_BOX"; |
|
const string kGC_Label = "LABEL"; |
|
|
|
Dictionary<int, List<GUICommand>> guiCommands = new Dictionary<int, List<GUICommand>>(); |
|
|
|
bool initialized = false; |
|
AssetImporter shaderImporter; |
|
ulong lastTimestamp; |
|
void Initialize(MaterialEditor editor, bool force) |
|
{ |
|
if((!initialized || force) && editor != null) |
|
{ |
|
initialized = true; |
|
|
|
guiCommands.Clear(); |
|
|
|
//Find the shader and parse the source to find special comments that will organize the GUI |
|
//It's hackish, but at least it allows any character to be used (unlike material property drawers/decorators) and can be used along with property drawers |
|
|
|
var materials = new List<Material>(); |
|
foreach(var o in editor.targets) |
|
{ |
|
var m = o as Material; |
|
if(m != null) |
|
materials.Add(m); |
|
} |
|
if(materials.Count > 0 && materials[0].shader != null) |
|
{ |
|
var path = AssetDatabase.GetAssetPath(materials[0].shader); |
|
//get asset importer |
|
shaderImporter = AssetImporter.GetAtPath(path); |
|
if(shaderImporter != null) |
|
{ |
|
lastTimestamp = shaderImporter.assetTimeStamp; |
|
} |
|
//remove 'Assets' and replace with OS path |
|
path = Application.dataPath + path.Substring(6); |
|
//convert to cross-platform path |
|
path = path.Replace('/', Path.DirectorySeparatorChar); |
|
//open file for reading |
|
var lines = File.ReadAllLines(path); |
|
|
|
bool insideProperties = false; |
|
//regex pattern to find properties, as they need to be counted so that |
|
//special commands can be inserted at the right position when enumerating them |
|
var regex = new Regex(@"[a-zA-Z0-9_]+\s*\([^\)]*\)"); |
|
int propertyCount = 0; |
|
bool insideCommentBlock = false; |
|
foreach(var l in lines) |
|
{ |
|
var line = l.TrimStart(); |
|
|
|
if(insideProperties) |
|
{ |
|
bool isComment = line.StartsWith("//"); |
|
|
|
if(line.Contains("/*")) |
|
insideCommentBlock = true; |
|
if(line.Contains("*/")) |
|
insideCommentBlock = false; |
|
|
|
//finished properties block? |
|
if(line.StartsWith("}")) |
|
break; |
|
|
|
//comment |
|
if(line.StartsWith(kGuiCommandPrefix)) |
|
{ |
|
string command = line.Substring(kGuiCommandPrefix.Length).TrimStart(); |
|
//space |
|
if(string.IsNullOrEmpty(command)) |
|
AddGUICommand(propertyCount, new GC_Space()); |
|
//separator |
|
else if(command.StartsWith("---")) |
|
AddGUICommand(propertyCount, new GC_Separator()); |
|
//separator |
|
else if(command.StartsWith("===")) |
|
AddGUICommand(propertyCount, new GC_SeparatorDouble()); |
|
//if keyword |
|
else if(command.StartsWith(kGC_IfKeyword)) |
|
{ |
|
var expr = command.Substring(command.LastIndexOf(kGC_IfKeyword) + kGC_IfKeyword.Length + 1); |
|
AddGUICommand(propertyCount, new GC_IfKeyword() { expression = expr, materials = materials.ToArray() }); |
|
} |
|
//if property |
|
else if(command.StartsWith(kGC_IfProperty)) |
|
{ |
|
var expr = command.Substring(command.LastIndexOf(kGC_IfProperty) + kGC_IfProperty.Length + 1); |
|
AddGUICommand(propertyCount, new GC_IfProperty() { expression = expr, materials = materials.ToArray() }); |
|
} |
|
//end if |
|
else if(command.StartsWith(kGC_EndIf)) |
|
{ |
|
AddGUICommand(propertyCount, new GC_EndIf()); |
|
} |
|
//help box |
|
else if(command.StartsWith(kGC_HelpBox)) |
|
{ |
|
var messageType = MessageType.Error; |
|
var message = "Invalid format for HELP_BOX:\n" + command; |
|
var cmd = command.Substring(command.LastIndexOf(kGC_HelpBox) + kGC_HelpBox.Length + 1).Split(new string[] { "::" }, System.StringSplitOptions.RemoveEmptyEntries); |
|
if(cmd.Length == 1) |
|
{ |
|
message = cmd[0]; |
|
messageType = MessageType.None; |
|
} |
|
else if(cmd.Length == 2) |
|
{ |
|
try |
|
{ |
|
var msgType = (MessageType)System.Enum.Parse(typeof(MessageType), cmd[0], true); |
|
message = cmd[1].Replace(" ", "\n"); |
|
messageType = msgType; |
|
} |
|
catch { } |
|
} |
|
|
|
AddGUICommand(propertyCount, new GC_HelpBox() |
|
{ |
|
message = message, |
|
messageType = messageType |
|
}); |
|
} |
|
//label |
|
else if(command.StartsWith(kGC_Label)) |
|
{ |
|
var label = command.Substring(command.LastIndexOf(kGC_Label) + kGC_Label.Length + 1); |
|
AddGUICommand(propertyCount, new GC_Label() { label = label }); |
|
} |
|
//header: plain text after command |
|
else |
|
{ |
|
AddGUICommand(propertyCount, new GC_Header() { label = command }); |
|
} |
|
} |
|
else |
|
//property |
|
{ |
|
if(regex.IsMatch(line) && !insideCommentBlock && !isComment) |
|
propertyCount++; |
|
} |
|
} |
|
|
|
//start properties block? |
|
if(line.StartsWith("Properties")) |
|
{ |
|
insideProperties = true; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
void AddGUICommand(int propertyIndex, GUICommand command) |
|
{ |
|
if(!guiCommands.ContainsKey(propertyIndex)) |
|
guiCommands.Add(propertyIndex, new List<GUICommand>()); |
|
|
|
guiCommands[propertyIndex].Add(command); |
|
} |
|
|
|
public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader) |
|
{ |
|
initialized = false; |
|
base.AssignNewShaderToMaterial(material, oldShader, newShader); |
|
} |
|
|
|
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) |
|
{ |
|
//init: |
|
//- read metadata in properties comment to generate ui layout |
|
//- force update if timestamp doesn't match last (= file externally updated) |
|
bool force = (shaderImporter != null && shaderImporter.assetTimeStamp != lastTimestamp); |
|
Initialize(materialEditor, force); |
|
|
|
var shader = (materialEditor.target as Material).shader; |
|
materialEditor.SetDefaultGUIWidths(); |
|
|
|
//show all properties by default |
|
ShowNextProperty = true; |
|
ShowStack.Clear(); |
|
|
|
for(int i = 0; i < properties.Length; i++) |
|
{ |
|
if(guiCommands.ContainsKey(i)) |
|
{ |
|
for(int j = 0; j < guiCommands[i].Count; j++) |
|
{ |
|
guiCommands[i][j].OnGUI(); |
|
} |
|
} |
|
|
|
//Use custom properties to enable/disable groups based on keywords |
|
if(ShowNextProperty) |
|
{ |
|
if((properties[i].flags & (MaterialProperty.PropFlags.HideInInspector | MaterialProperty.PropFlags.PerRendererData)) == MaterialProperty.PropFlags.None) |
|
{ |
|
DisplayProperty(properties[i], materialEditor); |
|
} |
|
} |
|
} |
|
|
|
//make sure to show gui commands that are after properties |
|
int index = properties.Length; |
|
if(guiCommands.ContainsKey(index)) |
|
{ |
|
for(int j = 0; j < guiCommands[index].Count; j++) |
|
{ |
|
guiCommands[index][j].OnGUI(); |
|
} |
|
} |
|
|
|
//Special fields |
|
Styles.MaterialDrawSeparatorDouble(); |
|
materialEditor.RenderQueueField(); |
|
materialEditor.EnableInstancingField(); |
|
} |
|
|
|
virtual protected void DisplayProperty(MaterialProperty property, MaterialEditor materialEditor) |
|
{ |
|
float propertyHeight = materialEditor.GetPropertyHeight(property, property.displayName); |
|
Rect controlRect = EditorGUILayout.GetControlRect(true, propertyHeight, EditorStyles.layerMaskField, new GUILayoutOption[0]); |
|
materialEditor.ShaderProperty(controlRect, property, property.displayName); |
|
} |
|
} |
|
|
|
// Same as Toggle drawer, but doesn't set any keyword |
|
// This will avoid adding unnecessary shader keyword to the project |
|
internal class MaterialToggleNoKeywordDrawer : MaterialPropertyDrawer |
|
{ |
|
private static bool IsPropertyTypeSuitable(MaterialProperty prop) |
|
{ |
|
return prop.type == MaterialProperty.PropType.Float || prop.type == MaterialProperty.PropType.Range; |
|
} |
|
|
|
public override float GetPropertyHeight(MaterialProperty prop, string label, MaterialEditor editor) |
|
{ |
|
float height; |
|
if (!MaterialToggleNoKeywordDrawer.IsPropertyTypeSuitable(prop)) |
|
{ |
|
height = 40f; |
|
} |
|
else |
|
{ |
|
height = base.GetPropertyHeight(prop, label, editor); |
|
} |
|
return height; |
|
} |
|
|
|
public override void OnGUI(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor) |
|
{ |
|
if (!MaterialToggleNoKeywordDrawer.IsPropertyTypeSuitable(prop)) |
|
{ |
|
EditorGUI.HelpBox(position, "Toggle used on a non-float property: " + prop.name, MessageType.Warning); |
|
} |
|
else |
|
{ |
|
EditorGUI.BeginChangeCheck(); |
|
bool flag = Mathf.Abs(prop.floatValue) > 0.001f; |
|
EditorGUI.showMixedValue = prop.hasMixedValue; |
|
flag = EditorGUI.Toggle(position, label, flag); |
|
EditorGUI.showMixedValue = false; |
|
if (EditorGUI.EndChangeCheck()) |
|
{ |
|
prop.floatValue = ((!flag) ? 0f : 1f); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Same as KeywordEnum drawer, but uses the keyword supplied as is rather than adding a prefix to them |
|
internal class MaterialKeywordEnumNoPrefixDrawer : MaterialPropertyDrawer |
|
{ |
|
private readonly GUIContent[] labels; |
|
private readonly string[] keywords; |
|
|
|
public MaterialKeywordEnumNoPrefixDrawer(string lbl1, string kw1) : this(new[] { lbl1 }, new[] { kw1 }) { } |
|
public MaterialKeywordEnumNoPrefixDrawer(string lbl1, string kw1, string lbl2, string kw2) : this(new[] { lbl1, lbl2 }, new[] { kw1, kw2 }) { } |
|
public MaterialKeywordEnumNoPrefixDrawer(string lbl1, string kw1, string lbl2, string kw2, string lbl3, string kw3) : this(new[] { lbl1, lbl2, lbl3 }, new[] { kw1, kw2, kw3 }) { } |
|
public MaterialKeywordEnumNoPrefixDrawer(string lbl1, string kw1, string lbl2, string kw2, string lbl3, string kw3, string lbl4, string kw4) : this(new[] { lbl1, lbl2, lbl3, lbl4 }, new[] { kw1, kw2, kw3, kw4 }) { } |
|
public MaterialKeywordEnumNoPrefixDrawer(string lbl1, string kw1, string lbl2, string kw2, string lbl3, string kw3, string lbl4, string kw4, string lbl5, string kw5) : this(new[] { lbl1, lbl2, lbl3, lbl4, lbl5 }, new[] { kw1, kw2, kw3, kw4, kw5 }) { } |
|
public MaterialKeywordEnumNoPrefixDrawer(string lbl1, string kw1, string lbl2, string kw2, string lbl3, string kw3, string lbl4, string kw4, string lbl5, string kw5, string lbl6, string kw6) : this(new[] { lbl1, lbl2, lbl3, lbl4, lbl5, lbl6 }, new[] { kw1, kw2, kw3, kw4, kw5, kw6 }) { } |
|
|
|
public MaterialKeywordEnumNoPrefixDrawer(string[] labels, string[] keywords) |
|
{ |
|
this.labels= new GUIContent[keywords.Length]; |
|
this.keywords = new string[keywords.Length]; |
|
for (int i = 0; i < keywords.Length; ++i) |
|
{ |
|
this.labels[i] = new GUIContent(labels[i]); |
|
this.keywords[i] = keywords[i]; |
|
} |
|
} |
|
|
|
static bool IsPropertyTypeSuitable(MaterialProperty prop) |
|
{ |
|
return prop.type == MaterialProperty.PropType.Float || prop.type == MaterialProperty.PropType.Range; |
|
} |
|
|
|
void SetKeyword(MaterialProperty prop, int index) |
|
{ |
|
for (int i = 0; i < keywords.Length; ++i) |
|
{ |
|
string keyword = GetKeywordName(prop.name, keywords[i]); |
|
foreach (Material material in prop.targets) |
|
{ |
|
if (index == i) |
|
material.EnableKeyword(keyword); |
|
else |
|
material.DisableKeyword(keyword); |
|
} |
|
} |
|
} |
|
|
|
public override float GetPropertyHeight(MaterialProperty prop, string label, MaterialEditor editor) |
|
{ |
|
if (!IsPropertyTypeSuitable(prop)) |
|
{ |
|
return EditorGUIUtility.singleLineHeight * 2.5f; |
|
} |
|
return base.GetPropertyHeight(prop, label, editor); |
|
} |
|
|
|
public override void OnGUI(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor) |
|
{ |
|
if (!IsPropertyTypeSuitable(prop)) |
|
{ |
|
EditorGUI.HelpBox(position, "Toggle used on a non-float property: " + prop.name, MessageType.Warning); |
|
return; |
|
} |
|
|
|
EditorGUI.BeginChangeCheck(); |
|
|
|
EditorGUI.showMixedValue = prop.hasMixedValue; |
|
var value = (int)prop.floatValue; |
|
value = EditorGUI.Popup(position, label, value, labels); |
|
EditorGUI.showMixedValue = false; |
|
if (EditorGUI.EndChangeCheck()) |
|
{ |
|
prop.floatValue = value; |
|
SetKeyword(prop, value); |
|
} |
|
} |
|
|
|
public override void Apply(MaterialProperty prop) |
|
{ |
|
base.Apply(prop); |
|
if (!IsPropertyTypeSuitable(prop)) |
|
return; |
|
|
|
if (prop.hasMixedValue) |
|
return; |
|
|
|
SetKeyword(prop, (int)prop.floatValue); |
|
} |
|
|
|
// Final keyword name: property name + "_" + display name. Uppercased, |
|
// and spaces replaced with underscores. |
|
private static string GetKeywordName(string propName, string name) |
|
{ |
|
// Just return the supplied name |
|
return name; |
|
|
|
// Original code: |
|
/* |
|
string n = propName + "_" + name; |
|
return n.Replace(' ', '_').ToUpperInvariant(); |
|
*/ |
|
} |
|
} |
|
|
|
|
|
//================================================================================================================================================================================================ |
|
// GUI Commands System |
|
// |
|
// Workaround to Material Property Drawers limitations: |
|
// - uses shader comments to organize the GUI, and show/hide properties based on conditions |
|
// - can use any character (unlike property drawers) |
|
// - parsed once at material editor initialization |
|
|
|
internal class GUICommand |
|
{ |
|
public virtual bool Visible() { return true; } |
|
public virtual void OnGUI() { } |
|
} |
|
|
|
internal class GC_Separator : GUICommand { public override void OnGUI() { if(MaterialInspector.ShowNextProperty) Styles.MaterialDrawSeparator(); } } |
|
internal class GC_SeparatorDouble : GUICommand { public override void OnGUI() { if(MaterialInspector.ShowNextProperty) Styles.MaterialDrawSeparatorDouble(); } } |
|
internal class GC_Space : GUICommand { public override void OnGUI() { if(MaterialInspector.ShowNextProperty) GUILayout.Space(8); } } |
|
internal class GC_HelpBox : GUICommand |
|
{ |
|
public string message { get; set; } |
|
public MessageType messageType { get; set; } |
|
|
|
public override void OnGUI() |
|
{ |
|
if(MaterialInspector.ShowNextProperty) |
|
Styles.HelpBoxRichText(message, messageType); |
|
} |
|
} |
|
internal class GC_Header : GUICommand |
|
{ |
|
public string label { get; set; } |
|
GUIContent guiContent; |
|
|
|
public override void OnGUI() |
|
{ |
|
if(guiContent == null) |
|
guiContent = new GUIContent(label); |
|
|
|
if(MaterialInspector.ShowNextProperty) |
|
Styles.MaterialDrawHeader(guiContent); |
|
} |
|
} |
|
internal class GC_Label : GUICommand |
|
{ |
|
public string label { get; set; } |
|
GUIContent guiContent; |
|
|
|
public override void OnGUI() |
|
{ |
|
if(guiContent == null) |
|
guiContent = new GUIContent(label); |
|
|
|
if(MaterialInspector.ShowNextProperty) |
|
GUILayout.Label(guiContent); |
|
} |
|
} |
|
internal class GC_IfKeyword : GUICommand |
|
{ |
|
public string expression { get; set; } |
|
public Material[] materials { get; set; } |
|
public override void OnGUI() |
|
{ |
|
bool show = ExpressionParser.EvaluateExpression(expression, (string s) => |
|
{ |
|
foreach(var m in materials) |
|
{ |
|
if(m.IsKeywordEnabled(s)) |
|
return true; |
|
} |
|
return false; |
|
}); |
|
MaterialInspector.PushShowProperty(show); |
|
} |
|
} |
|
internal class GC_EndIf : GUICommand { public override void OnGUI() { MaterialInspector.PopShowProperty(); } } |
|
|
|
internal class GC_IfProperty : GUICommand |
|
{ |
|
string _expression; |
|
public string expression |
|
{ |
|
get { return _expression; } |
|
set { _expression = value.Replace("!=", "<>"); } |
|
} |
|
public Material[] materials { get; set; } |
|
|
|
public override void OnGUI() |
|
{ |
|
bool show = ExpressionParser.EvaluateExpression(expression, EvaluatePropertyExpression); |
|
MaterialInspector.PushShowProperty(show); |
|
} |
|
|
|
bool EvaluatePropertyExpression(string expr) |
|
{ |
|
//expression is expected to be in the form of: property operator value |
|
var reader = new StringReader(expr); |
|
string property = ""; |
|
string op = ""; |
|
float value = 0f; |
|
|
|
int overflow = 0; |
|
while(true) |
|
{ |
|
char c = (char)reader.Read(); |
|
|
|
//operator |
|
if(c == '=' || c == '>' || c == '<' || c == '!') |
|
{ |
|
op += c; |
|
//second operator character, if any |
|
char c2 = (char)reader.Peek(); |
|
if(c2 == '=' || c2 == '>') |
|
{ |
|
reader.Read(); |
|
op += c2; |
|
} |
|
|
|
//end of string is the value |
|
var end = reader.ReadToEnd(); |
|
if(!float.TryParse(end, out value)) |
|
{ |
|
Debug.LogError("Couldn't parse float from property expression:\n" + end); |
|
return false; |
|
} |
|
|
|
break; |
|
} |
|
|
|
//property name |
|
property += c; |
|
|
|
overflow++; |
|
if(overflow >= 9999) |
|
{ |
|
Debug.LogError("Expression parsing overflow!\n"); |
|
return false; |
|
} |
|
} |
|
|
|
//evaluate property |
|
bool conditionMet = false; |
|
foreach(var m in materials) |
|
{ |
|
float propValue = 0f; |
|
if(property.Contains(".x") || property.Contains(".y") || property.Contains(".z") || property.Contains(".w")) |
|
{ |
|
string[] split = property.Split('.'); |
|
string component = split[1]; |
|
switch(component) |
|
{ |
|
case "x": propValue = m.GetVector(split[0]).x; break; |
|
case "y": propValue = m.GetVector(split[0]).y; break; |
|
case "z": propValue = m.GetVector(split[0]).z; break; |
|
case "w": propValue = m.GetVector(split[0]).w; break; |
|
default: Debug.LogError("Invalid component for vector property: '" + property + "'"); break; |
|
} |
|
} |
|
else |
|
propValue = m.GetFloat(property); |
|
|
|
switch(op) |
|
{ |
|
case ">=": conditionMet = propValue >= value; break; |
|
case "<=": conditionMet = propValue <= value; break; |
|
case ">": conditionMet = propValue > value; break; |
|
case "<": conditionMet = propValue < value; break; |
|
case "<>": conditionMet = propValue != value; break; //not equal, "!=" is replaced by "<>" to prevent bug with leading ! ("not" operator) |
|
case "==": conditionMet = propValue == value; break; |
|
default: |
|
Debug.LogError("Invalid property expression:\n" + expr); |
|
break; |
|
} |
|
if(conditionMet) |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
} |