/// /// Shader Control - (C) Copyright 2016-2022 Ramiro Oliva (Kronnect) /// /// using UnityEngine; using UnityEditor; using System; using System.IO; using System.Collections.Generic; using System.Text; namespace ShaderControl { public partial class SCWindow : EditorWindow { class KeywordView { public SCKeyword keyword; public List shaders; public bool foldout; } const string PRAGMA_COMMENT_MARK = "// Edited by Shader Control: "; const string PRAGMA_DISABLED_MARK = "// Disabled by Shader Control: "; const string BACKUP_SUFFIX = "_backup"; const string PRAGMA_UNDERSCORE = "__ "; List shaders; Dictionary shadersDict; int minimumKeywordCount; int totalShaderCount; int maxKeywordsCountFound = 0; int totalKeywords, totalGlobalKeywords, totalVariants, totalUsedKeywords, totalBuildVariants, totalGlobalShaderFeatures, totalGlobalShaderFeaturesNonReadonly; int plusBuildKeywords; Dictionary> uniqueKeywords, uniqueEnabledKeywords; Dictionary keywordsDict; List keywordView; List keywordViewExtra; readonly StringBuilder convertToLocalLog = new StringBuilder(); #region Shader handling void ScanProject() { try { if (shaders == null) { shaders = new List(); } else { shaders.Clear(); } // Add shaders from Resources folder string[] guids = AssetDatabase.FindAssets("t:Shader"); totalShaderCount = guids.Length; for (int k = 0; k < totalShaderCount; k++) { string guid = guids[k]; string path = AssetDatabase.GUIDToAssetPath(guid); if (path != null) { string pathUpper = path.ToUpper(); if (scanAllShaders || pathUpper.Contains("\\RESOURCES\\") || pathUpper.Contains("/RESOURCES/")) { // this shader will be included in build Shader unityShader = AssetDatabase.LoadAssetAtPath(path); if (unityShader != null) { SCShader shader = new SCShader(); shader.fullName = unityShader.name; shader.name = SCShader.GetSimpleName(shader.fullName); // Path.GetFileNameWithoutExtension(path); shader.path = path; shader.isReadOnly = path.Contains("Packages/com.unity") || IsFileReadonly(path); shader.GUID = unityShader.GetInstanceID(); ScanShader(shader); if (shader.keywords.Count > 0) { shaders.Add(shader); } } } } } // Load and reference materials if (shadersDict == null) { shadersDict = new Dictionary(shaders.Count); } else { shadersDict.Clear(); } shaders.ForEach(shader => { shadersDict[shader.GUID] = shader; }); string[] matGuids = AssetDatabase.FindAssets("t:Material"); if (projectMaterials == null) { projectMaterials = new List(); } else { projectMaterials.Clear(); } for (int k = 0; k < matGuids.Length; k++) { string matGUID = matGuids[k]; string matPath = AssetDatabase.GUIDToAssetPath(matGUID); Material mat = AssetDatabase.LoadAssetAtPath(matPath); if (mat.shader == null) continue; SCMaterial scMat = new SCMaterial(mat, matPath, matGUID); scMat.SetKeywords(mat.shaderKeywords); if (mat.shaderKeywords != null && mat.shaderKeywords.Length > 0) { projectMaterials.Add(scMat); } string path = AssetDatabase.GetAssetPath(mat.shader); int shaderGUID = mat.shader.GetInstanceID(); SCShader shader; if (!shadersDict.TryGetValue(shaderGUID, out shader)) { if (mat.shaderKeywords == null || mat.shaderKeywords.Length == 0) continue; Shader shad = AssetDatabase.LoadAssetAtPath(path); // add non-sourced shader shader = new SCShader(); shader.isReadOnly = path.Contains("Packages/com.unity") || IsFileReadonly(path); shader.GUID = shaderGUID; if (shad != null) { shader.fullName = shad.name; shader.name = SCShader.GetSimpleName(shader.fullName); if (string.IsNullOrEmpty(shader.name)) { shader.name = Path.GetFileNameWithoutExtension(path); } shader.path = path; ScanShader(shader); } else { shader.fullName = mat.shader.name; shader.name = SCShader.GetSimpleName(shader.fullName); } shaders.Add(shader); shadersDict[shaderGUID] = shader; totalShaderCount++; } shader.materials.Add(scMat); shader.AddKeywordsByName(mat.shaderKeywords); } // sort materials by name projectMaterials.Sort(CompareMaterialsName); // refresh variant and keywords count due to potential additional added keywords from materials (rogue keywords) and shader features count maxKeywordsCountFound = 0; shaders.ForEach((SCShader shader) => { if (shader.keywordEnabledCount > maxKeywordsCountFound) { maxKeywordsCountFound = shader.keywordEnabledCount; } shader.UpdateVariantCount(); }); switch (sortType) { case SortType.VariantsCount: shaders.Sort((SCShader x, SCShader y) => { return y.actualBuildVariantCount.CompareTo(x.actualBuildVariantCount); }); break; case SortType.EnabledKeywordsCount: shaders.Sort((SCShader x, SCShader y) => { return y.keywordEnabledCount.CompareTo(x.keywordEnabledCount); }); break; case SortType.ShaderFileName: shaders.Sort((SCShader x, SCShader y) => { return x.name.CompareTo(y.name); }); break; } UpdateProjectStats(); } catch (Exception ex) { Debug.LogError("Unexpected exception caught while scanning project: " + ex.Message); } } static int CompareMaterialsName(SCMaterial m1, SCMaterial m2) { return m1.unityMaterial.name.CompareTo(m2.unityMaterial.name); } void ScanShader(SCShader shader) { // Inits shader shader.passes.Clear(); shader.keywords.Clear(); shader.hasBackup = File.Exists(shader.path + BACKUP_SUFFIX); shader.pendingChanges = false; shader.editedByShaderControl = shader.hasBackup; if (shader.path.EndsWith(".shadergraph")) { shader.isShaderGraph = true; try { ScanShaderGraph(shader); } catch (Exception ex) { Debug.LogError("Couldn't analyze shader graph at " + shader.path + ". Error found: " + ex.ToString()); } } else { try { ScanShaderNonGraph(shader); } catch (Exception ex) { Debug.LogError("Couldn't analyze shader at " + shader.path + ". Error found: " + ex.ToString()); } } } void UpdateProjectStats() { totalKeywords = 0; totalGlobalKeywords = 0; totalUsedKeywords = 0; totalVariants = 0; totalBuildVariants = 0; totalGlobalShaderFeatures = 0; totalGlobalShaderFeaturesNonReadonly = 0; if (shaders == null) return; if (keywordsDict == null) { keywordsDict = new Dictionary(); } else { keywordsDict.Clear(); } if (uniqueKeywords == null) { uniqueKeywords = new Dictionary>(); } else { uniqueKeywords.Clear(); } if (uniqueEnabledKeywords == null) { uniqueEnabledKeywords = new Dictionary>(); } else { uniqueEnabledKeywords.Clear(); } int shadersCount = shaders.Count; for (int k = 0; k < shadersCount; k++) { SCShader shader = shaders[k]; int keywordsCount = shader.keywords.Count; for (int w = 0; w < keywordsCount; w++) { SCKeyword keyword = shader.keywords[w]; List shadersWithThisKeyword; if (!uniqueKeywords.TryGetValue(keyword.name, out shadersWithThisKeyword)) { shadersWithThisKeyword = new List(); uniqueKeywords[keyword.name] = shadersWithThisKeyword; totalKeywords++; if (keyword.isGlobal) totalGlobalKeywords++; if (keyword.isGlobal && !keyword.isMultiCompile && keyword.enabled) { totalGlobalShaderFeatures++; if (!shader.isReadOnly) { totalGlobalShaderFeaturesNonReadonly++; } } keywordsDict[keyword.name] = keyword; } shadersWithThisKeyword.Add(shader); if (keyword.enabled) { List shadersWithThisKeywordEnabled; if (!uniqueEnabledKeywords.TryGetValue(keyword.name, out shadersWithThisKeywordEnabled)) { shadersWithThisKeywordEnabled = new List(); uniqueEnabledKeywords[keyword.name] = shadersWithThisKeywordEnabled; totalUsedKeywords++; } shadersWithThisKeywordEnabled.Add(shader); } if (!shader.isReadOnly) { keyword.canBeModified = true; if (keyword.isGlobal && !keyword.isMultiCompile) { keyword.canBeConvertedToLocal = true; } } } totalVariants += shader.totalVariantCount; totalBuildVariants += shader.actualBuildVariantCount; } if (keywordView == null) { keywordView = new List(); } else { keywordView.Clear(); } foreach (KeyValuePair> kvp in uniqueEnabledKeywords) { SCKeyword kw; if (!keywordsDict.TryGetValue(kvp.Key, out kw)) continue; KeywordView kv = new KeywordView { keyword = kw, shaders = kvp.Value }; keywordView.Add(kv); } keywordView.Sort(delegate (KeywordView x, KeywordView y) { return y.shaders.Count.CompareTo(x.shaders.Count); }); // Compute which keywords in build are not present in project if (keywordViewExtra == null) { keywordViewExtra = new List(); } else { keywordViewExtra.Clear(); } plusBuildKeywords = 0; if (buildKeywordView != null) { int count = buildKeywordView.Count; for (int k = 0; k < count; k++) { BuildKeywordView bkv = buildKeywordView[k]; if (!uniqueKeywords.ContainsKey(bkv.keyword)) { keywordViewExtra.Add(bkv); plusBuildKeywords++; } } } } bool IsFileReadonly(string path) { FileStream stream = null; try { FileAttributes fileAttributes = File.GetAttributes(path); if ((fileAttributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) { return true; } FileInfo file = new FileInfo(path); stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None); } catch { //the file is unavailable because it is: //still being written to //or being processed by another thread //or does not exist (has already been processed) return true; } finally { if (stream != null) stream.Close(); } //file is not locked return false; } void MakeBackup(SCShader shader) { string backupPath = shader.path + BACKUP_SUFFIX; if (!File.Exists(backupPath)) { AssetDatabase.CopyAsset(shader.path, backupPath); shader.hasBackup = true; } } void UpdateShader(SCShader shader) { if (shader.isReadOnly) { EditorUtility.DisplayDialog("Locked file", "Shader file " + shader.name + " is read-only.", "Ok"); return; } try { // Create backup MakeBackup(shader); if (shader.isShaderGraph) { UpdateShaderGraph(shader); } else { UpdateShaderNonGraph(shader); } // Also update materials CleanMaterials(shader); ScanShader(shader); // Rescan shader // do not include in build (sync with Build View) BuildUpdateShaderKeywordsState(shader); } catch (Exception ex) { Debug.LogError("Unexpected exception caught while updating shader: " + ex.Message); } } void RestoreShader(SCShader shader) { try { string shaderBackupPath = shader.path + BACKUP_SUFFIX; if (!File.Exists(shaderBackupPath)) { EditorUtility.DisplayDialog("Restore shader", "Shader backup is missing!", "OK"); return; } File.Copy(shaderBackupPath, shader.path, true); File.Delete(shaderBackupPath); if (File.Exists(shaderBackupPath + ".meta")) File.Delete(shaderBackupPath + ".meta"); AssetDatabase.Refresh(); ScanShader(shader); // Rescan shader UpdateProjectStats(); } catch (Exception ex) { Debug.LogError("Unexpected exception caught while restoring shader: " + ex.Message); } } void DeleteShader(SCShader shader) { try { if (File.Exists(shader.path)) { File.Delete(shader.path); } else { EditorUtility.DisplayDialog("Error", "Shader file was not found at " + shader.path + "!?", "Weird"); return; } File.Delete(shader.path); if (File.Exists(shader.path + ".meta")) { File.Delete(shader.path + ".meta"); } AssetDatabase.Refresh(); ScanProject(); } catch (Exception ex) { Debug.LogError("Unexpected exception caught while deleting shader: " + ex.Message); } } void ConvertToLocalStarted() { convertToLocalLog.Length = 0; } void ConvertToLocalFinished() { if (convertToLocalLog.Length > 0) { EditorUtility.DisplayDialog("Convert To Local Keyword", "The operation finished with the following results:\n\n" + convertToLocalLog.ToString(), "Ok"); } else { EditorUtility.DisplayDialog("Convert To Local Keyword", "The operation finished successfully.", "Ok"); } AssetDatabase.Refresh(); } void ConvertToLocal(SCKeyword keyword) { List shaders; if (!uniqueKeywords.TryGetValue(keyword.name, out shaders)) return; if (shaders == null) return; StringBuilder sb = new StringBuilder(); for (int k = 0; k < shaders.Count; k++) { SCShader shader = shaders[k]; if (shader.isReadOnly) { if (sb.Length > 0) { sb.Append(", "); } sb.Append(shader.name); } else { ConvertToLocal(keyword, shaders[k]); } } if (sb.Length > 0) { convertToLocalLog.AppendLine("The following shaders couldn't be modified because they're read-only: " + sb.ToString()); } } void ConvertToLocal(SCKeyword keyword, SCShader shader) { // Check total local keyword does not exceed 64 limit int potentialCount = 0; int kwCount = shader.keywords.Count; for (int k = 0; k < kwCount; k++) { SCKeyword kw = shader.keywords[k]; if (!kw.isMultiCompile) potentialCount++; } if (potentialCount > 64) return; if (shader.isReadOnly) { convertToLocalLog.AppendLine("The keyword " + keyword.name + " can't be converted to local in shader " + shader.name + " at " + shader.path + " because file is readonly."); return; } if (shader.isShaderGraph) { ConvertToLocalGraph(keyword, shader); } else { ConvertToLocalNonGraph(keyword, shader); } } void ConvertToLocalAll() { int kvCount = keywordView.Count; ConvertToLocalStarted(); for (int s = 0; s < kvCount; s++) { SCKeyword keyword = keywordView[s].keyword; if (keyword.isGlobal && !keyword.isMultiCompile) { ConvertToLocal(keyword); } } ConvertToLocalFinished(); } SCShader GetShaderByName(string shaderName) { if (shaders == null) { ScanProject(); } foreach (SCShader shader in shaders) { if (shader.fullName.Equals(shaderName)) { return shader; } } return null; } #endregion } }