본문 바로가기
[Unity]

[Unity] Mesh를 생성하여 도형 만들기

by 김기승 2023. 12. 18.

3D 게임을 만들기 위해서는 다양한 모델링을 활용한다.

이 모델링들은 Mesh 데이터를 통해 형상화하게 되는데,

유니티에서는 Mesh 클래스를 생성해서 원하는 도형을 만들 수 있다.

 

이번 글에서는 Mesh 클래스의 사용법을 이해하고,

스크립트를 작성해서 간단한 도형을 만들어보겠다.


원리

이미  많은 블로그에서 다룬 내용이기도 해서 간단히 설명하겠다.

 

2차원 공간에서 사각형 모델을 만든다고 가정해보자.

일단, 네 개의 정점(Vertice)을 지정해야 한다.

 

시계 방향으로 왼쪽 아래부터 오른쪽 아래까지 돌면서 정점 배열을 생성했다고 하자.

그러면, { (-1,-1), (-1,1), (1,1), (1,-1) } 순서로 구성이 된다.

 

그 다음으로 이 정점들을 이용해서 면을 만들어내야 한다.

면은 기본적으로 삼각형(Triangle)의 연속으로 만들어진다.

사각형은 두 개의 삼각형으로 만들 수 있을 것이다.

 

두 개의 삼각형이기에 6개의 정점으로 배열을 만들어야한다.

아까 만든 배열의 인덱스를 기준으로 생성하면,

{0, 1, 2,  2, 3, 0} 처럼 된다.

 

이때, 주의할 점은 시계방향으로 지정해줘야 한다는 것이다.

만약, {0, 3, 2,  2, 1, 0}일 경우에는 면, 노멀(Normal)이 뒤집어진다.

 

마지막으로, 사각형에 아래의 사과 그림을 입혀보자.

 

텍스쳐의 좌표계를 UV라고 하는데, 가로(U) 세로(V) 모두 0~1 사이의 값이다.

(0, 0)이면 텍스쳐의 왼쪽 아래고, (1, 1)이면 텍스쳐의 오른쪽 위다.

이는 텍스쳐의 비율이나 해상도에 따라 달라지지 않는다.

각 정점에 UV 좌표를 지정하면, 정점 사이를 보간하여 면에 텍스쳐를 입히게 된다.

 

왼쪽 아래부터 시계방향으로 돌면서 정점 배열을 생성했으므로,

{ (0, 0), (0, 1), (1, 1), (1, 0) } 순서로 UV 배열을 생성할 수 있다.

 

정점 배열, 삼각형 배열, UV 배열을 생성하는 원리만 이해한다면,

위의 삼각형 뿐만 아니라 어떠한 도형이든 만들 수 있다.


Square

원리에서 사용한 방식을 그대로 적용한 스크립트이다.

사각형을 만들어낸다.

using UnityEngine;

public class MeshSquare : MonoBehaviour
{
    public Texture2D texture;
    public string shaderName = "Standard";

    private void Awake()
    {
        MeshFilter filter = gameObject.AddComponent<MeshFilter>();
        MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
        renderer.material = new Material(Shader.Find(shaderName));
        renderer.material.mainTexture = texture;

        Mesh mesh = new();

        /** Vertices **/
        Vector3[] vertices = new Vector3[]
        {
            new Vector3(-1f, -1f, 0f),
            new Vector3(-1f, 1f, 0f),
            new Vector3(1f, 1f, 0f),
            new Vector3(1f, -1f, 0f)
        };

        /** UV **/
        Vector2[] uv = new Vector2[]
        {
            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f)
        };

        /** Triangles **/
        int[] triangles = new int[]
        {
            0, 1, 2,
            2, 3, 0
        };

        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();

        filter.mesh = mesh;
    }
}

초기에 MeshFilterMeshRenderer를 추가하여 Mesh, Material, Texture를 적용한다.

 

▼ 결과.gif


Cube

정육면체를 만드는 스크립트이다.

사각형을 6번 생성했다고 보면 된다.

using UnityEngine;

