내일배움캠프 Unity 54일차 TIL - Voxel 맵 렌더링 최적화 (2)

Wooooo·2024년 1월 12일
0

내일배움캠프Unity

목록 보기
56/94

[오늘의 키워드]

오늘은 이전처럼 큐브를 n개 그려내는 방법 대신 메쉬를 통째로 합치는 방법으로 최적화를 해봤다.


[Chunk]

Chunk는 사전적인 의미로 큰 덩어리이다.
Voxel 큐브들의 정점 좌표들을 이용해 하나의 거대한 메시(Chunk)를 생성한다.
Voxel 큐브들은 정점 데이터만 존재하고 인스턴스는 따로 존재하지 않는다.

VoxelData

using UnityEngine;

public static class VoxelData
{
    // Voxel과 관련된 LookUp Table을 정의해둡니다.

    /// <summary> 정육면체 8개 정점 위치 </summary>
    public static readonly Vector3[] voxelVerts = new Vector3[]
    {
        new(0f, 0f, 0f),
        new(1f, 0f, 0f),
        new(1f, 1f, 0f),
        new(0f, 1f, 0f),
        new(0f, 0f, 1f),
        new(1f, 0f, 1f),
        new(1f, 1f, 1f),
        new(0f, 1f, 1f),
    };

    /// <summary> 정육면체의 한 면을 이루는 두 삼각형의 정점 인덱스 데이터 </summary>
    public static readonly int[][] voxelTris = new int[][]
    {
        new int[]{ 0, 3, 1, 2 },
        new int[]{ 5, 6, 4, 7 },
        new int[]{ 3, 7, 2, 6 },
        new int[]{ 1, 5, 0, 4 },
        new int[]{ 4, 7, 0, 3 },
        new int[]{ 1, 2, 5, 6 },
    };

    /// <summary> 삼각형 정점 인덱스에 따라 정의된 UV 데이터 </summary>
    public static readonly Vector2[] voxelUVs = new Vector2[]
    {
        new(0f, 0f),
        new(0f, 1f),
        new(1f, 0f),
        new(1f, 1f),
    };

    /// <summary> 청크의 바깥 면만 그려주기 위한 LookUp Table </summary>
    public static readonly Vector3[] faceChecks = new Vector3[]
    {
        new( 0.0f,  0.0f, -1.0f),
        new( 0.0f,  0.0f, +1.0f),
        new( 0.0f, +1.0f,  0.0f),
        new( 0.0f, -1.0f,  0.0f),
        new(-1.0f,  0.0f,  0.0f),
        new(+1.0f,  0.0f,  0.0f),
    };

    public static readonly int ChunkWidth = 10;
    public static readonly int ChunkHeight = 10;

    public static readonly int TextureAtlasWidth = 9;
    public static readonly int TextureAtlasHeight = 10;

    public static float NormalizeTextureAtlasWidth => 1f / TextureAtlasWidth;
    public static float NormalizeTextureAtlasHeight => 1f / TextureAtlasHeight;
}

Voxel과 관련된 LookUp Table들을 모아놓은 정적 클래스다.
이후에 Scriptable Object로 바꿔도 될 것 같다.

Chunk의 멤버 필드들

public class Chunk : MonoBehaviour
{
    [SerializeField] private MeshRenderer meshRenderer;
    [SerializeField] private MeshFilter meshFilter;

    private Dictionary<Vector3Int, byte> voxelMap = new();

    private int vertexIdx = 0;
    private List<Vector3> vertices = new();
    private List<int> triangles = new();
    private List<Vector2> uvs = new();
}

정점, 트라이앵글, uv를 보니까 DX 공부하던 생각이 난다.......
vertices에는 Voxel들의 정점들이 들어있다.
triangles는 3D 그래픽스에서 면을 그리기위한 최소 단위로, 삼각형 2개를 붙여서 정육면체의 면 하나를 그려내는데, 그 삼각형들의 데이터들을 담을 리스트다.
uvs는 나중에 Voxel에 텍스처를 입힐 때 사용하기 위한 정보들이 들어갈 계획이다.

정점 데이터로 MeshData (Triangle) 생성하기

