// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.CSharp;
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
namespace Microsoft.MixedReality.GraphicsTools.Editor
{
///
/// General utility methods to help with shader development and usage.
///
public static class ShaderUtilities
{
///
/// CanvasMaterialAnimator formatting strings.
///
private static readonly string ClassBody =
@"// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEngine;
namespace Microsoft.MixedReality.GraphicsTools
{{
///
/// This class was auto generated via Assets > Graphics Tools > Generate Canvas Material Animator.
/// Use Unity's animation system to animate fields on this class to drive material properties on CanvasRenderers.
/// Version={0}
///
public class {1} : CanvasMaterialAnimatorBase
{{{2}
///
public override void InitializeFromMaterial(Material material)
{{{3}
}}
///
public override void ApplyToMaterial(Material material)
{{{4}
}}
///
public override string GetTargetShaderName()
{{{5}
}}
}}
}}
";
private static readonly string PropertyBody =
@" ///
/// Shader property.
/// ;
public {0} {1} = {2};";
private static readonly string PropertyBodyRange =
@" ///
/// Shader property.
/// ;
[Range({3}, {4})] public {0} {1} = {2};";
private static readonly string PropertyIDBody =
@" ///
/// Shader property ID.
///
public static int {0} = Shader.PropertyToID(""{1}"");";
private static readonly string FromMaterialBody = " {0} = material.{1}({2});";
private static readonly string FromMaterialBodyCast = " {0} = ({3})material.{1}({2});";
private static readonly string ToMaterialBody = " material.{0}({1}, {2});";
private static readonly string ToMaterialBodyCast = " material.{0}({1}, ({3}){2});";
private static readonly string TargetShaderNameBody = "{0} return \"{1}\";";
private static readonly string PropertyID = "{0}ID";
private static readonly string FloatPostfix = "f";
private static readonly string PropertyHeader =
@"
[Header(""{0}"")]";
///
/// Creates a new component which contains serialized properties for each of the shader's exposed properties and methods to set and apply their state.
///
public static void GenerateCanvasMaterialAnimator(Shader shader, bool saveToParentDirectory = false, bool saveToCanvasDirectory = false)
{
// Generate the properties and methods.
string properties = string.Empty;
string fromMaterial = string.Empty;
string toMaterial = string.Empty;
string targetShaderName = string.Format(TargetShaderNameBody, Environment.NewLine, shader.name);
int count = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < count; ++i)
{
string name = ShaderUtil.GetPropertyName(shader, i);
string nameID = string.Format(PropertyID, name);
ShaderUtil.ShaderPropertyType type = ShaderUtil.GetPropertyType(shader, i);
string[] propertyAttributes = shader.GetPropertyAttributes(i);
string headerAttribute = Array.Find(propertyAttributes, IsHeaderAttribute);
if (!string.IsNullOrEmpty(headerAttribute))
{
properties += Environment.NewLine;
properties += string.Format(PropertyHeader, GetHeaderName(headerAttribute));
}
// Textures.
if (type == ShaderUtil.ShaderPropertyType.TexEnv)
{
TextureDimension dimension = ShaderUtil.GetTexDim(shader, i);
string typeName = TextureDimensionToTypeName(dimension);
properties += Environment.NewLine;
properties += string.Format(PropertyBody, typeName, name, "null");
properties += Environment.NewLine;
properties += string.Format(PropertyIDBody, nameID, name);
fromMaterial += Environment.NewLine;
fromMaterial += string.Format(FromMaterialBodyCast, name, ShaderPropertyTypeToGettor(type), nameID, typeName);
toMaterial += Environment.NewLine;
toMaterial += string.Format(ToMaterialBodyCast, ShaderPropertyTypeToSettor(type), nameID, name, typeName);
}
else // All other types. Colors, floats, vectors, etc.
{
string defaultValue, minValue = null, maxValue = null;
if (type == ShaderUtil.ShaderPropertyType.Float)
{
defaultValue = ShaderUtil.GetRangeLimits(shader, i, 0).ToString() + FloatPostfix;
}
else if (type == ShaderUtil.ShaderPropertyType.Range)
{
defaultValue = ShaderUtil.GetRangeLimits(shader, i, 0).ToString() + FloatPostfix;
minValue = ShaderUtil.GetRangeLimits(shader, i, 1).ToString() + FloatPostfix;
maxValue = ShaderUtil.GetRangeLimits(shader, i, 2) + FloatPostfix;
}
else
{
defaultValue = ShaderPropertyTypeToDefault(type);
}
properties += Environment.NewLine;
if (minValue == null || maxValue == null)
{
properties += string.Format(PropertyBody, ShaderPropertyTypeToTypeName(type), name, defaultValue);
}
else
{
properties += string.Format(PropertyBodyRange, ShaderPropertyTypeToTypeName(type), name, defaultValue, minValue, maxValue);
}
properties += Environment.NewLine;
properties += string.Format(PropertyIDBody, nameID, name);
fromMaterial += Environment.NewLine;
fromMaterial += string.Format(FromMaterialBody, name, ShaderPropertyTypeToGettor(type), nameID);
toMaterial += Environment.NewLine;
toMaterial += string.Format(ToMaterialBody, ShaderPropertyTypeToSettor(type), nameID, name);
}
}
try
{
// Save a new component out as a C# class.
string version = "0.1.0";
string assetPath = AssetDatabase.GetAssetPath(shader);
string className = SanitizeIdentifier("CanvasMaterialAnimator" + Path.GetFileNameWithoutExtension(assetPath));
string classText = string.Format(ClassBody, version, className, properties, fromMaterial, toMaterial, targetShaderName);
string directory = Path.GetDirectoryName(assetPath);
if (saveToParentDirectory)
{
directory = Path.Combine(directory, "..");
}
if (saveToCanvasDirectory)
{
directory = Path.Combine(directory, "Canvas");
Directory.CreateDirectory(directory);
}
string path = Path.Combine(directory, string.Format("{0}.cs", className));
File.WriteAllText(path, classText);
AssetDatabase.Refresh();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
///
/// Menu item to right click on a shader asset in the Project window and call GenerateCanvasMaterialAnimator on that specific shader.
///
[MenuItem("Assets/Graphics Tools/Generate Canvas Material Animator")]
private static void GenerateCanvasMaterialAnimator()
{
foreach (UnityEngine.Object selection in Selection.objects)
{
Shader shader = selection as Shader;
if (shader == null)
{
Debug.LogWarningFormat("The selection {0} is not a shader.", selection.name);
continue;
}
GenerateCanvasMaterialAnimator(shader);
}
}
///
/// Ensures a shader asset was right clicked on.
///
[MenuItem("Assets/Graphics Tools/Generate Canvas Material Animator", true)]
private static bool ValidateGenerateCanvasMaterialAnimator()
{
foreach (UnityEngine.Object selection in Selection.objects)
{
if (selection.GetType() == typeof(Shader))
{
return true;
}
}
return false;
}
///
/// Toggles if text color inversion is enabled on the material.
///
[MenuItem("Assets/Graphics Tools/Accessibility/Toggle Text Color Inversion")]
private static void ToggleTextColorInversion()
{
foreach (UnityEngine.Object selection in Selection.objects)
{
Material material = selection as Material;
if (material == null)
{
Debug.LogWarningFormat("The selection {0} is not a material.", selection.name);
continue;
}
AccessibilityUtilities.ToggleTextColorInversion(material);
}
}
///
/// Ensures a material asset was right clicked on.
///
[MenuItem("Assets/Graphics Tools/Accessibility/Toggle Text Color Inversion", true)]
private static bool ValidateToggleTextColorInversion()
{
foreach (UnityEngine.Object selection in Selection.objects)
{
if (selection.GetType() == typeof(Material))
{
return true;
}
}
return false;
}
///
/// Menu item to call GenerateCanvasMaterialAnimator for each shader within Assets/Runtime/Shaders that does not have "Non-Canvas" in the name.
///
[MenuItem("Window/Graphics Tools/Canvas Material Animators/Generate All")]
private static void CanvasMaterialAnimatorsGenerateAll()
{
string[] paths = Directory.GetFiles(string.Format("Packages/{0}/Runtime/Shaders", DevelopmentUtilities.PackageName), "*.shader");
foreach (string path in paths)
{
Shader shader = (Shader)AssetDatabase.LoadAssetAtPath(path, typeof(Shader));
if (shader != null && !shader.name.Contains("Non-Canvas"))
{
GenerateCanvasMaterialAnimator(shader, true, true);
}
}
}
///
/// Menu item validation.
///
[MenuItem("Window/Graphics Tools/Canvas Material Animators/Generate All", true)]
private static bool ValidateCanvasMaterialAnimatorsGenerateAll()
{
return DevelopmentUtilities.IsPackageMutable();
}
///
/// ShaderPropertyType to C# type conversion.
///
private static string ShaderPropertyTypeToTypeName(ShaderUtil.ShaderPropertyType type)
{
switch (type)
{
case ShaderUtil.ShaderPropertyType.Color: return "Color";
case ShaderUtil.ShaderPropertyType.Vector: return "Vector4";
default:
case ShaderUtil.ShaderPropertyType.Float:
case ShaderUtil.ShaderPropertyType.Range: return "float";
case ShaderUtil.ShaderPropertyType.TexEnv: return "Texture";
}
}
///
/// ShaderPropertyType to Unity material gettor method.
///
private static string ShaderPropertyTypeToGettor(ShaderUtil.ShaderPropertyType type)
{
switch (type)
{
case ShaderUtil.ShaderPropertyType.Color: return "GetColor";
case ShaderUtil.ShaderPropertyType.Vector: return "GetVector";
default:
case ShaderUtil.ShaderPropertyType.Float:
case ShaderUtil.ShaderPropertyType.Range: return "GetFloat";
case ShaderUtil.ShaderPropertyType.TexEnv: return "GetTexture";
}
}
///
/// ShaderPropertyType to Unity material settor method.
///
private static string ShaderPropertyTypeToSettor(ShaderUtil.ShaderPropertyType type)
{
switch (type)
{
case ShaderUtil.ShaderPropertyType.Color: return "SetColor";
case ShaderUtil.ShaderPropertyType.Vector: return "SetVector";
default:
case ShaderUtil.ShaderPropertyType.Float:
case ShaderUtil.ShaderPropertyType.Range: return "SetFloat";
case ShaderUtil.ShaderPropertyType.TexEnv: return "SetTexture";
}
}
///
/// TextureDimension to C# type conversion.
///
private static string TextureDimensionToTypeName(TextureDimension dimension)
{
switch (dimension)
{
default:
case TextureDimension.Tex2D: return "Texture2D";
case TextureDimension.Tex3D: return "Texture3D";
case TextureDimension.Cube: return "Cubemap";
case TextureDimension.Tex2DArray: return "Texture2DArray";
case TextureDimension.CubeArray: return "CubemapArray";
}
}
///
/// ShaderPropertyType to default C# type value.
///
private static string ShaderPropertyTypeToDefault(ShaderUtil.ShaderPropertyType type)
{
switch (type)
{
case ShaderUtil.ShaderPropertyType.Color: return "Color.white";
case ShaderUtil.ShaderPropertyType.Vector: return "Vector4.zero";
default:
case ShaderUtil.ShaderPropertyType.Float:
case ShaderUtil.ShaderPropertyType.Range: return "0.0f";
case ShaderUtil.ShaderPropertyType.TexEnv: return "null";
}
}
///
/// Modifies a string so that it adheres to C# identifier rules: https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
///
private static string SanitizeIdentifier(string input)
{
bool isValid = CSharpCodeProvider.CreateProvider("C#").IsValidIdentifier(input);
if (!isValid)
{
// Remove invalid characters.
Regex regex = new Regex(@"[^\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}\p{Nl}\p{Mn}\p{Mc}\p{Cf}\p{Pc}\p{Lm}]");
input = regex.Replace(input, string.Empty);
// Ensure class/variable name begins with a letter or underscore.
if (!char.IsLetter(input, 0))
{
input = input.Insert(0, "_");
}
}
return RemoveWhitespace(input);
}
///
/// Removes all white space from a string (spaces, tabs, etc.).
///
private static string RemoveWhitespace(string input)
{
return new string(input.ToCharArray().Where(c => !Char.IsWhiteSpace(c)).ToArray());
}
///
/// Checks if string is a Header attribute
///
private static bool IsHeaderAttribute(string a)
{
// Regex to check if attribute is type, Header
return Regex.IsMatch(a, @"Header\(.*\)");
}
///
/// Returns the Header name as a string
///
private static string GetHeaderName(string headerAttribute)
{
int startPosition = headerAttribute.IndexOf("(") + 1;
int wordLength = headerAttribute.IndexOf(")", startPosition) - startPosition;
return headerAttribute.Substring(startPosition, wordLength);
}
}
}