You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HighGroundRoyaleNetcode/Assets/JMO Assets/Cartoon FX Remaster/CFXR Assets/Editor/CFXR_MaterialInspector.cs

592 lines
19 KiB
C#

//--------------------------------------------------------------------------------------------------------------------------------
// 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;
}
}
}