Unity

Unityのメッシュを合成する

Unityでいくつかの3Dモデルを同時に表示するには、3Dモデル(メッシュ)毎にGameObjectを作成して、それらをシーン上に配置することになります。異なるモデルデータだけでなく、同一のモデルデータを複数表示させる時も同様です。

例えば、Unityに最初から用意されている3D ObjectのCubeを複数表示する場合は、以下のようになります。

using UnityEngine;

public class CircleCubes : MonoBehaviour
{
    static readonly float R = 10;
    static readonly float CubeNum = 20;

    void Start ()
    {
        for (float rad = 0; rad < 2*Mathf.PI; rad += 2 * Mathf.PI / CubeNum)
        {
            var o = GameObject.CreatePrimitive(PrimitiveType.Cube);
            o.transform.parent = GetComponent<Transform>();

            // 親のGameObjectを中心に円形に配置
            o.transform.localPosition = new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0) * R;
            // BoxColliderは使わないので消す
            GameObject.Destroy(o.GetComponent<BoxCollider>());
        }
    }   
}

上記のスクリプトを空のGameObjectに付けてから実行すると以下のようになります。
image.png

同じ3Dモデルデータを表示するだけのために、同じ数だけのGameObjectを生成するのはどうにも効率がよくないように思えます(もちろん、表示以外の事をさせるGameObjectであれば話は別です)。

Cubeの3Dモデルデータは、CubeのMeshFilterコンポーネントのMeshに設定されているCubeです(Unityに最初から用意されているメッシュです)。
Cube_inspector.png
このCubeのメッシュの情報を、必要な数だけ複製&配置して、それらをまとめた単一のメッシュデータを作成して、それを使えばGameObjectは一つで済みそうです。

Unityには、複数のメッシュを1つのメッシュに合成するMesh.CombineMeshesというAPIが用意されていますから、これを使ってみることにします。

using UnityEngine;

public class CircleCubes : MonoBehaviour
{
    static readonly float R = 10;
    static readonly float CubeNum = 20;

    void Start ()
    {
        // ひとつだけCubeを生成して使いまわす。ただし、Mesh自体は差し替える
        var o = GameObject.CreatePrimitive(PrimitiveType.Cube);

        o.transform.parent = GetComponent<Transform>();
        // BoxColliderは使わないので消す
        GameObject.Destroy(o.GetComponent<BoxCollider>());

        // meshではなく、sharedMeshの方が良いそう
        Mesh cubeMesh = o.GetComponent<MeshFilter>().sharedMesh;

        // CombineMeshes()する時に使う配列
        CombineInstance[] combineInstanceAry = new CombineInstance[(int)CubeNum];

        int index = 0;

        for(float rad = 0; rad < 2*Mathf.PI; rad += 2 * Mathf.PI / CubeNum)
        {
            // 合成するMesh(同じMeshを円形に並べたMesh)
            combineInstanceAry[index].mesh = cubeMesh;
            // 配置場所。ここのtransformはMatrix4x4。名前が紛らわしい。
            combineInstanceAry[index].transform = Matrix4x4.Translate(new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0) * R);
            index++;
        }

        // 合成した(する)メッシュ
        var combinedMesh = new Mesh();
        combinedMesh.name = "Cubes";
        combinedMesh.CombineMeshes(combineInstanceAry);
        // 上書きする
        o.GetComponent<MeshFilter>().mesh = combinedMesh;
    }   
}

合成したいメッシュの個数分のCombineInstanceクラスの配列を用意して、メッシュデータを参照させます。
CombineInstanceクラスを使わずに、素直にメッシュの参照配列をCombineMeshes()に渡せるようになっていればよさそうなものですが、参照するメッシュの位置を変えたり、特定のサブメッシュだけを使ったりといった指定が必要になるので、CombineInstanceクラスを使うようです。

CombineMeshes()に渡すメッシュ配列は、同じメッシュを何度も参照しても大丈夫なのですが、その場合、CombineMeshes()の第二引数はtrue(デフォルト)にしてください。この引数がtrueの時、CombineMeshes()に渡されるメッシュを元にして新しい単一のメッシュを作成します。falseの場合は、オリジナルのメッシュを使ったメッシュを作るようで、同一メッシュを参照している時には1つしか表示されなくなってしまいます。未検証ですが、trueを指定した時はメッシュデータのコピーを作成する分、遅いと思います。

上記のプログラムを実行すると、1つのCubeが生成され、そのMeshを動的に生成&差し替えた結果が表示されます。
image.png

最初に参照にしているCubeメッシュを複製して配置しただけでは全てが同じ場所に重なってしまいますから、CombineInstance.transformを使って個別に位置を指定しています。

このプログラムでは、Cubeに最初から付いているBoxColliderコンポーネントを外しています。

