본문 바로가기

유니티/Catlikecoding

[메시 기초] 절차적 그리드 (Catlikecoding)

반응형

해당 글은 CatlikeCoding의 튜토리얼을 실습하며 번역해놓은 글입니다. 번역 과정에서 제가 내용을 덧붙이거나 삭제한 부분이 존재합니다. 원본의 링크는 글 하단에 있습니다.

 

요약

- 점으로 그리드를 만듭니다.

- 코루틴을 사용하여 점의 배치를 분석합니다.

- 삼각형으로 표면을 정의합니다.

- 법선을 자동으로 생성합니다.

- 텍스처 좌표와 탄젠트를 추가합니다.

 

이 튜토리얼에서는 정점과 삼각형으로 구성된 간단한 그리드를 만들어 보겠습니다.

 

1. 렌더링이란

유니티에서 무언가를 시각화하려면 메시를 사용합니다. 이것은 다른 프로그램에서 익스포트한 3D모델일 수도 있고, 유니티에서 절차적으로 생성된 메시일 수도 있습니다. 스프라이트, UI, 파티클 시스템도 메시를 사용할 수 있으며, Unity에서도 메시를 사용하고 있습니다. 심지어 화면 효과도 메시를 이용하여 렌더링 됩니다.

 

 그렇다면 메시란 무엇일까요? 개념적으로 메쉬는 그래픽 하드웨어가 복잡한 것을 그리는 데 사용하는 구조입니다. 메시에는 3D공간에서 점을 정의하는 정점의 모음과 이러한 점을 연결하는 가장 기본적인 2D 도형인 삼각형 집합이 포함됩니다. 삼각형은 메시가 나타내는 모든 것의 표면을 형성합니다.

 

  삼각형은 평평하고 직선인 엣지를 가지기 때문에, 큐브의 면과 같이 평평하고 직선적인 사물을 완벽하게 시각화 하는 데 사용될 수 있습니다. 곡면이나 둥근 표면은 작은 삼각형을 많이 사용해야만 근사치를 구할 수 있습니다. 삼각형이 한 픽셀보다 크지 않을 정도로 작게 표시되면 근사치를 알아차리지 못합니다. 일반적으로 실시간 렌더링에서는 성능상 불가능하므로 서페이스는 항상 어느 정도 들쭉날쭉하게 나타납니다.

 

유니티의 Cube 게임 오브젝트

 

 게임 오브젝트에 3D 모델을 표시하려면 두 가지 구성 요소가 필요합니다. 첫 번째는 메시 필터(Mesh Filter)입니다. 이 컴포넌트는 표시하려는 메시의 레퍼런스를 보유합니다. 두 번째는 메시 렌더러(Mesh Renderer)입니다. 메시 렌더러를 사용하여 메시가 렌더링되는 방식을 구성할 수 있습니다. 어떤 메터리얼을 사용할지, 그림자를 드리우거나 받을지 등을 설정할 수 있습니다.

 

 메시의 메터리얼을 조정하여 메시의 모양을 완전히 바꿀 수 있습니다. Unity의 기본 메터리얼은 단순한 흰색 단색입니다. 새 메터리얼 생성시 기본적으로 Unity의 스탠다드 셰이더를 사용하며, 이를 통해 표면의 시각적 동작 방식을 조정할 수 있는 일련의 컨트롤을 제공합니다.

 

 메시에 많은 디테일을 추가하는 빠른 방법은 알베도 맵을 제공하는 것입니다. 알베도 맵은 메터리얼의 기본 색상을 나타내는 텍스처입니다. 물론 이 텍스처를 메시의 트라이앵글에 투영하는 방법을 알아야 합니다. 이는 정점에 2D 텍스처 좌표를 추가하여 수행됩니다. 텍스처 공간의 두 차원을 U와 V라고 하며, 이 때문에 UV좌표라고도 합니다. 이 좌표는 일반적으로 전체 텍스처를 포괄하는 (0,0)과 (1,1)사이에 위치합니다. 이 범위를 벗어나는 좌표는 텍스처 설정에 따라 고정되거나 타일링이 발생합니다.

