//Original Code from https://github.com/DarthShader/Kaj-Unity-Shaders /**MIT License Copyright (c) 2020 DarthShader Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**/ using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System; using System.IO; using System.Text.RegularExpressions; using System.Text; using System.Globalization; using System.Linq; using System.Security.Cryptography; using Object = UnityEngine.Object; using System.Reflection; #if VRC_SDK_VRCSDK3 using VRC.SDKBase; #endif #if VRC_SDK_VRCSDK2 using VRCSDK2; #endif #if VRC_SDK_VRCSDK2 || VRC_SDK_VRCSDK3 using VRC.SDKBase.Editor.BuildPipeline; #endif #if VRC_SDK_VRCSDK3 && !UDON using static VRC.SDK3.Avatars.Components.VRCAvatarDescriptor; using VRC.SDK3.Avatars.Components; #endif namespace Thry { public enum LightMode { Always=1, ForwardBase=2, ForwardAdd=4, Deferred=8, ShadowCaster=16, MotionVectors=32, PrepassBase=64, PrepassFinal=128, Vertex=256, VertexLMRGBM=512, VertexLM=1024 } // Static methods to generate new shader files with in-place constants based on a material's properties // and link that new shader to the material automatically public class ShaderOptimizer { // Tags public const string TAG_ORIGINAL_SHADER = "OriginalShader"; public const string TAG_ALL_MATERIALS_GUIDS_USING_THIS_LOCKED_SHADER = "AllLockedGUIDS"; //When locking don't include code from define blocks that are not enabled const bool REMOVE_UNUSED_IF_DEFS = true; // For some reason, 'if' statements with replaced constant (literal) conditions cause some compilation error // So until that is figured out, branches will be removed by default // Set to false if you want to keep UNITY_BRANCH and [branch] public static bool RemoveUnityBranches = true; // LOD Crossfade Dithing doesn't have multi_compile keyword correctly toggled at build time (its always included) so // this hard-coded material property will uncomment //#pragma multi_compile _ LOD_FADE_CROSSFADE in optimized .shader files public static readonly string LODCrossFadePropertyName = "_LODCrossfade"; // IgnoreProjector and ForceNoShadowCasting don't work as override tags, so material properties by these names // will determine whether or not //"IgnoreProjector"="True" etc. will be uncommented in optimized .shader files public static readonly string IgnoreProjectorPropertyName = "_IgnoreProjector"; public static readonly string ForceNoShadowCastingPropertyName = "_ForceNoShadowCasting"; // Material property suffix that controls whether the property of the same name gets baked into the optimized shader // e.g. if _Color exists and _ColorAnimated = 1, _Color will not be baked in public static readonly string AnimatedPropertySuffix = "Animated"; public static readonly string AnimatedTagSuffix = "Animated"; public static readonly string ExemptFromLockingSuffix = "NL"; // Currently, Material.SetShaderPassEnabled doesn't work on "ShadowCaster" lightmodes, // and doesn't let "ForwardAdd" lights get turned into vertex lights if "ForwardAdd" is simply disabled // vs. if the pases didn't exist at all in the shader. // The Optimizer will take a mask property by this name and attempt to correct these issues // by hard-removing the shadowcaster and fwdadd passes from the shader being optimized. public static readonly string DisabledLightModesPropertyName = "_LightModes"; // Property that determines whether or not to evaluate KSOInlineSamplerState comments. // Inline samplers can be used to get a wider variety of wrap/filter combinations at the cost // of only having 1x anisotropic filtering on all textures public static readonly string UseInlineSamplerStatesPropertyName = "_InlineSamplerStates"; private static bool UseInlineSamplerStates = true; // Material properties are put into each CGPROGRAM as preprocessor defines when the optimizer is run. // This is mainly targeted at culling interpolators and lines that rely on those interpolators. // (The compiler is not smart enough to cull VS output that isn't used anywhere in the PS) // Additionally, simply enabling the optimizer can define a keyword, whose name is stored here. // This keyword is added to the beginning of all passes, right after CGPROGRAM public static readonly string OptimizerEnabledKeyword = "OPTIMIZER_ENABLED"; // Mega shaders are expected to have geometry and tessellation shaders enabled by default, // but with the ability to be disabled by convention property names when the optimizer is run. // Additionally, they can be removed per-lightmode by the given property name plus // the lightmode name as a suffix (e.g. group_toggle_GeometryShadowCaster) // Geometry and Tessellation shaders are REMOVED by default, but if the main gorups // are enabled certain pass types are assumed to be ENABLED public static readonly string GeometryShaderEnabledPropertyName = "GeometryShader_Enabled"; public static readonly string TessellationEnabledPropertyName = "Tessellation_Enabled"; private static bool UseGeometry = false; private static bool UseGeometryForwardBase = true; private static bool UseGeometryForwardAdd = true; private static bool UseGeometryShadowCaster = true; private static bool UseGeometryMeta = true; private static bool UseTessellation = false; private static bool UseTessellationForwardBase = true; private static bool UseTessellationForwardAdd = true; private static bool UseTessellationShadowCaster = true; private static bool UseTessellationMeta = false; // Tessellation can be slightly optimized with a constant max tessellation factor attribute // on the hull shader. A non-animated property by this name will replace the argument of said // attribute if it exists. public static readonly string TessellationMaxFactorPropertyName = "_TessellationFactorMax"; enum LightModeType { None, ForwardBase, ForwardAdd, ShadowCaster, Meta }; private static LightModeType CurrentLightmode = LightModeType.None; // In-order list of inline sampler state names that will be replaced by InlineSamplerState() lines public static readonly string[] InlineSamplerStateNames = new string[] { "_linear_repeat", "_linear_clamp", "_linear_mirror", "_linear_mirroronce", "_point_repeat", "_point_clamp", "_point_mirror", "_point_mirroronce", "_trilinear_repeat", "_trilinear_clamp", "_trilinear_mirror", "_trilinear_mirroronce" }; // Would be better to dynamically parse the "C:\Program Files\UnityXXXX\Editor\Data\CGIncludes\" folder // to get version specific includes but eh public static readonly HashSet DefaultUnityShaderIncludes = new HashSet() { "UnityUI.cginc", "AutoLight.cginc", "GLSLSupport.glslinc", "HLSLSupport.cginc", "Lighting.cginc", "SpeedTreeBillboardCommon.cginc", "SpeedTreeCommon.cginc", "SpeedTreeVertex.cginc", "SpeedTreeWind.cginc", "TerrainEngine.cginc", "TerrainSplatmapCommon.cginc", "Tessellation.cginc", "UnityBuiltin2xTreeLibrary.cginc", "UnityBuiltin3xTreeLibrary.cginc", "UnityCG.cginc", "UnityCG.glslinc", "UnityCustomRenderTexture.cginc", "UnityDeferredLibrary.cginc", "UnityDeprecated.cginc", "UnityGBuffer.cginc", "UnityGlobalIllumination.cginc", "UnityImageBasedLighting.cginc", "UnityInstancing.cginc", "UnityLightingCommon.cginc", "UnityMetaPass.cginc", "UnityPBSLighting.cginc", "UnityShaderUtilities.cginc", "UnityShaderVariables.cginc", "UnityShadowLibrary.cginc", "UnitySprites.cginc", "UnityStandardBRDF.cginc", "UnityStandardConfig.cginc", "UnityStandardCore.cginc", "UnityStandardCoreForward.cginc", "UnityStandardCoreForwardSimple.cginc", "UnityStandardInput.cginc", "UnityStandardMeta.cginc", "UnityStandardParticleInstancing.cginc", "UnityStandardParticles.cginc", "UnityStandardParticleShadow.cginc", "UnityStandardShadow.cginc", "UnityStandardUtils.cginc" }; public static readonly HashSet ValidSeparators = new HashSet() { ' ', '\t', '\r', '\n', ';', ',', '.', '(', ')', '[', ']', '{', '}', '>', '<', '=', '!', '&', '|', '^', '+', '-', '*', '/', '#' }; public static readonly HashSet DontRemoveIfBranchesKeywords = new HashSet() { "UNITY_SINGLE_PASS_STEREO", "FORWARD_BASE_PASS", "FORWARD_ADD_PASS", "POINT", "SPOT" }; public static readonly HashSet KeywordsUsedByPragmas = new HashSet() { }; public static readonly string[] ValidPropertyDataTypes = new string[] { "int", "float", "float2", "float3", "float4", "half", "half2", "half3", "half4", "fixed", "fixed2", "fixed3", "fixed4" }; public static readonly HashSet IllegalPropertyRenames = new HashSet() { "_MainTex", "_Color", "_EmissionColor", "_BumpScale", "_Cutoff", "_DetailNormalMapScale", "_DstBlend", "_GlossMapScale", "_Glossiness", "_GlossyReflections", "_Metallic", "_Mode", "_OcclusionStrength", "_Parallax", "_SmoothnessTextureChannel", "_SpecularHighlights", "_SrcBlend", "_UVSec", "_ZWrite" }; public static readonly HashSet PropertiesToSkipInMaterialEquallityComparission = new HashSet { "shader_master_label", "shader_is_using_thry_editor" }; public enum PropertyType { Vector, Float } public class PropertyData { public PropertyType type; public string name; public Vector4 value; public string lastDeclarationType; } public class Macro { public string name; public string[] args; public string contents; } public class ParsedShaderFile { public string filePath; public string[] lines; } public class TextureProperty { public string name; public Texture texture; public int uv; public Vector2 scale; public Vector2 offset; } public class GrabPassReplacement { public string originalName; public string newName; } public static void CopyAnimatedTagToMaterials(Material[] targets, MaterialProperty source) { string val = (source.targets[0] as Material).GetTag(source.name + AnimatedTagSuffix, false, ""); foreach (Material m in targets) { m.SetOverrideTag(source.name+ AnimatedTagSuffix, val); } } public static void CopyAnimatedTagFromMaterial(Material source, MaterialProperty target) { string val = source.GetTag(target.name + AnimatedTagSuffix, false, ""); foreach (Material m in target.targets) { m.SetOverrideTag(target.name + AnimatedTagSuffix, val); } } public static void CopyAnimatedTagFromProperty(MaterialProperty source, MaterialProperty target) { string val = (source.targets[0] as Material).GetTag(source.name + AnimatedTagSuffix, false, ""); foreach (Material m in target.targets) { m.SetOverrideTag(target.name + AnimatedTagSuffix, val); } } public static void SetAnimatedTag(MaterialProperty prop, string value) { foreach (Material m in prop.targets) { m.SetOverrideTag(prop.name + AnimatedTagSuffix, value); } } public static string GetAnimatedTag(MaterialProperty prop) { return (prop.targets[0] as Material).GetTag(prop.name + AnimatedTagSuffix, false, ""); } public static string GetAnimatedTag(Material m, string prop) { return m.GetTag(prop + AnimatedTagSuffix, false, ""); } public static bool IsAnimated(Material m, string prop) { return m.GetTag(prop + AnimatedTagSuffix, false, "0") != "0"; } public static string CleanStringForPropertyNames(string s) { s = s.Trim().Replace(" ", ""); var nameByteArray = System.Text.Encoding.UTF8.GetBytes(s); string cleaned = ""; for (var i = 0; i < nameByteArray.Length; i++) { if ((nameByteArray[i] >= 65 && nameByteArray[i] <= 122 && nameByteArray[i] != 91 && nameByteArray[i] != 92 && nameByteArray[i] != 93 && nameByteArray[i] != 94 && nameByteArray[i] != 96) || // word characters (nameByteArray[i] >= 48 && nameByteArray[i] <= 57)) // numbers { cleaned += System.Text.Encoding.UTF8.GetString(new byte[] { nameByteArray[i] }); } else { cleaned += nameByteArray[i].ToString("X2"); } } return cleaned; } public static string GetRenamedPropertySuffix(Material m) { return CleanStringForPropertyNames(m.GetTag("thry_rename_suffix", false, m.name)); } public static bool HasCustomRenameSuffix(Material m) { string cleanedMaterialName = CleanStringForPropertyNames(m.name); string suffix = m.GetTag("thry_rename_suffix", false, cleanedMaterialName); return suffix != cleanedMaterialName; } struct RenamingProperty { public MaterialProperty Prop; public string Keyword; public string Replace; public RenamingProperty(MaterialProperty prop, string keyword, string replace) { this.Prop = prop; this.Keyword = keyword; this.Replace = replace; } } public static bool IsPropertyExcemptFromLocking(MaterialProperty prop) { // if not a texture, but has non-modifiable texture data flag, is used as indicator to prevent locking return prop.displayName.EndsWith(ExemptFromLockingSuffix, StringComparison.Ordinal) || (prop.type != MaterialProperty.PropType.Texture && prop.flags.HasFlag(MaterialProperty.PropFlags.NonModifiableTextureData)); } private static bool Lock(Material material, MaterialProperty[] props, bool applyShaderLater = false) { // File filepaths and names Shader shader = material.shader; string shaderFilePath = AssetDatabase.GetAssetPath(shader); string materialFilePath = AssetDatabase.GetAssetPath(material); string materialFolder = Path.GetDirectoryName(materialFilePath); string guid = AssetDatabase.AssetPathToGUID(materialFilePath); string newShaderName = "Hidden/Locked/" + shader.name + "/" + guid; string shaderOptimizerButtonDrawerName = $"[{nameof(ThryShaderOptimizerLockButtonDrawer).Replace("Drawer", "")}]"; //string newShaderDirectory = materialFolder + "/OptimizedShaders/" + material.name + "-" + smallguid + "/"; string newShaderDirectory = materialFolder + "/OptimizedShaders/" + material.name + "/"; // suffix for animated properties when renaming is enabled string animPropertySuffix = GetRenamedPropertySuffix(material); // Get collection of all properties to replace // Simultaneously build a string of #defines for each CGPROGRAM List<(string name,string value)> defines = new List<(string,string)>(); // Append all keywords active on the material foreach (string keyword in material.shaderKeywords) { if (keyword == "") continue; // idk why but null keywords exist if _ keyword is used and not removed by the editor at some point defines.Add((keyword,"")); } KeywordsUsedByPragmas.Clear(); List constantProps = new List(); List animatedPropsToRename = new List(); List animatedPropsToDuplicate = new List(); foreach (MaterialProperty prop in props) { if (prop == null) continue; // Every property gets turned into a preprocessor variable switch (prop.type) { case MaterialProperty.PropType.Texture: if (prop.textureValue != null) { defines.Add(($"PROP{prop.name.ToUpperInvariant()}", "")); } break; } if (prop.name.EndsWith(AnimatedPropertySuffix, StringComparison.Ordinal)) continue; else if (prop.name == UseInlineSamplerStatesPropertyName) { UseInlineSamplerStates = (prop.floatValue == 1); continue; } else if (prop.name.StartsWith(GeometryShaderEnabledPropertyName, StringComparison.Ordinal)) { if (prop.name == GeometryShaderEnabledPropertyName) UseGeometry = (prop.floatValue == 1); else if (prop.name == GeometryShaderEnabledPropertyName + "ForwardBase") UseGeometryForwardBase = (prop.floatValue == 1); else if (prop.name == GeometryShaderEnabledPropertyName + "ForwardAdd") UseGeometryForwardAdd = (prop.floatValue == 1); else if (prop.name == GeometryShaderEnabledPropertyName + "ShadowCaster") UseGeometryShadowCaster = (prop.floatValue == 1); else if (prop.name == GeometryShaderEnabledPropertyName + "Meta") UseGeometryMeta = (prop.floatValue == 1); } else if (prop.name.StartsWith(TessellationEnabledPropertyName, StringComparison.Ordinal)) { if (prop.name == TessellationEnabledPropertyName) UseTessellation = (prop.floatValue == 1); else if (prop.name == TessellationEnabledPropertyName + "ForwardBase") UseTessellationForwardBase = (prop.floatValue == 1); else if (prop.name == TessellationEnabledPropertyName + "ForwardAdd") UseTessellationForwardAdd = (prop.floatValue == 1); else if (prop.name == TessellationEnabledPropertyName + "ShadowCaster") UseTessellationShadowCaster = (prop.floatValue == 1); else if (prop.name == TessellationEnabledPropertyName + "Meta") UseTessellationMeta = (prop.floatValue == 1); } string animateTag = material.GetTag(prop.name + AnimatedTagSuffix, false, ""); if(string.IsNullOrEmpty(animateTag) == false) { // check if we're renaming the property as well if (animateTag == "2") { if (!prop.name.EndsWith("UV", StringComparison.Ordinal) && !prop.name.EndsWith("Pan", StringComparison.Ordinal)) // this property might be animated, but we're not allowed to rename it. this will break things. { if (IllegalPropertyRenames.Contains(prop.name)) animatedPropsToDuplicate.Add(new RenamingProperty(prop, prop.name, prop.name + "_" + animPropertySuffix)); else animatedPropsToRename.Add(new RenamingProperty(prop, prop.name, prop.name + "_" + animPropertySuffix)); if (prop.type == MaterialProperty.PropType.Texture) { animatedPropsToRename.Add(new RenamingProperty(prop, prop.name + "_ST", prop.name + "_" + animPropertySuffix + "_ST")); animatedPropsToRename.Add(new RenamingProperty(prop, prop.name + "_TexelSize", prop.name + "_" + animPropertySuffix + "_TexelSize")); } } } continue; } if (IsPropertyExcemptFromLocking(prop)) continue; PropertyData propData; switch(prop.type) { case MaterialProperty.PropType.Color: propData = new PropertyData(); propData.type = PropertyType.Vector; propData.name = prop.name; if ((prop.flags & MaterialProperty.PropFlags.HDR) != 0) { if ((prop.flags & MaterialProperty.PropFlags.Gamma) != 0) propData.value = prop.colorValue.linear; else propData.value = prop.colorValue; } else if ((prop.flags & MaterialProperty.PropFlags.Gamma) != 0) propData.value = prop.colorValue; else propData.value = prop.colorValue.linear; if (PlayerSettings.colorSpace == ColorSpace.Gamma) propData.value = prop.colorValue; constantProps.Add(propData); break; case MaterialProperty.PropType.Vector: propData = new PropertyData(); propData.type = PropertyType.Vector; propData.name = prop.name; propData.value = prop.vectorValue; constantProps.Add(propData); break; case MaterialProperty.PropType.Float: case MaterialProperty.PropType.Range: propData = new PropertyData(); propData.type = PropertyType.Float; propData.name = prop.name; propData.value = new Vector4(prop.floatValue, 0, 0, 0); constantProps.Add(propData); break; case MaterialProperty.PropType.Texture: PropertyData ST = new PropertyData(); ST.type = PropertyType.Vector; ST.name = prop.name + "_ST"; Vector2 offset = material.GetTextureOffset(prop.name); Vector2 scale = material.GetTextureScale(prop.name); ST.value = new Vector4(scale.x, scale.y, offset.x, offset.y); constantProps.Add(ST); PropertyData TexelSize = new PropertyData(); TexelSize.type = PropertyType.Vector; TexelSize.name = prop.name + "_TexelSize"; Texture t = prop.textureValue; if (t != null) TexelSize.value = new Vector4(1.0f / t.width, 1.0f / t.height, t.width, t.height); else TexelSize.value = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); constantProps.Add(TexelSize); break; } } // Get list of lightmode passes to delete List disabledLightModes = new List(); var disabledLightModesProperty = Array.Find(props, x => x.name == DisabledLightModesPropertyName); if (disabledLightModesProperty != null) { int lightModesMask = (int)disabledLightModesProperty.floatValue; if ((lightModesMask & (int)LightMode.ForwardAdd) != 0) disabledLightModes.Add("ForwardAdd"); if ((lightModesMask & (int)LightMode.ShadowCaster) != 0) disabledLightModes.Add("ShadowCaster"); } // Parse shader and cginc files, also gets preprocessor macros List shaderFiles = new List(); List macros = new List(); if (!ParseShaderFilesRecursive(shaderFiles, newShaderDirectory, shaderFilePath, macros, material)) return false; // Remove all defines where name if not in shader files List<(string,string)> definesToRemove = new List<(string,string)>(); foreach((string name,string) def in defines) { if (shaderFiles.All(x => x.lines.Any(l => l.Contains(def.name)) == false)) definesToRemove.Add(def); } defines.RemoveAll(x => definesToRemove.Contains(x)); // Append convention OPTIMIZER_ENABLED keyword defines.Add((OptimizerEnabledKeyword,"")); string optimizerDefines = ""; if(defines.Count > 0) optimizerDefines = defines.Select(m => $"\r\n #define {m.name} {m.value}").Aggregate((s1, s2) => s1 + s2); int commentKeywords = 0; Dictionary constantPropsDictionary = constantProps.GroupBy(x => x.name).Select(g => g.First()).ToDictionary(x => x.name); Macro[] macrosArray = macros.ToArray(); List grabPassVariables = new List(); // Loop back through and do macros, props, and all other things line by line as to save string ops // Will still be a massive n2 operation from each line * each property foreach (ParsedShaderFile psf in shaderFiles) { // replace property names when prop is animated for (int i = 0; i < psf.lines.Length; i++) { foreach (var animProp in animatedPropsToRename) { // don't have to match if that prop does not even exist in that line if (psf.lines[i].Contains(animProp.Keyword)) { string pattern = animProp.Keyword + @"(?!(\w|\d))"; psf.lines[i] = Regex.Replace(psf.lines[i], pattern, animProp.Replace, RegexOptions.Multiline); } } foreach (var animProp in animatedPropsToDuplicate) { if (psf.lines[i].Contains(animProp.Keyword)) { //if Line is property definition duplicate it bool isDefinition = Regex.Match(psf.lines[i], animProp.Keyword + @"\s*\(""[^""]+""\s*,\s*\w+\)\s*=").Success; string og = null; if (isDefinition) og = psf.lines[i]; string pattern = animProp.Keyword + @"(?!(\w|\d))"; psf.lines[i] = Regex.Replace(psf.lines[i], pattern, animProp.Replace, RegexOptions.Multiline); if (isDefinition) psf.lines[i] = og + "\r\n" + psf.lines[i]; } } } // Shader file specific stuff if (psf.filePath.EndsWith(".shader", StringComparison.Ordinal)) { for (int i=0; i x.name == LODCrossFadePropertyName); if (crossfadeProp != null && crossfadeProp.floatValue == 1) psf.lines[i] = psf.lines[i].Replace("//#pragma", "#pragma"); } else if (trimmedLine.StartsWith("//\"IgnoreProjector\"=\"True\"", StringComparison.Ordinal)) { MaterialProperty projProp = Array.Find(props, x => x.name == IgnoreProjectorPropertyName); if (projProp != null && projProp.floatValue == 1) psf.lines[i] = psf.lines[i].Replace("//\"IgnoreProjector", "\"IgnoreProjector"); } else if (trimmedLine.StartsWith("//\"ForceNoShadowCasting\"=\"True\"", StringComparison.Ordinal)) { MaterialProperty forceNoShadowsProp = Array.Find(props, x => x.name == ForceNoShadowCastingPropertyName); if (forceNoShadowsProp != null && forceNoShadowsProp.floatValue == 1) psf.lines[i] = psf.lines[i].Replace("//\"ForceNoShadowCasting", "\"ForceNoShadowCasting"); } else if (trimmedLine.StartsWith("GrabPass {", StringComparison.Ordinal)) { GrabPassReplacement gpr = new GrabPassReplacement(); string[] splitLine = trimmedLine.Split('\"'); if (splitLine.Length == 1) gpr.originalName = "_GrabTexture"; else gpr.originalName = splitLine[1]; gpr.newName = material.GetTag("GrabPass" + grabPassVariables.Count, false, "_GrabTexture"); psf.lines[i] = "GrabPass { \"" + gpr.newName + "\" }"; grabPassVariables.Add(gpr); } else if (trimmedLine.StartsWith("CGINCLUDE", StringComparison.Ordinal)) { for (int j=i+1; j=0;j--) if (psf.lines[j].Replace(" ", "").Replace("\t", "") == "Pass") break; // then delete each line until a standalone ENDCG line is found for (;j applyStructsLater = new Dictionary(); private struct ApplyStruct { public Material material; public Shader shader; public string smallguid; public string newShaderName; public List animatedPropsToRename; public List animatedPropsToDuplicate; public string animPropertySuffix; public bool shared; } private static bool LockApplyShader(Material material) { if (applyStructsLater.ContainsKey(material) == false) return false; ApplyStruct applyStruct = applyStructsLater[material]; if (applyStruct.shared) { material.shader = applyStruct.material.shader; return true; } //applyStructsLater.Remove(material); return LockApplyShader(applyStruct); } public static void ApplyMaterialPropertyDrawersPatch(Material material) {} static MethodInfo ApplyMaterialPropertyDrawersOriginalMethodInfo = typeof(MaterialEditor).GetMethod("ApplyMaterialPropertyDrawers", new Type[] {typeof(Material)}); static MethodInfo ApplyMaterialPropertyDrawersPatchMethodInfo = typeof(ShaderOptimizer).GetMethod(nameof(ApplyMaterialPropertyDrawersPatch), BindingFlags.Public | BindingFlags.Static); public static void DetourApplyMaterialPropertyDrawers() { Helper.TryDetourFromTo(ApplyMaterialPropertyDrawersOriginalMethodInfo, ApplyMaterialPropertyDrawersPatchMethodInfo); } public static void RestoreApplyMaterialPropertyDrawers() { Helper.RestoreDetour(ApplyMaterialPropertyDrawersOriginalMethodInfo); } private static bool LockApplyShader(ApplyStruct applyStruct) { Material material = applyStruct.material; Shader shader = applyStruct.shader; string newShaderName = applyStruct.newShaderName; List animatedPropsToRename = applyStruct.animatedPropsToRename; List animatedPropsToDuplicate = applyStruct.animatedPropsToDuplicate; string animPropertySuffix = applyStruct.animPropertySuffix; // Write original shader to override tag material.SetOverrideTag(TAG_ORIGINAL_SHADER, shader.name); // Write the new shader folder name in an override tag so it will be deleted // For some reason when shaders are swapped on a material the RenderType override tag gets completely deleted and render queue set back to -1 // So these are saved as temp values and reassigned after switching shaders string renderType = material.GetTag("RenderType", false, ""); int renderQueue = material.renderQueue; // Actually switch the shader Shader newShader = Shader.Find(newShaderName); if (newShader == null) { Debug.LogError("[Shader Optimizer] Generated shader " + newShaderName + " could not be found"); return false; } DetourApplyMaterialPropertyDrawers(); material.shader = newShader; RestoreApplyMaterialPropertyDrawers(); //ShaderEditor.reload(); material.SetOverrideTag("RenderType", renderType); material.renderQueue = renderQueue; material.SetOverrideTag("OriginalKeywords", string.Join(" ", material.shaderKeywords)); // Remove ALL keywords foreach (string keyword in material.shaderKeywords) if(material.IsKeywordEnabled(keyword)) material.DisableKeyword(keyword); foreach (var animProp in animatedPropsToRename) { var newName = animProp.Prop.name + "_" + animPropertySuffix; switch (animProp.Prop.type) { case MaterialProperty.PropType.Color: material.SetColor(newName, animProp.Prop.colorValue); break; case MaterialProperty.PropType.Vector: material.SetVector(newName, animProp.Prop.vectorValue); break; case MaterialProperty.PropType.Float: material.SetFloat(newName, animProp.Prop.floatValue); break; case MaterialProperty.PropType.Range: material.SetFloat(newName, animProp.Prop.floatValue); break; case MaterialProperty.PropType.Texture: material.SetTexture(newName, animProp.Prop.textureValue); material.SetTextureScale(newName, new Vector2(animProp.Prop.textureScaleAndOffset.x, animProp.Prop.textureScaleAndOffset.y)); material.SetTextureOffset(newName, new Vector2(animProp.Prop.textureScaleAndOffset.z, animProp.Prop.textureScaleAndOffset.w)); break; default: throw new ArgumentOutOfRangeException(nameof(material), "This property type should not be renamed and can not be set."); } } foreach (var animProp in animatedPropsToDuplicate) { var newName = animProp.Prop.name + "_" + animPropertySuffix; switch (animProp.Prop.type) { case MaterialProperty.PropType.Color: material.SetColor(newName, animProp.Prop.colorValue); break; case MaterialProperty.PropType.Vector: material.SetVector(newName, animProp.Prop.vectorValue); break; case MaterialProperty.PropType.Float: material.SetFloat(newName, animProp.Prop.floatValue); break; case MaterialProperty.PropType.Range: material.SetFloat(newName, animProp.Prop.floatValue); break; case MaterialProperty.PropType.Texture: material.SetTexture(newName, animProp.Prop.textureValue); material.SetTextureScale(newName, new Vector2(animProp.Prop.textureScaleAndOffset.x, animProp.Prop.textureScaleAndOffset.y)); material.SetTextureOffset(newName, new Vector2(animProp.Prop.textureScaleAndOffset.z, animProp.Prop.textureScaleAndOffset.w)); break; default: throw new ArgumentOutOfRangeException(nameof(material), "This property type should not be renamed and can not be set."); } } return true; } /** Find longest common directoy */ public static int GetLongestCommonDirectoryLength(string[] s) { int k = s[0].Length; for (int i = 1; i < s.Length; i++) { k = Math.Min(k, s[i].Length); for (int j = 0; j < k; j++) if ( AreCharsInPathEqual(s[i][j] , s[0][j]) == false) { k = j; break; } } string p = s[0].Substring(0, k); if (Directory.Exists(p)) return p.Length; else return Path.GetDirectoryName(p).Length; } private static bool AreCharsInPathEqual(char c1, char c2) { return (c1 == c2) || ((c1 == '/' || c1 == '\\') && (c2 == '/' || c2 == '\\')); } // Preprocess each file for macros and includes // Save each file as string[], parse each macro with //KSOEvaluateMacro // Only editing done is replacing #include "X" filepaths where necessary // most of these args could be private static members of the class private static bool ParseShaderFilesRecursive(List filesParsed, string newTopLevelDirectory, string filePath, List macros, Material material) { // Infinite recursion check if (filesParsed.Exists(x => x.filePath == filePath)) return true; ParsedShaderFile psf = new ParsedShaderFile(); psf.filePath = filePath; filesParsed.Add(psf); // Read file string fileContents = null; try { StreamReader sr = new StreamReader(filePath); fileContents = sr.ReadToEnd(); sr.Close(); } catch (FileNotFoundException e) { Debug.LogError("[Shader Optimizer] Shader file " + filePath + " not found. " + e.ToString()); return false; } catch (IOException e) { Debug.LogError("[Shader Optimizer] Error reading shader file. " + e.ToString()); return false; } // Parse file line by line List macrosList = new List(); string[] fileLines = fileContents.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); List includedLines = new List(); bool isIncluded = true; int isNotIncludedAtDepth = 0; int ifStacking = 0; Stack removeEndifStack = new Stack(); bool isCommentedOut = false; int currentExcludeDepth = 0; bool doExclude = false; int excludeStartDepth = 0; for (int i=0; i constants, Macro[] macros, GrabPassReplacement[] grabPassVariables) { List uniqueSampledTextures = new List(); // Outside loop is each line for (int i=startLine;i x.name == args[1]); if (texProp != null) { Texture t = texProp.textureValue; int inlineSamplerIndex = 0; if (t != null) { switch (t.filterMode) { case FilterMode.Bilinear: break; case FilterMode.Point: inlineSamplerIndex += 1 * 4; break; case FilterMode.Trilinear: inlineSamplerIndex += 2 * 4; break; } switch (t.wrapMode) { case TextureWrapMode.Repeat: break; case TextureWrapMode.Clamp: inlineSamplerIndex += 1; break; case TextureWrapMode.Mirror: inlineSamplerIndex += 2; break; case TextureWrapMode.MirrorOnce: inlineSamplerIndex += 3; break; } } // Replace the token on the following line lines[i+1] = lines[i+1].Replace(args[0], InlineSamplerStateNames[inlineSamplerIndex]); } } else if (lineTrimmed.StartsWith("//KSODuplicateTextureCheckStart", StringComparison.Ordinal)) { // Since files are not fully parsed and instead loosely processed, each shader function needs to have // its sampled texture list reset somewhere before KSODuplicateTextureChecks are made. // As long as textures are sampled in-order inside a single function, this method will work. uniqueSampledTextures = new List(); } else if (lineTrimmed.StartsWith("//KSODuplicateTextureCheck", StringComparison.Ordinal)) { // Each KSODuplicateTextureCheck line gets evaluated when the shader is optimized // If the texture given has already been sampled as another texture (i.e. one texture is used in two slots) // AND has been sampled with the same UV mode - as indicated by a convention UV property, // AND has been sampled with the exact same Tiling/Offset values // AND has been logged by KSODuplicateTextureCheck, // then the variable corresponding to the first instance of that texture being // sampled will be assigned to the variable corresponding to the given texture. // The compiler will then skip the duplicate texture sample since its variable is overwritten before being used // Parse line for argument texture property name string lineParsed = lineTrimmed.Replace(" ", "").Replace("\t", ""); int firstParenthesis = lineParsed.IndexOf('('); int lastParenthesis = lineParsed.IndexOf(')'); string argName = lineParsed.Substring(firstParenthesis+1, lastParenthesis-firstParenthesis-1); // Check if texture property by argument name exists and has a texture assigned if (Array.Exists(props, x => x.name == argName)) { MaterialProperty argProp = Array.Find(props, x => x.name == argName); if (argProp.textureValue != null) { // If no convention UV property exists, sampled UV mode is assumed to be 0 // Any UV enum or mode indicator can be used for this int UV = 0; if (Array.Exists(props, x => x.name == argName + "UV")) UV = (int)(Array.Find(props, x => x.name == argName + "UV").floatValue); Vector2 texScale = material.GetTextureScale(argName); Vector2 texOffset = material.GetTextureOffset(argName); // Check if this texture has already been sampled if (uniqueSampledTextures.Exists(x => (x.texture == argProp.textureValue) && (x.uv == UV) && (x.scale == texScale) && x.offset == texOffset)) { string texName = uniqueSampledTextures.Find(x => (x.texture == argProp.textureValue) && (x.uv == UV)).name; // convention _var variables requried. i.e. _MainTex_var and _CoverageMap_var lines[i] = argName + "_var = " + texName + "_var;"; } else { // Texture/UV/ST combo hasn't been sampled yet, add it to the list TextureProperty tp = new TextureProperty(); tp.name = argName; tp.texture = argProp.textureValue; tp.uv = UV; tp.scale = texScale; tp.offset = texOffset; uniqueSampledTextures.Add(tp); } } } } else if (lineTrimmed.StartsWith("[maxtessfactor(", StringComparison.Ordinal)) { MaterialProperty maxTessFactorProperty = Array.Find(props, x => x.name == TessellationMaxFactorPropertyName); if (maxTessFactorProperty != null) { float maxTessellation = maxTessFactorProperty.floatValue; string animateTag = material.GetTag(TessellationMaxFactorPropertyName + AnimatedTagSuffix, false, "0"); if (animateTag != "" && animateTag == "1") maxTessellation = 64.0f; lines[i] = "[maxtessfactor(" + maxTessellation.ToString(".0######") + ")]"; } } // then replace macros foreach (Macro macro in macros) { // Expects only one instance of a macro per line! int macroIndex; if ((macroIndex = lines[i].IndexOf(macro.name + "(", StringComparison.Ordinal)) != -1) { // Macro exists on this line, make sure its not the definition string lineParsed = lineTrimmed.Replace(" ","").Replace("\t",""); if (lineParsed.StartsWith("#define", StringComparison.Ordinal)) continue; // parse args between first '(' and first ')' int firstParenthesis = macroIndex + macro.name.Length; int lastParenthesis = lines[i].IndexOf(')', macroIndex + macro.name.Length+1); string allArgs = lines[i].Substring(firstParenthesis+1, lastParenthesis-firstParenthesis-1); string[] args = allArgs.Split(','); // Replace macro parts string newContents = macro.contents; for (int j=0; j= 0) charLeft = newContents[argIndex-1]; char charRight = ' '; if (argIndex+macro.args[j].Length < newContents.Length) charRight = newContents[argIndex+macro.args[j].Length]; if (ValidSeparators.Contains(charLeft) && ValidSeparators.Contains(charRight)) { // Replcae the arg! StringBuilder sbm = new StringBuilder(newContents.Length - macro.args[j].Length + args[j].Length); sbm.Append(newContents, 0, argIndex); sbm.Append(args[j]); sbm.Append(newContents, argIndex + macro.args[j].Length, newContents.Length - argIndex - macro.args[j].Length); newContents = sbm.ToString(); } } } newContents = newContents.Replace("##", ""); // Remove token pasting separators // Replace the line with the evaluated macro StringBuilder sb = new StringBuilder(lines[i].Length + newContents.Length); sb.Append(lines[i], 0, macroIndex); sb.Append(newContents); sb.Append(lines[i], lastParenthesis+1, lines[i].Length - lastParenthesis-1); lines[i] = sb.ToString(); } } foreach(string token in tokens) { if(constants.ContainsKey(token)) { PropertyData constant = constants[token]; int constantIndex; int lastIndex = 0; bool declarationFound = false; while ((constantIndex = lines[i].IndexOf(constant.name, lastIndex, StringComparison.Ordinal)) != -1) { lastIndex = constantIndex+1; char charLeft = ' '; if (constantIndex-1 >= 0) charLeft = lines[i][constantIndex-1]; char charRight = ' '; if (constantIndex + constant.name.Length < lines[i].Length) charRight = lines[i][constantIndex + constant.name.Length]; // Skip invalid matches (probably a subname of another symbol) if (!(ValidSeparators.Contains(charLeft) && ValidSeparators.Contains(charRight))) continue; // Skip inline comments if (charLeft == '*' && charRight == '*' && constantIndex >= 2 && lines[i][constantIndex - 2] == '/') continue; // Skip basic declarations of unity shader properties i.e. "uniform float4 _Color;" if (!declarationFound) { string precedingText = lines[i].Substring(0, constantIndex-1).TrimEnd(); // whitespace removed string immediately to the left should be float or float4 string restOftheFile = lines[i].Substring(constantIndex + constant.name.Length).TrimStart(); // whitespace removed character immediately to the right should be ; if (Array.Exists(ValidPropertyDataTypes, x => precedingText.EndsWith(x, StringComparison.Ordinal)) && restOftheFile.StartsWith(";", StringComparison.Ordinal)) { constant.lastDeclarationType = precedingText.TrimStart(); declarationFound = true; continue; } } // Replace with constant! // This could technically be more efficient by being outside the IndexOf loop StringBuilder sb = new StringBuilder(lines[i].Length * 2); sb.Append(lines[i], 0, constantIndex); switch (constant.type) { case PropertyType.Float: string constantValue; // Special Handling for ints if(constant.lastDeclarationType == "int") constantValue = constant.value.x.ToString("F0", CultureInfo.InvariantCulture); else constantValue = constant.value.x.ToString("0.0####################", CultureInfo.InvariantCulture); // Add comment with property name, for easier debug sb.Append($"({constantValue} /*{constant.name}*/)"); break; case PropertyType.Vector: sb.Append("float4("+constant.value.x.ToString(CultureInfo.InvariantCulture)+"," +constant.value.y.ToString(CultureInfo.InvariantCulture)+"," +constant.value.z.ToString(CultureInfo.InvariantCulture)+"," +constant.value.w.ToString(CultureInfo.InvariantCulture)+")"); break; } sb.Append(lines[i], constantIndex+constant.name.Length, lines[i].Length-constantIndex-constant.name.Length); lines[i] = sb.ToString(); // Check for Unity branches on previous line here? } } } // Then replace grabpass variable names foreach (GrabPassReplacement gpr in grabPassVariables) { // find indexes of all instances of gpr.originalName that exist on this line int lastIndex = 0; int gbIndex; while ((gbIndex = lines[i].IndexOf(gpr.originalName, lastIndex, StringComparison.Ordinal)) != -1) { lastIndex = gbIndex+1; char charLeft = ' '; if (gbIndex-1 >= 0) charLeft = lines[i][gbIndex-1]; char charRight = ' '; if (gbIndex + gpr.originalName.Length < lines[i].Length) charRight = lines[i][gbIndex + gpr.originalName.Length]; // Skip invalid matches (probably a subname of another symbol) if (!(ValidSeparators.Contains(charLeft) && ValidSeparators.Contains(charRight))) continue; // Replace with new variable name // This could technically be more efficient by being outside the IndexOf loop StringBuilder sb = new StringBuilder(lines[i].Length * 2); sb.Append(lines[i], 0, gbIndex); sb.Append(gpr.newName); sb.Append(lines[i], gbIndex+gpr.originalName.Length, lines[i].Length-gbIndex-gpr.originalName.Length); lines[i] = sb.ToString(); } } // Then remove Unity branches if (RemoveUnityBranches) lines[i] = lines[i].Replace("UNITY_BRANCH", "").Replace("[branch]", ""); } } public enum UnlockSuccess { hasNoSavedShader, wasNotLocked, couldNotFindOriginalShader, couldNotDeleteLockedShader, success} private static void Unlock(Material material, MaterialProperty shaderOptimizer = null) { //if unlock success set floats. not done for locking cause the sucess is checked later when applying the shaders UnlockSuccess success = ShaderOptimizer.UnlockConcrete(material); if (success == UnlockSuccess.success || success == UnlockSuccess.wasNotLocked || success == UnlockSuccess.couldNotDeleteLockedShader) { if (shaderOptimizer != null) shaderOptimizer.floatValue = 0; else material.SetFloat(GetOptimizerPropertyName(material.shader), 0); } } public static bool GuessShader(Shader locked, out Shader shader) { string name = locked.name; name = Regex.Match(name.Substring(7), @".*(?=\/)").Value; ShaderInfo[] allShaders = ShaderUtil.GetAllShaderInfo(); int closestDistance = int.MaxValue; string closestShaderName = null; foreach (ShaderInfo s in allShaders) { if (!s.supported) continue; int d = Helper.LevenshteinDistance(s.name, name); if(d < closestDistance) { closestDistance = d; closestShaderName = s.name; } } shader = Shader.Find(closestShaderName); return shader != null && closestDistance < name.Length / 2; } private static UnlockSuccess UnlockConcrete (Material material) { Shader lockedShader = material.shader; // Revert to original shader string originalShaderName = material.GetTag(TAG_ORIGINAL_SHADER, false, ""); Shader orignalShader = null; if (originalShaderName == "" && !GuessShader(lockedShader, out orignalShader)) { if (material.shader.name.StartsWith("Hidden/")) { if (EditorUtility.DisplayDialog("Unlock Material", $"The original shader for {material.name} could not be resolved.\nPlease select a shader manually.", "Ok")) { } Debug.LogError("[Shader Optimizer] Original shader not saved to material, could not unlock shader"); return UnlockSuccess.hasNoSavedShader; } else { Debug.LogWarning("[Shader Optimizer] Original shader not saved to material, but material also doesnt seem to be locked."); return UnlockSuccess.wasNotLocked; } } if(orignalShader == null) orignalShader = Shader.Find(originalShaderName); if (orignalShader == null && !GuessShader(lockedShader, out orignalShader)) { if (material.shader.name.StartsWith("Hidden/")) { if (EditorUtility.DisplayDialog("Unlock Material", $"The original shader for {material.name} could not be resolved.\nPlease select a shader manually.", "Ok")) { } Debug.LogError("[Shader Optimizer] Original shader " + originalShaderName + " could not be found"); return UnlockSuccess.couldNotFindOriginalShader; } else { Debug.LogWarning("[Shader Optimizer] Original shader not saved to material, but material also doesnt seem to be locked."); return UnlockSuccess.wasNotLocked; } } // For some reason when shaders are swapped on a material the RenderType override tag gets completely deleted and render queue set back to -1 // So these are saved as temp values and reassigned after switching shaders string renderType = material.GetTag("RenderType", false, ""); int renderQueue = material.renderQueue; string unlockedMaterialGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(material)); DetourApplyMaterialPropertyDrawers(); material.shader = orignalShader; RestoreApplyMaterialPropertyDrawers(); material.SetOverrideTag("RenderType", renderType); material.renderQueue = renderQueue; material.shaderKeywords = material.GetTag("OriginalKeywords", false, string.Join(" ", material.shaderKeywords)).Split(' '); // Delete the variants folder and all files in it, as to not orhpan files and inflate Unity project // But only if no other material is using the locked shader string[] lockedMaterials = material.GetTag(TAG_ALL_MATERIALS_GUIDS_USING_THIS_LOCKED_SHADER, false, "").Split(','); string newTag = string.Join(",", lockedMaterials.Where(guid => guid != unlockedMaterialGUID).ToArray()); bool isOtherMaterialUsingLockedShader = false; foreach(string guid in lockedMaterials) { if (string.IsNullOrWhiteSpace(guid)) continue; Material m = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); if (m != null) { isOtherMaterialUsingLockedShader |= m.shader == lockedShader; m.SetOverrideTag(TAG_ALL_MATERIALS_GUIDS_USING_THIS_LOCKED_SHADER, newTag); } } if (!isOtherMaterialUsingLockedShader) { string materialFilePath = AssetDatabase.GetAssetPath(lockedShader); string lockedFolder = Path.GetDirectoryName(materialFilePath); FileUtil.DeleteFileOrDirectory(lockedFolder); FileUtil.DeleteFileOrDirectory(lockedFolder + ".meta"); } //AssetDatabase.Refresh(); return UnlockSuccess.success; } public static void DeleteTags(Material[] materials) { foreach(Material m in materials) { var it = new SerializedObject(m).GetIterator(); while (it.Next(true)) { if (it.name == "stringTagMap") { for (int i = 0; i < it.arraySize; i++) { string tagName = it.GetArrayElementAtIndex(i).displayName; if (tagName.EndsWith(AnimatedTagSuffix)) { m.SetOverrideTag(tagName, ""); } } } } } } #region Upgrade public static void UpgradeAnimatedPropertiesToTagsOnAllMaterials() { IEnumerable materials = Resources.FindObjectsOfTypeAll(); UpgradeAnimatedPropertiesToTags(materials); Debug.Log("[Thry][Optimizer] Update animated properties of all materials to tags."); } public static void UpgradeAnimatedPropertiesToTags(IEnumerable iMaterials) { IEnumerable materialsToChange = iMaterials.Where(m => m != null && string.IsNullOrEmpty(AssetDatabase.GetAssetPath(m)) == false && string.IsNullOrEmpty(AssetDatabase.GetAssetPath(m.shader)) == false && IsShaderUsingThryOptimizer(m.shader)).Distinct().OrderBy(m => m.shader.name); int i = 0; foreach (Material m in materialsToChange) { if(EditorUtility.DisplayCancelableProgressBar("Upgrading Materials", "Upgrading animated tags of " + m.name, (float)i / materialsToChange.Count())) { break; } string path = AssetDatabase.GetAssetPath(m); StreamReader reader = new StreamReader(path); string line; while((line = reader.ReadLine()) != null) { if (line.Contains(AnimatedPropertySuffix) && line.Length > 6) { string[] parts = line.Substring(6, line.Length - 6).Split(':'); float f; if (float.TryParse(parts[1], out f)) { if( f != 0) { string name = parts[0].Substring(0, parts[0].Length - AnimatedPropertySuffix.Length); m.SetOverrideTag(name + AnimatedTagSuffix, "" + f); } } } } reader.Close(); i++; } EditorUtility.ClearProgressBar(); } static void ClearConsole() { var logEntries = System.Type.GetType("UnityEditor.LogEntries, UnityEditor.dll"); var clearMethod = logEntries.GetMethod("Clear", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); clearMethod.Invoke(null, null); } #endregion // ifex indenting [MenuItem("Assets/Thry/Shaders/Ifex Indenting", false, 303)] static void IfExIndenting() { Shader s = Selection.objects[0] as Shader; if (s == null) return; string path = AssetDatabase.GetAssetPath(s); if(string.IsNullOrEmpty(path)) return; // Load the shader file string[] lines = File.ReadAllLines(path); int indent = 0; for (int i = 0; i < lines.Length; i++) { string line = lines[i].Trim(); if (line.Contains("//endex")) indent = Mathf.Max(0,indent - 1); lines[i] = new string(' ', indent * 4) + line; if (line.StartsWith("//ifex")) indent++; } GUIUtility.systemCopyBuffer = string.Join("\n", lines); } [MenuItem("Assets/Thry/Shaders/Ifex Indenting", true)] static bool IfExIndentingValidator() { return Selection.objects.Length == 1 && Selection.objects[0] is Shader; } //---GameObject + Children Locking [MenuItem("GameObject/Thry/Materials/Unlock All", false,0)] static void UnlockAllChildren() { SetLockForAllChildren(Selection.gameObjects, 0, true); } [MenuItem("GameObject/Thry/Materials/Lock All", false,0)] static void LockAllChildren() { SetLockForAllChildren(Selection.gameObjects, 1, true); } //---Asset Unlocking [MenuItem("Assets/Thry/Materials/Unlock All", false, 303)] static void UnlockAllMaterials() { IEnumerable mats = Selection.assetGUIDs.Select(g => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g))); SetLockedForAllMaterials(mats, 0, true); } [MenuItem("Assets/Thry/Materials/Unlock All", true)] static bool UnlockAllMaterialsValidator() { return SelectedObjectsAreLockableMaterials(); } //---Asset Locking [MenuItem("Assets/Thry/Materials/Lock All", false, 303)] static void LockAllMaterials() { IEnumerable mats = Selection.assetGUIDs.Select(g => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g))); SetLockedForAllMaterials(mats, 1, true); } [MenuItem("Assets/Thry/Materials/Lock All", true)] static bool LockAllMaterialsValidator() { return SelectedObjectsAreLockableMaterials(); } //----Folder Lock //This does not work for folders on the left side of the project explorer, because they are not exposed to Selection [MenuItem("Assets/Thry/Materials/Lock Folder", false, 303)] static void LockFolder() { IEnumerable folderPaths = Selection.objects.Select(o => AssetDatabase.GetAssetPath(o)).Where(p => Directory.Exists(p)); List materials = new List(); foreach (string f in folderPaths) FindMaterialsRecursive(f, materials); SetLockedForAllMaterials(materials, 1, true); } [MenuItem("Assets/Thry/Materials/Lock Folder", true)] static bool LockFolderValidator() { return Selection.objects.Select(o => AssetDatabase.GetAssetPath(o)).Where(p => Directory.Exists(p)).Count() == Selection.objects.Length; } //-----Folder Unlock [MenuItem("Assets/Thry/Materials/Unlock Folder", false, 303)] static void UnLockFolder() { IEnumerable folderPaths = Selection.objects.Select(o => AssetDatabase.GetAssetPath(o)).Where(p => Directory.Exists(p)); List materials = new List(); foreach (string f in folderPaths) FindMaterialsRecursive(f, materials); SetLockedForAllMaterials(materials, 0, true); } [MenuItem("Assets/Thry/Materials/Unlock Folder", true)] static bool UnLockFolderValidator() { return Selection.objects.Select(o => AssetDatabase.GetAssetPath(o)).Where(p => Directory.Exists(p)).Count() == Selection.objects.Length; } private static void FindMaterialsRecursive(string folderPath, List materials) { foreach(string f in Directory.GetFiles(folderPath)) { if(AssetDatabase.GetMainAssetTypeAtPath(f) == typeof(Material)) { materials.Add(AssetDatabase.LoadAssetAtPath(f)); } } foreach(string f in Directory.GetDirectories(folderPath)){ FindMaterialsRecursive(f, materials); } } //----Folder Unlock static bool SelectedObjectsAreLockableMaterials() { if (Selection.assetGUIDs != null && Selection.assetGUIDs.Length > 0) { return Selection.assetGUIDs.All(g => { if (AssetDatabase.GetMainAssetTypeAtPath(AssetDatabase.GUIDToAssetPath(g)) != typeof(Material)) return false; Material m = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g)); return IsShaderUsingThryOptimizer(m.shader); }); } return false; } //----VRChat Callback to force Locking on upload #if VRC_SDK_VRCSDK2 || VRC_SDK_VRCSDK3 public class LockMaterialsOnUpload : IVRCSDKPreprocessAvatarCallback { public int callbackOrder => 100; public bool OnPreprocessAvatar(GameObject avatarGameObject) { List materials = avatarGameObject.GetComponentsInChildren(true).SelectMany(r => r.sharedMaterials).ToList(); #if VRC_SDK_VRCSDK3 && !UDON VRCAvatarDescriptor descriptor = avatarGameObject.GetComponent(); if(descriptor != null) { IEnumerable clips = descriptor.baseAnimationLayers.Select(l => l.animatorController).Where(a => a != null).SelectMany(a => a.animationClips).Distinct(); foreach (AnimationClip clip in clips) { IEnumerable clipMaterials = AnimationUtility.GetObjectReferenceCurveBindings(clip).Where(b => b.isPPtrCurve && b.type.IsSubclassOf(typeof(Renderer)) && b.propertyName.StartsWith("m_Materials")) .SelectMany(b => AnimationUtility.GetObjectReferenceCurve(clip, b)).Select(r => r.value as Material); materials.AddRange(clipMaterials); } } #endif if(SetLockedForAllMaterials(materials, 1, showProgressbar: true, showDialog: PersistentData.Get("ShowLockInDialog", true), allowCancel: false) == false) return false; //returning true all the time, because build process cant be stopped it seems return true; } } #endif #if VRC_SDK_VRCSDK2 || VRC_SDK_VRCSDK3 public class LockMaterialsOnWorldUpload : IVRCSDKBuildRequestedCallback { public int callbackOrder => 100; bool IVRCSDKBuildRequestedCallback.OnBuildRequested(VRCSDKRequestedBuildType requestedBuildType) { List materials = new List(); if (requestedBuildType == VRCSDKRequestedBuildType.Scene) { if (UnityEngine.Object.FindObjectsOfType(typeof(VRC_SceneDescriptor)) is VRC_SceneDescriptor[] descriptors && descriptors.Length > 0){ var renderers = UnityEngine.Object.FindObjectsOfType(); foreach (var rend in renderers) { foreach (var mat in rend.sharedMaterials){ materials.Add(mat); } } } SetLockedForAllMaterials(materials, 1, showProgressbar: true, showDialog: PersistentData.Get("ShowLockInDialog", true), allowCancel: false); } return true; } } #endif const string DidStripUnlockedShadersSessionStateKey = "ShaderOptimizerDidStripUnlockedShaders"; public class StripUnlockedShadersFromBuild : UnityEditor.Build.IPreprocessShaders { // Thanks to z3y for this function public int callbackOrder => 4; public void OnProcessShader(Shader shader, UnityEditor.Rendering.ShaderSnippetData snippet, IList data) { // Strip if the shader should be optimized (has the optimizer property) but isn't (name doesn't start with Hidden/Locked/) bool shouldStrip = shader.FindPropertyIndex("_ShaderOptimizerEnabled") >= 0 && !shader.name.StartsWith("Hidden/Locked/"); if (shouldStrip) { // Try to warn the user if there's an unlocked shader if (!SessionState.GetBool(DidStripUnlockedShadersSessionStateKey, false)) { EditorUtility.DisplayDialog("Shader Optimizer: Unlocked Shader", "An Unlocked shader was found, and will not be included in the build (this will cause pink materials).\nThis shouldn't happen. Make sure all lockable materials are Locked, and try again.\nIf it happens again, please report the issue via GitHub or Discord!", "OK"); SessionState.SetBool(DidStripUnlockedShadersSessionStateKey, true); } data.Clear(); } } } [InitializeOnLoad] public static class ResetStrippedShaderWarning { static ResetStrippedShaderWarning() { EditorApplication.update -= ResetWarning; EditorApplication.update += ResetWarning; } private static void ResetWarning() { if(SessionState.GetBool(DidStripUnlockedShadersSessionStateKey, false)) { Debug.LogError($"[Shader Optimizer] Unlocked shaders were removed from build. Materials will be pink. Use Thry -> Lock All on hierarchy items to ensure materials are locked."); SessionState.SetBool(DidStripUnlockedShadersSessionStateKey, false); } } } static string MaterialToShaderPropertyHash(Material m) { StringBuilder stringBuilder = new StringBuilder(m.shader.name); foreach (MaterialProperty prop in MaterialEditor.GetMaterialProperties(new Object[] { m })) { string propName = prop.name; if (PropertiesToSkipInMaterialEquallityComparission.Contains(propName)) continue; string isAnimated = GetAnimatedTag(m, propName); if (isAnimated == "1") { stringBuilder.Append(isAnimated); } else if(isAnimated == "2") { //This is because materials with renaming should not share shaders stringBuilder.Append(m.name); } else { switch (prop.type) { case MaterialProperty.PropType.Color: stringBuilder.Append(m.GetColor(propName).ToString()); break; case MaterialProperty.PropType.Vector: stringBuilder.Append(m.GetVector(propName).ToString()); break; case MaterialProperty.PropType.Range: case MaterialProperty.PropType.Float: stringBuilder.Append(m.GetFloat(propName) .ToString(CultureInfo.InvariantCulture)); break; case MaterialProperty.PropType.Texture: Texture t = m.GetTexture(propName); Vector4 texelSize = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); if (t != null) texelSize = new Vector4(1.0f / t.width, 1.0f / t.height, t.width, t.height); stringBuilder.Append(m.GetTextureOffset(propName).ToString()); stringBuilder.Append(m.GetTextureScale(propName).ToString()); break; } } } // https://forum.unity.com/threads/hash-function-for-game.452779/ ASCIIEncoding encoding = new ASCIIEncoding(); byte[] bytes = encoding.GetBytes(stringBuilder.ToString()); var sha = new MD5CryptoServiceProvider(); return BitConverter.ToString(sha.ComputeHash(bytes)).Replace("-", "").ToLower(); } public static bool SetLockForAllChildren(GameObject[] objects, int lockState, bool showProgressbar = false, bool showDialog = false, bool allowCancel = true) { IEnumerable materials = objects.Select(o => o.GetComponentsInChildren(true)).SelectMany(rA => rA.SelectMany(r => r.sharedMaterials)); return SetLockedForAllMaterials(materials, lockState, showProgressbar, showDialog); } static Dictionary> s_shaderPropertyCombinations = new Dictionary>(); public static bool SetLockedForAllMaterials(IEnumerable materials, int lockState, bool showProgressbar = false, bool showDialog = false, bool allowCancel = true, MaterialProperty shaderOptimizer = null) { Helper.RegisterEditorUse(); //first the shaders are created. compiling is suppressed with start asset editing AssetDatabase.StartAssetEditing(); bool seekedIsLocked = lockState == 1; //Get cleaned materia list // The GetPropertyDefaultFloatValue is changed from 0 to 1 when the shader is locked in IEnumerable materialsToChangeLock = materials.Where(m => m != null && string.IsNullOrEmpty(AssetDatabase.GetAssetPath(m)) == false && string.IsNullOrEmpty(AssetDatabase.GetAssetPath(m.shader)) == false && IsShaderUsingThryOptimizer(m.shader) && ( m.shader.name.StartsWith("Hidden/Locked/") || (m.shader.name.StartsWith("Hidden/") && m.GetTag("OriginalShader",false,"") != "" && m.shader.GetPropertyDefaultFloatValue(m.shader.FindPropertyIndex(GetOptimizerPropertyName(m.shader))) == 1) ) != seekedIsLocked) .Distinct(); float i = 0; float length = materialsToChangeLock.Count(); //show popup dialog if defined if (showDialog && length > 0) { if(EditorUtility.DisplayDialog("Locking Materials", EditorLocale.editor.Get("auto_lock_dialog").ReplaceVariables(length), "More information","OK")) { Application.OpenURL("https://www.youtube.com/watch?v=asWeDJb5LAo"); } PersistentData.Set("ShowLockInDialog", false); } //Create shader assets foreach (Material m in materialsToChangeLock.ToList()) //have to call ToList() here otherwise the Unlock Shader button in the ShaderGUI doesn't work { //do progress bar if (showProgressbar) { if (allowCancel) { if (EditorUtility.DisplayCancelableProgressBar((lockState == 1) ? "Locking Materials" : "Unlocking Materials", m.name, i / length)) break; } else { EditorUtility.DisplayProgressBar((lockState == 1) ? "Locking Materials" : "Unlocking Materials", m.name, i / length); } } //create the assets try { if (lockState == 1) { string hash = MaterialToShaderPropertyHash(m); // Check that shader has already been created for this hash and still exists // Or that the shader is being created for this has during this session Material reference = null; if(s_shaderPropertyCombinations.ContainsKey(hash)) { reference = s_shaderPropertyCombinations[hash].FirstOrDefault(m2 => m2 != m && (materialsToChangeLock.Contains(m2) || Shader.Find(applyStructsLater[m2].newShaderName) != null)); } if (reference != null) { // Reuse existing shader and struct ApplyStruct applyStruct = applyStructsLater[reference]; applyStruct.material = m; applyStructsLater[m] = applyStruct; //Disable shader keywords foreach (string keyword in m.shaderKeywords) if (m.IsKeywordEnabled(keyword)) m.DisableKeyword(keyword); } // Create new locked shader else { ShaderOptimizer.Lock(m, MaterialEditor.GetMaterialProperties(new UnityEngine.Object[] { m }), applyShaderLater: true); s_shaderPropertyCombinations[hash] = new List(); } // Add material to list of materials with same shader property hash s_shaderPropertyCombinations[hash].Add(m); // Update TAG_ALL_MATERIALS_GUIDS_USING_THIS_LOCKED_SHADER of all materials with same shader property hash string tag = string.Join(",", s_shaderPropertyCombinations[hash].Select(m2 => AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(m2)))); foreach (Material m2 in s_shaderPropertyCombinations[hash]) m2.SetOverrideTag(TAG_ALL_MATERIALS_GUIDS_USING_THIS_LOCKED_SHADER, tag); } else if (lockState == 0) { ShaderOptimizer.Unlock(m, shaderOptimizer); } } catch (Exception e) { Debug.Log(e); string position = e.StackTrace.Split('\n').FirstOrDefault(l => l.Contains("ThryEditor")); if(position != null) { position = position.Split(new string[]{ "ThryEditor" }, StringSplitOptions.None).LastOrDefault(); Debug.LogError("Could not un-/lock material " + m.name + " | Error thrown at " + position+ "\n"+e.StackTrace); }else { Debug.LogError("Could not un-/lock material " + m.name + "\n"+e.StackTrace); } EditorUtility.ClearProgressBar(); AssetDatabase.StopAssetEditing(); return false; } i++; } EditorUtility.ClearProgressBar(); AssetDatabase.StopAssetEditing(); //unity now compiles all the shaders //now all new shaders are applied. this has to happen after unity compiled the shaders if (lockState == 1) { AssetDatabase.Refresh(); //Apply new shaders foreach (Material m in materialsToChangeLock) { if (ShaderOptimizer.LockApplyShader(m)) { m.SetFloat(GetOptimizerPropertyName(m.shader), 1); } } } AssetDatabase.Refresh(); if (ShaderEditor.Active != null && ShaderEditor.Active.IsDrawing) { GUIUtility.ExitGUI(); } return true; } public static string GetOptimizerPropertyName(Shader shader) { if (isShaderUsingThryOptimizer.ContainsKey(shader)) { if (isShaderUsingThryOptimizer[shader] == false) return null; return shaderThryOptimizerPropertyName[shader]; } else { if (IsShaderUsingThryOptimizer(shader) == false) return null; return shaderThryOptimizerPropertyName[shader]; } } private static Dictionary shaderThryOptimizerPropertyName = new Dictionary(); private static Dictionary isShaderUsingThryOptimizer = new Dictionary(); public static bool IsShaderUsingThryOptimizer(Shader shader) { if (isShaderUsingThryOptimizer.ContainsKey(shader)) { return isShaderUsingThryOptimizer[shader]; } SerializedObject shaderObject = new SerializedObject(shader); SerializedProperty props = shaderObject.FindProperty("m_ParsedForm.m_PropInfo.m_Props"); if (props != null) { foreach (SerializedProperty p in props) { SerializedProperty at = p.FindPropertyRelative("m_Attributes"); if (at.arraySize > 0) { if (at.GetArrayElementAtIndex(0).stringValue == "ThryShaderOptimizerLockButton") { //Debug.Log(shader.name + " found to use optimizer "); isShaderUsingThryOptimizer[shader] = true; shaderThryOptimizerPropertyName[shader] = p.displayName; return true; } } } } isShaderUsingThryOptimizer[shader] = false; return false; } public static bool IsMaterialLocked(Material material) { return material.shader.name.StartsWith("Hidden/") && material.GetTag(TAG_ORIGINAL_SHADER, false, "") != ""; } private static Dictionary shaderUsedTextureReferencesCount = new Dictionary(); public static int GetUsedTextureReferencesCount(Shader s) { //Shader.m_ParsedForm.m_SubShaders[i].m_Passes[j].m_Programs[k].m_SubPrograms[l].m_Parameters[m].m_TextureParams[n] //m_Programs not avaiable in unity 2019 return 0; /*if (shaderUsedTextureReferencesCount.ContainsKey(s)) return shaderUsedTextureReferencesCount[s]; SerializedObject shaderObject = new SerializedObject(s); SerializedProperty m_SubShaders = shaderObject.FindProperty("m_ParsedForm.m_SubShaders"); for (int i_subShader = 0; i_subShader < m_SubShaders.arraySize; i_subShader++) { SerializedProperty m_Passes = m_SubShaders.GetArrayElementAtIndex(i_subShader).FindPropertyRelative("m_Passes"); for (int i_passes = 0; i_passes < m_Passes.arraySize; i_passes++) { SerializedProperty m_Programs = m_Passes.GetArrayElementAtIndex(i_passes); foreach (SerializedProperty p in m_Programs) Debug.Log(p.displayName); } } return 0;*/ } } public class UnlockedMaterialsList : EditorWindow { private Vector2 scrollPosition = Vector2.zero; static Dictionary> unlockedMaterialsByShader = new Dictionary>(); static Dictionary> lockedMaterialsByShader = new Dictionary>(); static Dictionary lockedMaterialsByOriginalShader = new Dictionary(); static Dictionary unlockedFoldouts = new Dictionary(); static Dictionary lockedFoldouts = new Dictionary(); string searchTerm = ""; private void OnEnable() { UpdateList(); scrollPosition = Vector2.zero; } bool LockAllWarning(List materialsToLock) { return EditorUtility.DisplayDialog("Lock All Materials", $"You're about to lock {materialsToLock.Count} materials. This might take a while. Are you sure you want to proceed?", "Lock All", "Cancel"); } bool UnlockAllWarning(List materialsToUnlock) { return EditorUtility.DisplayDialog("Unlock All Materials", $"You're about to unlock {materialsToUnlock.Count} materials. This might cause crashes if over 64 textures are used in all materials on a single shader.\n\nAre you sure you want to proceed?", "Unlock All", "Cancel"); } void UpdateList() { lockedMaterialsByOriginalShader.Clear(); unlockedMaterialsByShader.Clear(); lockedMaterialsByShader.Clear(); string[] guids = AssetDatabase.FindAssets($"t:material {searchTerm}"); float step = 1.0f / guids.Length; float f = 0; EditorUtility.DisplayProgressBar("Searching materials...", "", f); List unlockedMaterials = new List(); List lockedMaterials = new List(); foreach (string g in guids) { Material m = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g)); if (m != null && m.shader != null && ShaderOptimizer.IsShaderUsingThryOptimizer(m.shader)) { if(ShaderOptimizer.IsMaterialLocked(m)) lockedMaterials.Add(m); else unlockedMaterials.Add(m); } f = f + step; EditorUtility.DisplayProgressBar("Searching materials...", m.name, f); } foreach (IGrouping materials in unlockedMaterials.GroupBy(m => m.shader)) { unlockedMaterialsByShader.Add(materials.Key, materials.ToList()); if(!unlockedFoldouts.ContainsKey(materials.Key.name)) unlockedFoldouts.Add(materials.Key.name, false); } foreach (Material material in lockedMaterials) { string originalShaderName = material.GetTag(ShaderOptimizer.TAG_ORIGINAL_SHADER, false, ""); Shader originalShader = null; if (originalShaderName == "" && originalShader == null) ShaderOptimizer.GuessShader(material.shader, out originalShader); if (originalShader == null) originalShader = Shader.Find(originalShaderName); if (originalShader != null) { if (!lockedMaterialsByShader.ContainsKey(originalShader)) lockedMaterialsByShader[originalShader] = new List(); lockedMaterialsByShader[originalShader].Add(material); if (!lockedFoldouts.ContainsKey(originalShader.name)) lockedFoldouts.Add(originalShader.name, false); } } EditorUtility.ClearProgressBar(); } private void OnGUI() { List materialsToLock = new List(); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); searchTerm = EditorGUILayout.DelayedTextField(searchTerm); if (GUILayout.Button("Update/Search") || EditorGUI.EndChangeCheck()) UpdateList(); EditorGUILayout.EndHorizontal(); int unlockedMaterials = unlockedMaterialsByShader.Values.SelectMany(col => col).ToList().Count; int lockedMaterials = lockedMaterialsByShader.Values.SelectMany(col => col).ToList().Count; EditorGUILayout.Space(10, true); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.LabelField($"Unlocked Materials ({unlockedMaterials})", Styles.EDITOR_LABEL_HEADER); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Lock All")) { List allUnlockedMaterials = unlockedMaterialsByShader.Values.SelectMany(col => col).ToList(); if(LockAllWarning(allUnlockedMaterials)) materialsToLock = allUnlockedMaterials; } if (GUILayout.Button("Expand All")) unlockedFoldouts.Keys.ToList().ForEach(f => unlockedFoldouts[f] = true); if (GUILayout.Button("Collapse All")) unlockedFoldouts.Keys.ToList().ForEach(f => unlockedFoldouts[f] = false); EditorGUILayout.EndHorizontal(); if (unlockedMaterialsByShader.Count == 0) GUILayout.Label("No Locked materials found for search term.", Styles.greenStyle); foreach (KeyValuePair> shaderMaterials in unlockedMaterialsByShader) { EditorGUILayout.Space(); unlockedFoldouts[shaderMaterials.Key.name] = EditorGUILayout.BeginFoldoutHeaderGroup(unlockedFoldouts[shaderMaterials.Key.name], $"{shaderMaterials.Key.name} ({shaderMaterials.Value.Count.ToString()})"); if (unlockedFoldouts[shaderMaterials.Key.name]) { if (GUILayout.Button("Lock All")) { if(LockAllWarning(shaderMaterials.Value)) materialsToLock = shaderMaterials.Value; } foreach (Material m in shaderMaterials.Value) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.ObjectField(m, typeof(Material), false); //EditorGUILayout.IntField(ShaderOptimizer.GetUsedTextureReferencesCount(m.shader)); if (GUILayout.Button("Lock")) materialsToLock.Add(m); EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndFoldoutHeaderGroup(); } if (materialsToLock.Count > 0) { ShaderOptimizer.SetLockedForAllMaterials(materialsToLock, 1, true, false, true); materialsToLock.Clear(); UpdateList(); } EditorGUILayout.Space(10, true); EditorGUILayout.LabelField($"Locked Materials ({lockedMaterials})", Styles.EDITOR_LABEL_HEADER); List materialsToUnlock = new List(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Unlock All")) { List allLockedMaterials = lockedMaterialsByShader.Values.SelectMany(col => col).ToList(); if (UnlockAllWarning(allLockedMaterials)) { materialsToUnlock = allLockedMaterials; } } if (GUILayout.Button("Expand All")) lockedFoldouts.Keys.ToList().ForEach(f => lockedFoldouts[f] = true); if (GUILayout.Button("Collapse All")) lockedFoldouts.Keys.ToList().ForEach(f => lockedFoldouts[f] = false); EditorGUILayout.EndHorizontal(); if (lockedMaterialsByShader.Count == 0) GUILayout.Label("No Unlocked materials found for search term.", Styles.greenStyle); foreach (KeyValuePair> shaderMaterials in lockedMaterialsByShader) { EditorGUILayout.Space(); lockedFoldouts[shaderMaterials.Key.name] = EditorGUILayout.BeginFoldoutHeaderGroup(lockedFoldouts[shaderMaterials.Key.name], $"{shaderMaterials.Key.name} ({shaderMaterials.Value.Count.ToString()})"); if (lockedFoldouts[shaderMaterials.Key.name]) { if (GUILayout.Button("Unlock All")) { if(UnlockAllWarning(shaderMaterials.Value)) materialsToUnlock = shaderMaterials.Value; } foreach (Material m in shaderMaterials.Value) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.ObjectField(m, typeof(Material), false); //EditorGUILayout.IntField(ShaderOptimizer.GetUsedTextureReferencesCount(m.shader)); if (GUILayout.Button("Unlock")) materialsToUnlock.Add(m); EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndFoldoutHeaderGroup(); } if (materialsToUnlock.Count > 0) { ShaderOptimizer.SetLockedForAllMaterials(materialsToUnlock, 0, true, false, true); materialsToUnlock.Clear(); UpdateList(); } EditorGUILayout.EndScrollView(); } } }