SCWindow.Project.Shaders.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /// <summary>
  2. /// Shader Control - (C) Copyright 2016-2022 Ramiro Oliva (Kronnect)
  3. /// </summary>
  4. ///
  5. using UnityEngine;
  6. using UnityEditor;
  7. using System;
  8. using System.IO;
  9. using System.Collections.Generic;
  10. using System.Text;
  11. namespace ShaderControl {
  12. public partial class SCWindow : EditorWindow {
  13. class KeywordView {
  14. public SCKeyword keyword;
  15. public List<SCShader> shaders;
  16. public bool foldout;
  17. }
  18. const string PRAGMA_COMMENT_MARK = "// Edited by Shader Control: ";
  19. const string PRAGMA_DISABLED_MARK = "// Disabled by Shader Control: ";
  20. const string BACKUP_SUFFIX = "_backup";
  21. const string PRAGMA_UNDERSCORE = "__ ";
  22. List<SCShader> shaders;
  23. Dictionary<int, SCShader> shadersDict;
  24. int minimumKeywordCount;
  25. int totalShaderCount;
  26. int maxKeywordsCountFound = 0;
  27. int totalKeywords, totalGlobalKeywords, totalVariants, totalUsedKeywords, totalBuildVariants, totalGlobalShaderFeatures, totalGlobalShaderFeaturesNonReadonly;
  28. int plusBuildKeywords;
  29. Dictionary<string, List<SCShader>> uniqueKeywords, uniqueEnabledKeywords;
  30. Dictionary<string, SCKeyword> keywordsDict;
  31. List<KeywordView> keywordView;
  32. List<BuildKeywordView> keywordViewExtra;
  33. readonly StringBuilder convertToLocalLog = new StringBuilder();
  34. #region Shader handling
  35. void ScanProject() {
  36. try {
  37. if (shaders == null) {
  38. shaders = new List<SCShader>();
  39. } else {
  40. shaders.Clear();
  41. }
  42. // Add shaders from Resources folder
  43. string[] guids = AssetDatabase.FindAssets("t:Shader");
  44. totalShaderCount = guids.Length;
  45. for (int k = 0; k < totalShaderCount; k++) {
  46. string guid = guids[k];
  47. string path = AssetDatabase.GUIDToAssetPath(guid);
  48. if (path != null) {
  49. string pathUpper = path.ToUpper();
  50. if (scanAllShaders || pathUpper.Contains("\\RESOURCES\\") || pathUpper.Contains("/RESOURCES/")) { // this shader will be included in build
  51. Shader unityShader = AssetDatabase.LoadAssetAtPath<Shader>(path);
  52. if (unityShader != null) {
  53. SCShader shader = new SCShader();
  54. shader.fullName = unityShader.name;
  55. shader.name = SCShader.GetSimpleName(shader.fullName); // Path.GetFileNameWithoutExtension(path);
  56. shader.path = path;
  57. shader.isReadOnly = path.Contains("Packages/com.unity") || IsFileReadonly(path);
  58. shader.GUID = unityShader.GetInstanceID();
  59. ScanShader(shader);
  60. if (shader.keywords.Count > 0) {
  61. shaders.Add(shader);
  62. }
  63. }
  64. }
  65. }
  66. }
  67. // Load and reference materials
  68. if (shadersDict == null) {
  69. shadersDict = new Dictionary<int, SCShader>(shaders.Count);
  70. } else {
  71. shadersDict.Clear();
  72. }
  73. shaders.ForEach(shader => {
  74. shadersDict[shader.GUID] = shader;
  75. });
  76. string[] matGuids = AssetDatabase.FindAssets("t:Material");
  77. if (projectMaterials == null) {
  78. projectMaterials = new List<SCMaterial>();
  79. } else {
  80. projectMaterials.Clear();
  81. }
  82. for (int k = 0; k < matGuids.Length; k++) {
  83. string matGUID = matGuids[k];
  84. string matPath = AssetDatabase.GUIDToAssetPath(matGUID);
  85. Material mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
  86. if (mat.shader == null)
  87. continue;
  88. SCMaterial scMat = new SCMaterial(mat, matPath, matGUID);
  89. scMat.SetKeywords(mat.shaderKeywords);
  90. if (mat.shaderKeywords != null && mat.shaderKeywords.Length > 0) {
  91. projectMaterials.Add(scMat);
  92. }
  93. string path = AssetDatabase.GetAssetPath(mat.shader);
  94. int shaderGUID = mat.shader.GetInstanceID();
  95. SCShader shader;
  96. if (!shadersDict.TryGetValue(shaderGUID, out shader)) {
  97. if (mat.shaderKeywords == null || mat.shaderKeywords.Length == 0)
  98. continue;
  99. Shader shad = AssetDatabase.LoadAssetAtPath<Shader>(path);
  100. // add non-sourced shader
  101. shader = new SCShader();
  102. shader.isReadOnly = path.Contains("Packages/com.unity") || IsFileReadonly(path);
  103. shader.GUID = shaderGUID;
  104. if (shad != null) {
  105. shader.fullName = shad.name;
  106. shader.name = SCShader.GetSimpleName(shader.fullName);
  107. if (string.IsNullOrEmpty(shader.name)) {
  108. shader.name = Path.GetFileNameWithoutExtension(path);
  109. }
  110. shader.path = path;
  111. ScanShader(shader);
  112. } else {
  113. shader.fullName = mat.shader.name;
  114. shader.name = SCShader.GetSimpleName(shader.fullName);
  115. }
  116. shaders.Add(shader);
  117. shadersDict[shaderGUID] = shader;
  118. totalShaderCount++;
  119. }
  120. shader.materials.Add(scMat);
  121. shader.AddKeywordsByName(mat.shaderKeywords);
  122. }
  123. // sort materials by name
  124. projectMaterials.Sort(CompareMaterialsName);
  125. // refresh variant and keywords count due to potential additional added keywords from materials (rogue keywords) and shader features count
  126. maxKeywordsCountFound = 0;
  127. shaders.ForEach((SCShader shader) => {
  128. if (shader.keywordEnabledCount > maxKeywordsCountFound) {
  129. maxKeywordsCountFound = shader.keywordEnabledCount;
  130. }
  131. shader.UpdateVariantCount();
  132. });
  133. switch (sortType) {
  134. case SortType.VariantsCount:
  135. shaders.Sort((SCShader x, SCShader y) => {
  136. return y.actualBuildVariantCount.CompareTo(x.actualBuildVariantCount);
  137. });
  138. break;
  139. case SortType.EnabledKeywordsCount:
  140. shaders.Sort((SCShader x, SCShader y) => {
  141. return y.keywordEnabledCount.CompareTo(x.keywordEnabledCount);
  142. });
  143. break;
  144. case SortType.ShaderFileName:
  145. shaders.Sort((SCShader x, SCShader y) => {
  146. return x.name.CompareTo(y.name);
  147. });
  148. break;
  149. }
  150. UpdateProjectStats();
  151. } catch (Exception ex) {
  152. Debug.LogError("Unexpected exception caught while scanning project: " + ex.Message);
  153. }
  154. }
  155. static int CompareMaterialsName(SCMaterial m1, SCMaterial m2) {
  156. return m1.unityMaterial.name.CompareTo(m2.unityMaterial.name);
  157. }
  158. void ScanShader(SCShader shader) {
  159. // Inits shader
  160. shader.passes.Clear();
  161. shader.keywords.Clear();
  162. shader.hasBackup = File.Exists(shader.path + BACKUP_SUFFIX);
  163. shader.pendingChanges = false;
  164. shader.editedByShaderControl = shader.hasBackup;
  165. if (shader.path.EndsWith(".shadergraph")) {
  166. shader.isShaderGraph = true;
  167. try {
  168. ScanShaderGraph(shader);
  169. } catch (Exception ex) {
  170. Debug.LogError("Couldn't analyze shader graph at " + shader.path + ". Error found: " + ex.ToString());
  171. }
  172. } else {
  173. try {
  174. ScanShaderNonGraph(shader);
  175. } catch (Exception ex) {
  176. Debug.LogError("Couldn't analyze shader at " + shader.path + ". Error found: " + ex.ToString());
  177. }
  178. }
  179. }
  180. void UpdateProjectStats() {
  181. totalKeywords = 0;
  182. totalGlobalKeywords = 0;
  183. totalUsedKeywords = 0;
  184. totalVariants = 0;
  185. totalBuildVariants = 0;
  186. totalGlobalShaderFeatures = 0;
  187. totalGlobalShaderFeaturesNonReadonly = 0;
  188. if (shaders == null)
  189. return;
  190. if (keywordsDict == null) {
  191. keywordsDict = new Dictionary<string, SCKeyword>();
  192. } else {
  193. keywordsDict.Clear();
  194. }
  195. if (uniqueKeywords == null) {
  196. uniqueKeywords = new Dictionary<string, List<SCShader>>();
  197. } else {
  198. uniqueKeywords.Clear();
  199. }
  200. if (uniqueEnabledKeywords == null) {
  201. uniqueEnabledKeywords = new Dictionary<string, List<SCShader>>();
  202. } else {
  203. uniqueEnabledKeywords.Clear();
  204. }
  205. int shadersCount = shaders.Count;
  206. for (int k = 0; k < shadersCount; k++) {
  207. SCShader shader = shaders[k];
  208. int keywordsCount = shader.keywords.Count;
  209. for (int w = 0; w < keywordsCount; w++) {
  210. SCKeyword keyword = shader.keywords[w];
  211. List<SCShader> shadersWithThisKeyword;
  212. if (!uniqueKeywords.TryGetValue(keyword.name, out shadersWithThisKeyword)) {
  213. shadersWithThisKeyword = new List<SCShader>();
  214. uniqueKeywords[keyword.name] = shadersWithThisKeyword;
  215. totalKeywords++;
  216. if (keyword.isGlobal) totalGlobalKeywords++;
  217. if (keyword.isGlobal && !keyword.isMultiCompile && keyword.enabled) {
  218. totalGlobalShaderFeatures++;
  219. if (!shader.isReadOnly) {
  220. totalGlobalShaderFeaturesNonReadonly++;
  221. }
  222. }
  223. keywordsDict[keyword.name] = keyword;
  224. }
  225. shadersWithThisKeyword.Add(shader);
  226. if (keyword.enabled) {
  227. List<SCShader> shadersWithThisKeywordEnabled;
  228. if (!uniqueEnabledKeywords.TryGetValue(keyword.name, out shadersWithThisKeywordEnabled)) {
  229. shadersWithThisKeywordEnabled = new List<SCShader>();
  230. uniqueEnabledKeywords[keyword.name] = shadersWithThisKeywordEnabled;
  231. totalUsedKeywords++;
  232. }
  233. shadersWithThisKeywordEnabled.Add(shader);
  234. }
  235. if (!shader.isReadOnly) {
  236. keyword.canBeModified = true;
  237. if (keyword.isGlobal && !keyword.isMultiCompile) {
  238. keyword.canBeConvertedToLocal = true;
  239. }
  240. }
  241. }
  242. totalVariants += shader.totalVariantCount;
  243. totalBuildVariants += shader.actualBuildVariantCount;
  244. }
  245. if (keywordView == null) {
  246. keywordView = new List<KeywordView>();
  247. } else {
  248. keywordView.Clear();
  249. }
  250. foreach (KeyValuePair<string, List<SCShader>> kvp in uniqueEnabledKeywords) {
  251. SCKeyword kw;
  252. if (!keywordsDict.TryGetValue(kvp.Key, out kw)) continue;
  253. KeywordView kv = new KeywordView { keyword = kw, shaders = kvp.Value };
  254. keywordView.Add(kv);
  255. }
  256. keywordView.Sort(delegate (KeywordView x, KeywordView y) {
  257. return y.shaders.Count.CompareTo(x.shaders.Count);
  258. });
  259. // Compute which keywords in build are not present in project
  260. if (keywordViewExtra == null) {
  261. keywordViewExtra = new List<BuildKeywordView>();
  262. } else {
  263. keywordViewExtra.Clear();
  264. }
  265. plusBuildKeywords = 0;
  266. if (buildKeywordView != null) {
  267. int count = buildKeywordView.Count;
  268. for (int k = 0; k < count; k++) {
  269. BuildKeywordView bkv = buildKeywordView[k];
  270. if (!uniqueKeywords.ContainsKey(bkv.keyword)) {
  271. keywordViewExtra.Add(bkv);
  272. plusBuildKeywords++;
  273. }
  274. }
  275. }
  276. }
  277. bool IsFileReadonly(string path) {
  278. FileStream stream = null;
  279. try {
  280. FileAttributes fileAttributes = File.GetAttributes(path);
  281. if ((fileAttributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) {
  282. return true;
  283. }
  284. FileInfo file = new FileInfo(path);
  285. stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);
  286. } catch {
  287. //the file is unavailable because it is:
  288. //still being written to
  289. //or being processed by another thread
  290. //or does not exist (has already been processed)
  291. return true;
  292. } finally {
  293. if (stream != null)
  294. stream.Close();
  295. }
  296. //file is not locked
  297. return false;
  298. }
  299. void MakeBackup(SCShader shader) {
  300. string backupPath = shader.path + BACKUP_SUFFIX;
  301. if (!File.Exists(backupPath)) {
  302. AssetDatabase.CopyAsset(shader.path, backupPath);
  303. shader.hasBackup = true;
  304. }
  305. }
  306. void UpdateShader(SCShader shader) {
  307. if (shader.isReadOnly) {
  308. EditorUtility.DisplayDialog("Locked file", "Shader file " + shader.name + " is read-only.", "Ok");
  309. return;
  310. }
  311. try {
  312. // Create backup
  313. MakeBackup(shader);
  314. if (shader.isShaderGraph) {
  315. UpdateShaderGraph(shader);
  316. } else {
  317. UpdateShaderNonGraph(shader);
  318. }
  319. // Also update materials
  320. CleanMaterials(shader);
  321. ScanShader(shader); // Rescan shader
  322. // do not include in build (sync with Build View)
  323. BuildUpdateShaderKeywordsState(shader);
  324. } catch (Exception ex) {
  325. Debug.LogError("Unexpected exception caught while updating shader: " + ex.Message);
  326. }
  327. }
  328. void RestoreShader(SCShader shader) {
  329. try {
  330. string shaderBackupPath = shader.path + BACKUP_SUFFIX;
  331. if (!File.Exists(shaderBackupPath)) {
  332. EditorUtility.DisplayDialog("Restore shader", "Shader backup is missing!", "OK");
  333. return;
  334. }
  335. File.Copy(shaderBackupPath, shader.path, true);
  336. File.Delete(shaderBackupPath);
  337. if (File.Exists(shaderBackupPath + ".meta"))
  338. File.Delete(shaderBackupPath + ".meta");
  339. AssetDatabase.Refresh();
  340. ScanShader(shader); // Rescan shader
  341. UpdateProjectStats();
  342. } catch (Exception ex) {
  343. Debug.LogError("Unexpected exception caught while restoring shader: " + ex.Message);
  344. }
  345. }
  346. void DeleteShader(SCShader shader) {
  347. try {
  348. if (File.Exists(shader.path)) {
  349. File.Delete(shader.path);
  350. } else {
  351. EditorUtility.DisplayDialog("Error", "Shader file was not found at " + shader.path + "!?", "Weird");
  352. return;
  353. }
  354. File.Delete(shader.path);
  355. if (File.Exists(shader.path + ".meta")) {
  356. File.Delete(shader.path + ".meta");
  357. }
  358. AssetDatabase.Refresh();
  359. ScanProject();
  360. } catch (Exception ex) {
  361. Debug.LogError("Unexpected exception caught while deleting shader: " + ex.Message);
  362. }
  363. }
  364. void ConvertToLocalStarted() {
  365. convertToLocalLog.Length = 0;
  366. }
  367. void ConvertToLocalFinished() {
  368. if (convertToLocalLog.Length > 0) {
  369. EditorUtility.DisplayDialog("Convert To Local Keyword", "The operation finished with the following results:\n\n" + convertToLocalLog.ToString(), "Ok");
  370. } else {
  371. EditorUtility.DisplayDialog("Convert To Local Keyword", "The operation finished successfully.", "Ok");
  372. }
  373. AssetDatabase.Refresh();
  374. }
  375. void ConvertToLocal(SCKeyword keyword) {
  376. List<SCShader> shaders;
  377. if (!uniqueKeywords.TryGetValue(keyword.name, out shaders)) return;
  378. if (shaders == null) return;
  379. StringBuilder sb = new StringBuilder();
  380. for (int k = 0; k < shaders.Count; k++) {
  381. SCShader shader = shaders[k];
  382. if (shader.isReadOnly) {
  383. if (sb.Length > 0) {
  384. sb.Append(", ");
  385. }
  386. sb.Append(shader.name);
  387. } else {
  388. ConvertToLocal(keyword, shaders[k]);
  389. }
  390. }
  391. if (sb.Length > 0) {
  392. convertToLocalLog.AppendLine("The following shaders couldn't be modified because they're read-only: " + sb.ToString());
  393. }
  394. }
  395. void ConvertToLocal(SCKeyword keyword, SCShader shader) {
  396. // Check total local keyword does not exceed 64 limit
  397. int potentialCount = 0;
  398. int kwCount = shader.keywords.Count;
  399. for (int k = 0; k < kwCount; k++) {
  400. SCKeyword kw = shader.keywords[k];
  401. if (!kw.isMultiCompile) potentialCount++;
  402. }
  403. if (potentialCount > 64) return;
  404. if (shader.isReadOnly) {
  405. convertToLocalLog.AppendLine("The keyword " + keyword.name + " can't be converted to local in shader " + shader.name + " at " + shader.path + " because file is readonly.");
  406. return;
  407. }
  408. if (shader.isShaderGraph) {
  409. ConvertToLocalGraph(keyword, shader);
  410. } else {
  411. ConvertToLocalNonGraph(keyword, shader);
  412. }
  413. }
  414. void ConvertToLocalAll() {
  415. int kvCount = keywordView.Count;
  416. ConvertToLocalStarted();
  417. for (int s = 0; s < kvCount; s++) {
  418. SCKeyword keyword = keywordView[s].keyword;
  419. if (keyword.isGlobal && !keyword.isMultiCompile) {
  420. ConvertToLocal(keyword);
  421. }
  422. }
  423. ConvertToLocalFinished();
  424. }
  425. SCShader GetShaderByName(string shaderName) {
  426. if (shaders == null) {
  427. ScanProject();
  428. }
  429. foreach (SCShader shader in shaders) {
  430. if (shader.fullName.Equals(shaderName)) {
  431. return shader;
  432. }
  433. }
  434. return null;
  435. }
  436. #endregion
  437. }
  438. }