본문 바로가기
개발/Unity

유니티 - 절차적 메시와 삼각분할로 다각형 만들기 (Polygon Triangulation with Procedural Mesh)

by 피로물든딸기 2022. 11. 1.
반응형

Unity 전체 링크

 

임의의 오목 다각형을 삼각분할하면 아래와 같은 결과를 얻을 수 있다.

 

주어지는 좌표와 삼각분할을 이용하여 절차적 메시로 다각형을 만들어보자.


PolygonTriangulation.cs를 수정한다.

LineRenderer의 triangles는 lineForTriangles이름을 변경하고 createProceduralMesh 함수를 가져온다.

여기서는 vertices와 triangles를 List로 관리하는 것이 편하므로 create에서 ToArray()를 이용하였다.

    Mesh mesh;
    List<Vector3> vertices = new List<Vector3>();
    List<int> triangles = new List<int>();

    void createProceduralMesh()
    {
        mesh.Clear();
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();

        Destroy(this.GetComponent<MeshCollider>());
        this.gameObject.AddComponent<MeshCollider>();
    }

 

Start에서는 mesh를 할당한다.

    void Start()
    {
        mesh = GetComponent<MeshFilter>().mesh;

        ...
    }

 

triangluation에서 vertices와 triangles를 초기화한다.

그리고 makeTriangle 아래에서 vertices와 triangle에 값을 추가하면 된다.

    void triangluation(int count)
    {
        vertices.Clear();
        triangles.Clear();

        dotList.Clear();
        ...
        
        for (int i = 0; i < count; i++)
        {
            List<Vector3> copy = new List<Vector3>(dotList);

            for (int k = 0; k < copy.Count - 2; k++)
            {
                bool ccw = (CCWby2D(copy[k], copy[k + 1], copy[k + 2]) > 0);
                bool cross = CrossCheckAll(copy, k);

                if (ccw == true && cross == false)
                {
                    /* triangle[0]은 부모의 LineRenderer */
                    makeTriangle(lineForTriangles[i + 1], copy[k], copy[k + 1], copy[k + 2]);

                    vertices.Add(copy[k]);
                    vertices.Add(copy[k + 1]);
                    vertices.Add(copy[k + 2]);

                    triangles.Add(vertices.Count - 3);
                    triangles.Add(vertices.Count - 2);
                    triangles.Add(vertices.Count - 1);

                    copy.RemoveAt(k + 1);
                    dotList = new List<Vector3>(copy);

                    break;
                }
            }
        }
    }

 

OnValidate에서는 triangulation 다음에 createProceduralMesh를 호출하면 된다.

    void OnValidate()
    {
        if(numOfTriangle > 0)
        {
            triangluation(numOfTriangle);
            createProceduralMesh();
        }
    }

 

마지막으로 Mesh Filter와 Renderer를 추가하고 Default-Material로 설정하자.

 

게임을 실행해서 NumOfTriangle을 변경해보자.

 

라인 렌더러를 off하면 아래와 같이 메시만 볼 수 있다.

 

Mesh Renderer를 off하면 콜라이더를 볼 수 있다.

 