public class MeshCube : MonoBehaviour
{
    public Texture2D texture;
    public string shaderName = "Standard";

    private void Awake()
    {
        MeshFilter filter = gameObject.AddComponent<MeshFilter>();
        MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
        renderer.material = new Material(Shader.Find(shaderName));
        renderer.material.mainTexture = texture;

        Mesh mesh = new();

        /** Vertices **/
        Vector3[] vertices = new Vector3[]
        {
            new Vector3(-1f, -1f, -1f),
            new Vector3(-1f, 1f, -1f),
            new Vector3(1f, 1f, -1f),
            new Vector3(1f, -1f, -1f),

            new Vector3(1f, -1f, 1f),
            new Vector3(1f, 1f, 1f),
            new Vector3(-1f, 1f, 1f),
            new Vector3(-1f, -1f, 1f),

            new Vector3(-1f, -1f, 1f),
            new Vector3(-1f, -1f, -1f),
            new Vector3(1f, -1f, -1f),
            new Vector3(1f, -1f, 1f),

            new Vector3(-1f, 1f, -1f),
            new Vector3(-1f, 1f, 1f),
            new Vector3(1f, 1f, 1f),
            new Vector3(1f, 1f, -1f),

            new Vector3(-1f, -1f, 1f),
            new Vector3(-1f, 1f, 1f),
            new Vector3(-1f, 1f, -1f),
            new Vector3(-1f, -1f, -1f),

            new Vector3(1f, -1f, -1f),
            new Vector3(1f, 1f, -1f),
            new Vector3(1f, 1f, 1f),
            new Vector3(1f, -1f, 1f),
        };

        /** UV **/
        Vector2[] uv = new Vector2[]
        {
            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f),

            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f),

            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f),

            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f),

            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f),

            new Vector2(0f, 0f),
            new Vector2(0f, 1f),
            new Vector2(1f, 1f),
            new Vector2(1f, 0f),
        };

        /** Triangles **/
        int[] triangles = new int[]
        {
            0, 1, 2,
            3, 0, 2,

            4, 5, 6,
            7, 4, 6,

            8, 9, 10,
            11, 8, 10,

            12, 13, 14,
            15, 12, 14,
            
            16, 17, 18,
            19, 16, 18,

            20, 21, 22,
            23, 20, 22,
        };

        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();

        filter.mesh = mesh;
    }
}

 

▼ 결과.gif


Circle

을 만드는 스크립트이다.

using UnityEngine;

public class MeshCircle : MonoBehaviour
{
    public Texture2D texture;
    [Min(3)] public int edges = 64;
    public string shaderName = "Standard";

    private void Awake()
    {
        MeshFilter filter = gameObject.AddComponent<MeshFilter>();
        MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
        renderer.material = new Material(Shader.Find(shaderName));
        renderer.material.mainTexture = texture;

        Mesh mesh = new();

        /** Vertices **/
        Vector3[] vertices = new Vector3[edges + 1];
        for (int i = 0, l = edges; i < l; ++i)
        {
            float rad = Mathf.PI * 2 * i / edges;
            vertices[i] = new Vector3(Mathf.Sin(rad), Mathf.Cos(rad), 0f); //한 바퀴를 돌면서 정점 지정
        }
        vertices[edges] = Vector3.zero; //중앙 정점

        /** UV **/
        Vector2[] uv = new Vector2[edges + 1];
        for (int i = 0, l = edges + 1; i < l; ++i)
        {
            uv[i] = new Vector2((vertices[i].x + 1) * 0.5f, (vertices[i].y + 1) * 0.5f); //-1 ~ 1 범위를 0 ~ 1로 변경
        }

        /** Triangles **/
        int[] triangles = new int[edges * 3];
        for (int i = 0, l = edges; i < l; ++i)
        {
            triangles[i * 3] = edges;
            triangles[i * 3 + 1] = i;
            triangles[i * 3 + 2] = (i + 1) % edges;
        }

        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();

        filter.mesh = mesh;
    }
}

정확히는 삼각함수로 n각형을 만든다고 볼 수 있다.

중앙(0, 0)에 정점을 두어 피자 형태로 원을 만들어낸다.

 

