LoginSignup
3
0

More than 1 year has passed since last update.

インポートしたモデルのマテリアルをまとめて外に出す [Unityエディター拡張]

Last updated at Posted at 2022-12-04

はじめに

Unityでは、インポートした3Dモデルをインポートした時にマテリアルはモデルの中に格納されている状態で表示されます。
スクリーンショット (218).png

モデルに格納されている状態のマテリアルはリードオンリーであり、このままではパラメータをいじることができません。
スクリーンショット (217).png

このようなインポートされたマテリアルを調整したい場合は、インポート設定のExtract Materials...ボタンを押下してマテリアルを抽出する必要があります。
スクリーンショット (216).png

モデルが少数であれば特にこの作業も問題ないのですが、モデルが大量に存在するようなプロジェクトだと、いちいち個別のモデルに対してボタンを押下→出力先のフォルダを作成・指定→エクスポートのような順で作業するのは面倒です。
この記事では以下のようなUIでこの作業をまとめて行えるエディター拡張の実装を紹介します。
スクリーンショット 2022-12-04 234330.png

実装の準備

まず、抽出用のウィンドウを作る前に、エディター拡張内で用いている、Path属性の実装を示します。これはProjectタブからファイルやフォルダをドラッグアンドドロップすることでパスを指定することができる領域を描画する属性です。
kan_kikuchiさんの以下のブログ記事を参考にさせていただきました。

PathAttribute.cs
using UnityEngine;

/// <summary>
/// ドラッグアンドドロップでパスを設定するための属性
/// 参考:https://kan-kikuchi.hatenablog.com/entry/PathAttribute_1
/// </summary>
public class PathAttribute : PropertyAttribute
{
}

属性を実際に描画するPropertyDrawerの実装を示します。

PathAttributeDrawer.cs
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

/// <summary>
/// PathAttributeが付加されたフィールドの描画
/// 参考:https://kan-kikuchi.hatenablog.com/entry/PathAttribute_1
/// </summary>
[CustomPropertyDrawer(typeof(PathAttribute))]
public class PathAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        //string以外に設定されている場合はスルー
        if (property.propertyType != SerializedPropertyType.String)
        {
            return;
        }

        //D&D出来るGUIを作成、 ドロップされたオブジェクトのリストを取得
        var dropObjects = CreateDragAndDropGUI(position);

        //オブジェクトがドロップされたらパスを設定
        if (dropObjects.Count > 0)
        {
            property.stringValue = AssetDatabase.GetAssetPath(dropObjects[0]);
        }

        //現在設定されているパスを表示
        GUI.Label(position, property.displayName + " : " + property.stringValue);
    }

    //D&DのGUIを作成
    private static List<Object> CreateDragAndDropGUI(Rect rect)
    {
        var dropObjects = new List<Object>();

        //D&D出来る場所を描画
        GUI.Box(rect, "");

        //マウスの位置がD&Dの範囲になければスルー
        if (!rect.Contains(Event.current.mousePosition))
        {
            return dropObjects;
        }

        //現在のイベントを取得
        var eventType = Event.current.type;

        //ドラッグ&ドロップで操作が 更新されたとき or 実行したとき
        if (eventType is EventType.DragUpdated or EventType.DragPerform)
        {
            //カーソルに+のアイコンを表示
            DragAndDrop.visualMode = DragAndDropVisualMode.Copy;

            //ドロップされたオブジェクトをリストに登録
            if (eventType == EventType.DragPerform)
            {
                dropObjects = new List<Object>(DragAndDrop.objectReferences);

                //ドラッグを受け付ける
                DragAndDrop.AcceptDrag();
            }

            //イベントを使用済みにする
            Event.current.Use();
        }

        return dropObjects;
    }
}
#endif

以上のスクリプトをプロジェクトの任意のフォルダに配置して下さい。

Material Extractorの実装

実際に抽出を行うエディタウィンドウの実装を示します。

