LoginSignup
2
0

More than 1 year has passed since last update.

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

Posted at

無題の動画 ‐ Clipchampで作成 (2).gif

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

マウスイベントからパスを生成する

Input.GetMouseButtonでマウスのクリックやホールド状態を取得します。
Input.mousePositionで取得したマウスの座標をCamera.main.ScreenPointToRayRayに変換し、ワールド座標を取得します。
Input.GetMouseButtonDown(0)を使うときの注意としてInput.GetMouseButtonUp(0)のイベントが来る前にInput.GetMouseButtonDown(0)が2度続くことがあったのでbool型のフラグ管理をしてDownイベントを2重に通さない工夫をしておくと安全かもしれません。
パス用の座標を格納するときはVector3.Distanceを使って現在のマウスポイントと1つ前の位置からの移動距離で新しい座標を格納するよにしました。

    [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
図のようにBox Colliderの配置とLayerMaskを指定してドローイングの範囲とメッシュの生成位置をコントロールすると楽です。
Physics.RaycastでヒットしたRaycastHit.pointBox Colliderの表面を取得出来るので位置調整をTransformをいじるだけで調整出来ます。

座標の向きの反転問題

スクリーンショット_20230225_215039.png
以下のコードのようにQuaternion.LookRotationを使ってパスに沿った座標を計算していましたが、所々で座標が反転してしまいました。

            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
Gizmoで可視化してみるとどうやら一つ前のX座標動かしているX座標の一致したときにおかしくなっていることが分かりました。
試行錯誤したところQuaternion.LookRotationを使ったやり方では解決方法が見いだせなかったので別の方法をとります。
(X座標が一致した場合は1つ前と同じ回転のオフセットをかけることで解決したかもしれません)

座標の向きの求め方を変更

            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
頂点2,3のようにY座標が一致した場合は向きが(0,1)になり、direction.yがX方向にオフセットされます。
頂点3,4ではX座標が一致し、向きが(1,0)となり、direction.xがY方向にオフセットされます。

メッシュの影がおかしい

スクリーンショット_20230225_230111.png
頂点の問題は解決しましたが影の表示がおかしくて丸みを帯びたチューブ状のよに見えます。
実際はキューブ状なので平になってるはずですが、影の付き方によっておかしく見えてしまっています。
今回作ったメッシュはキューブでいうと頂点が8で構成されています。
Unityのテンプレキューブの頂点を確認すると頂点が24あります。
つまり、単純に頂点の数が足りないため法線ベクトルの計算がうまく出来ていないことが分かりました。
8点で結ぶと頂点を使いまわしますが、そうすると1面の法線の向きは正しくても使いまわした法線の向きはかしな方向に向いていることになります。

頂点を増やして法線の向きを整える

スクリーンショット_20230225_231817.png
やることは単純で頂点の数を倍にしてトライアングルを結ぶときに同じ頂点を使わないようにするだけです。

    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

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);
        }
    }
}

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0