본문 바로가기
개발/Unity

유니티 - 절차적 메시와 삼각분할로 여러 구멍들이 있는 다각형 만들기 (Polygon Triangulation with Holes using Procedural Mesh)

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

Unity 전체 링크

 

총알을 뚫거나, 창문을 만들거나 할 때 Mesh에 구멍을 뚫고 싶을 때가 있다.

절차적 메시로 구멍이 뚫린 다각형을 만들어보자.

 

구멍이 뚫린 사각형의 삼각분할은 아래의 링크를 참고하자.

- 구멍이 있는 다각형의 삼각분할

- 여러 구멍들이 있는 다각형의 삼각분할


다각형의 시계방향 좌표와 구멍의 반시계방향 좌표가 있다면 절차적 메시로도 구멍이 뚫린 메시를 만들 수 있다.

(또는 다각형이 시계방향이고 구멍이 시계방향 좌표인 경우)

 

위 링크의 최종 코드 PolygonTriangulationWithHoles.cs에서 아래의 코드를 추가하면 메시를 만들 수 있다.

절차적 메시와 삼각분할로 다각형 만들기에서 메시를 만들 때 사용한 코드를 그대로 구멍이 있는 경우에 옮겼었다.

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

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

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

 

순서대로 메시를 만들면 아래와 같이 삼각분할로 메시가 생성된다.

 

라인 렌더러를 끄면 만들어진 메시를 볼 수 있다.

 

최종 생성된 메시는 아래와 같다.

 

렌더러를 제거하고 콜라이더만 남겼을 때의 모습은 아래와 같다.

 

최종 코드는 다음과 같다.

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

[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class PolygonTriangulationWithHoles : MonoBehaviour
{
    public GameObject polygon;
    public GameObject[] holes;

    LineRenderer pLine;
    LineRenderer[] hLines;

    List<Vector3> polygonPos = new List<Vector3>(); /* 시계 방향 */
    List<List<Vector3>> holePoses = new List<List<Vector3>>(); /* 반시계 방향 */

    List<Vector3> mergePolygon = new List<Vector3>();
    List<Vector3> mergePolygons = new List<Vector3>();

    Dictionary<Vector3, bool> dupCheck = new Dictionary<Vector3, bool>();

    #region Polygon Triangulation
    public int numOfTriangle;
    List<Vector3> dotList = new List<Vector3>();

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

    LineRenderer[] lineForTriangles;

    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 = 0.2f;
        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)
    {
        if (dot1 == checkPoint) return false;
        if (dot2 == checkPoint) return false;
        if (dot3 == checkPoint) return false;

        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 (Vector3 v in mergePolygons) // init
            dotList.Add(v);

        lineForTriangles = this.GetComponentsInChildren<LineRenderer>();

        for (int i = 0; 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)
                {
                    makeTriangle(lineForTriangles[i], 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;
                }
            }
        }
    }
    #endregion
    void initLineRenderer(LineRenderer lr, Color color, float length = 0.1f)
    {
        lr.startWidth = lr.endWidth = length;
        lr.material.color = color;
        lr.loop = true;
    }

    void setLineRenderer(LineRenderer lr, List<Vector3> pos)
    {
        lr.positionCount = pos.Count;

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

    Vector3[] getShortestDots(List<Vector3> a, List<Vector3> b)
    {
        float minValue = float.MaxValue;
        Vector3 dot1, dot2;

        dot1 = dot2 = Vector3.zero;
        foreach (Vector3 v1 in a)
        {
            foreach (Vector3 v2 in b)
            {
                if (dupCheck.ContainsKey(v1) == true
                    || dupCheck.ContainsKey(v2) == true) continue;

                float distance = Vector3.Distance(v1, v2);
                if (distance < minValue)
                {
                    minValue = distance;
                    dot1 = v1;
                    dot2 = v2;
                }
            }
        }

        return new Vector3[] { dot1, dot2 };
    }

    List<Vector3> sortListbyStartPoint(List<Vector3> list, Vector3 startPos)
    {
        List<Vector3> sort = new List<Vector3>();
        List<Vector3> doubleList = new List<Vector3>();
        int count = list.Count;

        foreach (Vector3 v in list) doubleList.Add(v);
        foreach (Vector3 v in list) doubleList.Add(v);

        int start;
        for (start = 0; start < list.Count; start++)
            if (list[start] == startPos) break;

        for (int i = start; i < start + count; i++) sort.Add(doubleList[i]);

        return sort;
    }

    List<Vector3> makePolygonWithHole(List<Vector3> polygonPos, List<Vector3> holePos, Vector3[] shortestDots)
    {
        List<Vector3> sortPolygon = sortListbyStartPoint(polygonPos, shortestDots[0]);
        List<Vector3> sortHole = sortListbyStartPoint(holePos, shortestDots[1]);
        List<Vector3> merge = new List<Vector3>();

        foreach (Vector3 v in sortPolygon) merge.Add(v);
        merge.Add(sortPolygon[0]);

        foreach (Vector3 v in sortHole) merge.Add(v);
        merge.Add(sortHole[0]);

        return merge;
    }

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

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

        pLine = polygon.GetComponent<LineRenderer>();

        hLines = new LineRenderer[holes.Length];
        for(int i = 0; i < holes.Length; i++)
            hLines[i] = holes[i].GetComponent<LineRenderer>();

        initLineRenderer(pLine, Color.blue);
        foreach (Transform tr in polygon.transform)
        {
            Vector3 v = tr.transform.position;
            polygonPos.Add(v);
        }

        setLineRenderer(pLine, polygonPos);

        /* ---------------------- */

        for (int i = 0; i < holes.Length; i++)
            initLineRenderer(hLines[i], Color.cyan);

        int index = 0;
        foreach (GameObject hole in holes)
        {
            List<Vector3> tmp = new List<Vector3>();
            foreach (Transform tr in hole.transform)
            {
                Vector3 v = tr.transform.position;
                tmp.Add(v);
            }

            holePoses.Add(tmp);
            setLineRenderer(hLines[index], holePoses[index]);
            index++;
        }

        mergePolygons = polygonPos;
        for (int i = 0; i < holes.Length; i++)
        {
            Vector3[] shortestDots = getShortestDots(mergePolygons, holePoses[i]);

            dupCheck[shortestDots[0]] = true;
            dupCheck[shortestDots[1]] = true;

            mergePolygon = makePolygonWithHole(mergePolygons, holePoses[i], shortestDots);
            mergePolygons = mergePolygon;

            //{ // 가장 짧은 점 표시하기
            //    GameObject temp;
            //    temp = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            //    temp.transform.position = shortestDots[0];
            //    temp.transform.localScale = new Vector3(1.1f, 1.1f, 1.1f);

            //    temp = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            //    temp.transform.position = shortestDots[1];
            //    temp.transform.localScale = new Vector3(1.1f, 1.1f, 1.1f);
            //}
        }

        //foreach (Vector3 m in mergePolygons)
        //{
        //    GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
        //    go.transform.localScale = new Vector3(1.5f, 1.5f, 1.5f);
        //    go.transform.position = m;
        //}

        //LineRenderer mergeLine = this.GetComponent<LineRenderer>();
        //initLineRenderer(mergeLine, Color.green, 0.5f);
        //setLineRenderer(mergeLine, mergePolygons);
    }
}

 

위의 실행결과는 아래의 unitypackage에서 확인 가능하다.

PolygonTriangulationWithHoles.unitypackage
0.03MB

 

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

반응형

댓글