1. Quad를 이용한 Cube구축

2021. 5. 26. 15:13[Unity] Game Programing/유니티를 활용한 마인크래프트 청크 시스템 구현하기

우리는 왜 Cube오브젝트로 마인크래프트를 구축할 수 없는지에 대해서 배웠고, 해결방안도 알아봤습니다.

그렇다면 이제는 직접 만들어보면서 익혀봅시다!


CombineMeshes를 써보신 분이라면 '오브젝트를 하나로 만들었다고 한들 내가 어떤 블럭을 눌렀고, 어느면을 눌렀는지 어떻게 알까?' 이런 의문이 들 것입니다.

기존은 오브젝트에 Ray쏴서 그 오브젝트의 정보를 볼 수 있었습니다. (hit.transform.position)

하지만 하나로 합쳐진 오브젝트는 Ray를 쏜다고 해서 그 오브젝트 안에 여러 블럭의 정보를 알기란 쉽지 않을 겁니다.

그러한 정보를 알기 위해서 우리는 Block의 정보를 저장하고, 생성하는 클래스를 만들어야 합니다.

1. Block 스크립트 생성하기

우리는 우선 Block 스크립트를 생성하고 간단하게 블럭의 위치와 부모 오브젝트를 알 수 있는 변수를 만들어 주고, 생성자를 생성해 줍니다.

 

- Block 스크립트 -

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

public class Block
{
    public GameObject parent;
    public Vector3 position;

    public Block(Vector3 pos, GameObject p)
    {
        parent = p;
        position = pos;
    }
}

여기서 중요한 것은 유니티에서 직접적으로 사용할 클래스는 아니기 때문에 ( : MonoBehaviour)을 사용하지 않는다는 것입니다.

 

2. Quad생성하기

우리는 간단하게나마 블럭에대한 정보를 저장하는 Class를 만들었습니다. 이제는 그 정보를 바탕으로 시각적으로 보이게끔 만들어야 할 때입니다.

하지만 우리는 유니티에대한 기본조작법은 알아도 스크립트로 작성하여 오브젝트(Mesh)를 생성하는 법은 모릅니다.

그렇기에 우선 간단하게 따라하면서 Quad를 생성해보도록 합시다.

 

- Block 스크립트 -

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

public class Block : MonoBehaviour
{
    public GameObject parent;
    public Vector3 position;

    public Block(Vector3 pos, GameObject p)
    {
        parent = p;
        position = pos;
    }

    private void Start()
    {
        CreateQuad();
    }

    void CreateQuad()
    {
        Mesh mesh = new Mesh();
        mesh.name = "ScriptedMesh";

        Vector3[] vertices = new Vector3[4];
        int[] triangles = new int[6];

        Vector3 p0, p1, p2, p3, p4, p5, p6, p7;

        p0 = new Vector3(-0.5f, -0.5f, 0.5f);
        p1 = new Vector3(0.5f, -0.5f, 0.5f);
        p2 = new Vector3(0.5f, -0.5f, -0.5f);
        p3 = new Vector3(-0.5f, -0.5f, -0.5f);
        p4 = new Vector3(-0.5f, 0.5f, 0.5f);
        p5 = new Vector3(0.5f, 0.5f, 0.5f);
        p6 = new Vector3(0.5f, 0.5f, -0.5f);
        p7 = new Vector3(-0.5f, 0.5f, -0.5f);

        vertices = new Vector3[] { p6, p7, p3, p2 };
        triangles = new int[] { 3, 1, 0, 3, 2, 1 };

        mesh.vertices = vertices;
        mesh.triangles = triangles;

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();

        GameObject quad = new GameObject("Quad");

        MeshFilter meshFilter = quad.AddComponent<MeshFilter>();
        meshFilter.mesh = mesh;

        MeshRenderer meshRenderer = quad.AddComponent<MeshRenderer>();
    }
}

결과물을 보기 위해서 잠시 ( : MonoBehaviour)를 다시 추가해 주고, Start구문에 CreateQuad(); 구문을 추가해 줍니다.

