[Unity] 数式が苦手でもスクリプトでメッシュ生成（モデリング）をする方法

Unity上でスクリプトを使って動的にメッシュを生成するには**頂点座標**と**頂点を結ぶトライアングル**の理解が必要になります。
私もトライアングルの概念や図形を描くための関数が面倒で拒否感がありました。
しかし、Unity Editor上で視覚的に捉えて力業で覚えることで理解していくことができました。
**「1点の座標を中心にキューブを作る」** を例にして解説していきます。

### 頂点座標を作る
`StartPosition`の座標を中心としてキューブを構成する８つの頂点を作ります。
頂点座標が視覚的に表示するため`void OnDrawGizmos()`関数を使います。
この関数はプレイ状態にしなくてもシーンビュー上でギズモとして表示できるので便利です。
![スクリーンショット_20230218_233853.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/7a5a0217-81a3-9105-ca3c-e0a68804e575.png)
```C#:GenerateCube.cs
using System.Collections.Generic;
using UnityEngine;

public class GenerateCube : MonoBehaviour
{
    [SerializeField] Transform StartPosition;
    [SerializeField] Material Material;
    [SerializeField] float Width = 1f;
    [SerializeField] float Height = 1f;
    [SerializeField] float Forward = 1f;
    [SerializeField] bool IsDrawGizmo = true;

    private List<Vector3> CubePositions()
    {
        var vertices = new List<Vector3>();

        Vector3 startPos = StartPosition.transform.position;
        //Vector3 lowerLeft = startPos + new Vector3(Width, Height, Forward) / -2f;
        Vector3 lowerLeft = startPos + (Vector3.left * Width) / 2f + (Vector3.down * Height) / 2f + (Vector3.back * Forward) / 2f;
        Vector3 lowerRight = lowerLeft + Vector3.right * Width;
        Vector3 upperLeft = lowerLeft + Vector3.up * Height;
        Vector3 upperRight = lowerRight + Vector3.up * Height;
        Vector3 forwardLowerLeft = lowerLeft + Vector3.forward * Forward;
        Vector3 forwardLowerRight = lowerRight + Vector3.forward * Forward;
        Vector3 forwardUpperLeft = upperLeft + Vector3.forward * Forward;
        Vector3 forwardUpperRight = upperRight + Vector3.forward * Forward;

        vertices.Add(lowerLeft);
        vertices.Add(lowerRight);
        vertices.Add(upperLeft);
        vertices.Add(upperRight);
        vertices.Add(forwardLowerLeft);
        vertices.Add(forwardLowerRight);
        vertices.Add(forwardUpperLeft);
        vertices.Add(forwardUpperRight);

        return vertices;
    }

    private void OnDrawGizmos()
    {
        if(!IsDrawGizmo || StartPosition == null)
        {
            return;
        }

        var vertices = CubePositions();
        float size = 0.1f;

        foreach(var vertex in vertices)
        {
            Gizmos.DrawSphere(vertex, size);
        }
    }
}
```
最初に左下の座標を決めて、そこを起点として**Width, Height, Forward**のオフセットをして７点の座標を取ります。

### 頂点座標からトライアングルを作る
３つの頂点を結ぶ三角形が１つの面となります。四角形の形成には２つの三角形で構成されます。
トライアングルを作るルールとして、int型の頂点配列番号を３つでワンセットとし３の倍数になるようにトライアングル配列を作ります。
三角形の配列順は正面から見て右回りになるようにします。左回りで格納すると裏から見えるメッシュとなります。
プレーンのような平面の場合は右回りと左回りの三角形を作ることもありますが、キューブのように閉じたモデルの場合は裏側から見ることがないので左回りの三角形を作る必要がありません。
![スクリーンショット_20230219_001454.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/a19a9b40-6989-8697-1deb-35b44d9fd7a0.png)
１つ目の四角形を右回りの三角形の配列順直すと以下のようになります。
[ 1 , 0 , 2 ] [ 1 , 2 , 3 ]

