본문 바로가기
개발/Unity

유니티 - 절차적 메시로 2D 복셀 만들기 (Make 2D Voxels with Procedural Mesh)

by 피로물든딸기 2022. 11. 7.
반응형

Unity 전체 링크

 

참고

- 절차적 메시로 정점이 24개인 큐브 만들기

 

픽셀(Pixel)은 2차원 그림을 구성하는 사각형의 점이자 기본 단위이다.

 

복셀(Voxel)은 2차원 픽셀을 3차원 형태로 구현한 것이다. (Volume + Pixel)

 

절차적 메시를 이용하여 2차원 좌표를 보고 2차원 Voxels를 만들어보자.


단순히 큐브로 복셀을 그리는 경우

 

알파벳 A를 복셀로 표현하면 아래와 같다.

 

절차적 메시를 이용하여 큐브를 만들 수 있기 때문에 입력된 좌표를 바탕으로 큐브를 만들면 된다.

 

하지만 동그라미로 표시된 큐브6개의 면모두 그릴 필요가 없다. 

다른 큐브에 의해 가려지기 때문이다.

 

큐브마다 모든 면에 대해 메시를 그리면 GPU 성능에 무리가 간다.

 

따라서 Voxels를 그릴 때는 6면의 큐브가 아니라 필요한 면만 그려야 한다.


구현

 

ProceduralCube.cs를 그대로 ProceduralVoxels.cs에 복사한다. 

면을 몇 개 그려야 하는지 확인하기 어렵기 때문에 vertices와 triangles를 배열에서 List로 변경한다.

배열과 관련된 내용을 List로 변경하고 BoxCollider도 MeshCollider로 다시 변경한다.

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

