Prog-G

岐阜大学プログラミングサークル

Unityでスクリプトからメッシュを生成する

岐大祭2018 Unity

この記事は、岐大祭に向けてアドベントカレンダー的な記事を書く企画の 2 日目です。

Unity で3次元モデルを扱うには、あらかじめ Blender などのモデリングができるソフトウェアによって作成したデータをインポートするという方法を取るのが一般的です。 しかしながら、 Unity エディタ内でプログラムによってモデリングをすることも可能です。 岐大祭で展示しているレースゲーム(まだ未完成ですが)では、コース作成をスクリプトを用いて Unity エディタ内部で行っています。 他にはスリップ痕などの動的表現なども動的モデリングのよくある使用例です。

これからメッシュをゼロから作る方法を説明します。

今回説明に使ったプロジェクト をアップロードしておきます。

板ポリを置いてみる

まずは単純な正方形の板を置いてみましょう。

3D モデルはメッシュというポリゴン平面(多角形の平面)の集合で表されており、この多角形は一般に三角形です。 三角形の板をたくさん配置することによって物体を表していると直感的に理解するといいでしょう。 Unity でメッシュを触るには、メッシュがどのように三角形の集合を表現するか理解する必要があります。 メッシュの表現でまず重要なのは VerticesTriangles 、すなわち頂点と三角形です。

正方形の板では4つの頂点があります。 まず頂点の位置を Unity に教えてあげます。 「頂点 A は(0,0,0)にある。 頂点 B は(0,0,1)……」 といった感じです。

頂点の与え方

次に頂点をどのように繋いで三角形を構成するかを伝えます。 「頂点 A→B→D と繋いで三角形を作れ。 次は頂点 B→C→D と繋げ」 といった感じです。

三角形の与え方

3 点を繋ぐ順番は右回り左回りどちらでもいいわけではなくて、右回り(時計回り)に見える側が面の表になります。

実際のスクリプトを見てみましょう。

using UnityEngine;

[RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer))]
public class DynamicMeshSample1 : MonoBehaviour
{
    private MeshFilter meshFilter;

    private void Start()
    {
        meshFilter = GetComponent<MeshFilter>();

        Mesh mesh = new Mesh();
        mesh.vertices = new Vector3[] { new Vector3(0f, 0f, 0f), new Vector3(0f, 0f, 1f), new Vector3(1f, 0f, 1f), new Vector3(1f, 0f, 0f) };
        mesh.triangles = new int[] { 0, 1, 3, 1, 2, 3 };

        mesh.RecalculateBounds();

        meshFilter.sharedMesh = mesh;
    }
}

まず新しい Mesh インスタンスを作ります。 そこに4つの頂点を配列として渡し、2つの三角形平面を与えてやります。 三角形は頂点配列のインデックスを順番に書いた配列として表現されます(つまり配列の要素数は 3 の倍数)。 RecalculateBounds() はメッシュの包含範囲を自動的に計算します。 これはカメラのカリングに使われ、どの範囲をカメラが映しているときにこのメッシュが投影されるかを伝えます。 最後に MeshFilter へ出来上がったメッシュを渡します。

このスクリプトを動作させるときは適当なゲームオブジェクトにアタッチして再生します。 必要なコンポーネントである MeshFilterMeshRenderer も勝手にアタッチされます。

スクリプトのインスペクター

再生するとこのように表示されます。

動的メッシュ1の結果

補足ですが、 Mesh の頂点情報は Transformローカル座標系です。 ワールド座標で頂点を決めたときは、レンダラーの Transform を利用してローカル座標に変換する必要があります。

単色マテリアルを適用する

先ほどの板ポリはマテリアルが適切に設定されていないので例の禍々しいピンクが表示されています。 マテリアルの設定をスクリプトからしてみましょう。

Project ビューで適当なマテリアルを作り、色だけ設定します(テクスチャは次の項で説明します)。

先ほどのスクリプトにマテリアルの設定を付け加えます。

using UnityEngine;

[RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer))]
public class DynamicMeshSample2 : MonoBehaviour
{
    private MeshFilter meshFilter;

    private MeshRenderer meshRenderer;

    [SerializeField]
    private Material material;

    private void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();

