Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active November 18, 2025 13:44
Show Gist options
  • Select an option

  • Save yasirkula/3d3ffe9fdf6330a1328dd026b9f1cc62 to your computer and use it in GitHub Desktop.

Select an option

Save yasirkula/3d3ffe9fdf6330a1328dd026b9f1cc62 to your computer and use it in GitHub Desktop.

Revisions

  1. yasirkula revised this gist Dec 11, 2021. 1 changed file with 124 additions and 11 deletions.
    135 changes: 124 additions & 11 deletions BatchExtractMaterials.cs
    Original file line number Diff line number Diff line change
    @@ -23,18 +23,25 @@ public ExtractData() { }

    private class RemapAllPopup : EditorWindow
    {
    private Material remapFrom, remapTo;
    private System.Action<Material, Material> onRemapConfirmed;
    private List<Material> remapFrom = new List<Material>( 2 );
    private Material remapTo;
    private bool skipIgnoredMaterials;

    public static void ShowAt( Rect buttonRect, Vector2 size, System.Action<Material, Material> onRemapConfirmed )
    private Vector2 scrollPos;

    private System.Action<List<Material>, Material, bool> onRemapConfirmed;

    public static void ShowAt( Rect buttonRect, Vector2 size, System.Action<List<Material>, Material, bool> onRemapConfirmed )
    {
    buttonRect.position = GUIUtility.GUIToScreenPoint( buttonRect.position );

    remapAllPopup = GetWindow<RemapAllPopup>( true );
    remapAllPopup.position = new Rect( buttonRect.position + new Vector2( ( buttonRect.width - size.x ) * 0.5f, buttonRect.height ), size );
    remapAllPopup.minSize = size;
    remapAllPopup.titleContent = new GUIContent( "Remap All..." );
    remapAllPopup.skipIgnoredMaterials = EditorPrefs.GetBool( "BEM_SkipIgnoredMats", true );
    remapAllPopup.onRemapConfirmed = onRemapConfirmed;
    remapAllPopup.scrollPos = Vector2.zero;
    remapAllPopup.Show();
    }

    @@ -60,22 +67,118 @@ private void OnGUI()
    GUIUtility.ExitGUI();
    }

    EditorGUILayout.LabelField( "This will find all materials that point to 'From' and remap them to 'To'. If 'From' isn't assigned, all materials will be remapped to 'To'.", EditorStyles.wordWrappedLabel );
    Event ev = Event.current;

    remapFrom = EditorGUILayout.ObjectField( "From", remapFrom, typeof( Material ), false ) as Material;
    remapTo = EditorGUILayout.ObjectField( "To", remapTo, typeof( Material ), false ) as Material;
    EditorGUILayout.LabelField( "This will find all materials that point to 'Remap From' and remap them to 'Remap To'. If 'Remap From' is empty, all materials will be remapped to 'Remap To'.", EditorStyles.wordWrappedLabel );

    scrollPos = EditorGUILayout.BeginScrollView( scrollPos );

    GUILayout.BeginHorizontal();
    GUILayout.Label( "Remap From (drag & drop here)" );

    if( remapFrom.Count == 0 )
    remapFrom.Add( null );

    // Allow drag & dropping materials to array
    // Credit: https://answers.unity.com/answers/657877/view.html
    if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) )
    {
    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
    if( ev.type == EventType.DragPerform )
    {
    DragAndDrop.AcceptDrag();

    Object[] draggedObjects = DragAndDrop.objectReferences;
    for( int i = 0; i < draggedObjects.Length; i++ )
    {
    Material material = draggedObjects[i] as Material;
    if( !material )
    continue;

    if( !remapFrom.Contains( material ) )
    {
    bool replacedNullElement = false;
    for( int j = 0; j < remapFrom.Count; j++ )
    {
    if( !remapFrom[j] )
    {
    remapFrom[j] = material;
    replacedNullElement = true;
    break;
    }
    }

    if( !replacedNullElement )
    remapFrom.Add( material );
    }
    }
    }

    ev.Use();
    }

    if( GUILayout.Button( "+", GL_WIDTH_25 ) )
    remapFrom.Insert( 0, null );

    GUILayout.EndHorizontal();

    for( int i = 0; i < remapFrom.Count; i++ )
    {
    GUILayout.BeginHorizontal();

    remapFrom[i] = EditorGUILayout.ObjectField( GUIContent.none, remapFrom[i], typeof( Material ), false ) as Material;

    if( GUILayout.Button( "+", GL_WIDTH_25 ) )
    remapFrom.Insert( i + 1, null );

    if( GUILayout.Button( "-", GL_WIDTH_25 ) )
    {
    // Lists with no elements look ugly, always keep a dummy null variable
    if( remapFrom.Count > 1 )
    remapFrom.RemoveAt( i-- );
    else
    remapFrom[0] = null;
    }

    GUILayout.EndHorizontal();
    }

    EditorGUILayout.EndScrollView();

    remapTo = EditorGUILayout.ObjectField( "Remap To", remapTo, typeof( Material ), false ) as Material;

    EditorGUI.BeginChangeCheck();
    skipIgnoredMaterials = EditorGUILayout.Toggle( "Skip Ignored Materials", skipIgnoredMaterials );
    if( EditorGUI.EndChangeCheck() )
    EditorPrefs.SetBool( "BEM_SkipIgnoredMats", skipIgnoredMaterials );

    EditorGUILayout.Space();

    GUILayout.BeginHorizontal();
    if( GUILayout.Button( "Cancel" ) )
    Close();
    if( GUILayout.Button( "Apply" ) )
    {
    if( remapTo && onRemapConfirmed != null )
    onRemapConfirmed( remapFrom, remapTo );
    {
    bool remapFromIsFilled = false;
    for( int i = 0; i < remapFrom.Count; i++ )
    {
    if( remapFrom[i] )
    {
    remapFromIsFilled = true;
    break;
    }
    }

    onRemapConfirmed( remapFromIsFilled ? remapFrom : null, remapTo, skipIgnoredMaterials );
    }

    Close();
    }
    GUILayout.EndHorizontal();

    GUILayout.Space( 5f );
    }
    }

    @@ -89,7 +192,7 @@ private void OnGUI()
    "- Ignore: material's current value will stay intact (when Ignore is the default value, either the material couldn't be found " +
    "or it was already extracted)";

    private readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );
    private static readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );
    private readonly GUILayoutOption GL_WIDTH_75 = GUILayout.Width( 75f );
    private readonly GUILayoutOption GL_MIN_WIDTH_50 = GUILayout.MinWidth( 50f );

    @@ -373,7 +476,7 @@ private void DrawMaterialRemapList()

    if( GUILayout.Button( "Remap All..." ) )
    {
    RemapAllPopup.ShowAt( remapAllButtonRect, new Vector2( 300f, 125f ), ( Material remapFrom, Material remapTo ) =>
    RemapAllPopup.ShowAt( remapAllButtonRect, new Vector2( 325f, 250f ), ( List<Material> remapFrom, Material remapTo, bool skipIgnoredMaterials ) =>
    {
    for( int i = 0; i < modelData.Count; i++ )
    {
    @@ -384,7 +487,7 @@ private void DrawMaterialRemapList()
    {
    case ExtractMode.Extract:
    {
    if( !remapFrom || data.originalMaterials[j] == remapFrom )
    if( remapFrom == null || ( data.originalMaterials[j] && remapFrom.Contains( data.originalMaterials[j] ) ) )
    {
    data.materialExtractModes[j] = ExtractMode.Remap;
    data.remappedMaterials[j] = remapTo;
    @@ -394,9 +497,19 @@ private void DrawMaterialRemapList()
    }
    case ExtractMode.Remap:
    {
    if( !remapFrom || data.remappedMaterials[j] == remapFrom )
    if( remapFrom == null || ( data.remappedMaterials[j] && remapFrom.Contains( data.remappedMaterials[j] ) ) )
    data.remappedMaterials[j] = remapTo;

    break;
    }
    case ExtractMode.Ignore:
    {
    if( !skipIgnoredMaterials && ( remapFrom == null || ( data.originalMaterials[j] && remapFrom.Contains( data.originalMaterials[j] ) ) ) )
    {
    data.materialExtractModes[j] = ExtractMode.Remap;
    data.remappedMaterials[j] = remapTo;
    }

    break;
    }
    }
  2. yasirkula revised this gist Aug 4, 2021. 1 changed file with 48 additions and 24 deletions.
    72 changes: 48 additions & 24 deletions BatchExtractMaterials.cs
    Original file line number Diff line number Diff line change
    @@ -198,8 +198,35 @@ private void OnGUI()

    private void DrawDestinationPathField()
    {
    Event ev = Event.current;

    GUILayout.BeginHorizontal();

    materialsFolder = EditorGUILayout.TextField( "Extract Materials To", materialsFolder );

    // Allow drag & dropping a folder to the text field
    // Credit: https://answers.unity.com/answers/657877/view.html
    if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) )
    {
    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
    if( ev.type == EventType.DragPerform )
    {
    DragAndDrop.AcceptDrag();

    string[] draggedFiles = DragAndDrop.paths;
    for( int i = 0; i < draggedFiles.Length; i++ )
    {
    if( !string.IsNullOrEmpty( draggedFiles[i] ) && AssetDatabase.IsValidFolder( draggedFiles[i] ) )
    {
    materialsFolder = draggedFiles[i];
    break;
    }
    }
    }

    ev.Use();
    }

    if( GUILayout.Button( "o", GL_WIDTH_25 ) )
    {
    string selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "Assets", "" );
    @@ -214,8 +241,8 @@ private void DrawDestinationPathField()

    GUIUtility.keyboardControl = 0; // Remove focus from active text field
    }
    GUILayout.EndHorizontal();

    GUILayout.EndHorizontal();
    EditorGUILayout.Space();
    }

    @@ -239,39 +266,36 @@ private void DrawModelsToProcessList()
    DragAndDrop.AcceptDrag();

    Object[] draggedObjects = DragAndDrop.objectReferences;
    if( draggedObjects.Length > 0 )
    for( int i = 0; i < draggedObjects.Length; i++ )
    {
    for( int i = 0; i < draggedObjects.Length; i++ )
    if( !( draggedObjects[i] as GameObject ) || PrefabUtility.GetPrefabAssetType( draggedObjects[i] ) != PrefabAssetType.Model )
    continue;

    bool modelAlreadyExists = false;
    for( int j = 0; j < modelData.Count; j++ )
    {
    if( !( draggedObjects[i] as GameObject ) || PrefabUtility.GetPrefabAssetType( draggedObjects[i] ) != PrefabAssetType.Model )
    continue;
    if( modelData[j].model == draggedObjects[i] )
    {
    modelAlreadyExists = true;
    break;
    }
    }

    bool modelAlreadyExists = false;
    if( !modelAlreadyExists )
    {
    bool replacedNullElement = false;
    for( int j = 0; j < modelData.Count; j++ )
    {
    if( modelData[j].model == draggedObjects[i] )
    if( !modelData[j].model )
    {
    modelAlreadyExists = true;
    modelData[j] = new ExtractData( draggedObjects[i] as GameObject );
    replacedNullElement = true;
    break;
    }
    }

    if( !modelAlreadyExists )
    {
    bool replacedNullElement = false;
    for( int j = 0; j < modelData.Count; j++ )
    {
    if( !modelData[j].model )
    {
    modelData[j] = new ExtractData( draggedObjects[i] as GameObject );
    replacedNullElement = true;
    break;
    }
    }

    if( !replacedNullElement )
    modelData.Add( new ExtractData( draggedObjects[i] as GameObject ) );
    }
    if( !replacedNullElement )
    modelData.Add( new ExtractData( draggedObjects[i] as GameObject ) );
    }
    }
    }
  3. yasirkula revised this gist Jul 28, 2021. 1 changed file with 128 additions and 1 deletion.
    129 changes: 128 additions & 1 deletion BatchExtractMaterials.cs
    Original file line number Diff line number Diff line change
    @@ -21,6 +21,64 @@ public ExtractData() { }
    public ExtractData( GameObject model ) { this.model = model; }
    }

    private class RemapAllPopup : EditorWindow
    {
    private Material remapFrom, remapTo;
    private System.Action<Material, Material> onRemapConfirmed;

    public static void ShowAt( Rect buttonRect, Vector2 size, System.Action<Material, Material> onRemapConfirmed )
    {
    buttonRect.position = GUIUtility.GUIToScreenPoint( buttonRect.position );

    remapAllPopup = GetWindow<RemapAllPopup>( true );
    remapAllPopup.position = new Rect( buttonRect.position + new Vector2( ( buttonRect.width - size.x ) * 0.5f, buttonRect.height ), size );
    remapAllPopup.minSize = size;
    remapAllPopup.titleContent = new GUIContent( "Remap All..." );
    remapAllPopup.onRemapConfirmed = onRemapConfirmed;
    remapAllPopup.Show();
    }

    public static void Hide()
    {
    if( remapAllPopup )
    {
    remapAllPopup.Close();
    remapAllPopup = null;
    }
    }

    private void OnDestroy()
    {
    remapAllPopup = null;
    }

    private void OnGUI()
    {
    if( !remapAllPopup )
    {
    Close();
    GUIUtility.ExitGUI();
    }

    EditorGUILayout.LabelField( "This will find all materials that point to 'From' and remap them to 'To'. If 'From' isn't assigned, all materials will be remapped to 'To'.", EditorStyles.wordWrappedLabel );

    remapFrom = EditorGUILayout.ObjectField( "From", remapFrom, typeof( Material ), false ) as Material;
    remapTo = EditorGUILayout.ObjectField( "To", remapTo, typeof( Material ), false ) as Material;

    GUILayout.BeginHorizontal();
    if( GUILayout.Button( "Cancel" ) )
    Close();
    if( GUILayout.Button( "Apply" ) )
    {
    if( remapTo && onRemapConfirmed != null )
    onRemapConfirmed( remapFrom, remapTo );

    Close();
    }
    GUILayout.EndHorizontal();
    }
    }

    private const string HELP_TEXT =
    "- Extract: material will be extracted to the destination folder\n" +
    "- Remap: material will be remapped to an existing material asset" +
    @@ -33,6 +91,7 @@ public ExtractData() { }

    private readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );
    private readonly GUILayoutOption GL_WIDTH_75 = GUILayout.Width( 75f );
    private readonly GUILayoutOption GL_MIN_WIDTH_50 = GUILayout.MinWidth( 50f );

    private string materialsFolder = "Assets/Materials";
    private List<ExtractData> modelData = new List<ExtractData>( 16 );
    @@ -46,6 +105,9 @@ public ExtractData() { }

    private bool inModelSelectionPhase = true;

    private Rect remapAllButtonRect;
    private static RemapAllPopup remapAllPopup;

    private Vector2 scrollPos;

    [MenuItem( "Window/Batch Extract Materials" )]
    @@ -57,6 +119,23 @@ private static void Init()
    window.Show();
    }

    private void OnDestroy()
    {
    // Close RemapAllPopup with this window
    RemapAllPopup.Hide();
    }

    private void OnFocus()
    {
    // Don't let RemapAllPopup be obstructed by this window
    // We are using delayCall because otherwise clicking an ObjectField in this window doesn't highlight that material in the Project window
    EditorApplication.delayCall += () =>
    {
    if( remapAllPopup )
    remapAllPopup.Focus();
    };
    }

    private void OnGUI()
    {
    scrollPos = EditorGUILayout.BeginScrollView( scrollPos );
    @@ -89,6 +168,8 @@ private void OnGUI()
    if( GUILayout.Button( "Back" ) )
    {
    inModelSelectionPhase = true;
    RemapAllPopup.Hide();

    GUIUtility.ExitGUI();
    }

    @@ -99,6 +180,7 @@ private void OnGUI()
    if( GUILayout.Button( "Extract!" ) )
    {
    inModelSelectionPhase = true;
    RemapAllPopup.Hide();

    ExtractMaterials();
    GUIUtility.ExitGUI();
    @@ -265,6 +347,45 @@ private void DrawMaterialRemapList()
    }
    }

    if( GUILayout.Button( "Remap All..." ) )
    {
    RemapAllPopup.ShowAt( remapAllButtonRect, new Vector2( 300f, 125f ), ( Material remapFrom, Material remapTo ) =>
    {
    for( int i = 0; i < modelData.Count; i++ )
    {
    ExtractData data = modelData[i];
    for( int j = 0; j < data.remappedMaterials.Count; j++ )
    {
    switch( data.materialExtractModes[j] )
    {
    case ExtractMode.Extract:
    {
    if( !remapFrom || data.originalMaterials[j] == remapFrom )
    {
    data.materialExtractModes[j] = ExtractMode.Remap;
    data.remappedMaterials[j] = remapTo;
    }

    break;
    }
    case ExtractMode.Remap:
    {
    if( !remapFrom || data.remappedMaterials[j] == remapFrom )
    data.remappedMaterials[j] = remapTo;

    break;
    }
    }
    }
    }

    Repaint();
    } );
    }

    if( Event.current.type == EventType.Repaint )
    remapAllButtonRect = GUILayoutUtility.GetLastRect();

    if( GUILayout.Button( "Ignore All" ) )
    {
    for( int i = 0; i < modelData.Count; i++ )
    @@ -301,10 +422,16 @@ private void DrawMaterialRemapList()
    if( data.materialExtractModes[j] == ExtractMode.Remap )
    {
    EditorGUI.BeginChangeCheck();
    data.remappedMaterials[j] = EditorGUILayout.ObjectField( GUIContent.none, data.remappedMaterials[j], typeof( Material ), false ) as Material;
    data.remappedMaterials[j] = EditorGUILayout.ObjectField( GUIContent.none, data.remappedMaterials[j], typeof( Material ), false, GL_MIN_WIDTH_50 ) as Material;
    if( EditorGUI.EndChangeCheck() && ( !data.remappedMaterials[j] || data.remappedMaterials[j] == data.originalMaterials[j] ) )
    data.materialExtractModes[j] = ExtractMode.Ignore;
    }
    else
    {
    GUI.enabled = false;
    EditorGUILayout.ObjectField( GUIContent.none, data.originalMaterials[j], typeof( Material ), false, GL_MIN_WIDTH_50 );
    GUI.enabled = true;
    }

    GUILayout.EndHorizontal();
    }
  4. yasirkula revised this gist Jul 28, 2021. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions BatchExtractMaterials.cs
    Original file line number Diff line number Diff line change
    @@ -71,7 +71,7 @@ private void OnGUI()

    if( inModelSelectionPhase )
    {
    GUI.enabled = modelsToProcessListIsFilled && !string.IsNullOrEmpty( materialsFolder ) && materialsFolder.StartsWith( "Assets/" );
    GUI.enabled = modelsToProcessListIsFilled && !string.IsNullOrEmpty( materialsFolder ) && materialsFolder.StartsWith( "Assets" );
    if( GUILayout.Button( "Next" ) )
    {
    inModelSelectionPhase = false;
    @@ -123,11 +123,11 @@ private void DrawDestinationPathField()
    string selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "Assets", "" );
    if( !string.IsNullOrEmpty( selectedPath ) )
    {
    selectedPath = selectedPath.Replace( '\\', '/' );
    selectedPath = selectedPath.Replace( '\\', '/' ) + "/";

    int relativePathIndex = selectedPath.IndexOf( "/Assets/" ) + 1;
    if( relativePathIndex > 0 )
    materialsFolder = selectedPath.Substring( relativePathIndex );
    materialsFolder = selectedPath.Substring( relativePathIndex, selectedPath.Length - relativePathIndex - 1 );
    }

    GUIUtility.keyboardControl = 0; // Remove focus from active text field
  5. yasirkula created this gist Jul 27, 2021.
    601 changes: 601 additions & 0 deletions BatchExtractMaterials.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,601 @@
    using UnityEngine;
    using UnityEditor;
    using System.Collections.Generic;
    using System.IO;

    public class BatchExtractMaterials : EditorWindow
    {
    private enum ExtractMode { Extract = 0, Remap = 1, Ignore = 2 };

    [System.Serializable]
    private class ExtractData
    {
    public GameObject model;

    public List<string> materialNames = new List<string>();
    public List<Material> originalMaterials = new List<Material>();
    public List<Material> remappedMaterials = new List<Material>();
    public List<ExtractMode> materialExtractModes = new List<ExtractMode>();

    public ExtractData() { }
    public ExtractData( GameObject model ) { this.model = model; }
    }

    private const string HELP_TEXT =
    "- Extract: material will be extracted to the destination folder\n" +
    "- Remap: material will be remapped to an existing material asset" +
    #if UNITY_2019_1_OR_NEWER
    " (when Remap is the default value, then it means that a material that satisfies 'Default Material Remap Conditions' was found)" +
    #endif
    ". If Remap points to an embedded material, then that embedded material will first be extracted\n" +
    "- Ignore: material's current value will stay intact (when Ignore is the default value, either the material couldn't be found " +
    "or it was already extracted)";

    private readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );
    private readonly GUILayoutOption GL_WIDTH_75 = GUILayout.Width( 75f );

    private string materialsFolder = "Assets/Materials";
    private List<ExtractData> modelData = new List<ExtractData>( 16 );

    #if UNITY_2019_1_OR_NEWER
    private bool remappedMaterialNamesMustMatch = false;
    private bool remappedMaterialPropertiesMustMatch = true;
    private bool dontRemapExtractedMaterials = true;
    private bool dontRemapMaterialsAcrossDifferentModels = false;
    #endif

    private bool inModelSelectionPhase = true;

    private Vector2 scrollPos;

    [MenuItem( "Window/Batch Extract Materials" )]
    private static void Init()
    {
    BatchExtractMaterials window = GetWindow<BatchExtractMaterials>();
    window.titleContent = new GUIContent( "Extract Materials" );
    window.minSize = new Vector2( 300f, 120f );
    window.Show();
    }

    private void OnGUI()
    {
    scrollPos = EditorGUILayout.BeginScrollView( scrollPos );

    GUI.enabled = inModelSelectionPhase;
    DrawDestinationPathField();
    DrawModelsToProcessList();
    DrawMaterialRemapConditionsField();
    GUI.enabled = true;

    bool modelsToProcessListIsFilled = modelData.Find( ( data ) => data.model ) != null;

    if( inModelSelectionPhase )
    {
    GUI.enabled = modelsToProcessListIsFilled && !string.IsNullOrEmpty( materialsFolder ) && materialsFolder.StartsWith( "Assets/" );
    if( GUILayout.Button( "Next" ) )
    {
    inModelSelectionPhase = false;
    CalculateRemappedMaterials();

    GUIUtility.ExitGUI();
    }
    }
    else
    {
    DrawMaterialRemapList();

    GUILayout.BeginHorizontal();

    if( GUILayout.Button( "Back" ) )
    {
    inModelSelectionPhase = true;
    GUIUtility.ExitGUI();
    }

    Color c = GUI.backgroundColor;
    GUI.backgroundColor = Color.green;

    GUI.enabled = modelsToProcessListIsFilled;
    if( GUILayout.Button( "Extract!" ) )
    {
    inModelSelectionPhase = true;

    ExtractMaterials();
    GUIUtility.ExitGUI();
    }

    GUI.backgroundColor = c;
    GUILayout.EndHorizontal();
    }

    GUI.enabled = true;

    EditorGUILayout.Space();
    EditorGUILayout.EndScrollView();
    }

    private void DrawDestinationPathField()
    {
    GUILayout.BeginHorizontal();
    materialsFolder = EditorGUILayout.TextField( "Extract Materials To", materialsFolder );
    if( GUILayout.Button( "o", GL_WIDTH_25 ) )
    {
    string selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "Assets", "" );
    if( !string.IsNullOrEmpty( selectedPath ) )
    {
    selectedPath = selectedPath.Replace( '\\', '/' );

    int relativePathIndex = selectedPath.IndexOf( "/Assets/" ) + 1;
    if( relativePathIndex > 0 )
    materialsFolder = selectedPath.Substring( relativePathIndex );
    }

    GUIUtility.keyboardControl = 0; // Remove focus from active text field
    }
    GUILayout.EndHorizontal();

    EditorGUILayout.Space();
    }

    private void DrawModelsToProcessList()
    {
    Event ev = Event.current;

    GUILayout.BeginHorizontal();
    GUILayout.Label( "Models To Process (drag & drop here)" );

    if( modelData.Count == 0 )
    modelData.Add( new ExtractData() );

    // Allow drag & dropping models to array
    // Credit: https://answers.unity.com/answers/657877/view.html
    if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) )
    {
    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
    if( ev.type == EventType.DragPerform )
    {
    DragAndDrop.AcceptDrag();

    Object[] draggedObjects = DragAndDrop.objectReferences;
    if( draggedObjects.Length > 0 )
    {
    for( int i = 0; i < draggedObjects.Length; i++ )
    {
    if( !( draggedObjects[i] as GameObject ) || PrefabUtility.GetPrefabAssetType( draggedObjects[i] ) != PrefabAssetType.Model )
    continue;

    bool modelAlreadyExists = false;
    for( int j = 0; j < modelData.Count; j++ )
    {
    if( modelData[j].model == draggedObjects[i] )
    {
    modelAlreadyExists = true;
    break;
    }
    }

    if( !modelAlreadyExists )
    {
    bool replacedNullElement = false;
    for( int j = 0; j < modelData.Count; j++ )
    {
    if( !modelData[j].model )
    {
    modelData[j] = new ExtractData( draggedObjects[i] as GameObject );
    replacedNullElement = true;
    break;
    }
    }

    if( !replacedNullElement )
    modelData.Add( new ExtractData( draggedObjects[i] as GameObject ) );
    }
    }
    }
    }

    ev.Use();
    }

    if( GUILayout.Button( "+", GL_WIDTH_25 ) )
    modelData.Insert( 0, new ExtractData() );

    GUILayout.EndHorizontal();

    for( int i = 0; i < modelData.Count; i++ )
    {
    ExtractData element = modelData[i];

    GUI.changed = false;
    GUILayout.BeginHorizontal();

    GameObject prevObject = element.model;
    GameObject newObject = EditorGUILayout.ObjectField( GUIContent.none, prevObject, typeof( GameObject ), false ) as GameObject;
    if( newObject && PrefabUtility.GetPrefabAssetType( newObject ) != PrefabAssetType.Model )
    newObject = prevObject;

    modelData[i].model = newObject;

    if( GUILayout.Button( "+", GL_WIDTH_25 ) )
    modelData.Insert( i + 1, new ExtractData() );

    if( GUILayout.Button( "-", GL_WIDTH_25 ) )
    {
    // Lists with no elements look ugly, always keep a dummy null variable
    if( modelData.Count > 1 )
    modelData.RemoveAt( i-- );
    else
    modelData[0] = new ExtractData();
    }

    GUILayout.EndHorizontal();
    }

    EditorGUILayout.Space();
    }

    private void DrawMaterialRemapConditionsField()
    {
    #if UNITY_2019_1_OR_NEWER
    EditorGUILayout.LabelField( "Default Material Remap Conditions" );
    EditorGUI.indentLevel++;

    remappedMaterialNamesMustMatch = EditorGUILayout.ToggleLeft( "Material names must match", remappedMaterialNamesMustMatch );
    remappedMaterialPropertiesMustMatch = EditorGUILayout.ToggleLeft( "Material properties must match", remappedMaterialPropertiesMustMatch );
    dontRemapExtractedMaterials = EditorGUILayout.ToggleLeft( "Don't remap already extracted materials", dontRemapExtractedMaterials );
    dontRemapMaterialsAcrossDifferentModels = EditorGUILayout.ToggleLeft( "Don't remap Model A's materials to Model B (i.e. different models won't share the same materials)", dontRemapMaterialsAcrossDifferentModels );

    EditorGUI.indentLevel--;
    EditorGUILayout.Space();
    #endif
    }

    private void DrawMaterialRemapList()
    {
    EditorGUILayout.HelpBox( HELP_TEXT, MessageType.Info );

    GUILayout.BeginHorizontal();

    if( GUILayout.Button( "Extract All" ) )
    {
    for( int i = 0; i < modelData.Count; i++ )
    {
    for( int j = 0; j < modelData[i].materialExtractModes.Count; j++ )
    modelData[i].materialExtractModes[j] = ExtractMode.Extract;
    }
    }

    if( GUILayout.Button( "Ignore All" ) )
    {
    for( int i = 0; i < modelData.Count; i++ )
    {
    for( int j = 0; j < modelData[i].materialExtractModes.Count; j++ )
    modelData[i].materialExtractModes[j] = ExtractMode.Ignore;
    }
    }

    GUILayout.EndHorizontal();

    EditorGUILayout.Space();

    for( int i = 0; i < modelData.Count; i++ )
    {
    ExtractData data = modelData[i];
    if( !data.model )
    continue;

    GUI.enabled = false;
    EditorGUILayout.ObjectField( GUIContent.none, data.model, typeof( GameObject ), false );
    GUI.enabled = true;

    if( data.originalMaterials.Count == 0 )
    EditorGUILayout.LabelField( "This model has no materials..." );

    for( int j = 0; j < data.originalMaterials.Count; j++ )
    {
    GUILayout.BeginHorizontal();

    EditorGUILayout.PrefixLabel( data.materialNames[j] );

    data.materialExtractModes[j] = (ExtractMode) EditorGUILayout.EnumPopup( GUIContent.none, data.materialExtractModes[j], GL_WIDTH_75 );
    if( data.materialExtractModes[j] == ExtractMode.Remap )
    {
    EditorGUI.BeginChangeCheck();
    data.remappedMaterials[j] = EditorGUILayout.ObjectField( GUIContent.none, data.remappedMaterials[j], typeof( Material ), false ) as Material;
    if( EditorGUI.EndChangeCheck() && ( !data.remappedMaterials[j] || data.remappedMaterials[j] == data.originalMaterials[j] ) )
    data.materialExtractModes[j] = ExtractMode.Ignore;
    }

    GUILayout.EndHorizontal();
    }

    EditorGUILayout.Space();
    }
    }

    private void CalculateRemappedMaterials()
    {
    #if UNITY_2019_1_OR_NEWER
    // Key: Material CRC (material.ComputeCRC)
    // Value: All materials sharing that CRC
    Dictionary<int, HashSet<Material>> duplicateMaterialsLookup = new Dictionary<int, HashSet<Material>>( modelData.Count * 8 );

    // Add all existing materials at materialsFolder to the lookup table
    if( !dontRemapMaterialsAcrossDifferentModels && Directory.Exists( materialsFolder ) )
    {
    string[] existingMaterialPaths = Directory.GetFiles( materialsFolder, "*.mat", SearchOption.TopDirectoryOnly );
    for( int i = 0; i < existingMaterialPaths.Length; i++ )
    {
    Material material = AssetDatabase.LoadMainAssetAtPath( existingMaterialPaths[i] ) as Material;
    if( material )
    GetMaterialsWithCRC( duplicateMaterialsLookup, material ).Add( material );
    }
    }
    #endif

    for( int i = 0; i < modelData.Count; i++ )
    {
    ExtractData data = modelData[i];
    if( !data.model )
    {
    modelData.RemoveAt( i-- );
    continue;
    }

    string modelPath = AssetDatabase.GetAssetPath( data.model );
    ModelImporter modelImporter = AssetImporter.GetAtPath( modelPath ) as ModelImporter;
    if( !modelImporter )
    {
    Debug.LogWarning( "Couldn't get ModelImporter from asset: " + AssetDatabase.GetAssetPath( data.model ), data.model );
    modelData.RemoveAt( i-- );
    continue;
    }

    // Reset previously assigned values to this entry (if any)
    data = modelData[i] = new ExtractData( data.model );

    Object[] embeddedAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath( modelPath );
    List<Material> embeddedMaterials = new List<Material>( embeddedAssets.Length );
    for( int j = 0; j < embeddedAssets.Length; j++ )
    {
    Material embeddedMaterial = embeddedAssets[j] as Material;
    if( embeddedMaterial )
    embeddedMaterials.Add( embeddedMaterial );
    }

    // Get the model's current material remapping
    // Credit: https://forum.unity.com/threads/batch-change-all-fbx-default-materials-help.626341/#post-6530939
    using( SerializedObject so = new SerializedObject( modelImporter ) )
    {
    SerializedProperty materials = so.FindProperty( "m_Materials" );
    SerializedProperty externalObjects = so.FindProperty( "m_ExternalObjects" );

    for( int materialIndex = 0; materialIndex < materials.arraySize; materialIndex++ )
    {
    SerializedProperty id = materials.GetArrayElementAtIndex( materialIndex );
    string name = id.FindPropertyRelative( "name" ).stringValue;
    string type = id.FindPropertyRelative( "type" ).stringValue;

    Material material = null;
    for( int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex++ )
    {
    SerializedProperty pair = externalObjects.GetArrayElementAtIndex( externalObjectIndex );
    string externalName = pair.FindPropertyRelative( "first.name" ).stringValue;
    string externalType = pair.FindPropertyRelative( "first.type" ).stringValue;

    if( externalType == type && externalName == name && ( pair = pair.FindPropertyRelative( "second" ) ) != null )
    {
    material = pair.objectReferenceValue as Material;
    break;
    }
    }

    if( !material )
    material = embeddedMaterials.Find( ( m ) => m.name == name );

    data.materialNames.Add( name );
    data.originalMaterials.Add( material );

    if( !material )
    {
    data.materialExtractModes.Add( ExtractMode.Ignore );
    data.remappedMaterials.Add( null );
    }
    else
    {
    bool materialAlreadyExtracted = AssetDatabase.IsMainAsset( material );
    #if UNITY_2019_1_OR_NEWER
    HashSet<Material> duplicateMaterials = GetMaterialsWithCRC( duplicateMaterialsLookup, material );
    Material remappedMaterial = null;

    // - Material was already extracted: remap the material only if 'dontRemapExtractedMaterials' is false
    // - 'dontRemapMaterialsAcrossDifferentModels' is true: only remap with a material from the same model
    // - 'remappedMaterialPropertiesMustMatch' is true: only remap with a material whose properties match
    // the current material's properties
    // - Only 'remappedMaterialNamesMustMatch' is true: remap with a material with the same name; properties
    // of the two materials may not match
    if( !materialAlreadyExtracted || !dontRemapExtractedMaterials )
    {
    if( remappedMaterialPropertiesMustMatch )
    {
    foreach( Material _material in duplicateMaterials )
    {
    if( _material.name == name || ( !remappedMaterial && !remappedMaterialNamesMustMatch ) )
    remappedMaterial = _material;
    }
    }
    else if( remappedMaterialNamesMustMatch )
    remappedMaterial = GetMaterialWithName( duplicateMaterialsLookup, name );
    }

    if( remappedMaterial && remappedMaterial != material )
    {
    data.materialExtractModes.Add( ExtractMode.Remap );
    data.remappedMaterials.Add( remappedMaterial );
    }
    else
    #endif
    {
    data.materialExtractModes.Add( materialAlreadyExtracted ? ExtractMode.Ignore : ExtractMode.Extract );
    data.remappedMaterials.Add( null );

    #if UNITY_2019_1_OR_NEWER
    duplicateMaterials.Add( material );
    #endif
    }
    }
    }
    }

    #if UNITY_2019_1_OR_NEWER
    if( dontRemapMaterialsAcrossDifferentModels )
    duplicateMaterialsLookup.Clear();
    #endif
    }
    }

    #if UNITY_2019_1_OR_NEWER
    private HashSet<Material> GetMaterialsWithCRC( Dictionary<int, HashSet<Material>> lookup, Material material )
    {
    int crcHash = material.ComputeCRC();
    HashSet<Material> result;
    if( !lookup.TryGetValue( crcHash, out result ) )
    lookup[crcHash] = result = new HashSet<Material>();

    return result;
    }

    private Material GetMaterialWithName( Dictionary<int, HashSet<Material>> lookup, string name )
    {
    foreach( HashSet<Material> allMaterials in lookup.Values )
    {
    foreach( Material material in allMaterials )
    {
    if( material.name == name )
    return material;
    }
    }

    return null;
    }
    #endif

    private void ExtractMaterials()
    {
    if( materialsFolder.EndsWith( "/" ) )
    materialsFolder = materialsFolder.Substring( 0, materialsFolder.Length - 1 );

    if( !Directory.Exists( materialsFolder ) )
    {
    Directory.CreateDirectory( materialsFolder );
    AssetDatabase.ImportAsset( materialsFolder, ImportAssetOptions.ForceUpdate );
    }

    List<AssetImporter> dirtyModelImporters = new List<AssetImporter>( modelData.Count );
    Dictionary<Material, Material> extractedMaterials = new Dictionary<Material, Material>( modelData.Count * 8 );

    for( int i = 0; i < modelData.Count; i++ )
    {
    ExtractData data = modelData[i];
    if( !data.model )
    continue;

    AssetImporter modelImporter = AssetImporter.GetAtPath( AssetDatabase.GetAssetPath( data.model ) );

    // Remap/extract the model's materials
    // Credit: https://forum.unity.com/threads/batch-change-all-fbx-default-materials-help.626341/#post-6530939
    using( SerializedObject so = new SerializedObject( modelImporter ) )
    {
    SerializedProperty materials = so.FindProperty( "m_Materials" );
    SerializedProperty externalObjects = so.FindProperty( "m_ExternalObjects" );

    for( int materialIndex = 0; materialIndex < materials.arraySize; materialIndex++ )
    {
    SerializedProperty id = materials.GetArrayElementAtIndex( materialIndex );
    string name = id.FindPropertyRelative( "name" ).stringValue;
    string type = id.FindPropertyRelative( "type" ).stringValue;

    // j: index of the target material in data's lists
    int j = ( materialIndex < data.materialNames.Count && data.materialNames[materialIndex] == name ) ? materialIndex : data.materialNames.IndexOf( name );
    if( j < 0 )
    {
    // This can only occur if user reimports the model with more materials when 'inModelSelectionPhase' is false
    Debug.LogWarning( data.model.name + "." + name + " material has no matching data, skipped", data.model );
    continue;
    }

    Material targetMaterial = null;
    switch( data.materialExtractModes[j] )
    {
    case ExtractMode.Extract:
    {
    if( data.originalMaterials[j] && !AssetDatabase.IsMainAsset( data.originalMaterials[j] ) )
    targetMaterial = data.originalMaterials[j];
    else
    Debug.LogWarning( data.model.name + "." + name + " isn't extracted because either the material doesn't exist or it is already extracted", data.model );

    break;
    }
    case ExtractMode.Remap:
    {
    if( data.remappedMaterials[j] && ( data.originalMaterials[j] != data.remappedMaterials[j] || !AssetDatabase.IsMainAsset( data.remappedMaterials[j] ) ) )
    targetMaterial = data.remappedMaterials[j];
    else
    Debug.LogWarning( data.model.name + "." + name + " isn't remapped because either the material doesn't exist or it is already extracted", data.model );

    break;
    }
    }

    if( !targetMaterial )
    continue;
    else if( !AssetDatabase.IsMainAsset( targetMaterial ) )
    {
    Material extractedMaterial;
    if( !extractedMaterials.TryGetValue( targetMaterial, out extractedMaterial ) )
    {
    extractedMaterials[targetMaterial] = extractedMaterial = new Material( targetMaterial );
    AssetDatabase.CreateAsset( extractedMaterial, AssetDatabase.GenerateUniqueAssetPath( materialsFolder + "/" + targetMaterial.name + ".mat" ) );
    }

    targetMaterial = extractedMaterial;
    }

    SerializedProperty materialProperty = null;
    for( int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex++ )
    {
    SerializedProperty pair = externalObjects.GetArrayElementAtIndex( externalObjectIndex );
    string externalName = pair.FindPropertyRelative( "first.name" ).stringValue;
    string externalType = pair.FindPropertyRelative( "first.type" ).stringValue;

    if( externalType == type && externalName == name )
    {
    materialProperty = pair.FindPropertyRelative( "second" );
    break;
    }
    }

    if( materialProperty == null )
    {
    SerializedProperty currentSerializedProperty = externalObjects.GetArrayElementAtIndex( externalObjects.arraySize++ );
    currentSerializedProperty.FindPropertyRelative( "first.name" ).stringValue = name;
    currentSerializedProperty.FindPropertyRelative( "first.type" ).stringValue = type;
    currentSerializedProperty.FindPropertyRelative( "first.assembly" ).stringValue = id.FindPropertyRelative( "assembly" ).stringValue;
    currentSerializedProperty.FindPropertyRelative( "second" ).objectReferenceValue = targetMaterial;
    }
    else
    materialProperty.objectReferenceValue = targetMaterial;
    }

    if( so.hasModifiedProperties )
    {
    dirtyModelImporters.Add( modelImporter );
    so.ApplyModifiedPropertiesWithoutUndo();
    }
    }
    }

    for( int i = 0; i < dirtyModelImporters.Count; i++ )
    dirtyModelImporters[i].SaveAndReimport();
    }
    }