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