MaterialAnalyzer

From Unify Community Wiki
Jump to: navigation, search

Author: Xadhoom

Description

Adds a new editor window at menu Custom‚ÜíMaterialAnalyzer. The editor window allows analyzing and list all materials used by the current selection in scene. Results can be dumped as material list or hierarchy to text files. This window helps to keep track of material usage to optimize drawcalls.

Background information can be found here: http://trianglestrip.blogspot.de/2012/07/material-analyzer.html

Usage

You must place the script in a folder named Editor in your project's Assets folder for it to work properly.

Click on Custom‚ÜíMaterialAnalyzer

Select one or more game objects in scene. Press Analyze Selection. Now the list should show all materials used by the selected game objects. The small button to the left in the list select the material asset. Selecting one or more list items selects the game objects using this/these materials.

Press Dump Hierarchy to dump the selection hierarchy including the used materials to text file. Press Dump List to dump the list of materials to text file including the game objects using each material.

C# - MaterialAnalyzer.cs (Developed for Unity 3.5)

using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;
 
 
/// ---------------------------------------------------------------------------
/// <summary>
/// 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.
/// </summary>
/// ---------------------------------------------------------------------------
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;
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Defines a single list item encapsulating a set of game objects and a selection state.
	/// </summary>
	/// -----------------------------------------------------------------------
	private class ListItem
	{
		public ListItem( bool selected = false )
		{
			this.Selected = selected;
		}
 
		public HashSet<GameObject> GameObjects = new HashSet<GameObject>();
		public bool Selected;
	};
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Material comparer calls the material name comparer.
	/// </summary>
	/// -----------------------------------------------------------------------
	private class MaterialComp : IComparer<Material>
	{
		public int Compare( Material x, Material y )
		{
			return x.name.CompareTo( y.name );
		}
	}
	/// <summary>
	/// Stores list items by material instance.
	/// </summary>
	private SortedDictionary<Material, ListItem> mSelectionMaterials = new SortedDictionary<Material, ListItem>( new MaterialComp() );
 
	/// <summary>
	/// The current scroll position.
	/// </summary>
	private Vector2 mScrollPosition;
 
	/// <summary>
	/// A text dump of the material hierarchy.
	/// </summary>
	private string mMaterialHierarchyDump = string.Empty;
 
	/// METHODS ===============================================================
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Adds menu named "Analyze Scene" to the "Debug" menu which creates and initializes a new instance of this class.
	/// </summary>
	/// -----------------------------------------------------------------------
	[MenuItem("Custom/Analyze Materials")]
	public static void Init()
	{
		MaterialAnalyzer win = EditorWindow.GetWindow( typeof(MaterialAnalyzer) ) as MaterialAnalyzer;
		win.init();
		win.Show();
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Draws the GUI window.
	/// </summary>
	/// -----------------------------------------------------------------------
	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<Material, ListItem> 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 : " <MISSING>", item.Value.Selected ? mItemSelectedStyle : mItemStyle, GUILayout.Width( 300 ) ) )
				processListItemClick( item.Value );
 
			GUILayout.EndHorizontal();
		}
 
		GUILayout.EndScrollView();
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Processes the list item click.
	/// </summary>
	/// <param name='itemClicked'>
	/// The item clicked.
	/// </param>
	/// -----------------------------------------------------------------------
	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();
		}
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Starts recursive analyze process iterating through every selected GameObject.
	/// </summary>
	/// -----------------------------------------------------------------------
	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();
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Analyzes the given game object.
	/// </summary>
	/// <param name='gameObject'>
	/// The game object to analyze.
	/// </param>
	/// -----------------------------------------------------------------------
	private void analyzeGameObject( GameObject gameObject, StringBuilder dump, string indent )
	{
		dump.Append( indent + gameObject.name + "\n" );
 
		foreach( Component component in gameObject.GetComponents<Component>() )
			analyzeComponent( component, dump, indent + "    " );
 
		foreach( Transform child in gameObject.transform )
			analyzeGameObject( child.gameObject, dump, indent + "    " );
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Analyzes the given component.
	/// </summary>
	/// <param name='component'>
	/// The component to analyze.
	/// </param>
	/// -----------------------------------------------------------------------
	private void analyzeComponent( Component component, StringBuilder dump, string indent )
	{
		// early out if component is missing
		if( component == null )
			return;
 
		List<Material> materials = new List<Material>();
		switch( component.GetType().ToString() )
		{
		case "UnityEngine.MeshRenderer":
			{
				MeshRenderer mr = component as MeshRenderer;
				foreach( Material mat in mr.sharedMaterials )
					materials.Add( mat );
			}
			break;
		case "UnityEngine.ParticleRenderer":
			{
				ParticleRenderer pr = component as ParticleRenderer;
				foreach( Material mat in pr.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 + " <MISSING>";
				dump.Append( indent + "> " + matName + "\n" );
			}
		}
 
		if( materialMissing )
			Debug.LogWarning( "Material(s) missing in game object '" + component.gameObject + "'!" );
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Dumps the current selection hierarchy to a file.
	/// </summary>
	/// -----------------------------------------------------------------------
	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 );
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Dumps the current list of materials.
	/// </summary>
	/// -----------------------------------------------------------------------
	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<Material, ListItem> 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() );
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Selects the game objects in scene stored in all selected list items.
	/// </summary>
	/// -----------------------------------------------------------------------
	private void updateSceneSelection()
	{
		HashSet<UnityEngine.Object> sceneObjectsToSelect = new HashSet<UnityEngine.Object>();
		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;
	}
 
	/// -----------------------------------------------------------------------
	/// <summary>
	/// Initializes GUI styles and textures used.
	/// </summary>
	/// -----------------------------------------------------------------------
	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;
	}
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox