チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

Mesh加工にちょっと挑戦、メッシュ最適化プラグインを試作

メッシュの操作、いわゆるUnityにおける頂点バッファ生加工は覚えておかなきゃいけないだろうと思ってたんですが良い機会に恵まれ勉強したのをチラ裏。PropertyDrawerもまとめきらないうちに別のネタを振り込む蛮行。

つたない覚書

Mesh

これがいわゆる頂点バッファクラス。UVやボーンウェイト値は基本的にMeshに存在する(まぁ普通。ただしマテリアルだけは別)。三角形の並びはよくあるトライアングルリストなので、頂点バッファへのインデックスを3頂点ずつ配列してるはず。なお、一つのメッシュの中でトライアングルリストは複数グループに分かれて、この分割されたトライアングルリスト1つ分をサブメッシュと呼ぶ。大事なのは、これがマテリアル単位ということ。Unityみたいなエンジンが内部でやるべきハードの操作フローとしては、「一つのゲームオブジェクトをレンダするたび、マテリアルの数だけシェーダーの切替処理をはさみながらハードウェアへの頂点配列のブロック転送」ってことになりやすいはずで、これがメッシュとサブメッシュの構造になってるんだと思う。だから一般に「drawcall増やさないため1モデルの中で無駄にオブジェクト分割しないほうがええ」というのは正しくは「描画するサブメッシュの数を増やすな」ということで、サブメッシュとマテリアル(シェーダー)の切り替えが多いと、ハードへの頂点配列転送処理が何度も割り込んでものっそ重くなるよ、という意味。マテリアル情報はMeshでは持たず、別のRenderer系コンポーネントから取得する。この別コンポーネントから後付けするマテリアル配列数がサブメッシュ配列数に足りない場合、マテリアル不足でメッシュ表示が欠ける。

「頂点バッファを持っていて、参照インデックスで三角形リスト」というのは分かったけど、ボーンウェイトやUVの配列は頂点バッファの配列に対応してるのか三角形リストのインデックス配列に対応してるのかはまだ調査不足。ただ後述するメッシュ統合クラスでは三角形リストだけ編集していて、それでボーンやUVがうまく動いて見えるので、このへんは多分頂点バッファの数だけ並んでるんじゃないかなぁ(つまりUVやウェイトも含めて完全一致する頂点だけが共有されて、どれか値の違う要素があれば別頂点扱いになり、バッファの利用率は若干下がる仕組み?…あぁまた適当な事を書いてしまっている私)。

Renderer

とりあえず今回大事なのはRendererがMeshのサブメッシュ配列に対応するマテリアル配列を持ってるということ。

習作:メッシュの最適化クラス

一応動的なメッシュ転送最適化とかあるんですが、結構条件があるとか、そもそもUnityの編集コマンドとしてサブメッシュ統合操作があれば、モデリング時にそこまでオブジェクト統合を意識しなくても良いから素敵じゃん、ということで、同一マテリアルを使用するサブメッシュ同士に限って統合してしまう編集コマンド的エディタプラグイン作ってみました。

SharedMaterialMeshOptimizer
using UnityEngine;
using UnityEditor;

// 1つのメッシュの中で複数のサブメッシュが同じマテリアルを使用するなら統合するプラグイン
public class SharedMaterialMeshOptimizer : EditorWindow {

    private bool generateInstantiatedMaterial;
    
    [MenuItem("Plugins/MeshOptimizer/SharedMaterialOptimize")]
    public static void WindowOpen()
    {
        GetWindow<SharedMaterialMeshOptimizer>();
    }

    private void OnGUI()
    {
        GUILayout.Label("MeshOptimizer/SharedMaterialOptimize Option");
        generateInstantiatedMaterial = GUILayout.Toggle(generateInstantiatedMaterial, "Clone instantiated material");
        GUILayout.FlexibleSpace();
        if (GUILayout.Button("Execute optimize")) OptimizeOnEdit();
    }
    
    public void OptimizeOnEdit()
    {
        if ((1 < Selection.objects.Length) &&
            (EditorUtility.DisplayDialog("MeshOptimizer/SharedMaterialOptimize", "Do you optimize multi-selected objects?", "Yes!!", "No...") == false)) return;

        foreach (UnityEngine.Object obj in Selection.objects) OptimizeOnEdit(obj as GameObject);
    }

    public void OptimizeOnEdit(GameObject obj)
    {
        if (obj != null)
        {
            SkinnedMeshRenderer skinned = obj.GetComponent<SkinnedMeshRenderer>();
            if (skinned != null) OptimizeOnEdit(skinned); else OptimizeOnEdit(obj.GetComponent<MeshRenderer>());
        }
    }