Unity의 메시에 적용된 UV 테스트 텍스처입니다.

 

2. 정점 그리드 만들기

 그렇다면 나만의 메시는 어떻게 만들까요? 간단한 직사각형 그리드를 생성해봅시다. 그리드는 단위 길이의 정사각형 타일(쿼드)로 구성됩니다. 새 C#스크립트를 생성하고 가로 및 세로 크기가 있는 Grid 컴포넌트를 생성합니다.

 

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Grid : MonoBehaviour
{
    private Vector3[] vertices;

    public int xSize, ySize;

    private void Awake () 
    {
        Generate();
    }
    
    private void Generate () 
    {
        vertices = new Vector3[(xSize + 1) * (ySize + 1)];
        for (int i = 0, y = 0; y <= ySize; y++) 
        {
            for (int x = 0; x <= xSize; x++, i++) 
            {
                vertices[i] = new Vector3(x, y);
            }
        }
    }
    
    private void OnDrawGizmos () 
    {
    	if (vertices == null) return;
    
        Gizmos.color = Color.black;
        for (int i = 0; i < vertices.Length; i++) 
        {
            Gizmos.DrawSphere(vertices[i], 0.1f);
        }
    }
}

 

 이제 빈 게임 오브젝트를 새로 만들고 Grid 컴포넌트를 추가할 수 있습니다. RequireComponent를 통해 해당 Grid 컴포넌트 추가시 자동으로 MeshFilter와 MeshRenderer컴포넌트를 포함하게 됩니다. 새로 만든 게임 오브젝트에 Grid 컴포넌트를 추가한 후, 렌더러의 메터리얼을 설정하고 필터의 메시는 정의하지 않은 채로 둡니다. 오브젝트의 Awake이벤트에서 실제 메시를 생성하며, 이는 플레이 모드로 전환될 때 실행됩니다.

 

 나중에 삼각형을 만들기 위한 정점을 살펴보겠습니다. 점을 저장하려면 3D 벡터 배열을 보유해야 합니다 .정점의 수는 그리드의 크기에 따라 달라집니다. 모든 사각형의 모서리에 정점이 필요하지만, 인접한 사각형은 같은 정점을 공유할 수 있습니다. 따라서 각 차원에 있는 타일 수보다 정점이 하나 더 필요합니다.

 

4×2 그리드의 정점 및 쿼드 인덱스입니다.

#x - x축에 존재하는 타일의 수

#y - y축에 존재하는 타일의 수