MaterialExtractor.cs
#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class MaterialExtractor : EditorWindow
{
    /// <summary>
    /// 抽出する対象ディレクトリのパス。
    /// このディレクトリ内の全てのモデルのマテリアルを抽出する。
    /// </summary>
    [Path] public string TargetPath = "Assets/Models/";

    /// <summary>
    /// マテリアル抽出先ディレクトリのサフィックス
    /// </summary>
    public string Suffix = "materials";

    /// <summary>
    /// 子ディレクトリも抽出の対象にするか
    /// </summary>
    public bool IncludeChildDirectories = true;

    [MenuItem("Window/MaterialExtractor")]
    public static MaterialExtractor ShowWindow()
    {
        return GetWindow<MaterialExtractor>(nameof(MaterialExtractor));
    }

    private void OnGUI()
    {
        // 自身のSerializedObjectを取得
        var so = new SerializedObject(this);
        so.Update();
        EditorGUILayout.PropertyField(so.FindProperty(nameof(TargetPath)), true);
        EditorGUILayout.PropertyField(so.FindProperty(nameof(Suffix)), true);
        EditorGUILayout.PropertyField(so.FindProperty(nameof(IncludeChildDirectories)), true);
        // SerializedObjectに変更を適用
        so.ApplyModifiedProperties();

        if (GUILayout.Button("マテリアルを抽出"))
        {
            var models = GetModels(TargetPath);
            models.ForEach(ExtractMaterials);
        }

        if (GUILayout.Button("抽出されたマテリアルをモデルに復元"))
        {
            var models = GetModels(TargetPath);
            models.ForEach(RestoreMaterials);
        }
    }

    private void ExtractMaterials(string modelPath)
    {
        var materials =
            AssetDatabase.LoadAllAssetsAtPath(modelPath).Where(x => x.GetType() == typeof(Material)).ToArray();

        Debug.Log($"{modelPath} has {materials.Length} materials.");
        if (materials.Length == 0) return;

        var assetsToReload = new HashSet<string> { modelPath };

        // 出力先のパス(Assets以下)
        var destinationPath =
            $"{Path.GetDirectoryName(modelPath)}/{Path.GetFileNameWithoutExtension(modelPath)}_{Suffix}/";
        Directory.CreateDirectory(destinationPath);

        foreach (var material in materials)
        {
            var newAssetPath = destinationPath + material.name + ".mat";
            newAssetPath = AssetDatabase.GenerateUniqueAssetPath(newAssetPath);
            assetsToReload.Add(newAssetPath);

            var error = AssetDatabase.ExtractAsset(material, newAssetPath);
            if (!string.IsNullOrEmpty(error))
            {
                Debug.Log($"[MaterialExtractor] error: {error}");
            }
        }

        foreach (var path in assetsToReload)
        {
            AssetDatabase.WriteImportSettingsIfDirty(path);
            AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
        }
    }

    private void RestoreMaterials(string modelPath)
    {
        var materialPath =
            $"{Path.GetDirectoryName(modelPath)}/{Path.GetFileNameWithoutExtension(modelPath)}_{Suffix}";
        if (!Directory.Exists(materialPath)) return;

        File.Delete(materialPath + ".meta");
        Directory.Delete(materialPath, true);

        var externalObjectKeys = AssetImporter.GetAtPath(modelPath).GetExternalObjectMap().Keys;
        foreach (var key in externalObjectKeys)
        {
            AssetImporter.GetAtPath(modelPath).RemoveRemap(key);
        }

        AssetDatabase.WriteImportSettingsIfDirty(modelPath);
        AssetDatabase.ImportAsset(modelPath, ImportAssetOptions.ForceUpdate);
        AssetDatabase.Refresh();
    }

    private List<string> GetModels(string path)
    {
        var models = new List<string>();

        foreach (var fileInfo in new DirectoryInfo(path).GetFiles())
        {
            var importedObject = AssetDatabase.LoadAssetAtPath($"{path}/{fileInfo.Name}", typeof(Object));
            if (importedObject == null) continue;
            var prefabType = PrefabUtility.GetPrefabAssetType(importedObject);

            if (prefabType == PrefabAssetType.Model)
            {
                models.Add($"{path}/{fileInfo.Name}");
            }
        }

        if (!IncludeChildDirectories) return models;

        foreach (var directoryInfo in new DirectoryInfo(path).GetDirectories())
        {
            models.AddRange(GetModels($"{path}/{directoryInfo.Name}"));
        }

        return models;
    }
}
#endif

このスクリプトもプロジェクトの任意のフォルダに配置して下さい。

使い方

Window/MaterialExtractorから起動します。
スクリーンショット (220).png
Target Pathの上にフォルダをドラッグアンドドロップすることで、抽出対象のフォルダを指定することができます。
Inkedスクリーンショット 2022-12-04 234330 - コピー.jpg
マテリアルを抽出ボタンを押すと抽出が実行されます。
モデルと同階層のモデル名_materialsフォルダの中にマテリアルが抽出されます。
スクリーンショット (222).png
抽出されたマテリアルをモデルに復元を押すとモデルの中にマテリアルが再格納され、外に抽出されていたマテリアルは削除されます。
スクリーンショット (223).png

さいごに

いかがだったでしょうか。
エディター拡張は一回作っておくと複数プロジェクトで使い回せるので、ぜひ使ってみてください。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0