여기까지 코드는 다음과 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class PolygonTriangulation : MonoBehaviour
{
    public int numOfTriangle; 

    public GameObject[] goArray;
    List<Vector3> dotList = new List<Vector3>();
    LineRenderer lr;

    LineRenderer[] lineForTriangles;

    Mesh mesh;
    List<Vector3> vertices = new List<Vector3>();
    List<int> triangles = new List<int>();

    void createProceduralMesh()
    {
        mesh.Clear();
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();
    }

    float CCWby2D(Vector3 a, Vector3 b, Vector3 c)
    {
        Vector3 p = b - a;
        Vector3 q = c - b;

        return Vector3.Cross(p, q).y;
    }

    void makeTriangle(LineRenderer lr, Vector3 a, Vector3 b, Vector3 c)
    {
        lr.startWidth = lr.endWidth = 1.0f;
        lr.material.color = Color.red;

        lr.positionCount = 3;

        lr.SetPosition(0, a);
        lr.SetPosition(1, b);
        lr.SetPosition(2, c);

        lr.loop = true;
    }

    float getAreaOfTriangle(Vector3 dot1, Vector3 dot2, Vector3 dot3)
    {
        Vector3 a = dot2 - dot1;
        Vector3 b = dot3 - dot1;
        Vector3 cross = Vector3.Cross(a, b);

        return cross.magnitude / 2.0f;
    }

    bool checkTriangleInPoint(Vector3 dot1, Vector3 dot2, Vector3 dot3, Vector3 checkPoint)
    {
        float area = getAreaOfTriangle(dot1, dot2, dot3);
        float dot12 = getAreaOfTriangle(dot1, dot2, checkPoint);
        float dot23 = getAreaOfTriangle(dot2, dot3, checkPoint);
        float dot31 = getAreaOfTriangle(dot3, dot1, checkPoint);

        return (dot12 + dot23 + dot31) <= area + 0.1f /* 오차 허용 */;
    }

    bool CrossCheckAll(List<Vector3> list, int index)
    {
        Vector3 a = list[index];
        Vector3 b = list[index + 1];
        Vector3 c = list[index + 2];

        for (int i = index + 3; i < list.Count; i++)
        {
            if (checkTriangleInPoint(a, b, c, list[i]) == true) return true;
        }

        return false;
    }

    void triangluation(int count)
    {
        vertices.Clear();
        triangles.Clear();

        dotList.Clear();
        foreach (GameObject go in goArray) // init
            dotList.Add(go.transform.position);

        lineForTriangles = this.GetComponentsInChildren<LineRenderer>();

        for (int i = 1; i < lineForTriangles.Length; i++) // init
            lineForTriangles[i].positionCount = 0;

        //int numOfTriangle = dotList.Count - 2;
        if (count > dotList.Count - 2) count = dotList.Count - 2;
        for (int i = 0; i < count; i++)
        {
            List<Vector3> copy = new List<Vector3>(dotList);

            for (int k = 0; k < copy.Count - 2; k++)
            {
                bool ccw = (CCWby2D(copy[k], copy[k + 1], copy[k + 2]) > 0);
                bool cross = CrossCheckAll(copy, k);

                if (ccw == true && cross == false)
                {
                    /* triangle[0]은 부모의 LineRenderer */
                    makeTriangle(lineForTriangles[i + 1], copy[k], copy[k + 1], copy[k + 2]);

                    vertices.Add(copy[k]);
                    vertices.Add(copy[k + 1]);
                    vertices.Add(copy[k + 2]);

                    triangles.Add(vertices.Count - 3);
                    triangles.Add(vertices.Count - 2);
                    triangles.Add(vertices.Count - 1);

                    copy.RemoveAt(k + 1);
                    dotList = new List<Vector3>(copy);

                    break;
                }
            }
        }
    }

    void OnValidate()
    {
        if(numOfTriangle > 0)
        {
            triangluation(numOfTriangle);
            createProceduralMesh();
        }
    }

    void Start()
    {
        mesh = GetComponent<MeshFilter>().mesh;

        foreach (GameObject go in goArray)
            dotList.Add(go.transform.position);

        lr = this.GetComponent<LineRenderer>();
        lr.startWidth = lr.endWidth = 1.0f;
        lr.material.color = Color.blue;

        lr.positionCount = dotList.Count;

        for (int i = 0; i < dotList.Count; i++) 
            lr.SetPosition(i, dotList[i]);

        lr.loop = true; 
    }
}

 

위의 실행결과는 아래의 unitypackage에서 확인 가능하다. (정점이 보완된 코드)

PolygonTriangulationWithProceduralMesh.unitypackage
0.01MB


코드 보완

 

쿼드는 삼각형 2개가 아닌 정점 4개로 이루어져 있다.

위의 코드는 중복되는 정점이 생기기 때문에, 존재하는 정점의 경우에는 추가하지 않는다.

따라서 딕셔너리를 이용해 정점의 존재 여부를 체크하고, index를 기억해둔다.

Dictionary<Vector3, int> dic = new Dictionary<Vector3, int>();

 

삼각형을 만드는 곳에서 좌표가 딕셔너리에 존재하지 않으면 추가한다.

딕셔너리에 index를 저장하기 때문에 triangles에 추가할 때 편리하다.

    if (ccw == true && cross == false)
    {
        /* triangle[0]은 부모의 LineRenderer */
        makeTriangle(lineForTriangles[i + 1], copy[k], copy[k + 1], copy[k + 2]);

        for(int c = 0; c < 3; c++)
        {
            if (dic.ContainsKey(copy[k + c])) continue;

            dic[copy[k + c]] = vertices.Count;                     
            vertices.Add(copy[k + c]);
        }

        for(int c = 0; c < 3; c++)
            triangles.Add(dic[copy[k + c]]);

        copy.RemoveAt(k + 1);
        dotList = new List<Vector3>(copy);

        break;
    }

 

