using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; namespace Editor { /// --------------------------------------------------------------------------- /// /// Analyzes the currently selected gameobjects in scene recursively and lists all materials in an EditorWindow. /// The list allows to (mutli)select materials which automatically selects the scene game objects which use it. /// Additionally every list item provides a button to jump to the material asset in project window. /// /// --------------------------------------------------------------------------- public class MaterialAnalyzer : EditorWindow { /// PRIVATE =============================================================== private static GUIStyle mListStyle; private static GUIStyle mItemStyle; private static GUIStyle mItemSelectedStyle; private Texture2D mListBackgroundTex; private Texture2D mItemBackgroundTex; private Texture2D mItemSelectedBackgroundTex; /// ----------------------------------------------------------------------- /// /// Defines a single list item encapsulating a set of game objects and a selection state. /// /// ----------------------------------------------------------------------- private class ListItem { public ListItem( bool selected = false ) { this.Selected = selected; } public HashSet GameObjects = new HashSet(); public bool Selected; }; /// ----------------------------------------------------------------------- /// /// Material comparer calls the material name comparer. /// /// ----------------------------------------------------------------------- private class MaterialComp : IComparer { public int Compare( Material x, Material y ) { return x.name.CompareTo( y.name ); } } /// /// Stores list items by material instance. /// private SortedDictionary mSelectionMaterials = new SortedDictionary( new MaterialComp() ); /// /// The current scroll position. /// private Vector2 mScrollPosition; /// /// A text dump of the material hierarchy. /// private string mMaterialHierarchyDump = string.Empty; /// METHODS =============================================================== /// ----------------------------------------------------------------------- /// /// Adds menu named "Analyze Scene" to the "Debug" menu which creates and initializes a new instance of this class. /// /// ----------------------------------------------------------------------- [MenuItem("Custom/Analyze Materials")] public static void Init() { MaterialAnalyzer win = EditorWindow.GetWindow( typeof(MaterialAnalyzer) ) as MaterialAnalyzer; win.init(); win.Show(); } /// ----------------------------------------------------------------------- /// /// Draws the GUI window. /// /// ----------------------------------------------------------------------- private void OnGUI() { GUILayout.BeginVertical(); if( GUILayout.Button( "Analyze Selection" ) ) analyzeSelection(); if( GUILayout.Button( "Dump Hierarchy" ) ) dumpMaterialHierarchy(); if( GUILayout.Button( "Dump List" ) ) dumpMaterialList(); GUILayout.EndVertical(); GUILayout.Label( "Materials: " + mSelectionMaterials.Count.ToString(), EditorStyles.boldLabel ); mScrollPosition = GUILayout.BeginScrollView( mScrollPosition, false, true, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, mListStyle, GUILayout.MinWidth( 400 ), GUILayout.MaxWidth( 1000 ), GUILayout.MaxHeight( 1000 ) ); foreach( KeyValuePair item in mSelectionMaterials ) { GUILayout.BeginHorizontal(); // select the material asset in project hierarchy if( GUILayout.Button( "<", EditorStyles.miniButton, GUILayout.Width( 20 ) ) ) { // unselect all selected and select the material instance in project foreach( ListItem listItem in mSelectionMaterials.Values ) listItem.Selected = false; Selection.activeObject = item.Key; } if( GUILayout.Button( item.Key.name, item.Value.Selected ? mItemSelectedStyle : mItemStyle, GUILayout.MinWidth( 200 ) ) ) processListItemClick( item.Value ); if( GUILayout.Button( item.Key.shader != null ? item.Key.shader.name : " ", item.Value.Selected ? mItemSelectedStyle : mItemStyle, GUILayout.Width( 300 ) ) ) processListItemClick( item.Value ); GUILayout.EndHorizontal(); } GUILayout.EndScrollView(); } /// ----------------------------------------------------------------------- /// /// Processes the list item click. /// /// /// The item clicked. /// /// ----------------------------------------------------------------------- private void processListItemClick( ListItem itemClicked ) { Event e = Event.current; // if shift/control is pressed just add this element if( e.control ) { itemClicked.Selected = !itemClicked.Selected; updateSceneSelection(); } else { // unselect all selected and select this foreach( ListItem listItem in mSelectionMaterials.Values ) listItem.Selected = false; itemClicked.Selected = true; updateSceneSelection(); } } /// ----------------------------------------------------------------------- /// /// Starts recursive analyze process iterating through every selected GameObject. /// /// ----------------------------------------------------------------------- private void analyzeSelection() { mSelectionMaterials.Clear(); if( Selection.transforms.Length == 0 ) { Debug.LogError( "Please select the object(s) you wish to analyze." ); return; } StringBuilder dump = new StringBuilder(); foreach( Transform transform in Selection.transforms ) analyzeGameObject( transform.gameObject, dump, "" ); mMaterialHierarchyDump = dump.ToString(); } /// ----------------------------------------------------------------------- /// /// Analyzes the given game object. /// /// /// The game object to analyze. /// /// ----------------------------------------------------------------------- private void analyzeGameObject( GameObject gameObject, StringBuilder dump, string indent ) { dump.Append( indent + gameObject.name + "\n" ); foreach( Component component in gameObject.GetComponents() ) analyzeComponent( component, dump, indent + " " ); foreach( Transform child in gameObject.transform ) analyzeGameObject( child.gameObject, dump, indent + " " ); } /// ----------------------------------------------------------------------- /// /// Analyzes the given component. /// /// /// The component to analyze. /// /// ----------------------------------------------------------------------- private void analyzeComponent( Component component, StringBuilder dump, string indent ) { // early out if component is missing if( component == null ) return; List materials = new List(); switch( component.GetType().ToString() ) { case "UnityEngine.MeshRenderer": { MeshRenderer mr = component as MeshRenderer; foreach( Material mat in mr.sharedMaterials ) materials.Add( mat ); } break; default: break; } bool materialMissing = false; foreach( Material mat in materials ) { if( mat == null ) { materialMissing = true; dump.Append( indent + "> MISSING\n" ); } else { ListItem item; mSelectionMaterials.TryGetValue( mat, out item ); if( item == null ) { item = new ListItem(); mSelectionMaterials.Add( mat, item ); } item.GameObjects.Add( component.gameObject ); string matName = mat.shader != null ? mat.name + " <" + mat.shader.name + ">" : mat.name + " "; dump.Append( indent + "> " + matName + "\n" ); } } if( materialMissing ) Debug.LogWarning( "Material(s) missing in game object '" + component.gameObject + "'!" ); } /// ----------------------------------------------------------------------- /// /// Dumps the current selection hierarchy to a file. /// /// ----------------------------------------------------------------------- private void dumpMaterialHierarchy() { if( mMaterialHierarchyDump == string.Empty ) { Debug.LogError( "There is nothing to dump yet." ); return; } string path = EditorUtility.SaveFilePanel( "Hierarchy Dump File", "", "material_hierarchy.txt", "txt" ); File.WriteAllText( path, mMaterialHierarchyDump ); } /// ----------------------------------------------------------------------- /// /// Dumps the current list of materials. /// /// ----------------------------------------------------------------------- private void dumpMaterialList() { if( mSelectionMaterials.Count == 0 ) { Debug.LogError( "The material list is empty. Dump aborted." ); return; } // create string from current list StringBuilder materialItems = new StringBuilder(); materialItems.Append( String.Format( "{0,-40}", "Material" ) ).Append( " | " ); materialItems.Append( String.Format( "{0,-60}", "Shader" ) ).Append( " | " ); materialItems.Append( String.Format( "{0,-9}", "Occurence" ) + " | " ); materialItems.Append( "GameObject List\n" ); materialItems.Append( "----------------------------------------------------------------------------" + "----------------------------------------------------------------------------\n" ); foreach( KeyValuePair item in mSelectionMaterials ) { materialItems.Append( String.Format( "{0,-40}", item.Key.name ) ).Append( " | " ); materialItems.Append( String.Format( "{0,-60}", item.Key.shader != null ? item.Key.shader.name : " MISSING" ) ).Append( " | " ); materialItems.Append( String.Format( "{0,-9}", " " + item.Value.GameObjects.Count.ToString() ) + " | " ); foreach( GameObject go in item.Value.GameObjects ) materialItems.Append( " " + go.name + " |" ); materialItems.Append( "\n" ); } string path = EditorUtility.SaveFilePanel( "Material Dump File", "", "material_list.txt", "txt" ); File.WriteAllText( path, materialItems.ToString() ); } /// ----------------------------------------------------------------------- /// /// Selects the game objects in scene stored in all selected list items. /// /// ----------------------------------------------------------------------- private void updateSceneSelection() { HashSet sceneObjectsToSelect = new HashSet(); foreach( ListItem item in mSelectionMaterials.Values ) if( item.Selected ) foreach( GameObject go in item.GameObjects ) sceneObjectsToSelect.Add( go ); UnityEngine.Object[] array = new UnityEngine.Object[sceneObjectsToSelect.Count]; sceneObjectsToSelect.CopyTo( array ); Selection.objects = array; } /// ----------------------------------------------------------------------- /// /// Initializes GUI styles and textures used. /// /// ----------------------------------------------------------------------- private void init() { // list background mListBackgroundTex = new Texture2D( 1, 1 ); mListBackgroundTex.SetPixel( 0, 0, new Color( 0.6f, 0.6f, 0.6f ) ); mListBackgroundTex.Apply(); mListBackgroundTex.wrapMode = TextureWrapMode.Repeat; // list style mListStyle = new GUIStyle(); mListStyle.normal.background = mListBackgroundTex; mListStyle.margin = new RectOffset( 4, 4, 4, 4 ); mListStyle.border = new RectOffset( 1, 1, 1, 1 ); // item background mItemBackgroundTex = new Texture2D( 1, 1 ); Color wBGColor = new Color( 0.0f, 0.0f, 0.0f ); string wBGColorData = EditorPrefs.GetString( "Windows/Background" ); string[] wBGColorArray = wBGColorData.Split( ';' ); if( wBGColorArray.Length == 5 ) wBGColor = new Color( float.Parse( wBGColorArray[1] ), float.Parse( wBGColorArray[2] ), float.Parse( wBGColorArray[3] ) ); else Debug.LogError( "Invalid window color in EditorPref found." + wBGColorArray.Length ); mItemBackgroundTex.SetPixel( 0, 0, wBGColor ); mItemBackgroundTex.Apply(); mItemBackgroundTex.wrapMode = TextureWrapMode.Repeat; // item selected background mItemSelectedBackgroundTex = new Texture2D( 1, 1 ); mItemSelectedBackgroundTex.SetPixel( 0, 0, new Color( 0.239f, 0.376f, 0.568f ) ); mItemSelectedBackgroundTex.Apply(); mItemSelectedBackgroundTex.wrapMode = TextureWrapMode.Repeat; // item style mItemStyle = new GUIStyle( EditorStyles.textField ); mItemStyle.normal.background = mItemBackgroundTex; mItemStyle.hover.textColor = Color.cyan; mItemStyle.padding = new RectOffset( 2, 2, 2, 2 ); mItemStyle.margin = new RectOffset( 1, 2, 1, 1 ); mItemSelectedStyle = new GUIStyle( mItemStyle ); mItemSelectedStyle.normal.background = mItemSelectedBackgroundTex; mItemSelectedStyle.normal.textColor = Color.white; } } }