2. UV및 텍스쳐 아틀라스

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

지난 시간에 Quad를 이용하여 Cube를 만들어 봤습니다.

하지만, 아직 모양만 있을 뿐입니다.

이번 시간에는 Quad에 UV(Texture)를 입히는 방법에 대해서 알아보겠습니다.


1. UV

UV는 기본적으로 3D모델링의 '전개도'라고 생각하시면 됩니다.

또한 UV는 float2로 이루어진 숫자이고, 0~1까지로 표현할 수 있습니다.

(UV=XY=RG)

 

그렇지만 우리는 각 엔진이나 툴마다 UV 배치가 조금 다를 수 있다는 것을 알아야 합니다.

 

언리얼 엔진이나 DirectX는 좌측 상단이 float2(0, 0)인데, 유니티 엔진이나 OpenGL은 좌측 하단이 float2(0, 0) 입니다.

각 그래픽 API에 따른 기준점

그러므로 만약 3ds Max에서 UV를 배열하고 이를 유니티로 넘겼다면, 각 버텍스에는 0에서 1사이의 float2의 숫자가 들어 있는 것과 동일하다고 생각하면 됩니다.

 

하지만 우리는 쉐이더를 공부하는것이 아니기 때문에 여기까지만 알고 있어도 큰 문제는 없습니다.

 

2. UV적용하기

그렇다면 이제 직접 UV를 적용해 볼까요?

 

Block 스크립트를 열어서 CreateQuad() 함수안에

Vector2[] uvs = new Vector2[4];

Vector2 uv00, uv10, uv01, uv11;

uv00 = new Vector2(0, 0);
uv10 = new Vector2(1, 0);
uv01 = new Vector2(0, 1);
uv11 = new Vector2(1, 1);

이렇게 추가해 주고, 스위치 구문 밑에 이 구문들을 추가해 주세요

uvs = new Vector2[] { uv11, uv01, uv00, uv10 };

mesh.uv = uvs;

여기서 중요한 점은, UV좌표를 배열에 대입할때 Vertex를 적용한 순서대로 똑같이 적용해야 한다는 점임니다.

 

이렇게 UV를 오브젝트에 적용을 해줬습니다.

 

하지만, 아직 제대로 적용이 됐는지 확인할 수 없습니다.

 

Material을 새로 만들고 Material에 임시로 체크무늬 이미지를 적용해 봅시다.

P6_check.jpg
0.05MB

그리고 Block 스크립트에 Material을 받을 수 있게 작성해 주고,

public Material m;

마지막으로 Material이 Quad에 적용될 수 있게 해주겠습니다.

meshRenderer.material = m;

스크립트를 저장후 실행 시키면 다음과 같은 결과물이 나옵니다.

UV가 적용된 모습

 

- 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 Material m;

    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";

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

        Vector2 uv00, uv10, uv01, uv11;

        uv00 = new Vector2(0, 0);
        uv10 = new Vector2(1, 0);
        uv01 = new Vector2(0, 1);
        uv11 = new Vector2(1, 1);

        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;
        }

        uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
        triangles = new int[] { 3, 1, 0, 3, 2, 1 };

        mesh.vertices = vertices;
        mesh.uv = uvs;
        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>();
        meshRenderer.material = m;
    }
}

 

3. TextureAtlas

앞서 우리는 오브젝트에 UV를 적용하는 방법에 대해서 알아봤습니다.

 

이번에는 최적화를 위해서 UV를 효율적으로 입히는 방법인 TextureAtlas에 대해서 알아보도록 합시다.

이러한 이미지가 TextureAtlas라는 것은 저번시간을 통해서 보셨을 겁니다.

TextureAtlas의 핵심은 여러가지의 이미지를 하나의 이미지로 통합한다고 생각하시면 되는데요.

 

그렇다면 우리는 어떻게 이러한 이미지를 오브젝트에 적용 시키는 걸까요?

 

단순히 이 이미지를 Material의 Albedo에 집어넣고 적용시켜 보면 이 이미지가 통채로 나오게 될 것입니다.

하지만, 우리가 원하는 결과물은 이 이미지 안에있는 특정 부분의 이미지만 보이게끔 하는 것입니다.

 

그러기 위해서는 UV의 개념을 잘 이용해야 하는데요!

 

UV는 0~1로 표현할 수 있다고 했습니다. 그렇다면 수학시간에 배운 ( x, y )좌표로 쉽계 생각해 봅시다.

마침 Unity는 OpenGL방식이여서 제1사분면 이라는것도 동일하네요.

 

우선 직접 적용을 시켜보기 위해 체크무늬 이미지를 이용하여 1번이 써져있는 곳만 보이게끔 해보겠습니다.

Quad에 1번이 보이게 하려면 우선 기준점 4개를 잡아야 합니다. (Quad의 Vertex는 총 4개 이기 때문이죠)

 

저는 (0,0), (0.33, 0), (0, 0.33), (0.33, 0.33) 이렇게 4개의 기준점을 잡아줬습니다.

 

1번과 2번의 경계선의 위치는 대략 33%정도의 위치이므로 이것을 0~1로 표현을 하면 0.33이 되는 것입니다.

코드를 다음과 같이 수정해 준 후에 실행시면, 다음과 같은 결과물이 나옵니다.

uv00 = new Vector2(0, 0);
uv10 = new Vector2(0.33f, 0);
uv01 = new Vector2(0, 0.33f);
uv11 = new Vector2(0.33f, 0.33f);