▼ 결과.gif

8 Edges
16 Edges
32 Edges


Sphere

를 만드는 스크립트이다.

using UnityEngine;
using System.Collections.Generic;

public class MeshSphere : MonoBehaviour
{
    public Texture2D texture;
    [Min(3)] public int horizontal = 32;
    [Min(2)] public int vertical = 32;
    public string shaderName = "Unlit/Texture";

    [Header("Flip")]
    public bool flipU;
    public bool flipV;
    public bool flipTri;

    private void Awake()
    {
        MeshFilter filter = gameObject.AddComponent<MeshFilter>();
        MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
        renderer.material = new Material(Shader.Find(shaderName));
        renderer.material.mainTexture = texture;

        Mesh mesh = new();

        /** Vertices & UV **/
        int vertLen = (horizontal + 1) * (vertical + 1);
        Vector3[] vertices = new Vector3[vertLen];
        Vector2[] uv = new Vector2[vertLen];

        for (int i = 0; i < vertical + 1; ++i)
        {
            float r = Mathf.Sin(Mathf.PI * i / vertical); //각 층의 반지름
            float h = Mathf.Cos(Mathf.PI * i / vertical); //각 층의 높이
            for (int j = 0; j < horizontal + 1; ++j)
            {
                Vector3 v3; //각 층을 회전하면서 정점 지정
                v3.x = Mathf.Sin(2f * Mathf.PI * j / horizontal) * r;
                v3.y = h;
                v3.z = Mathf.Cos(2f * Mathf.PI * j / horizontal) * r;
                vertices[i * (horizontal + 1) + j] = v3;

                Vector2 v2;
                v2.x = flipU ? ((float)j / horizontal) : (1f - (float)j / horizontal);
                v2.y = flipV ? (1f - (h + 1) * 0.5f) : ((h + 1) * 0.5f);
                uv[i * (horizontal + 1) + j] = v2;
            }
        }

        /** Triangles **/
        List<int> triangles = new();
        for (int i = 0; i < vertical; ++i)
        {
            int baseIdx = (horizontal + 1) * i;
            for (int j = 0; j < horizontal; ++j)
            {
                //각 층마다 두 개의 삼각형을 이용해 사각형을 구성
                if (flipTri)
                {
                    triangles.Add(baseIdx + j);
                    triangles.Add(baseIdx + j + 1);
                    triangles.Add(baseIdx + j + horizontal + 2);

                    triangles.Add(baseIdx + j + horizontal + 2);
                    triangles.Add(baseIdx + j + horizontal + 1);
                    triangles.Add(baseIdx + j);
                }
                else
                {
                    triangles.Add(baseIdx + j);
                    triangles.Add(baseIdx + j + horizontal + 1);
                    triangles.Add(baseIdx + j + horizontal + 2);

                    triangles.Add(baseIdx + j + horizontal + 2);
                    triangles.Add(baseIdx + j + 1);
                    triangles.Add(baseIdx + j);
                }
            }
        }

        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();

        filter.mesh = mesh;
    }
}

위에서부터 아래로 정점을 추가했다.

Filp 기능으로 UV나, 면을 뒤집을 수 있도록 했다.

 

▼ 결과.gif

Shader : Standard
Shader : Unlit/Texture
Shader : Unlit/Texture, Flip Tri

구를 활용하면, 위의 움짤처럼 HDRI 이미지를 표현하는 데에도 유용할 것이다.


 

지금까지 Mesh를 스크립트에서 생성하는 방법을 알아보았다.

활용방안이 무궁무진하기에 다음에도 이와 관련된 글을 쓸 수 있을 것 같다.

이만 글을 마친다.

 

※ 참고로 런타임에 이 방식으로 Mesh를 생성할 때는 문제가 되지 않지만,

에디터에서 생성하면, 메시 데이터가 Scene 파일에(.unity)에 직접 저장이 된다.

이 말은 즉, Scene 파일이 무거워지면서 로딩도 느려진다는 것이므로,

이 경우에는 모델링 파일을 직접 사용하자.

댓글