using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; /// --------------------------------------------------------------------------- /// /// 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; } }