        Mesh mesh = new Mesh();
        mesh.vertices = new Vector3[] { new Vector3(0f, 0f, 0f), new Vector3(0f, 0f, 1f), new Vector3(1f, 0f, 1f), new Vector3(1f, 0f, 0f) };
        mesh.triangles = new int[] { 0, 1, 3, 1, 2, 3 };

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
        mesh.RecalculateTangents();

        meshFilter.sharedMesh = mesh;
        meshRenderer.sharedMaterial = material;
    }
}

マテリアルは MeshRenderer に渡します。 RecalculateNormals()RecalculateTangents() でシェーディングに必要な情報である法線と正接を自動的に計算させます。

Inspector からスクリプトのフィールドにマテリアルをセットして再生します。 再生するとこのように表示されます。

動的メッシュ2の結果

今回はスクリプトからマテリアルを設定していきますが、 MeshRenderer に Inspector から予めセットしても同じ結果になります。

テクスチャ付きのマテリアルを適用する

次はテクスチャを設定しましょう。 テクスチャを設定するためには UV について知る必要があります。

UV 情報は頂点とテクスチャの位置を対応付けます。 テクスチャ画像の水平軸が u で、鉛直軸が v です。 UV 座標はテクスチャ画像の左下端が (0,0) 、右上端が (1,1) になります。

UVの概念

テクスチャをきちんと表示するにはこの UV を設定して 「この頂点はテクスチャのこの位置で……」 とシェーダーに教えてあげる必要があるわけです。

UV 設定を付加したスクリプトです。

using UnityEngine;

[RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer))]
public class DynamicMeshSample3 : MonoBehaviour
{
    private MeshFilter meshFilter;

    private MeshRenderer meshRenderer;

    [SerializeField]
    private Material material;

    private void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();

        Mesh mesh = new Mesh();
        mesh.vertices = new Vector3[] { new Vector3(0f, 0f, 0f), new Vector3(0f, 0f, 1f), new Vector3(1f, 0f, 1f), new Vector3(1f, 0f, 0f) };
        mesh.triangles = new int[] { 0, 1, 3, 1, 2, 3 };
        mesh.uv = new Vector2[] { new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(1f, 0f) };

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
        mesh.RecalculateTangents();

        meshFilter.sharedMesh = mesh;
        meshRenderer.sharedMaterial = material;
    }
}

UV 座標は 2 次元ベクトルの配列として渡します。 UV 座標は頂点と一対一の対応です。 したがってそれぞれの配列の要素数は一致します。

今回使用するテクスチャです。

テクスチャ

テクスチャをセットしたマテリアルを用意してスクリプトのコンポーネントに入れておきます。

動的メッシュ3の結果

再生するとこのようになります。 それぞれの頂点とテクスチャの端がきちんと対応しているのがわかると思います。

板を敷き詰める

さっきの板を 3×3 で配置することを考えてみましょう。 テクスチャは 1 区画ごとに 1 枚となるようにします。 植木算的に考えて必要な頂点は 16 個で 18 個の三角形が構成できそうです。 しかしこのやり方では UV に問題が生じます。

板の内部の頂点ではある一方の平面から見ると U の値が 0 なのに、もう一方から見ると 1 になるのです。 これは v についても同様に発生します。

UVの衝突

これは同じ位置に頂点を何個も配置することで解決します。 4 頂点の正方形を 9 個配置するので 36 頂点です。 これなら UV を正しく設定できます。

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer))]
public class DynamicMeshSample4 : MonoBehaviour
{
    private MeshFilter meshFilter;

    private MeshRenderer meshRenderer;

    [SerializeField]
    private Material material;

    private void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();

        List<Vector3> verts = new List<Vector3>();
        List<Vector2> uvs = new List<Vector2>();
        List<int> tris = new List<int>();
        Mesh mesh = new Mesh();

        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                int pointer = verts.Count;
                verts.Add(new Vector3(i, 0f, j));
                verts.Add(new Vector3(i, 0f, j + 1f));
                verts.Add(new Vector3(i + 1f, 0f, j + 1f));
                verts.Add(new Vector3(i + 1f, 0f, j));
                uvs.Add(new Vector2(0f, 0f));
                uvs.Add(new Vector2(0f, 1f));
                uvs.Add(new Vector2(1f, 1f));
                uvs.Add(new Vector2(1f, 0f));
                tris.Add(0 + pointer);
                tris.Add(1 + pointer);
                tris.Add(3 + pointer);
                tris.Add(1 + pointer);
                tris.Add(2 + pointer);
                tris.Add(3 + pointer);
            }
        }

        mesh.vertices = verts.ToArray();
        mesh.triangles = tris.ToArray();
        mesh.uv = uvs.ToArray();

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
        mesh.RecalculateTangents();

        meshFilter.sharedMesh = mesh;
        meshRenderer.sharedMaterial = material;
    }
}