최종 보완된 코드는 다음과 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class PolygonTriangulation : MonoBehaviour
{
    public int numOfTriangle; 

    public GameObject[] goArray;
    List<Vector3> dotList = new List<Vector3>();
    LineRenderer lr;

    LineRenderer[] lineForTriangles;

    Mesh mesh;
    List<Vector3> vertices = new List<Vector3>();
    List<int> triangles = new List<int>();
    Dictionary<Vector3, int> dic = new Dictionary<Vector3, int>();

    void createProceduralMesh()
    {
        mesh.Clear();
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();

        Destroy(this.GetComponent<MeshCollider>());
        this.gameObject.AddComponent<MeshCollider>();
    }

    float CCWby2D(Vector3 a, Vector3 b, Vector3 c)
    {
        Vector3 p = b - a;
        Vector3 q = c - b;

        return Vector3.Cross(p, q).y;
    }

    void makeTriangle(LineRenderer lr, Vector3 a, Vector3 b, Vector3 c)
    {
        lr.startWidth = lr.endWidth = 1.0f;
        lr.material.color = Color.red;

        lr.positionCount = 3;

        lr.SetPosition(0, a);
        lr.SetPosition(1, b);
        lr.SetPosition(2, c);

        lr.loop = true;
    }

    float getAreaOfTriangle(Vector3 dot1, Vector3 dot2, Vector3 dot3)
    {
        Vector3 a = dot2 - dot1;
        Vector3 b = dot3 - dot1;
        Vector3 cross = Vector3.Cross(a, b);

        return cross.magnitude / 2.0f;
    }

    bool checkTriangleInPoint(Vector3 dot1, Vector3 dot2, Vector3 dot3, Vector3 checkPoint)
    {
        float area = getAreaOfTriangle(dot1, dot2, dot3);
        float dot12 = getAreaOfTriangle(dot1, dot2, checkPoint);
        float dot23 = getAreaOfTriangle(dot2, dot3, checkPoint);
        float dot31 = getAreaOfTriangle(dot3, dot1, checkPoint);

        return (dot12 + dot23 + dot31) <= area + 0.1f /* 오차 허용 */;
    }

    bool CrossCheckAll(List<Vector3> list, int index)
    {
        Vector3 a = list[index];
        Vector3 b = list[index + 1];
        Vector3 c = list[index + 2];

        for (int i = index + 3; i < list.Count; i++)
        {
            if (checkTriangleInPoint(a, b, c, list[i]) == true) return true;
        }

        return false;
    }

    void triangluation(int count)
    {
        vertices.Clear();
        triangles.Clear();
        dic.Clear();

        dotList.Clear();
        foreach (GameObject go in goArray) // init
            dotList.Add(go.transform.position);

        lineForTriangles = this.GetComponentsInChildren<LineRenderer>();

        for (int i = 1; i < lineForTriangles.Length; i++) // init
            lineForTriangles[i].positionCount = 0;

        //int numOfTriangle = dotList.Count - 2;
        if (count > dotList.Count - 2) count = dotList.Count - 2;
        for (int i = 0; i < count; i++)
        {
            List<Vector3> copy = new List<Vector3>(dotList);

            for (int k = 0; k < copy.Count - 2; k++)
            {
                bool ccw = (CCWby2D(copy[k], copy[k + 1], copy[k + 2]) > 0);
                bool cross = CrossCheckAll(copy, k);

                if (ccw == true && cross == false)
                {
                    /* triangle[0]은 부모의 LineRenderer */
                    makeTriangle(lineForTriangles[i + 1], copy[k], copy[k + 1], copy[k + 2]);

                    for(int c = 0; c < 3; c++)
                    {
                        if (dic.ContainsKey(copy[k + c])) continue;

                        dic[copy[k + c]] = vertices.Count;                     
                        vertices.Add(copy[k + c]);
                    }

                    for(int c = 0; c < 3; c++)
                        triangles.Add(dic[copy[k + c]]);

                    copy.RemoveAt(k + 1);
                    dotList = new List<Vector3>(copy);

                    break;
                }
            }
        }
    }

    void OnValidate()
    {
        if(numOfTriangle > 0)
        {
            triangluation(numOfTriangle);
            createProceduralMesh();
        }
    }

    void Start()
    {
        mesh = GetComponent<MeshFilter>().mesh;

        foreach (GameObject go in goArray)
            dotList.Add(go.transform.position);

        lr = this.GetComponent<LineRenderer>();
        lr.startWidth = lr.endWidth = 1.0f;
        lr.material.color = Color.blue;

        lr.positionCount = dotList.Count;

        for (int i = 0; i < dotList.Count; i++) 
            lr.SetPosition(i, dotList[i]);

        lr.loop = true; 
    }
}

 

Unity Plus:

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Unity Pro:

 

Unity Pro

The complete solutions for professionals to create and operate.

unity.com

 

Unity 프리미엄 학습:

 

Unity Learn

Advance your Unity skills with live sessions and over 750 hours of on-demand learning content designed for creators at every skill level.

unity.com

반응형

댓글