private void CreateMeshData()
{
    for (int y = 0; y < VoxelData.ChunkHeight; y++)
        for (int x = 0; x < VoxelData.ChunkWidth; x++)
            for (int z = 0; z < VoxelData.ChunkWidth; z++)
                AddVoxelDataToChunk(new(x, y, z));
}

private void AddVoxelDataToChunk(Vector3 pos)
{
    Vector3Int intPos = new(Mathf.FloorToInt(pos.x), Mathf.FloorToInt(pos.y), Mathf.FloorToInt(pos.z));

    for (int i = 0; i < 6; i++)
    {
        if (CheckVoxel(pos + VoxelData.faceChecks[i]))
            continue;

        for (int j = 0; j < 4; j++)
        {
            vertices.Add(intPos + VoxelData.voxelVerts[VoxelData.voxelTris[i][j]]);
            //AddTextureUV(43);
        }
        triangles.Add(vertexIdx);
        triangles.Add(vertexIdx + 1);
        triangles.Add(vertexIdx + 2);
        triangles.Add(vertexIdx + 2);
        triangles.Add(vertexIdx + 1);
        triangles.Add(vertexIdx + 3);
        vertexIdx += 4;
    }
}

테스트용으로 xyz 모두 10개씩 총 1000개의 Voxel을 생성해봤다.
CheckVoxel 메서드는 Chunk 내부에 있는 면인지를 판단해주는 메서드다. 내부에 있다면 Mesh Data를 생성하지 않는다. (결과적으로 그려지지 않는다.)

청크 내부에 있는 면인지 검사하기

private bool CheckVoxel(Vector3 pos)
{
    Vector3Int intPos = new(Mathf.FloorToInt(pos.x), Mathf.FloorToInt(pos.y), Mathf.FloorToInt(pos.z));

    if (!voxelMap.ContainsKey(intPos))
        return false;
    else
        return world.BlockTypes[voxelMap[intPos]].isSolid;
}

매개변수로 받아온 pos의 칸에 Voxel이 채워져 있는지 검사하는 메서드다. 위에서 VoxelData.faceChekcs를 더해서 넘겨줬는데, 해당 면이 바라보는 방향으로 1칸씩 더해서 이 메서드를 호출했기 때문에 Voxel끼리 맞닿아 있는 면은 그려지지 않게 된다.

생성한 MeshData로 Mesh 생성하기

private void CreateMesh()
{
    Mesh mesh = new()
    {
        vertices = vertices.ToArray(),
        triangles = triangles.ToArray(),
        uv = uvs.ToArray(),
    };
    mesh.RecalculateNormals();
    meshFilter.mesh = mesh;
}

위에서 생성한 데이터를 이용해 메쉬를 만들면...

생성된 10 * 10 * 10 Chunk

청크가 만들어진다!
청크의 내부도 살펴보자.

청크 내부를 그려내지 않도록 한 경우

Wireframe을 켜고 생성해봤다.

청크 내부를 그려내지 않도록 하지 않은 경우

엄청나게 생긴 녀석이 생성됐다.. 렌더링될 triangle의 숫자가 어마어마하게 차이난다.


[FPS, DrawCall 비교]

이전에 비교헀던 100 * 100, 즉 Voxel 10,000개로 테스트했던 것처럼 10 * 10의 청크를 100개 생성하고 비교해봤다.

210 ~ 250 fps
배치 수는 231개가 나온다.

이전 글에서는 100 ~ 125 프레임, 배치 수는 1100개가 나왔던 걸 생각하면 어마어마한 차이다...


[주의할 점]

이후에는 플레이어 주변 시야범위만큼의 Chunk를 생성하고, 플레이어가 이동할 때마다 Chunk의 정보를 바꿔가며 Mesh를 재생성해야할 것이다.

Mesh는 재생성해도 되지만 MeshCollider도 재생성하게 된다면?
너무 무겁거나 하지 않을까? 아무래도 Collider니까 말이다.

이에 대한 해결방법으로는 위에 만들어둔 isSolid를 이용해서 직접 물리 충돌 로직을 만들어야 한다.


[참고 자료]

유니티 - Voxel System(마인크래프트 구현하기) [TODO]
Code a Game Like Minecraft in Unity

profile
game developer

0개의 댓글