    public void OptimizeOnEdit(SkinnedMeshRenderer meshRenderer)
    {
        Mesh srcMesh = null;
        if (meshRenderer != null)
        {
            srcMesh = meshRenderer.sharedMesh;
            Material[] materials = meshRenderer.sharedMaterials;
            if (OptimizeOnEdit(ref srcMesh, ref materials))
            {
                meshRenderer.sharedMaterials = materials;
                meshRenderer.sharedMesh = srcMesh;
            }
        }
    }

    public void OptimizeOnEdit(MeshRenderer meshRenderer)
    {
        MeshFilter meshFilter = null;
        Mesh srcMesh = null;
        if (meshRenderer != null)
        {
            meshFilter = meshRenderer.GetComponent<MeshFilter>();
            if (meshFilter != null) srcMesh = meshFilter.sharedMesh;
            Material[] materials = meshRenderer.sharedMaterials;
            if (OptimizeOnEdit(ref srcMesh, ref materials))
            {
                meshRenderer.sharedMaterials = materials;
                meshFilter.sharedMesh = srcMesh;
            }
        }
    }

    public bool OptimizeOnEdit(ref Mesh mesh, ref Material[] materials)
    {
        if ((mesh == null) || (materials == null)) return false;

        if (materials.Length < mesh.subMeshCount)
        {
            Debug.Log("Material Count Miss Matched");
            return false;
        }
        else
        {
            System.Collections.Generic.List<Material> uniqueMaterialList = new System.Collections.Generic.List<Material>(materials.Length);
            bool optimizeNeeds = false;
            foreach (Material mat in materials)
            {
                if (uniqueMaterialList.Contains(mat)) optimizeNeeds = true; else uniqueMaterialList.Add(mat);
            }
            if (optimizeNeeds == false) return false;
        }

        // clone and optimize
        Mesh newMesh = Instantiate(mesh) as Mesh;
        newMesh.name = mesh.name + " (Optimized)";

        System.Collections.Generic.List<Material> rebuildMaterials = new System.Collections.Generic.List<Material>(materials.Length);
        System.Collections.Generic.List<int[]> rebuildSubMeshes = new System.Collections.Generic.List<int[]>(mesh.subMeshCount);
        for (int subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++)
        {
            Material mat = materials[subMeshIndex];
            int index = rebuildMaterials.FindIndex(item => { return ReferenceEquals(item, mat); });
            if (index < 0)
            {
                // adding new set
                rebuildMaterials.Add(mat);
                rebuildSubMeshes.Add(mesh.GetTriangles(subMeshIndex));
            }
            else
            {
                // combine to registed, and replace
                int[] registedSubMeshes = rebuildSubMeshes[index];
                int[] srcSubMeshes = mesh.GetTriangles(subMeshIndex);
                int[] combineSubMeshes = new int[registedSubMeshes.Length + srcSubMeshes.Length];
                registedSubMeshes.CopyTo(combineSubMeshes, 0);
                srcSubMeshes.CopyTo(combineSubMeshes, registedSubMeshes.Length);
                rebuildSubMeshes[index] = combineSubMeshes;
            }
        }
        newMesh.subMeshCount = rebuildSubMeshes.Count;
        for (int index = 0; index < rebuildSubMeshes.Count; index++) newMesh.SetTriangles(rebuildSubMeshes[index], index);

        if (generateInstantiatedMaterial)
        {
            for (int index = 0; index < rebuildMaterials.Count; index++)
            {
                Material mat = new Material(rebuildMaterials[index]);
                mat.name += " (Clone)";
                rebuildMaterials[index] = mat;
            }
        }

        System.String savePath = EditorUtility.SaveFilePanel("Mesh asset", "Assets/", newMesh.name, "asset");
        int subIndex = savePath.IndexOf("/Assets/");
        if (subIndex < 0) return false;
        savePath = savePath.Substring(subIndex + 1);

        AssetDatabase.CreateAsset(newMesh, savePath);
        AssetDatabase.SaveAssets();

        materials = rebuildMaterials.ToArray();
        mesh = newMesh;
        return true;
    }
}

Editorフォルダに保存してね。エディタ上でメッシュを最適化したいオブジェクトを選択して、メニューからPlugins/MeshOptimizer/SharedMaterialOptimizeをクリックすると、最適化されたメッシュのアセットを新規に保存するダイアログが開き、保存するとメッシュ情報がその最適化済みメッシュに置換され、マテリアル配列も重複の無いユニークな状態になります。MeshRendererまたはSkinnedMeshRendererに対応。

ワイ、これの子階層再帰統合バージョン書いたらSourceForgeにうpるんや…(フラグ)