GameObject.Destroy(o.GetComponent<BoxCollider>())

その代わりに、MeshColliderコンポーネントを付けてみましょう。

// Meshコライダーを付ける
o.AddComponent<MeshCollider>();

ついでにシーン内にSphereを配置してRigidbodyコンポーネントを付けて落としてみると、ちゃんと形状に従って接触することがわかります。
animation_1.gif

なんでも合成できるの?

上の例では、シンプルなCubeメッシュを合成しましたが、外部から読み込んだメッシュも基本的には合成できます。ただし、モデルデータによってはうまくいかない場合もあると思います。FBXやOBJをインポートした場合は、Read/Write Enabledにチェックが入っている必要があります。

また、メッシュ情報に三角形/四角形以外の形状指定がされているとエラーになるようです。
Failed getting triangles. Submesh topology is lines or points.

具体的には、Mesh.SetIndicesで、MeshTopology.PointsMeshTopology.Linesなどが指定されていると上記のエラーが表示されます。

リアルタイムに点群を随時読込みしながらメッシュを生成&表示しようとしたら、そもそも点データのメッシュはCombineMeshes()出来なかったという悲しいソースコードはこちらです。
負け惜しみですが、CombineMeshes()はデータの参照&コピーの繰り返しできっと重い&遅いでしょうから、リアルタイムに合成は厳しいかもしれません。

using System.Collections.Generic;
using UnityEngine;

public class CreateMeshObjs : MonoBehaviour
{
    class MeshData
    {
        internal string name;
        internal Color c;
        internal Vector3 offset;
    };

    // メッシュを2個作成する
    readonly MeshData[] meshDataSet =
    {
        new MeshData { name = "red",    c = new Color(1, 0, 0), offset = new Vector3(0, 0, 0) },
        new MeshData { name = "white",  c = new Color(1, 1, 1), offset = new Vector3(0, 0, 0) },
    };

    static readonly int NUM_POINTS = 5000;      // 点の個数

    /// <summary>
    /// メッシュを表示するだけのGameObjectを作成する
    /// </summary>
    /// <returns></returns>
    private GameObject CreateGameObjectWithMeshRenderer()
    {
        var o = new GameObject();
        o.transform.parent = GetComponent<Transform>();
        o.AddComponent<MeshFilter>();
        o.AddComponent<MeshRenderer>();

        // 組み込みのSprite-Defaultマテリアルを取得
#if UNITY_EDITOR
        var spriteMat = UnityEditor.AssetDatabase.GetBuiltinExtraResource<Material>("Sprites-Default.mat");
#else
        var spriteMat = Resources.GetBuiltinResource<Material>("Sprites-Default.mat");
#endif
        o.GetComponent<MeshRenderer>().material = spriteMat;

        return o;
    }

    /// <summary>
    /// 立方体の空間内のランダムな位置に点を生成したメッシュを生成する
    /// </summary>
    /// <param name="numPoints">点の数</param>
    /// <param name="c">色</param>
    /// <param name="name">メッシュ名</param>
    /// <param name="offset">中心位置</param>
    /// <returns>生成したメッシュ</returns>
    Mesh CreateSimpleSurfacePointMesh(int numPoints, Color c, string name, Vector3 offset)
    {
        Vector3[] points = new Vector3[numPoints];
        int[] indecies = new int[numPoints];
        Color[] colors = new Color[numPoints];

        for(int i = 0; i < numPoints; i++)
        {
            points[i] = new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)) + offset;  // 頂点座標
            indecies[i] = i;                                                                                        // 配列番号をそのままインデックス番号に流用
            colors[i] = c;
        }

        Mesh mesh = new Mesh();
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        mesh.vertices = points;
        mesh.SetIndices(indecies, MeshTopology.Points, 0);  // 1頂点が1インデックスの関係
        mesh.colors = colors;
        mesh.name = name;

        return mesh;
    }

    void Start()
    {
        var meshes = new List<Mesh>();

        // メッシュを作成
        foreach(var item in meshDataSet)
        {
            Mesh meshSurface = CreateSimpleSurfacePointMesh(NUM_POINTS, item.c, item.name, item.offset);
            meshes.Add(meshSurface);
            var o = CreateGameObjectWithMeshRenderer();
            o.name = item.name;
            o.GetComponent<MeshFilter>().mesh = meshSurface;
        }

        // 合成したメッシュを作成
        CombineInstance[] combineInstanceAry = new CombineInstance[meshes.Count];
        for(int i = 0; i < meshes.Count; i++)
        {
            // エラー:Failed getting triangles. Submesh topology is lines or points.
            combineInstanceAry[i].mesh = meshes[i];
        }
        var combinedMesh = new Mesh();
        combinedMesh.CombineMeshes(combineInstanceAry);
    }
}