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;
}
}