動的メッシュ4の結果

複数のマテリアルを持つメッシュ

人間や建造物など、 3D モデルでは1つのメッシュに複数のマテリアルが付与されているのが普通です。 これまでは1つのメッシュに1つのマテリアルが対応していました。 メッシュに複数のマテリアルを適用するにはサブメッシュを使います。

サブメッシュは三角形のグループ分けをしていると考えることができます。 1つのサブメッシュに1つのマテリアルが対応します。 「サブメッシュ 0 にはこれだけの三角形があってマテリアル 0 が対応、サブメッシュ 1 にはこれだけの三角形があってマテリアル 1 が対応……」 といった感じです。

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer))]
public class DynamicMeshSample5 : MonoBehaviour
{
    private MeshFilter meshFilter;

    private MeshRenderer meshRenderer;

    [SerializeField]
    private Material[] materials;

    private void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
        meshRenderer = GetComponent<MeshRenderer>();

        List<Vector3> verts = new List<Vector3>();
        List<Vector2> uvs = new List<Vector2>();
        List<int> tris1 = new List<int>();
        List<int> tris2 = new List<int>();
        Mesh mesh = new Mesh();

        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                int pointer = verts.Count;
                verts.Add(new Vector3(i, 0f, j));
                verts.Add(new Vector3(i, 0f, j + 1f));
                verts.Add(new Vector3(i + 1f, 0f, j + 1f));
                verts.Add(new Vector3(i + 1f, 0f, j));
                uvs.Add(new Vector2(0f, 0f));
                uvs.Add(new Vector2(0f, 1f));
                uvs.Add(new Vector2(1f, 1f));
                uvs.Add(new Vector2(1f, 0f));
                tris1.Add(0 + pointer);
                tris1.Add(1 + pointer);
                tris1.Add(3 + pointer);
                tris1.Add(1 + pointer);
                tris1.Add(2 + pointer);
                tris1.Add(3 + pointer);
            }
        }

        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                int pointer = verts.Count;
                verts.Add(new Vector3(0f, j, i));
                verts.Add(new Vector3(0f, j + 1f, i));
                verts.Add(new Vector3(0f, j + 1f, i + 1f));
                verts.Add(new Vector3(0f, j, i + 1f));
                uvs.Add(new Vector2(0f, 0f));
                uvs.Add(new Vector2(0f, 1f));
                uvs.Add(new Vector2(1f, 1f));
                uvs.Add(new Vector2(1f, 0f));
                tris2.Add(0 + pointer);
                tris2.Add(1 + pointer);
                tris2.Add(3 + pointer);
                tris2.Add(1 + pointer);
                tris2.Add(2 + pointer);
                tris2.Add(3 + pointer);
            }
        }

        mesh.vertices = verts.ToArray();
        mesh.subMeshCount = 2;
        mesh.SetTriangles(tris1, 0);
        mesh.SetTriangles(tris2, 1);
        mesh.uv = uvs.ToArray();

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
        mesh.RecalculateTangents();

        meshFilter.sharedMesh = mesh;
        meshRenderer.sharedMaterials = materials;
    }
}

複数のサブメッシュを持つメッシュを作るには、まず subMeshCount プロパティにサブメッシュの数をセットします。 三角形は triangles プロパティでなく SetTriangles メソッドを用いてセットします。 第二引数でサブメッシュのインデックスを指定します。 MeshRenderer のプロパティも複数版の sharedMaterials を使います。

動的メッシュ5の結果

Mesh クラスを扱えるようになれば、既存モデルのメッシュをスクリプトから変更することでモデルを変形するということもできます。 それもまた別の機会に記事として書くかもしれません。

明日も引き続き Unity の話題です。