우리는 앞선 결과물을 통해서 Quad를 만들어 봤지만, 우리는 정확히 어떻게 만들었는지 모릅니다.

하나씩 차분히 봐볼까요?

 

3. Mesh

mesh란 쉽게 말해 점, 폴리곤(삼각형), UV를 관리하는 구조체입니다.

컴퓨터 그래픽, 특히 게임 그래픽은 삼각형이 기본이 되는데

이 삼각형(폴리곤) 2개를 가지고 사각형을 만든 다음 텍스쳐를 입히는 방식입니다.

 

폴리곤에 대해서 좀 더 얘기해보자면 mesh의 한 단위를 가리키는 것으로

이것이 많이 사용될수록 3D모델의 디테일이 좋다진다 할 수 있습니다.

 

이렇듯 mesh는 게임 제작에서 사용되는 모든 모델의 기초구조라고 생각하시면 됩니다.


출처: https://grayt.tistory.com/171 (Mesh가 만들어지는 과정이 잘 설명되어 있습니다.)

 

이러한 이유때문에 먼저 Mesh를 생성해 줬습니다.

Mesh mesh = new Mesh();

이 Mesh에는 대표적으로 Vertex, Normal, Uv, Triangles의 정보를 가지고 있습니다.

하지만 우리는 단순히 새로운 Mesh를 생성했을 뿐이여서 이러한 정보를 가지고 있지 않습니다.

 

그렇기 때문에 우리는 직접 그 정보를 입력해줘야 합니다.

우선 모양을 형성하는데 필요한 Vertex에 대해 알아볼까요?

4. Vertex

폴리곤에서 두 개의 변이 만나는 지점을 정점(vertex) 이라고 합니다.

하나의 삼각형을 만들기 위해서는 삼각형의 세 정점에 해당하는 세 개의 포인트 위치를 저장해야하며, 이 삼각형을 지정하여 물체를 묘사합니다.

 

출처: https://hellowoori.tistory.com/30 (Vertex, Polygon, Edge, Mesh에 대해 간단히 설명되어 있습니다.)

 


그렇다면 우리가쓴 코드중에서 Vertex에 관련된 코드는 어디에 있을까요?

Vector3[] vertices = new Vector3[4];

Vector3 p0, p1, p2, p3, p4, p5, p6, p7;

p0 = new Vector3(-0.5f, -0.5f, 0.5f);
p1 = new Vector3(0.5f, -0.5f, 0.5f);
p2 = new Vector3(0.5f, -0.5f, -0.5f);
p3 = new Vector3(-0.5f, -0.5f, -0.5f);
p4 = new Vector3(-0.5f, 0.5f, 0.5f);
p5 = new Vector3(0.5f, 0.5f, 0.5f);
p6 = new Vector3(0.5f, 0.5f, -0.5f);
p7 = new Vector3(-0.5f, 0.5f, -0.5f);

vertices = new Vector3[] { p6, p7, p3, p2 };

mesh.vertices = vertices;

이 구문들이 Vertex에 관련있다고 할 수 있습니다.

 

우리는 우선 Vector3형식의 p0 ~ p7 이라는 변수를 만들었습니다.

 

그리고 각 변수들에 맞는 값을 대입해 줬습니다.

 

아래의 이미지를 보시면 p0 ~ p7이 모서리를 뜻하는 것을 알 것입니다.

이렇게 우리는 Cube형태의 모양을 만들기 위해 정육면체의 각 모서리 부분을 Vector3를 이용해서 위치를 표현해 줬습니다.

 

하지만 지금은 간단하게 Cube의 뒤쪽 면만을 그리기 위해 그에 해당하는 Vertex만 사용 하였습니다.

vertices = new Vector3[] { p6, p7, p3, p2 };

그 후에 이 vertices라는 배열을 mesh.vertices에 대입해 줬습니다.

mesh.vertices = vertices;

이로써 각 정점들의 위치 정보를 추가해 줬습니다.

