MaterialAnalyzer.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. using UnityEditor;
  6. using UnityEngine;
  7. /// ---------------------------------------------------------------------------
  8. /// <summary>
  9. /// Analyzes the currently selected gameobjects in scene recursively and lists all materials in an EditorWindow.
  10. /// The list allows to (mutli)select materials which automatically selects the scene game objects which use it.
  11. /// Additionally every list item provides a button to jump to the material asset in project window.
  12. /// </summary>
  13. /// ---------------------------------------------------------------------------
  14. public class MaterialAnalyzer : EditorWindow
  15. {
  16. /// PRIVATE ===============================================================
  17. private static GUIStyle mListStyle;
  18. private static GUIStyle mItemStyle;
  19. private static GUIStyle mItemSelectedStyle;
  20. private Texture2D mListBackgroundTex;
  21. private Texture2D mItemBackgroundTex;
  22. private Texture2D mItemSelectedBackgroundTex;
  23. /// -----------------------------------------------------------------------
  24. /// <summary>
  25. /// Defines a single list item encapsulating a set of game objects and a selection state.
  26. /// </summary>
  27. /// -----------------------------------------------------------------------
  28. private class ListItem
  29. {
  30. public ListItem(bool selected = false)
  31. {
  32. this.Selected = selected;
  33. }
  34. public HashSet<GameObject> GameObjects = new HashSet<GameObject>();
  35. public bool Selected;
  36. };
  37. /// -----------------------------------------------------------------------
  38. /// <summary>
  39. /// Material comparer calls the material name comparer.
  40. /// </summary>
  41. /// -----------------------------------------------------------------------
  42. private class MaterialComp : IComparer<Material>
  43. {
  44. public int Compare(Material x, Material y)
  45. {
  46. return x.name.CompareTo(y.name);
  47. }
  48. }
  49. /// <summary>
  50. /// Stores list items by material instance.
  51. /// </summary>
  52. private SortedDictionary<Material, ListItem> mSelectionMaterials =
  53. new SortedDictionary<Material, ListItem>(new MaterialComp());
  54. /// <summary>
  55. /// The current scroll position.
  56. /// </summary>
  57. private Vector2 mScrollPosition;
  58. /// <summary>
  59. /// A text dump of the material hierarchy.
  60. /// </summary>
  61. private string mMaterialHierarchyDump = string.Empty;
  62. /// METHODS ===============================================================
  63. /// -----------------------------------------------------------------------
  64. /// <summary>
  65. /// Adds menu named "Analyze Scene" to the "Debug" menu which creates and initializes a new instance of this class.
  66. /// </summary>
  67. /// -----------------------------------------------------------------------
  68. [MenuItem("Custom/Analyze Materials")]
  69. public static void Init()
  70. {
  71. MaterialAnalyzer win = EditorWindow.GetWindow(typeof(MaterialAnalyzer)) as MaterialAnalyzer;
  72. win.init();
  73. win.Show();
  74. }
  75. /// -----------------------------------------------------------------------
  76. /// <summary>
  77. /// Draws the GUI window.
  78. /// </summary>
  79. /// -----------------------------------------------------------------------
  80. private void OnGUI()
  81. {
  82. GUILayout.BeginVertical();
  83. if (GUILayout.Button("Analyze Selection"))
  84. analyzeSelection();
  85. if (GUILayout.Button("Dump Hierarchy"))
  86. dumpMaterialHierarchy();
  87. if (GUILayout.Button("Dump List"))
  88. dumpMaterialList();
  89. GUILayout.EndVertical();
  90. GUILayout.Label("Materials: " + mSelectionMaterials.Count.ToString(), EditorStyles.boldLabel);
  91. mScrollPosition = GUILayout.BeginScrollView(mScrollPosition, false, true, GUI.skin.horizontalScrollbar,
  92. GUI.skin.verticalScrollbar, mListStyle, GUILayout.MinWidth(400), GUILayout.MaxWidth(1000),
  93. GUILayout.MaxHeight(1000));
  94. foreach (KeyValuePair<Material, ListItem> item in mSelectionMaterials)
  95. {
  96. GUILayout.BeginHorizontal();
  97. // select the material asset in project hierarchy
  98. if (GUILayout.Button("<", EditorStyles.miniButton, GUILayout.Width(20)))
  99. {
  100. // unselect all selected and select the material instance in project
  101. foreach (ListItem listItem in mSelectionMaterials.Values)
  102. listItem.Selected = false;
  103. Selection.activeObject = item.Key;
  104. }
  105. if (GUILayout.Button(item.Key.name, item.Value.Selected ? mItemSelectedStyle : mItemStyle,
  106. GUILayout.MinWidth(200)))
  107. processListItemClick(item.Value);
  108. if (GUILayout.Button(item.Key.shader != null ? item.Key.shader.name : " <MISSING>",
  109. item.Value.Selected ? mItemSelectedStyle : mItemStyle, GUILayout.Width(300)))
  110. processListItemClick(item.Value);
  111. GUILayout.EndHorizontal();
  112. }
  113. GUILayout.EndScrollView();
  114. }
  115. /// -----------------------------------------------------------------------
  116. /// <summary>
  117. /// Processes the list item click.
  118. /// </summary>
  119. /// <param name='itemClicked'>
  120. /// The item clicked.
  121. /// </param>
  122. /// -----------------------------------------------------------------------
  123. private void processListItemClick(ListItem itemClicked)
  124. {
  125. Event e = Event.current;
  126. // if shift/control is pressed just add this element
  127. if (e.control)
  128. {
  129. itemClicked.Selected = !itemClicked.Selected;
  130. updateSceneSelection();
  131. }
  132. else
  133. {
  134. // unselect all selected and select this
  135. foreach (ListItem listItem in mSelectionMaterials.Values)
  136. listItem.Selected = false;
  137. itemClicked.Selected = true;
  138. updateSceneSelection();
  139. }
  140. }
  141. /// -----------------------------------------------------------------------
  142. /// <summary>
  143. /// Starts recursive analyze process iterating through every selected GameObject.
  144. /// </summary>
  145. /// -----------------------------------------------------------------------
  146. private void analyzeSelection()
  147. {
  148. mSelectionMaterials.Clear();
  149. if (Selection.transforms.Length == 0)
  150. {
  151. Debug.LogError("Please select the object(s) you wish to analyze.");
  152. return;
  153. }
  154. StringBuilder dump = new StringBuilder();
  155. foreach (Transform transform in Selection.transforms)
  156. analyzeGameObject(transform.gameObject, dump, "");
  157. mMaterialHierarchyDump = dump.ToString();
  158. }
  159. /// -----------------------------------------------------------------------
  160. /// <summary>
  161. /// Analyzes the given game object.
  162. /// </summary>
  163. /// <param name='gameObject'>
  164. /// The game object to analyze.
  165. /// </param>
  166. /// -----------------------------------------------------------------------
  167. private void analyzeGameObject(GameObject gameObject, StringBuilder dump, string indent)
  168. {
  169. dump.Append(indent + gameObject.name + "\n");
  170. foreach (Component component in gameObject.GetComponents<Component>())
  171. analyzeComponent(component, dump, indent + " ");
  172. foreach (Transform child in gameObject.transform)
  173. analyzeGameObject(child.gameObject, dump, indent + " ");
  174. }
  175. /// -----------------------------------------------------------------------
  176. /// <summary>
  177. /// Analyzes the given component.
  178. /// </summary>
  179. /// <param name='component'>
  180. /// The component to analyze.
  181. /// </param>
  182. /// -----------------------------------------------------------------------
  183. private void analyzeComponent(Component component, StringBuilder dump, string indent)
  184. {
  185. // early out if component is missing
  186. if (component == null)
  187. return;
  188. List<Material> materials = new List<Material>();
  189. switch (component.GetType().ToString())
  190. {
  191. case "UnityEngine.MeshRenderer":
  192. {
  193. MeshRenderer mr = component as MeshRenderer;
  194. foreach (Material mat in mr.sharedMaterials)
  195. materials.Add(mat);
  196. }
  197. break;
  198. default:
  199. break;
  200. }
  201. bool materialMissing = false;
  202. foreach (Material mat in materials)
  203. {
  204. if (mat == null)
  205. {
  206. materialMissing = true;
  207. dump.Append(indent + "> MISSING\n");
  208. }
  209. else
  210. {
  211. ListItem item;
  212. mSelectionMaterials.TryGetValue(mat, out item);
  213. if (item == null)
  214. {
  215. item = new ListItem();
  216. mSelectionMaterials.Add(mat, item);
  217. }
  218. item.GameObjects.Add(component.gameObject);
  219. string matName = mat.shader != null ? mat.name + " <" + mat.shader.name + ">" : mat.name + " <MISSING>";
  220. dump.Append(indent + "> " + matName + "\n");
  221. }
  222. }
  223. if (materialMissing)
  224. Debug.LogWarning("Material(s) missing in game object '" + component.gameObject + "'!");
  225. }
  226. /// -----------------------------------------------------------------------
  227. /// <summary>
  228. /// Dumps the current selection hierarchy to a file.
  229. /// </summary>
  230. /// -----------------------------------------------------------------------
  231. private void dumpMaterialHierarchy()
  232. {
  233. if (mMaterialHierarchyDump == string.Empty)
  234. {
  235. Debug.LogError("There is nothing to dump yet.");
  236. return;
  237. }
  238. string path = EditorUtility.SaveFilePanel("Hierarchy Dump File", "", "material_hierarchy.txt", "txt");
  239. File.WriteAllText(path, mMaterialHierarchyDump);
  240. }
  241. /// -----------------------------------------------------------------------
  242. /// <summary>
  243. /// Dumps the current list of materials.
  244. /// </summary>
  245. /// -----------------------------------------------------------------------
  246. private void dumpMaterialList()
  247. {
  248. if (mSelectionMaterials.Count == 0)
  249. {
  250. Debug.LogError("The material list is empty. Dump aborted.");
  251. return;
  252. }
  253. // create string from current list
  254. StringBuilder materialItems = new StringBuilder();
  255. materialItems.Append(String.Format("{0,-40}", "Material")).Append(" | ");
  256. materialItems.Append(String.Format("{0,-60}", "Shader")).Append(" | ");
  257. materialItems.Append(String.Format("{0,-9}", "Occurence") + " | ");
  258. materialItems.Append("GameObject List\n");
  259. materialItems.Append("----------------------------------------------------------------------------" +
  260. "----------------------------------------------------------------------------\n");
  261. foreach (KeyValuePair<Material, ListItem> item in mSelectionMaterials)
  262. {
  263. materialItems.Append(String.Format("{0,-40}", item.Key.name)).Append(" | ");
  264. materialItems.Append(String.Format("{0,-60}", item.Key.shader != null ? item.Key.shader.name : " MISSING"))
  265. .Append(" | ");
  266. materialItems.Append(String.Format("{0,-9}", " " + item.Value.GameObjects.Count.ToString()) + " | ");
  267. foreach (GameObject go in item.Value.GameObjects)
  268. materialItems.Append(" " + go.name + " |");
  269. materialItems.Append("\n");
  270. }
  271. string path = EditorUtility.SaveFilePanel("Material Dump File", "", "material_list.txt", "txt");
  272. File.WriteAllText(path, materialItems.ToString());
  273. }
  274. /// -----------------------------------------------------------------------
  275. /// <summary>
  276. /// Selects the game objects in scene stored in all selected list items.
  277. /// </summary>
  278. /// -----------------------------------------------------------------------
  279. private void updateSceneSelection()
  280. {
  281. HashSet<UnityEngine.Object> sceneObjectsToSelect = new HashSet<UnityEngine.Object>();
  282. foreach (ListItem item in mSelectionMaterials.Values)
  283. if (item.Selected)
  284. foreach (GameObject go in item.GameObjects)
  285. sceneObjectsToSelect.Add(go);
  286. UnityEngine.Object[] array = new UnityEngine.Object[sceneObjectsToSelect.Count];
  287. sceneObjectsToSelect.CopyTo(array);
  288. Selection.objects = array;
  289. }
  290. /// -----------------------------------------------------------------------
  291. /// <summary>
  292. /// Initializes GUI styles and textures used.
  293. /// </summary>
  294. /// -----------------------------------------------------------------------
  295. private void init()
  296. {
  297. // list background
  298. mListBackgroundTex = new Texture2D(1, 1);
  299. mListBackgroundTex.SetPixel(0, 0, new Color(0.6f, 0.6f, 0.6f));
  300. mListBackgroundTex.Apply();
  301. mListBackgroundTex.wrapMode = TextureWrapMode.Repeat;
  302. // list style
  303. mListStyle = new GUIStyle();
  304. mListStyle.normal.background = mListBackgroundTex;
  305. mListStyle.margin = new RectOffset(4, 4, 4, 4);
  306. mListStyle.border = new RectOffset(1, 1, 1, 1);
  307. // item background
  308. mItemBackgroundTex = new Texture2D(1, 1);
  309. Color wBGColor = new Color(0.0f, 0.0f, 0.0f);
  310. string wBGColorData = EditorPrefs.GetString("Windows/Background");
  311. string[] wBGColorArray = wBGColorData.Split(';');
  312. if (wBGColorArray.Length == 5)
  313. wBGColor = new Color(float.Parse(wBGColorArray[1]), float.Parse(wBGColorArray[2]),
  314. float.Parse(wBGColorArray[3]));
  315. else
  316. Debug.LogError("Invalid window color in EditorPref found." + wBGColorArray.Length);
  317. mItemBackgroundTex.SetPixel(0, 0, wBGColor);
  318. mItemBackgroundTex.Apply();
  319. mItemBackgroundTex.wrapMode = TextureWrapMode.Repeat;
  320. // item selected background
  321. mItemSelectedBackgroundTex = new Texture2D(1, 1);
  322. mItemSelectedBackgroundTex.SetPixel(0, 0, new Color(0.239f, 0.376f, 0.568f));
  323. mItemSelectedBackgroundTex.Apply();
  324. mItemSelectedBackgroundTex.wrapMode = TextureWrapMode.Repeat;
  325. // item style
  326. mItemStyle = new GUIStyle(EditorStyles.textField);
  327. mItemStyle.normal.background = mItemBackgroundTex;
  328. mItemStyle.hover.textColor = Color.cyan;
  329. mItemStyle.padding = new RectOffset(2, 2, 2, 2);
  330. mItemStyle.margin = new RectOffset(1, 2, 1, 1);
  331. mItemSelectedStyle = new GUIStyle(mItemStyle);
  332. mItemSelectedStyle.normal.background = mItemSelectedBackgroundTex;
  333. mItemSelectedStyle.normal.textColor = Color.white;
  334. }
  335. }