[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class ProceduralVoxels : MonoBehaviour
{
    public Vector3 size = new Vector3(1.0f, 1.0f, 1.0f);
    public Vector3 offset = new Vector3(0, 0, 0);

    Mesh mesh;
    List<Vector3> vertices = new List<Vector3>();
    List<int> triangles = new List<int>();

    public enum Direction
    {
        FORWARD, // (0, 0, +1)
        RIGHT,   // (+1, 0, 0)
        UP,      // (0, +1, 0)
        BACK,    // (0, 0, -1)
        LEFT,    // (-1, 0, 0)
        DOWN     // (0, -1, 0)
    }

    public int[][] faceNumber =
    {
        new int[] {0, 1, 2, 3}, // FORWARD
        new int[] {5, 0, 3, 6}, // RIGHT 
        new int[] {5, 4, 1, 0}, // UP     
        new int[] {4, 5, 6, 7}, // BACK  
        new int[] {1, 4, 7, 2}, // LEFT 
        new int[] {3, 2, 7, 6}, // DOWN    
    };

    void OnValidate()
    {
        if (mesh == null) return;

        if (size.magnitude > 0 || offset.magnitude > 0)
        {
            setMeshData(size);
            createProceduralMesh();
        }
    }

    void Start()
    {
        mesh = GetComponent<MeshFilter>().mesh;

        setMeshData(size);
        createProceduralMesh();
    }

    void setMeshData(Vector3 size)
    {
        vertices.Clear();

        Vector3 v0 = Vector3.Scale(new Vector3(+0.5f, +0.5f, +0.5f), size) + offset;
        Vector3 v1 = Vector3.Scale(new Vector3(-0.5f, +0.5f, +0.5f), size) + offset;
        Vector3 v2 = Vector3.Scale(new Vector3(-0.5f, -0.5f, +0.5f), size) + offset;
        Vector3 v3 = Vector3.Scale(new Vector3(+0.5f, -0.5f, +0.5f), size) + offset;
        Vector3 v4 = Vector3.Scale(new Vector3(-0.5f, +0.5f, -0.5f), size) + offset;
        Vector3 v5 = Vector3.Scale(new Vector3(+0.5f, +0.5f, -0.5f), size) + offset;
        Vector3 v6 = Vector3.Scale(new Vector3(+0.5f, -0.5f, -0.5f), size) + offset;
        Vector3 v7 = Vector3.Scale(new Vector3(-0.5f, -0.5f, -0.5f), size) + offset;

        Vector3[] vSet = new Vector3[] {
            v0, v1, v2, v3, v4, v5, v6, v7
        };

        triangles.Clear();

        for (int dir = 0; dir < 6; dir++)
        {
            vertices.Add(vSet[faceNumber[dir][0]]);
            vertices.Add(vSet[faceNumber[dir][1]]);
            vertices.Add(vSet[faceNumber[dir][2]]);
            vertices.Add(vSet[faceNumber[dir][3]]);

            int vIdx = vertices.Count;

            triangles.Add(vIdx - 4 + 0);
            triangles.Add(vIdx - 4 + 1);
            triangles.Add(vIdx - 4 + 3);

            triangles.Add(vIdx - 4 + 1);
            triangles.Add(vIdx - 4 + 2);
            triangles.Add(vIdx - 4 + 3);
        }
    }

    void createProceduralMesh()
    {
        mesh.Clear();
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();

        Destroy(this.GetComponent<MeshCollider>());
        this.gameObject.AddComponent<MeshCollider>();
    }
}

 

Voxel의 좌표와 data에 대한 정보를 제공하는 메서드를 만들기 위해 Voxels class를 만들자.

public class Voxels
{
    int[,] voxelData = new int[,] {
        { 0, 1, 1, 0 }, // => x
        { 1, 0, 0, 1 }, // ↓ z
        { 1, 0, 0, 1 },
        { 1, 1, 1, 1 },
        { 1, 0, 0, 1 },
    };

    Vector3Int[] direction =
    {
        new Vector3Int(0, 0, +1), // FORWARD
        new Vector3Int(+1, 0, 0), // RIGHT
        new Vector3Int(0, +1, 0), // UP    
        new Vector3Int(0, 0, -1), // BACK   
        new Vector3Int(-1, 0, 0), // LEFT
        new Vector3Int(0, -1, 0), // DOWN    
    };

    public int zLength
    {
        get { return voxelData.GetLength(0); }
    }

    public int xLength
    {
        get { return voxelData.GetLength(1); }
    }

    public int getData(int x, int z)
    {
        return voxelData[z, x];
    }
    
    public bool isPossibleDrawing(int x, int z, int dir)
    {
        Vector3Int coord = new Vector3Int(x, 0, z) + direction[dir];

        if (coord.z < 0 || coord.z >= zLength
            || coord.x < 0 || coord.x >= xLength) return true;

        if (coord.y != 0) return true; // 2D

        return getData(coord.x, coord.z) == 0;
    }
}

 

voxelData는 복셀을 그려야할 좌표이며, 오른쪽이 x 축, 아래로 내려갈수록 z 축으로 정의한다.

그리고 각각의 길이를 알 수 있도록 z/xLength를 만든다.

    int[,] voxelData = new int[,] {
        { 0, 1, 1, 0 }, // => x
        { 1, 0, 0, 1 }, // ↓ z
        { 1, 0, 0, 1 },
        { 1, 1, 1, 1 },
        { 1, 0, 0, 1 },
    };
    
    public int zLength
    {
        get { return voxelData.GetLength(0); }
    }

    public int xLength
    {
        get { return voxelData.GetLength(1); }
    }

 

 오른쪽이 x 축, 아래로 내려갈수록 z 축이므로 voxelData[z, x]로 data를 얻어야 한다.

    public int getData(int x, int z)
    {
        return voxelData[z, x];
    }

 

큐브의 면을 그릴 수 있는 것은 voxelData의 좌표를 보고 판단할 수 있다.

2차원 좌표이므로 UP, DOWN인 경우는 반드시 면을 그리고, 그 외에는 옆에 큐브가 없다면 그린다.

그리고 좌표를 벗어나는 곳은 빈 공간이므로 그린다.

    Vector3Int[] direction =
    {
        new Vector3Int(0, 0, +1), // FORWARD
        new Vector3Int(+1, 0, 0), // RIGHT
        new Vector3Int(0, +1, 0), // UP    
        new Vector3Int(0, 0, -1), // BACK   
        new Vector3Int(-1, 0, 0), // LEFT
        new Vector3Int(0, -1, 0), // DOWN    
    };
  
    public bool isPossibleDrawing(int x, int z, int dir)
    {
        Vector3Int coord = new Vector3Int(x, 0, z) + direction[dir];

        if (coord.z < 0 || coord.z >= zLength
            || coord.x < 0 || coord.x >= xLength) return true;

        if (coord.y != 0) return true; // 2D

        return getData(coord.x, coord.z) == 0;
    }

 

ProceduralVoxels.cs에서 좌표와 방향에 대해 면 하나만 그리도록 함수를 따로 정리한다.

    Vector3[] baseVertices =
    {
        new Vector3(+0.5f, +0.5f, +0.5f),
        new Vector3(-0.5f, +0.5f, +0.5f),
        new Vector3(-0.5f, -0.5f, +0.5f),
        new Vector3(+0.5f, -0.5f, +0.5f),
        new Vector3(-0.5f, +0.5f, -0.5f),
        new Vector3(+0.5f, +0.5f, -0.5f),
        new Vector3(+0.5f, -0.5f, -0.5f),
        new Vector3(-0.5f, -0.5f, -0.5f),
    };

    void makeFace(int x, int z, int dir)
    {   
        for(int i = 0; i < 4; i++)
        {
            Vector3 v = Vector3.Scale(baseVertices[faceNumber[dir][i]], size) + offset;

            v.x += (x * size.x);
            v.z += (z * size.z);

            vertices.Add(v);
        }

        int vIdx = vertices.Count;

        triangles.Add(vIdx - 4 + 0);
        triangles.Add(vIdx - 4 + 1);
        triangles.Add(vIdx - 4 + 3);

        triangles.Add(vIdx - 4 + 1);
        triangles.Add(vIdx - 4 + 2);
        triangles.Add(vIdx - 4 + 3);
    }

 

setMeshData는 Voxels를 받은 후, 모든 (x, z)에 대해 6방향 검사하며 가능하다면 면을 그린다.

    void setMeshData(Voxels voxel)
    {
        vertices.Clear();
        triangles.Clear();

        for(int z = 0; z < voxel.zLength; z++)
        {
            for(int x = 0; x < voxel.xLength; x++)
            {
                if (voxel.getData(x, z) == 0) continue;

                for(int dir = 0; dir < 6; dir++)
                {
                    if(voxel.isPossibleDrawing(x, z, dir))
                    {
                        makeFace(x, z, dir);
                    }
                }
            }
        }
    }

 

게임을 실행하면 voxelData에 설정한대로 복셀 이미지가 만들어졌다.

 

콜라이더 또는 메시만 남겨보면 보이지 않는 부분에는 메시가 없는 것을 알 수 있다.

(잘 안보이겠지만... 큐브가 결합한 부분에 대각선이 없다.)

 

전체 코드는 다음과 같다.

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

public class Voxels
{
    int[,] voxelData = new int[,] {
        { 0, 1, 1, 0 }, // => x
        { 1, 0, 0, 1 }, // ↓ z
        { 1, 0, 0, 1 },
        { 1, 1, 1, 1 },
        { 1, 0, 0, 1 },
    };

    Vector3Int[] direction =
    {
        new Vector3Int(0, 0, +1), // FORWARD
        new Vector3Int(+1, 0, 0), // RIGHT
        new Vector3Int(0, +1, 0), // UP    
        new Vector3Int(0, 0, -1), // BACK   
        new Vector3Int(-1, 0, 0), // LEFT
        new Vector3Int(0, -1, 0), // DOWN    
    };

    public int zLength
    {
        get { return voxelData.GetLength(0); }
    }

    public int xLength
    {
        get { return voxelData.GetLength(1); }
    }

    public int getData(int x, int z)
    {
        return voxelData[z, x];
    }
    
    public bool isPossibleDrawing(int x, int z, int dir)
    {
        Vector3Int coord = new Vector3Int(x, 0, z) + direction[dir];

        if (coord.z < 0 || coord.z >= zLength
            || coord.x < 0 || coord.x >= xLength) return true;

        if (coord.y != 0) return true; // 2D

        return getData(coord.x, coord.z) == 0;
    }
}

[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class ProceduralVoxels : MonoBehaviour
{
    public Vector3 size = new Vector3(1.0f, 1.0f, 1.0f);
    public Vector3 offset = new Vector3(0, 0, 0);

    Mesh mesh;
    List<Vector3> vertices = new List<Vector3>();
    List<int> triangles = new List<int>();

    public enum Direction
    {
        FORWARD, // (0, 0, +1)
        RIGHT,   // (+1, 0, 0)
        UP,      // (0, +1, 0)
        BACK,    // (0, 0, -1)
        LEFT,    // (-1, 0, 0)
        DOWN     // (0, -1, 0)
    }

    public int[][] faceNumber =
    {
        new int[] {0, 1, 2, 3}, // FORWARD
        new int[] {5, 0, 3, 6}, // RIGHT 
        new int[] {5, 4, 1, 0}, // UP     
        new int[] {4, 5, 6, 7}, // BACK  
        new int[] {1, 4, 7, 2}, // LEFT 
        new int[] {3, 2, 7, 6}, // DOWN    
    };

    Vector3[] baseVertices =
    {
        new Vector3(+0.5f, +0.5f, +0.5f),
        new Vector3(-0.5f, +0.5f, +0.5f),
        new Vector3(-0.5f, -0.5f, +0.5f),
        new Vector3(+0.5f, -0.5f, +0.5f),
        new Vector3(-0.5f, +0.5f, -0.5f),
        new Vector3(+0.5f, +0.5f, -0.5f),
        new Vector3(+0.5f, -0.5f, -0.5f),
        new Vector3(-0.5f, -0.5f, -0.5f),
    };

    void OnValidate()
    {
        if (mesh == null) return;

        if (size.magnitude > 0 || offset.magnitude > 0)
        {
            setMeshData(new Voxels());
            createProceduralMesh();
        }
    }

    void Start()
    {
        mesh = GetComponent<MeshFilter>().mesh;

        setMeshData(new Voxels());
        createProceduralMesh();
    }

    void makeFace(int x, int z, int dir)
    {   
        for(int i = 0; i < 4; i++)
        {
            Vector3 v = Vector3.Scale(baseVertices[faceNumber[dir][i]], size) + offset;

            v.x += (x * size.x);
            v.z += (z * size.z);

            vertices.Add(v);
        }

        int vIdx = vertices.Count;

        triangles.Add(vIdx - 4 + 0);
        triangles.Add(vIdx - 4 + 1);
        triangles.Add(vIdx - 4 + 3);

        triangles.Add(vIdx - 4 + 1);
        triangles.Add(vIdx - 4 + 2);
        triangles.Add(vIdx - 4 + 3);
    }

    void setMeshData(Voxels voxel)
    {
        vertices.Clear();
        triangles.Clear();

        for(int z = 0; z < voxel.zLength; z++)
        {
            for(int x = 0; x < voxel.xLength; x++)
            {
                if (voxel.getData(x, z) == 0) continue;

                for(int dir = 0; dir < 6; dir++)
                {
                    if(voxel.isPossibleDrawing(x, z, dir))
                    {
                        makeFace(x, z, dir);
                    }
                }
            }
        }
    }

    void createProceduralMesh()
    {
        mesh.Clear();
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();

        Destroy(this.GetComponent<MeshCollider>());
        this.gameObject.AddComponent<MeshCollider>();
    }
}

참고

 

voxelData를 R 모양으로 만들어보자.

 

그러면 반대방향으로 보이게 된다.

 

좌표가 뒤집힌 것 처럼 보이는 이유는 voxelData의 x, z 축은 밑에서 바라본 방향이기 때문이다.

밑에서 바라보면 정상적으로 2차원 배열 data가 mapping된다.

 

여기서 y축에 대해 확장하면 3D 복셀을 만들 수 있다.

 

Unity Plus:

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Unity Pro:

 

Unity Pro

The complete solutions for professionals to create and operate.

unity.com

 

Unity 프리미엄 학습:

 

Unity Learn

Advance your Unity skills with live sessions and over 750 hours of on-demand learning content designed for creators at every skill level.

unity.com

반응형

댓글