버텍스의 수 = (#x + 1)(#y + 1)

 

xSize = 10, ySzie = 5 일 때, 플레이시 씬 뷰에서 생성되는 Grid Gizmo

 

 이제 정점은 렌더링되지만, 정점이 배치된 순서는 알 수 없습니다. 색상을 사용하여 표시할 수도 있지만 코루틴을 사용하여 프로세스 속도를 늦츨 수도 있습니다.

 

    private void Awake () 
    {
        StartCoroutine(Generate());
    }
    
    private IEnumerator Generate()
    {
        WaitForSeconds wait = new WaitForSeconds(0.05f);
        vertices = new Vector3[(xSize + 1) * (ySize + 1)];
        for (int i = 0, y = 0; y <= ySize; y++) 
        {
            for (int x = 0; x <= xSize; x++, i++) 
            {
                vertices[i] = new Vector3(x, y);
                yield return wait;
            }
        }
    }

 

3. 메시 만들기

이제 정점이 올바르게 배치되었다는 것을 알았으니 실제 메쉬를 다룰 수 있습니다. 자체 컴포넌트에 참조를 보유하는 것 외에도 메쉬 필터에 할당해야 합니다. 그런 다음 정점을 처리하고 나면 메쉬에 정점을 할당할 수 있습니다.

 

    private Mesh mesh;
    
    private IEnumerator Generate()
    {
        WaitForSeconds wait = new WaitForSeconds(0.05f);

        GetComponent<MeshFilter>().mesh = mesh = new Mesh();
        mesh.name = "Procedural Grid";
        
        vertices = new Vector3[(xSize + 1) * (ySize + 1)];
        for (int i = 0, y = 0; y <= ySize; y++) 
        {
            for (int x = 0; x <= xSize; x++, i++) 
            {
                vertices[i] = new Vector3(x, y);
                yield return wait;
            }
        }
        mesh.vertices = vertices;
    }

 

 이제 플레이 모드에 메시가 있지만, 아직 삼각형을 정의하지 않았기 때문에 메시가 표시되지 않습니다. 삼각형은 버텍스 인덱스 배열을 통해 정의됩니다. 각 삼각형에는 세 개의 점이 있으므로 연속된 세 개의 인덱스는 하나의 삼각형을 나타냅니다. 하나의 삼각형부터 시작하겠습니다.

 

 삼각형이 어느 쪽에서 보이는지는 정점 인덱스의 방향에 따라 결정됩니다. 기본적으로 정점 인덱스가 시계 방향으로 배열된 경우 삼각형은 정면을 향하고 보이는 것으로 간주됩니다. 시계 반대 방향의 삼각형은 버려지므로 일반적으로 보이지 않는 오브젝트의 내부를 렌더링하는 데 시간을 소비할 필요가 없습니다.

 

..index array 유도방식 생략

 

이제 버텍스와 그리드가 생성되는 과정을 확인했으니, 코루틴 코드를 제거해서 지연 없이 한번에 모든 그리드가 생성되게 변경하겠습니다.

 

    private Mesh mesh;

    private Vector3[] vertices;

    public int xSize, ySize;

    private void Awake () 
    {
        Generate();
    }
    
    private void Generate()
    {
        WaitForSeconds wait = new WaitForSeconds(0.05f);

        GetComponent<MeshFilter>().mesh = mesh = new Mesh();
        mesh.name = "Procedural Grid";
        
        vertices = new Vector3[(xSize + 1) * (ySize + 1)];
        for (int i = 0, y = 0; y <= ySize; y++) 
        {
            for (int x = 0; x <= xSize; x++, i++) 
            {
                vertices[i] = new Vector3(x, y);
            }
        }
        mesh.vertices = vertices;

        int[] triangles = new int[xSize * ySize * 6];
        for (int ti = 0, vi = 0, y = 0; y < ySize; y++, vi++)
        {
            for (int x = 0; x < xSize; x++, ti += 6, vi++)
            {
                triangles[ti]     = vi;
                triangles[ti + 3] = triangles[ti + 2] = vi + 1;
                triangles[ti + 4] = triangles[ti + 1] = vi + xSize + 1;
                triangles[ti + 5] = vi + xSize + 2;
                mesh.triangles    = triangles;
            }
        }
    }

 

 

4. 추가적인 버텍스 데이터 생성

 현재 그리드의 조명이 특이한 방식으로 적용되어 있습니다. 그 이유는 아직 메쉬에 법선벡터를 지정하지 않았기 떄문입니다. 기본 노멀 방향은 (0,0,1)로 우리가 원하는 방향과 정 반대입니다.

 

더보기

법선은 표면에 수직인 벡터입니다. 우리는 항상 단위 길이의 법선을 사용하며, 법선은 내부가 아닌 표면의 외부를 가리킵니다.

 

법선은 빛이 표면에 닿는 각도를 결정하는 데 사용할 수 있습니다. 구체적인 사용 방법은 셰이더에 따라 다릅니다.

 

삼각형은 항상 평평하므로 노멀에 대한 별도의 정보를 제공할 필요가 없습니다. 하지만 법선 정보를 추가함으로써 속임수를 쓸 수 있습니다. 각 정점에 커스텀 법선을 붙이고 삼각형 사이에 보간을 하면 평평한 삼각형이 아닌 부드러운 곡면이 있는 것처럼 표현할 수 있습니다. 메시의 날카로운 실루엣에 주의를 기울이지 않는 한 이 트릭은 충분히 사람들을 속일 수 있습니다.

 

법선은 정점 별로 정의되므로 다른 벡터 배열을 생성해서 채워야 합니다. 또는 메시가 트라이앵글을 기반으로 노멀을 스스로 계산하도록 요청할 수도 있습니다. 이번에는 이 방법을 사용해보겠습니다.

 

    private void Generate()
    {
    	...
                mesh.triangles    = triangles;
		        mesh.RecalculateNormals();
        ...
    }

 

 

다음은 UV 좌표입니다. 알베도 텍스처가 있는 메터리얼을 사용하고 있음에도 불구하고 그리드의 색상이 균일하다는 것을 눈치챘을 것입니다.(위 스샷은 사실 알베도 텍스처가 없는 Default-Material입니다) UV좌표를 직접 제공하지 않으면 모두 0이 되기 때문에 이는 당연한 결과입니다. 전체 그리드에 맞게 텍스쳐를 만들려면 버텍스의 위치를 그리드 치수로 나누기만 하면 됩니다.

 

        vertices = new Vector3[(xSize + 1) * (ySize + 1)];
        var uv = new Vector2[vertices.Length];
        for (int i = 0, y = 0; y <= ySize; y++) 
        {
            for (int x = 0; x <= xSize; x++, i++) 
            {
                vertices[i] = new Vector3(x, y);
                uv[i]       = new Vector2((float)x / xSize, (float)y / ySize);
            }
        }
        mesh.vertices = vertices;
        mesh.uv       = uv;

 

 

이제 텍스처가 전체 그리드에 투영됩니다. 그리드 크기를 가로로 길게 설정해두었기 때문에 텍스처가 가로로 늘어져 보일 수 있습니다. 이 문제를 해결하기 위해서는 메터리얼의 텍스처 타일링 설정을 조절하여 해결할 수 있습니다.

 

 서페이스에 더 뚜렷한 디테일을 추가하는 방법은 노멀 맵을 사용하는 것입니다. 이 맵에는 컬러로 인코딩된 노멀 벡터가 포함되어 있습니다. 이를 표면에 적용하면 버텍스 노멀만으로 만들 수 있는 것보다 훨씬 더 세밀한 조명 효과를 얻을 수 있습니다. 노멀맵을 그리드에 적용하면 범프가 생성되지만 올바르지 않습니다. 메시의 방향을 올바르게 지정하려면 탄젠트 벡터를 추가해야 합니다.

 

평면이 평평하기 때문에 모든 탄젠트는 단순히 오른쪽이라는 같은 방향을 나타냅니다.

 

        var uv       = new Vector2[vertices.Length];
        var tangents = new Vector4[vertices.Length];
        // 탄젠트의 4번째 값은 항상 -1 또는 1이며, 이는 탄젠트 공간 차원의 방향을 앞뒤로 제어하는데 사용합니다.
        // 이를 통해 노멀맵의 미러링이 용이해집니다.
        var tangent  = new Vector4(1f, 0f, 0f, -1f);
        for (int i = 0, y = 0; y <= ySize; y++) 
        {
            for (int x = 0; x <= xSize; x++, i++) 
            {
                vertices[i] = new Vector3(x, y);
                uv[i]       = new Vector2((float)x / xSize, (float)y / ySize);
                tangents[i] = tangent;
            }
        }
        mesh.vertices = vertices;
        mesh.uv       = uv;
        mesh.tangents = tangents;

 

 

이제 간단한 메시를 만들고 메터리얼을 사용하여 더 복잡하게 보이게 만드는 방법을 배웠습니다. 메시에는 버텍스 위치와 트라이앵글(일반적으로는 UV좌표까지)필요하며, 종종 탄젠트도 필요합니다. 버텍스 컬러를 추가할 수도 있지만, Unity의 표준 셰이더는 이를 사용하지 않습니다. 이러한 색상을 사용하는 셰이더를 직접 만들 수 있지만 이는 다른 튜토리얼에서 다룰 내용입니다.

 

원본

 

Procedral Grid, a Unity C# Tutorial

A Unity C# scripting tutorial in which we'll create a simple grid of vertices and triangles.

catlikecoding.com

 

 

반응형