이렇게 TextureAtlas는 UV를 이용하여 여러 오브젝트가 하나의 Material을 사용함으로써, DrawCall을 줄일 수 있습니다!

 

4. 블럭 종류 추가하기

이제 마인크래프트에 어울리게 여러 블럭의 Texture를 사용 할 수 있게 만들어 봅시다.

 

우선 제가 직접 수정한 Texture Atlas를 다운 받아 주시고, Material에 적용해 주세요!

blocks.png
0.05MB

그리고, Block스크립트에 다음과 같이 수정해 주세요.

 

- Block 스크립트 -

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

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

    public enum BlockType
    {
        AIR, GRASS, DIRT
    };

    public BlockType bType;

    public bool isSolid;
    public GameObject parent;
    public Vector3 position;
    public Material m;

    // 블럭에 따른 UV들
    Vector2[,] blockUVs =
    {
        /* GRASS TOP */                         {new Vector2(0.2125f, 0.9875f)  , new Vector2(0.2250f, 0.9875f) , new Vector2(0.2125f, 1f)  , new Vector2(0.2250f, 1f)},
        /* GRASS SIDE */                        {new Vector2(0.2250f, 0.9875f)  , new Vector2(0.2375f, 0.9875f) , new Vector2(0.2250f, 1f)  , new Vector2(0.2375f, 1f)},
        /* DIRT */                              {new Vector2(0.2000f, 0.9875f)  , new Vector2(0.2125f, 0.9875f) , new Vector2(0.2000f, 1f)  , new Vector2(0.2125f, 1f)},
    };

    public Block(BlockType b, Vector3 pos, GameObject p, Material cm)
    {
        m = cm;
        bType = b;
        parent = p;
        position = pos;
        if (bType == BlockType.AIR) isSolid = false;
        else isSolid = true;
    }

    public void Draw()
    {
        if (bType.Equals(BlockType.AIR)) return;

        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";

        Vector2[] uvs = new Vector2[4];
        Vector3[] normals = new Vector3[4];
        Vector3[] vertices = new Vector3[4];
        int[] triangles = new int[6];

        Vector2 uv00, uv10, uv01, uv11;

        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);


        // 블럭의 종류가 GRASS이고 Top을 그릴 경우
        if (bType == BlockType.GRASS && side == Cubeside.TOP)
        {
            uv00 = blockUVs[0, 0];
            uv10 = blockUVs[0, 1];
            uv01 = blockUVs[0, 2];
            uv11 = blockUVs[0, 3];
        } // BOTTOM을 그릴 경우
        else if (bType == BlockType.GRASS && side == Cubeside.BOTTOM)
        {
            uv00 = blockUVs[(int)(BlockType.DIRT), 0];
            uv10 = blockUVs[(int)(BlockType.DIRT), 1];
            uv01 = blockUVs[(int)(BlockType.DIRT), 2];
            uv11 = blockUVs[(int)(BlockType.DIRT), 3];
        }
        else
        {
            uv00 = blockUVs[(int)(bType), 0];
            uv10 = blockUVs[(int)(bType), 1];
            uv01 = blockUVs[(int)(bType), 2];
            uv11 = blockUVs[(int)(bType), 3];
        }

        switch (side)
        {
            case Cubeside.BOTTOM:
                vertices = new Vector3[] { p0, p1, p2, p3 };
                normals = new Vector3[] { Vector3.down, Vector3.down, Vector3.down, Vector3.down };
                break;
            case Cubeside.TOP:
                vertices = new Vector3[] { p7, p6, p5, p4 };
                normals = new Vector3[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
                break;
            case Cubeside.LEFT:
                vertices = new Vector3[] { p7, p4, p0, p3 };
                normals = new Vector3[] { Vector3.left, Vector3.left, Vector3.left, Vector3.left };
                break;
            case Cubeside.RIGHT:
                vertices = new Vector3[] { p5, p6, p2, p1 };
                normals = new Vector3[] { Vector3.right, Vector3.right, Vector3.right, Vector3.right };
                break;
            case Cubeside.FRONT:
                vertices = new Vector3[] { p4, p5, p1, p0 };
                normals = new Vector3[] { Vector3.forward, Vector3.forward, Vector3.forward, Vector3.forward };
                break;
            case Cubeside.BACK:
                vertices = new Vector3[] { p6, p7, p3, p2 };
                normals = new Vector3[] { Vector3.back, Vector3.back, Vector3.back, Vector3.back };
                break;
        }

        uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
        triangles = new int[] { 3, 1, 0, 3, 2, 1 };

        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.uv = uvs;
        mesh.triangles = triangles;

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

        GameObject quad = new GameObject("Quad");
        quad.transform.position = position;
        quad.transform.parent = parent.transform;

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

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

여기서 isSolid를 추가해준 이유는, 블럭이 고체인지 아닌지를 판단하기 위해서 입니다. (공기나 물이 여기에 해당하겠네요)

 

또한, if문으로 BlockType에 알맞는 UV를 입혀주고, Normals를 추가해 줬습니다.

(잔디 블럭 같은 경우는 Bottom은 흙 텍스쳐, Side는 흙과 잔디가 있는 텍스쳐, Top은 잔디만 있는 텍스쳐 이기 때문에 if 문을 이용하여 구분해 주었습니다.)

 

 


다음 글에서는 본격적으로 Chunk를 만들어 보도록 하겠습니다. 

앞의 내용들을 완벽히 이해하셨다면 쉽게 따라 올수 있으실 겁니다!


Block.cs
0.00MB