하지만 아직 정점들의 위치 정보만 있고, 각각의 정점들이 어떻게 연결되어 있는지 모르기 때문에 면을 그릴 수 없습니다.

 

5. Triangles

앞서 말씀드렸듯이 mesh란 삼각형들이 모여 만들어진 것입니다.

위의 그림에 보이는 p0,p1,p2.. 가 vertex(꼭지점)가 되는 것이고, 그것들로 이어진 삼각형이

triangle polygon이 되는 것입니다. 그렇다면 꼭지점을 만들고 이어붙여 폴리곤(triangle)을 만든다음 mesh로 만들면 되겠네요.

 

그렇다면 이제 이것을 코드로 만들어야 합니다. triangles와 관련된 코드는 아래와 같습니다.

int[] triangles = new int[6];

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

mesh.triangles = triangles;

보기에는 특별할것이 없어 보입니다만, 여기서 꼭 지켜야할 규칙이 숨어있는데요.

 

1. triangles는 int형으로된 배열을 사용한다는 것입니다.

 

2. triangles의 배열의 크기는 반드시 3의 배수여야 합니다.

( 유니티 공식 레퍼런스에도 잘 나와있습니다. https://docs.unity3d.com/kr/530/ScriptReference/Mesh-triangles.html )

 

3. 배열에 대입하는 순서입니다.

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

이 부분을 보시면 단순히 숫자를 써 넣은것 같이 보이지만, 사실 이 숫자는 vertices에 순서대로 대입한 각 정점들의 순서입니다. (0=p6, 1=p7, 2=p3, 3=p2 라고 보시면 됩니다.)

 


잠시 돌아가 쉐이더에 관련된 내용을 이야기 하겠습니다.

 

컴퓨터 그래픽은 오브젝트를 시각적으로 표현할때 어떤식으로 표현을 하는 걸까요?

 

기본적으로 모든것은 필요없는곳에 자원을 낭비하지 않으려고 합니다.

 

컴퓨터 그래픽도 마찬가지로 눈에 보이지 않는 곳은 그리지 않으므로써 자원낭비를 줄이는 겁니다.

 

그래서 기본적으로 오브젝트를 시각적으로 표현할때 한쪽면만 그리는 '백페이스 컬링(Backface Culling)'을 사용합니다.

(예를 들어 Plane을 만들고 뒤로 돌려 보면 Plane이 보이지 않는데 이것을 '백페이스 컬링'이라고 합니다.)

 

쉐이더 코드로 따지자면 'cull back'이 여기에 해당하겠네요.

 

그렇다면 이것이 triangles에 어떤 관련이 있을까요?

 

바로, 어느쪽 면을 그리느냐 입니다.

 

우선 polygon은 삼각형이라고 했습니다.

우리가 이 삼각형을 만들기 위해서는 정점이 3개가 필요하다는 것 까지도 알고 있고요!

 

하지만 어느쪽 면을 그리느냐를 결정하는 것은 triangles에 대입한 각 정점위치의 순서입니다.

왼쪽은 정상적인 면, 오른쪽은 뒤집어진 면

위 그림과 같이 각 정점들을 대입한 순서에 따라 시계방향과 반시계 방향으로 나뉩니다.

시계 방향은 정상적인 면이 그려집니다. 하지만 반시계 방향은 면이 뒤집어진체 그려지게 됩니다.

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

이 코드에서 3개씩 나눠서 보면. { 3, 1, 0 } { 3, 2, 1 }로 볼 수 있습니다.

각 숫자는 0=p6, 1=p7, 2=p3, 3=p2 와 같으므로 각각의 Polygon은 시계방향으로 정상적인 면이 그려진다는 것을 알 수 있습니다. (VertexImage)

 

ps. 한번 triangles의 대입 순서를 바꿔보세요! 정말 재미있는일이 일어날 겁니다.

 

6. 그 외

아직 따로 언급하지 않았지만 중요한 코드가 남아 있습니다.

mesh.RecalculateBounds();
mesh.RecalculateNormals();

 

각 함수의 역할은 다음과 같습니다.

 

RecalculateBounds() : 정점에서 메쉬의 겅계 볼륨을 재 계산 하는 함수

RecalculateNormals() : 정점과 삼각형으로 부터 메쉬의 노멀을 재 계산하는 함수

 

지금 당장은 크게 필요하지 않을 수 있지만 앞으로 우리가 블럭을 설치/삭제 하는 과정(Reload Chunk)에서 반드시 필요하다고 할 수 있습니다.

 

7. Cube 만들기

우리는 이렇게 하나의 Quad를 만드는 과정을 알았고, 직접 만들어도 보았습니다.

이제는 이 Quad를 이용하여 하나의 Cube를 만들어 봅시다.

 

우선은 코드를 다음과 같이 수정해 줍니다.

 

- Block 스크립트 -

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

public class Block : MonoBehaviour
{
    public enum Cubeside { BOTTOM, TOP, LEFT, RIGHT, FRONT, BACK };

    public GameObject parent;
    public Vector3 position;

    public Block(Vector3 pos, GameObject p)
    {
        parent = p;
        position = pos;
    }

    private void Start()
    {
        Draw();
    }

    void Draw()
    {
        CreateQuad(Cubeside.BOTTOM);
        CreateQuad(Cubeside.TOP);
        CreateQuad(Cubeside.LEFT);
        CreateQuad(Cubeside.RIGHT);
        CreateQuad(Cubeside.FRONT);
        CreateQuad(Cubeside.BACK);
    }

    void CreateQuad(Cubeside side)
    {
        Mesh mesh = new Mesh();
        mesh.name = "ScriptedMesh";

        Vector3[] vertices = new Vector3[4];
        int[] triangles = new int[6];

        Vector3 p0, p1, p2, p3, p4, p5, p6, p7;

        p0 = new Vector3(-0.5f, -0.5f, 0.5f);
        p1 = new Vector3(0.5f, -0.5f, 0.5f);
        p2 = new Vector3(0.5f, -0.5f, -0.5f);
        p3 = new Vector3(-0.5f, -0.5f, -0.5f);
        p4 = new Vector3(-0.5f, 0.5f, 0.5f);
        p5 = new Vector3(0.5f, 0.5f, 0.5f);
        p6 = new Vector3(0.5f, 0.5f, -0.5f);
        p7 = new Vector3(-0.5f, 0.5f, -0.5f);

        switch (side)
        {
            case Cubeside.BOTTOM:
                vertices = new Vector3[] { p0, p1, p2, p3 };
                break;
            case Cubeside.TOP:
                vertices = new Vector3[] { p7, p6, p5, p4 };
                break;
            case Cubeside.LEFT:
                vertices = new Vector3[] { p7, p4, p0, p3 };
                break;
            case Cubeside.RIGHT:
                vertices = new Vector3[] { p5, p6, p2, p1 };
                break;
            case Cubeside.FRONT:
                vertices = new Vector3[] { p4, p5, p1, p0 };
                break;
            case Cubeside.BACK:
                vertices = new Vector3[] { p6, p7, p3, p2 };
                break;
        }
        
        triangles = new int[] { 3, 1, 0, 3, 2, 1 };

        mesh.vertices = vertices;
        mesh.triangles = triangles;

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();

        GameObject quad = new GameObject("Quad");

        MeshFilter meshFilter = quad.AddComponent<MeshFilter>();
        meshFilter.mesh = mesh;

        MeshRenderer meshRenderer = quad.AddComponent<MeshRenderer>();
    }
}

여기서 Switch구문을 이용하여 각 방향을 따로 지정해준 이유는, 나중에 보이지 않는 면은 그리지 않기 위해서 구분해 놓은 것입니다.

 

이것으로 Quad를 이용해 Cube를 만들어 보았습니다.

하지만 아직 이것만으로는 많이 부족합니다.

다음에는 우리가 만든 Mesh에 UV(Texture)를 효율적으로 입히는 방법에 대해서 배울것입니다.


Block.cs
0.00MB