コメントアウトしている`triangles.AddRange`は左回りの三角形の例です。裏側に作った場合、どういった見た目になるか興味あればコメントアウトを逆にして試してみてください。
```C#:GenerateCube.cs
    private List<int> CubeTriangles()
    {
        var triangles = new List<int>();

        int indexLowerLeft = 0;
        int indexLowerRight = 1;
        int indexUpperLeft = 2;
        int indexUpperRight = 3;
        int indexFowardLowerLeft = 4;
        int indexFowardLowerRight = 5;
        int indexFowardUpperLeft = 6;
        int indexFowardUpperRight = 7;

        // 正面
        //triangles.AddRange(SquareToTriangles(indexLowerRight, indexLowerLeft, indexUpperRight, indexUpperLeft));
        triangles.AddRange(SquareToTriangles(indexLowerLeft, indexLowerRight, indexUpperLeft, indexUpperRight));
        // 左側
        //triangles.AddRange(SquareToTriangles(indexLowerLeft, indexFowardLowerLeft, indexUpperLeft, indexFowardUpperLeft));
        triangles.AddRange(SquareToTriangles(indexFowardLowerLeft, indexLowerLeft, indexFowardUpperLeft, indexUpperLeft));
        // 右側
        //triangles.AddRange(SquareToTriangles(indexFowardLowerRight, indexLowerRight, indexFowardUpperRight, indexUpperRight));
        triangles.AddRange(SquareToTriangles(indexLowerRight, indexFowardLowerRight, indexUpperRight, indexFowardUpperRight));
        // 上側
        //triangles.AddRange(SquareToTriangles(indexUpperRight, indexUpperLeft, indexFowardUpperRight, indexFowardUpperLeft));
        triangles.AddRange(SquareToTriangles(indexUpperLeft, indexUpperRight, indexFowardUpperLeft, indexFowardUpperRight));
        // 下側
        //triangles.AddRange(SquareToTriangles(indexLowerLeft, indexLowerRight, indexFowardLowerLeft, indexFowardLowerRight));
        triangles.AddRange(SquareToTriangles(indexLowerRight, indexLowerLeft, indexFowardLowerRight, indexFowardLowerLeft));
        // 背面
        //triangles.AddRange(SquareToTriangles(indexFowardLowerLeft, indexFowardLowerRight, indexFowardUpperLeft, indexFowardUpperRight));
        triangles.AddRange(SquareToTriangles(indexFowardLowerRight, indexFowardLowerLeft, indexFowardUpperRight, indexFowardUpperLeft));

        return triangles;
    }

    private List<int> SquareToTriangles(int lowerLeft, int lowerRight, int upperLeft, int upperRight)
    {
        var triangles = new List<int>();

        triangles.Add(lowerRight);  // 1
        triangles.Add(lowerLeft);   // 0
        triangles.Add(upperLeft);   // 2

        triangles.Add(lowerRight);  // 1
        triangles.Add(upperLeft);   // 2
        triangles.Add(upperRight);  // 3

        return triangles;
    }
```
キューブを作りたい場合、四角形を６面必要になるので`SquareToTriangles`のように面を構成するトライアングルを振り分けるための関数を用意しておくと楽かもしれません。
そうすることでギズモでビジュアル化した頂点を見ながら６面をそろえるだけになります。

### UVを作る
単色でよければUVは不要ですがメッシュにテクスチャを張りたい場合、UVの座標が必要となります。
![スクリーンショット_20230219_010554.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/eb182692-f02a-1c75-b8a0-6ce00143043f.png)
UVの座標を配列の0~3と4~7を同じにしています。裏から見た場合、反転した状態で張られました。
正しい向きで張るには4,5番を反転させ6,7番も反転させて格納する必要があります。
```C#:GenerateCube.cs
    private List<Vector2> CubeUV()
    {
        var uvs = new List<Vector2>();

        uvs.Add(new Vector2(0, 0));
        uvs.Add(new Vector2(1, 0));
        uvs.Add(new Vector2(0, 1));
        uvs.Add(new Vector2(1, 1));

        uvs.Add(new Vector2(0, 0));
        uvs.Add(new Vector2(1, 0));
        uvs.Add(new Vector2(0, 1));
        uvs.Add(new Vector2(1, 1));

        return uvs;
    }
```
### メッシュを生成して表示する
これまでに準備した頂点、トライアングル、UVでメッシュを生成します。
生成したメッシュは`MeshFilter`に格納します。
```C#:GenerateCube.cs
    public void GenerateMesh()
    {
        var vertices = CubePositions();
        var triangles = CubeTriangles();
        var uvs = CubeUV();

        var mesh = new Mesh();
        mesh.SetVertices(vertices);
        mesh.SetTriangles(triangles, 0);
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();

        var obj = new GameObject();
        obj.name = "GenerateCube";
        obj.transform.parent = transform;
        var meshFilter = obj.AddComponent<MeshFilter>();
        var meshRenderer = obj.AddComponent<MeshRenderer>();
        var meshCollider = obj.AddComponent<MeshCollider>();

        meshFilter.mesh = mesh;
        meshCollider.sharedMesh = mesh;
        meshRenderer.material = Material;

        cubeMesh = meshFilter;
    }
```

### テクスチャをキューブの６面に貼る場合
上記で紹介したUVの作り方では正面と裏面の２面しかテクスチャが貼られていませんでした。
それは８頂点では足りなかったからです。
では、いくつほしいか調査するためにUnityの３Dオブジェクトのキューブを作ってみましょう。
プレイ状態でメッシュをダブルクリックすると以下のようにメッシュの情報を得られます。
![スクリーンショット_20230219_200524.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/cdc09788-a625-904c-7067-e41ab37134b9.png)
Verticesが24個あるため`24(頂点) / 6(面) = 4(頂点/面)`であることがわかります。
キューブのメッシュを構成するには８頂点で十分ですが、６面にテクスチャを張るためには２４頂点必要になることがわかります。

### 任意のメッシュを生成する場合
自身で頂点を置いてみるのもいいですが、Unityの定型オブジェクトやProBuilder、Blenderで作ったメッシュを用意して頂点の情報を抜き出してみるのもいいかと思います。
時間はかかりますが、逆解析から法則を見出して数式に起こすことで理解が深まります。