[Unity] ドローイングでキューブ状のメッシュ生成

![無題の動画 ‐ Clipchampで作成 (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/1329e89b-dfc1-a977-85a1-f692cf98909e.gif)

パスに沿ったメッシュ生成を応用してパスの部分をドローイングに置き換えました。
記事のスクリプトを応用した場合、不具合があったためその解決方法を含めて解説していきます。

https://qiita.com/okome184/items/c1e0b25d455ca6fd2ef0

### マウスイベントからパスを生成する
`Input.GetMouseButton`でマウスのクリックやホールド状態を取得します。
`Input.mousePosition`で取得したマウスの座標を`Camera.main.ScreenPointToRay`で`Ray`に変換し、ワールド座標を取得します。
`Input.GetMouseButtonDown(0)`を使うときの注意として`Input.GetMouseButtonUp(0)`のイベントが来る前に`Input.GetMouseButtonDown(0)`が2度続くことがあったので`bool`型のフラグ管理をしてDownイベントを２重に通さない工夫をしておくと安全かもしれません。
パス用の座標を格納するときは`Vector3.Distance`を使って現在のマウスポイントと１つ前の位置からの移動距離で新しい座標を格納するよにしました。
``` c#:
    [SerializeField] LayerMask Mask;
    [SerializeField] float DistanceInterval = 0.3f;
    bool isDrawing = false;
    List<Vector3> pathPositions = new List<Vector3>();

    private void Update()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, Mask))
        {
            if (Input.GetMouseButtonDown(0) && !isDrawing)
            {
                isDrawing = true;

                CreateMeshObject();
                pathPositions.Clear();
                pathPositions.Add(hit.point);
            }
            else if (Input.GetMouseButton(0) && isDrawing)
            {
                var lastPos = pathPositions[pathPositions.Count - 1];
                var currentPos = hit.point;

                if (Vector3.Distance(lastPos, currentPos) >= DistanceInterval)
                {
                    pathPositions.Add(hit.point);
                    UpdateMeshObject();
                }
            }
        }
        else
        {
            DrawEnd();
        }

        if (Input.GetMouseButtonUp(0))
        {
            DrawEnd();
        }
    }
```


![スクリーンショット_20230225_211905.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/38e38266-90a8-2ffb-8597-96c742af699e.png)
図のように`Box Collider`の配置とLayerMaskを指定してドローイングの範囲とメッシュの生成位置をコントロールすると楽です。
`Physics.Raycast`でヒットした`RaycastHit.point`が`Box Collider`の表面を取得出来るので位置調整をTransformをいじるだけで調整出来ます。

### 座標の向きの反転問題
![スクリーンショット_20230225_215039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/4ea448d5-1673-34d0-e50d-f3130888a906.png)
以下のコードのように`Quaternion.LookRotation`を使ってパスに沿った座標を計算していましたが、所々で座標が反転してしまいました。

``` c#:
            Vector3 direction = (secondPos - firstPos).normalized;
            Quaternion rot = Quaternion.LookRotation(direction);
            var square = new Square()
            {
                lowerLeft = positions[i] + rot * new Vector3(-1f * Width, 0f, 0f),
                lowerRight = positions[i] + rot * new Vector3(1f * Width, 0f, 0f),
                upperLeft = positions[i] + rot * new Vector3(-1f * Width, 1f * Height, 0f),
                upperRight = positions[i] + rot * new Vector3(1f * Width, 1f * Height, 0f),
            };
```
![無題の動画 ‐ Clipchampで作成 (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/c5b0221d-264f-b322-2a86-06c5e6b21d92.gif)
Gizmoで可視化してみるとどうやら**一つ前のX座標**と**動かしているX座標**の一致したときにおかしくなっていることが分かりました。
試行錯誤したところ`Quaternion.LookRotation`を使ったやり方では解決方法が見いだせなかったので別の方法をとります。
（X座標が一致した場合は１つ前と同じ回転のオフセットをかけることで解決したかもしれません）

### 座標の向きの求め方を変更
``` c#:
            Vector3 direction = (secondPos - firstPos).normalized;
            Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
            Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;

            var square = new Square()
            {
                lowerLeft = lowerLeft,
                lowerRight = lowerRight,
                upperLeft = lowerLeft + Vector3.back * Height,
                upperRight = lowerRight + Vector3.back * Height,
            };
```
`Vector3.normalized`で求めた向きを使って、`direction.x`をY座標に`direction.y`をX座標にオフセットしてすることにしました。（Z座標はドローイングの場合不要なので省いています）
こうすることで回転させずに向きを制御することができました。
![スクリーンショット_20230225_222434.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/8ee6bd49-4539-024d-2693-44f503d71bbf.png)
頂点２，３のようにY座標が一致した場合は向きが(0,1)になり、`direction.y`がX方向にオフセットされます。
頂点３，４ではX座標が一致し、向きが(1,0)となり、`direction.x`がY方向にオフセットされます。

### メッシュの影がおかしい
![スクリーンショット_20230225_230111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/44ed9b37-b028-34a7-5b50-7e912b1c5cd4.png)
頂点の問題は解決しましたが影の表示がおかしくて丸みを帯びたチューブ状のよに見えます。
実際はキューブ状なので平になってるはずですが、影の付き方によっておかしく見えてしまっています。
今回作ったメッシュはキューブでいうと頂点が８で構成されています。
Unityのテンプレキューブの頂点を確認すると頂点が２４あります。
つまり、単純に頂点の数が足りないため法線ベクトルの計算がうまく出来ていないことが分かりました。
８点で結ぶと頂点を使いまわしますが、そうすると１面の法線の向きは正しくても使いまわした法線の向きはかしな方向に向いていることになります。

### 頂点を増やして法線の向きを整える
![スクリーンショット_20230225_231817.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/8f215980-e467-00c3-31fd-e49b36a148cc.png)
やることは単純で頂点の数を倍にしてトライアングルを結ぶときに同じ頂点を使わないようにするだけです。
``` c#:
    private Square[] PathSquares24()
    {
        int length = pathPositions.Count;
        var positions = pathPositions;
        var squares = new Square[length * 2 + 2];

        for (int i = 0; i < length; i++)
        {
            Vector3 firstPos, secondPos;

            if (i == 0)
            {
                firstPos = positions[0];
                secondPos = positions[1];
            }
            else
            {
                firstPos = positions[i - 1];
                secondPos = positions[i];
            }

            Vector3 direction = (secondPos - firstPos).normalized;
            Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
            Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;

            var square = new Square()
            {
                lowerLeft = lowerLeft,
                lowerRight = lowerRight,
                upperLeft = lowerLeft + Vector3.back * Height,
                upperRight = lowerRight + Vector3.back * Height,
            };

            if (i == 0)
            {
                squares[0] = square;
            }
            else if (i == length - 1)
            {
                squares[squares.Length - 1] = square;
            }

            for (int j = 0; j < 2; j++)
            {
                squares[i * 2 + 1 + j] = square;
            }
        }

        return squares;
    }

    private Mesh GenerateMesh24()
    {
        var squares = PathSquares24();
        var mesh = new Mesh();

        var vertices = new List<Vector3>();
        var triangles = new List<int>();
        var uvs = new List<Vector2>();

        for (int i = 0; i < squares.Length; i++)
        {
            vertices.Add(squares[i].lowerLeft);
            vertices.Add(squares[i].lowerRight);
            vertices.Add(squares[i].upperLeft);
            vertices.Add(squares[i].upperRight);

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

            int currrentLowerLeft = (i * 4) + 0;
            int currrentLowerRight = (i * 4) + 1;
            int currrentUpperLeft = (i * 4) + 2;
            int currrentUpperRight = (i * 4) + 3;

            if (i == 0)
            {
                // 正面
                triangles.AddRange(SquareToTriangles(currrentLowerLeft, currrentLowerRight, currrentUpperLeft, currrentUpperRight));
            }
            else if (i == squares.Length - 1)
            {
                // 背面
                triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
            }
            else if(i > 2)
            {
                int lastLowerLeft = ((i - 2) * 4) + 0;
                int lastLowerRight = ((i - 2) * 4) + 1;
                int lastUpperLeft = ((i - 2) * 4) + 2;
                int lastUpperRight = ((i - 2) * 4) + 3;

                if (i % 2 == 1)
                {
                    // 左側面
                    triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
                    // 右側面
                    triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
                }
                else
                {
                    // 上側面
                    triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
                    // 下側面
                    triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));
                }

                
            }
        }

        mesh.SetVertices(vertices);
        mesh.SetTriangles(triangles, 0);
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();

        return mesh;
    }
```

### 全体のスクリプト
使用する時のは以下を参考にしてください。
![スクリーンショット_20230225_231928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2689374/45de249b-b124-3572-92ac-d668a3ddd54b.png)

``` c#:DrawGenerateMesh.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DrawGenerateMesh : MonoBehaviour
{
    class Square
    {
        public Vector3 lowerLeft;
        public Vector3 upperLeft;
        public Vector3 lowerRight;
        public Vector3 upperRight;
    }

    [SerializeField] Material Material;
    [SerializeField] float Width = 1f;
    [SerializeField] float Height = 2f;
    [SerializeField] LayerMask Mask;
    [SerializeField] float DistanceInterval = 0.3f;
    [SerializeField] Transform[] PathPosition;
    [SerializeField] bool IsDrawGizmo = false;

    GameObject currentObject;

    bool isDrawing = false;
    List<Vector3> pathPositions = new List<Vector3>();

    // Start is called before the first frame update
    void Start()
    {
    }

    private void Update()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, Mask))
        {
            if (Input.GetMouseButtonDown(0) && !isDrawing)
            {
                isDrawing = true;

                CreateMeshObject();
                pathPositions.Clear();
                pathPositions.Add(hit.point);
            }
            else if (Input.GetMouseButton(0) && isDrawing)
            {
                var lastPos = pathPositions[pathPositions.Count - 1];
                var currentPos = hit.point;

                if (Vector3.Distance(lastPos, currentPos) >= DistanceInterval)
                {
                    pathPositions.Add(hit.point);
                    UpdateMeshObject();
                }
            }
        }
        else
        {
            DrawEnd();
        }

        if (Input.GetMouseButtonUp(0))
        {
            DrawEnd();
        }
    }

    private void CreateMeshObject()
    {
        currentObject = new GameObject();
        currentObject.transform.parent = transform;
        currentObject.AddComponent<MeshFilter>();
        currentObject.AddComponent<MeshCollider>();

        var meshRen = currentObject.AddComponent<MeshRenderer>();
        meshRen.material = Material;
    }

    private void UpdateMeshObject()
    {
        var mesh = GenerateMesh24();
        currentObject.GetComponent<MeshFilter>().mesh = mesh;
        currentObject.GetComponent<MeshCollider>().sharedMesh = mesh;
    }

    private void DrawEnd()
    {
        if (isDrawing)
        {
            isDrawing = false;

            if(pathPositions.Count < 2)
            {
                Destroy(currentObject);
            }
        }
    }


    private Square[] PathSquares()
    {
        int length = pathPositions.Count;
        var positions = pathPositions;
        var squares = new Square[length];

        for (int i = 0; i < length; i++)
        {
            Vector3 firstPos, secondPos;

            if (i == 0)
            {
                firstPos = positions[0];
                secondPos = positions[1];
            }
            else
            {
                firstPos = positions[i - 1];
                secondPos = positions[i];
            }

            Vector3 direction = (secondPos - firstPos).normalized;

            Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
            Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;

            var square = new Square()
            {
                lowerLeft = lowerLeft,
                lowerRight = lowerRight,
                upperLeft = lowerLeft + Vector3.back * Height,
                upperRight = lowerRight + Vector3.back * Height,
            };

            squares[i] = square;
        }

        return squares;
    }

    private Mesh GenerateMesh()
    {
        var squares = PathSquares();
        var mesh = new Mesh();

        var vertices = new List<Vector3>();
        var triangles = new List<int>();
        var uvs = new List<Vector2>();

        for (int i = 0; i < squares.Length; i++)
        {
            vertices.Add(squares[i].lowerLeft);
            vertices.Add(squares[i].lowerRight);
            vertices.Add(squares[i].upperLeft);
            vertices.Add(squares[i].upperRight);

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

            int currrentLowerLeft = (i * 4) + 0;
            int currrentLowerRight = (i * 4) + 1;
            int currrentUpperLeft = (i * 4) + 2;
            int currrentUpperRight = (i * 4) + 3;

            if (i == 0)
            {
                // 正面
                triangles.AddRange(SquareToTriangles(currrentLowerLeft, currrentLowerRight, currrentUpperLeft, currrentUpperRight));
            }
            else
            {
                int lastLowerLeft = ((i - 1) * 4) + 0;
                int lastLowerRight = ((i - 1) * 4) + 1;
                int lastUpperLeft = ((i - 1) * 4) + 2;
                int lastUpperRight = ((i - 1) * 4) + 3;

                // 左側面
                triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
                // 右側面
                triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
                // 上側面
                triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
                // 下側面
                triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));

                if (i == squares.Length - 1)
                {
                    // 背面
                    triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
                }
            }
        }

        mesh.SetVertices(vertices);
        mesh.SetTriangles(triangles, 0);
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();

        return mesh;
    }

    private Square[] PathSquares24()
    {
        int length = pathPositions.Count;
        var positions = pathPositions;
        var squares = new Square[length * 2 + 2];

        for (int i = 0; i < length; i++)
        {
            Vector3 firstPos, secondPos;

            if (i == 0)
            {
                firstPos = positions[0];
                secondPos = positions[1];
            }
            else
            {
                firstPos = positions[i - 1];
                secondPos = positions[i];
            }

            Vector3 direction = (secondPos - firstPos).normalized;
            Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
            Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;

            var square = new Square()
            {
                lowerLeft = lowerLeft,
                lowerRight = lowerRight,
                upperLeft = lowerLeft + Vector3.back * Height,
                upperRight = lowerRight + Vector3.back * Height,
            };

            if (i == 0)
            {
                squares[0] = square;
            }
            else if (i == length - 1)
            {
                squares[squares.Length - 1] = square;
            }

            for (int j = 0; j < 2; j++)
            {
                squares[i * 2 + 1 + j] = square;
            }
        }

        return squares;
    }

    private Mesh GenerateMesh24()
    {
        var squares = PathSquares24();
        var mesh = new Mesh();

        var vertices = new List<Vector3>();
        var triangles = new List<int>();
        var uvs = new List<Vector2>();

        for (int i = 0; i < squares.Length; i++)
        {
            vertices.Add(squares[i].lowerLeft);
            vertices.Add(squares[i].lowerRight);
            vertices.Add(squares[i].upperLeft);
            vertices.Add(squares[i].upperRight);

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

            int currrentLowerLeft = (i * 4) + 0;
            int currrentLowerRight = (i * 4) + 1;
            int currrentUpperLeft = (i * 4) + 2;
            int currrentUpperRight = (i * 4) + 3;

            if (i == 0)
            {
                // 正面
                triangles.AddRange(SquareToTriangles(currrentLowerLeft, currrentLowerRight, currrentUpperLeft, currrentUpperRight));
            }
            else if (i == squares.Length - 1)
            {
                // 背面
                triangles.AddRange(SquareToTriangles(currrentLowerRight, currrentLowerLeft, currrentUpperRight, currrentUpperLeft));
            }
            else if(i > 2)
            {
                int lastLowerLeft = ((i - 2) * 4) + 0;
                int lastLowerRight = ((i - 2) * 4) + 1;
                int lastUpperLeft = ((i - 2) * 4) + 2;
                int lastUpperRight = ((i - 2) * 4) + 3;

                if (i % 2 == 1)
                {
                    // 左側面
                    triangles.AddRange(SquareToTriangles(currrentLowerLeft, lastLowerLeft, currrentUpperLeft, lastUpperLeft));
                    // 右側面
                    triangles.AddRange(SquareToTriangles(lastLowerRight, currrentLowerRight, lastUpperRight, currrentUpperRight));
                }
                else
                {
                    // 上側面
                    triangles.AddRange(SquareToTriangles(lastUpperLeft, lastUpperRight, currrentUpperLeft, currrentUpperRight));
                    // 下側面
                    triangles.AddRange(SquareToTriangles(lastLowerRight, lastLowerLeft, currrentLowerRight, currrentLowerLeft));
                }

                
            }
        }

        mesh.SetVertices(vertices);
        mesh.SetTriangles(triangles, 0);
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();

        return mesh;
    }

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

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

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

        return triangles;
    }

    private Square[] PathSquaresGizmo()
    {
        int length = PathPosition.Length;
        var positions = new Vector3[length];
        var squares = new Square[length];

        for (int i = 0; i < length; i++)
        {
            positions[i] = PathPosition[i].position;
        }

        for (int i = 0; i < length; i++)
        {
            Vector3 firstPos, secondPos;

            if (i == 0)
            {
                firstPos = positions[0];
                secondPos = positions[1];
            }
            else
            {
                firstPos = positions[i - 1];
                secondPos = positions[i];
            }

            Vector3 direction = (secondPos - firstPos).normalized;
            Vector3 lowerLeft = positions[i] + new Vector3(-direction.y, direction.x, 0f) * Width;
            Vector3 lowerRight = positions[i] + new Vector3(direction.y, -direction.x, 0f) * Width;

            var square = new Square()
            {
                lowerLeft = lowerLeft,
                lowerRight = lowerRight,
                upperLeft = lowerLeft + Vector3.back * Height,
                upperRight = lowerRight + Vector3.back * Height,
            };

            squares[i] = square;
        }

        return squares;
    }

    private void OnDrawGizmos()
    {
        if ((PathPosition == null) || (PathPosition.Length < 2) || !IsDrawGizmo)
        {
            return;
        }

        var squares = PathSquaresGizmo();

        for (int i = 0; i < squares.Length; i++)
        {
            Gizmos.DrawSphere(squares[i].lowerLeft, 0.1f);
            Gizmos.DrawSphere(squares[i].lowerRight, 0.1f);
            Gizmos.DrawSphere(squares[i].upperLeft, 0.1f);
            Gizmos.DrawSphere(squares[i].upperRight, 0.1f);
        }
    }
}

```