MaterialAnalyzer.cs 13 KB

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