Unity 큐브 만들기

Ricon·2025년 10월 17일

Unity

목록 보기
7/7
using System;
using Cysharp.Threading.Tasks; // UniTask 사용을 위한 네임스페이스 (현재 코드에서는 사용되지 않음)
using UnityEngine;

// MeshFilter와 MeshRenderer 컴포넌트가 자동으로 추가되도록 보장합니다.
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class Cube : MonoBehaviour
{
    // 인스펙터 창에서 큐브의 크기를 조절할 수 있는 변수들
    public int xSize, ySize, zSize;

    // 메시를 구성하는 모든 정점(vertex)의 위치 정보를 저장할 배열
    private Vector3[] m_vertices;
    // 생성된 메시 데이터를 담을 Mesh 객체
    private Mesh m_mesh;

    // 게임이 시작될 때 한 번 호출되는 Unity 생명주기 메서드
    private void Awake()
    {
        Generate(); // 메시 생성 프로세스를 시작합니다.
    }

    /// <summary>
    /// 절차적(Procedural) 큐브 메시 생성을 총괄하는 메인 메서드입니다.
    /// </summary>
    private async void Generate() // async 키워드는 있지만 await가 없어 동기적으로 실행됩니다.
    {
        // 새로운 Mesh 객체를 생성합니다.
        m_mesh = new Mesh();
        // 현재 게임 오브젝트의 MeshFilter 컴포넌트에 생성한 메시를 할당합니다.
        GetComponent<MeshFilter>().mesh = m_mesh;
        // 메시의 이름을 지정하여 씬(Scene)에서 식별하기 용이하게 합니다.
        m_mesh.name = "Procedural Cube";

        // 1. 정점 생성
        CreateVertices();
        // 2. 정점을 연결하여 삼각형(면) 생성
        CreateTriangles();
    }

    /// <summary>
    /// 큐브를 구성하는 모든 정점(vertex)의 위치를 계산하고 배열에 저장합니다.
    /// </summary>
    private void CreateVertices()
    {
        // 정점의 총 개수를 계산합니다. 정점을 공유하여 중복을 최소화하는 방식입니다.
        int cornerVertices = 8; // 8개의 모서리 꼭짓점
        int edgeVertices = (xSize - 1 + ySize - 1 + zSize - 1) * 4; // 각 축의 모서리 선 위에 있는 정점
        int faceVertices = ( // 각 면의 내부에 있는 정점
            (xSize - 1) * (ySize - 1) +
            (xSize - 1) * (zSize - 1) +
            (ySize - 1) * (zSize - 1)) * 2;
        
        m_vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];

        int v = 0; // m_vertices 배열의 현재 인덱스

        // --- 옆면의 정점 링(Ring) 생성 ---
        // Y축을 따라 아래층부터 위층까지 한 층씩 정점 링을 쌓아 올립니다.
        for (int y = 0; y <= ySize; y++)
        {
            // 각 층(Y)마다 사각형 테두리를 따라 정점을 생성합니다.
            // 1. 앞면 (z=0): x가 0에서 xSize까지 증가
            for (int x = 0; x <= xSize; x++) m_vertices[v++] = new Vector3(x, y, 0);
            // 2. 오른쪽면 (x=xSize): z가 1에서 zSize까지 증가
            for (int z = 1; z <= zSize; z++) m_vertices[v++] = new Vector3(xSize, y, z);
            // 3. 뒷면 (z=zSize): x가 xSize-1에서 0까지 감소
            for (int x = xSize - 1; x >= 0; x--) m_vertices[v++] = new Vector3(x, y, zSize);
            // 4. 왼쪽면 (x=0): z가 zSize-1에서 1까지 감소
            for (int z = zSize - 1; z > 0; z--) m_vertices[v++] = new Vector3(0, y, z);
        }

        // --- 윗면과 아랫면의 내부 정점 생성 ---
        // 윗면(y=ySize)의 테두리를 제외한 내부 정점들을 생성합니다.
        for (int z = 1; z < zSize; z++)
        {
            for (int x = 1; x < xSize; x++)
            {
                m_vertices[v++] = new Vector3(x, ySize, z);
            }
        }
        // 아랫면(y=0)의 테두리를 제외한 내부 정점들을 생성합니다.
        for (int z = 1; z < zSize; z++)
        {
            for (int x = 1; x < xSize; x++)
            {
                m_vertices[v++] = new Vector3(x, 0, z);
            }
        }

        // 계산된 모든 정점 정보를 실제 메시 데이터에 할당합니다.
        m_mesh.vertices = m_vertices;
    }

    /// <summary>
    /// 생성된 정점들을 연결하여 큐브의 면(삼각형)을 만듭니다.
    /// </summary>
    private void CreateTriangles()
    {
        // 큐브 전체 면적에 필요한 사각형(quad) 개수를 기반으로 triangles 배열 크기를 계산합니다.
        // 사각형 하나는 삼각형 2개, 삼각형 하나는 인덱스 3개가 필요하므로 사각형 하나당 6개의 인덱스가 필요합니다.
        int quads = (xSize * ySize + xSize * zSize + ySize * zSize) * 2;
        int[] triangles = new int[quads * 6];
        
        // 수평 방향의 정점 링 하나를 구성하는 정점의 총 개수
        int ring = (xSize + zSize) * 2;
        
        // t: triangles 배열의 현재 인덱스, v: vertices 배열의 현재 인덱스
        int t = 0, v = 0;

        // --- 옆면 생성 ---
        // Y축을 따라 아래층부터 위층까지 한 층씩 벽면을 만듭니다.
        for (int y = 0; y < ySize; y++, v++)
        {
            // 한 층의 링을 따라 돌면서 사각형 벽을 만듭니다.
            for (int q = 0; q < ring - 1; q++, v++)
            {
                // 현재 층의 두 정점(v, v+1)과 바로 위층의 두 정점(v+ring, v+ring+1)을 연결해 사각형을 만듭니다.
                t = SetQuad(triangles, t, v, v + ring, v + 1, v + ring + 1);
            }
            // 링의 마지막 사각형을 생성하여 시작점과 끝점을 연결합니다. (링을 닫음)
            t = SetQuad(triangles, t, v, v + ring, v - ring + 1, v + 1);
        }

        // --- 윗면과 아랫면 생성 ---
        t = CreateTopFace(triangles, t, ring);
        t = CreateBottomFace(triangles, t, ring);

        // 완성된 삼각형 정보를 실제 메시 데이터에 할당합니다.
        m_mesh.triangles = triangles;
        m_mesh.RecalculateNormals(); // 조명을 올바르게 받기 위해 법선 벡터(Normal)를 자동 계산합니다.
    }
    
    /// <summary>
    /// 큐브의 윗면(Top Face)을 구성하는 삼각형들을 생성합니다.
    /// </summary>
    private int CreateTopFace(int[] triangles, int t, int ring)
    {
        // --- 첫 번째 줄 생성 (옆면 테두리와 윗면 내부 격자 연결) ---
        int v = ring * ySize; // 윗면 링의 시작 정점 인덱스
        for (int x = 0; x < xSize - 1; x++, v++)
        {
            t = SetQuad(triangles, t, v, v + 1, v + ring - 1, v + ring);
        }
        t = SetQuad(triangles, t, v, v + 1, v + ring - 1, v + 2); // 첫 줄의 마지막 조각

        // --- 중간 줄들 생성 (윗면 내부 격자 채우기) ---
        int vMin = ring * (ySize + 1) - 1; // 왼쪽 테두리 정점
        int vMid = vMin + 1; // 내부 격자 시작 정점
        int vMax = v + 2; // 오른쪽 테두리 정점

        for (int z = 1; z < ySize - 1; z++, vMin--, vMid++, vMax++)
        {
            t = SetQuad(triangles, t, vMin, vMid, vMin - 1, vMid + xSize - 1); // 왼쪽 테두리 연결
            for (int x = 1; x < xSize - 1; x++, vMid++)
            {
                t = SetQuad(triangles, t, vMid, vMid + 1, vMid + xSize - 1, vMid + xSize); // 내부 격자
            }
            t = SetQuad(triangles, t, vMid, vMax, vMid + xSize - 1, vMax + 1); // 오른쪽 테두리 연결
        }
        
        // --- 마지막 줄 생성 ---
        int vTop = vMin - 2; // 맨 뒤쪽 테두리 정점
        t = SetQuad(triangles, t, vMin, vMid, vTop + 1, vTop); // 마지막 줄의 왼쪽
        for (int x = 1; x < xSize - 1; x++, vTop--, vMid++)
        {
            t = SetQuad(triangles, t, vMid, vMid + 1, vTop, vTop - 1); // 마지막 줄의 중간
        }
        t = SetQuad(triangles, t, vMid, vTop - 2, vTop, vTop - 1); // 마지막 줄의 오른쪽 (면 닫기)
        
        return t;
    }

    /// <summary>
    /// 큐브의 아랫면(Bottom Face)을 구성하는 삼각형들을 생성합니다.
    /// 윗면과 로직은 유사하지만, 면이 아래를 향하도록 정점 순서를 반대로 지정합니다.
    /// </summary>
    private int CreateBottomFace(int[] triangles, int t, int ring)
    {
        int v = 1; // 아랫면 링의 시작 정점
        int vMid = m_vertices.Length - (xSize - 1) * (zSize - 1); // 아랫면 내부 격자 시작 정점
        t = SetQuad(triangles, t, ring - 1, vMid, 0, 1); // 첫 줄의 첫 조각 (정점 순서 반대)
        for (int x = 1; x < xSize - 1; x++, v++, vMid++)
        {
            // SetQuad의 파라미터 순서를 조정하여 면의 방향을 뒤집습니다.
            t = SetQuad(triangles, t, v, vMid, v + 1, vMid + 1); 
        }
        t = SetQuad(triangles, t, v, vMid, v + 1, v + 2);

        int vMin = ring - 2;
        vMid -= xSize - 2;
        int vMax = v + 2;

        for (int z = 1; z < zSize - 1; z++, vMin--, vMid++, vMax++)
        {
            t = SetQuad(triangles, t, vMin + 1, vMid, vMin, vMid + xSize - 1);
            for (int x = 1; x < xSize - 1; x++, vMid++)
            {
                t = SetQuad(triangles, t, vMid, vMid + 1, vMid + xSize - 1, vMid + xSize);
            }
            t = SetQuad(triangles, t, vMid, vMax, vMid + xSize - 1, vMax + 1);
        }

        int vTop = vMin - 1;
        t = SetQuad(triangles, t, vTop + 2, vMid, vTop + 1, vTop);
        for (int x = 1; x < xSize - 1; x++, vTop--, vMid++)
        {
            t = SetQuad(triangles, t, vMid, vMid + 1, vTop, vTop - 1);
        }
        t = SetQuad(triangles, t, vMid, vTop - 2, vTop, vTop - 1);

        return t;
    }
    
    /// <summary>
    /// 4개의 정점 인덱스를 받아 사각형(삼각형 2개)을 구성하고 triangles 배열에 추가하는 헬퍼 메서드.
    /// </summary>
    /// <param name="v00">아래-왼쪽 정점</param>
    /// <param name="v10">위-왼쪽 정점</param>
    /// <param name="v01">아래-오른쪽 정점</param>
    /// <param name="v11">위-오른쪽 정점</param>
    /// <returns>다음 데이터를 추가할 triangles 배열의 인덱스(i + 6)를 반환합니다.</returns>
    private static int SetQuad(int[] triangles, int i, int v00, int v10, int v01, int v11)
    {
        triangles[i] = v00;
        triangles[i + 1] = triangles[i + 4] = v01; // 첫 번째 삼각형, 두 번째 삼각형에서 공유
        triangles[i + 2] = triangles[i + 3] = v10; // 첫 번째 삼각형, 두 번째 삼각형에서 공유
        triangles[i + 5] = v11;
        return i + 6;
    }

    /// <summary>
    /// Unity 에디터의 씬(Scene) 뷰에서만 호출되는 메서드.
    /// 생성된 정점들의 위치를 시각적으로 확인하기 위해 사용됩니다. (디버깅용)
    /// </summary>
    private void OnDrawGizmos()
    {
        if (m_vertices == null) return; // 정점이 없으면 실행하지 않음

        Gizmos.color = Color.black;
        for (int i = 0; i < m_vertices.Length; i++)
        {
            // 각 정점의 위치에 작은 검은색 구체를 그립니다.
            Gizmos.DrawSphere(m_vertices[i], 0.1f);
        }
    }
}

📜 절차적 큐브(Procedural Cube) 스크립트 설명서

이 문서는 Unity에서 정점을 공유하는 최적화된 방식의 큐브 메시를 절차적으로 생성하는 Cube.cs 스크립트의 작동 원리를 설명합니다.

🧠 핵심 전략

단순히 6개의 평면을 만들어 붙이는 방식이 아닙니다. 이 스크립트는 모든 모서리와 꼭짓점의 정점을 공유하여 중복을 없애고 메모리를 효율적으로 사용하는 고급 기법을 사용합니다.

생성 과정은 크게 세 단계로 나뉩니다.

옆면(Side Walls) 생성: 사각형의 정점 띠(Ring)를 아래에서 위로 쌓아 올려 속이 빈 사각기둥을 만듭니다.

윗면/아랫면 내부 정점 생성: 뚜껑에 해당하는 평평한 격자(Grid) 정점들을 만듭니다.

면(Triangle) 생성: 생성된 정점들을 규칙에 따라 연결하여 옆면, 윗면, 아랫면을 완성합니다.

🏗️ 코드 분석

1. CreateVertices() - 정점 생성

이 메서드는 큐브를 구성하는 모든 점(Vertex)의 3D 좌표를 계산하고 배열에 순서대로 저장합니다. 이 순서는 삼각형을 만들 때 매우 중요합니다.

정점 개수 계산: 먼저 큐브의 크기(xSize, ySize, zSize)를 바탕으로 꼭짓점(8개), 모서리, 면 내부에 필요한 총 정점 수를 정확히 계산하여 배열의 크기를 할당합니다.

옆면 링(Ring) 생성: for (int y ...) 루프는 큐브의 옆면을 구성하는 정점들을 생성합니다. Y값을 0부터 ySize까지 증가시키며, 각 Y 레벨마다 사각형 테두리를 따라 한 바퀴 도는 정점 "링"을 만듭니다. 이 링은 4개의 직선 구간으로 구성됩니다 (앞 → 오른쪽 → 뒤 → 왼쪽).

윗면/아랫면 내부 정점 생성: 옆면 링을 모두 만든 후, 배열의 맨 뒤에 윗면과 아랫면의 테두리를 제외한 내부 격자 정점들을 추가합니다.

private void CreateVertices()
{
    // ... 정점 개수 계산 ...
    m_vertices = new Vector3[/* 계산된 크기 */];

    int v = 0;
    // 옆면의 정점 링(Ring)을 Y축으로 쌓아 올립니다.
    for (int y = 0; y <= ySize; y++)
    {
        // 앞면 -> 오른쪽면 -> 뒷면 -> 왼쪽면 순서로 한 바퀴 돕니다.
        for (int x = 0; x <= xSize; x++) m_vertices[v++] = new Vector3(x, y, 0);
        for (int z = 1; z <= zSize; z++) m_vertices[v++] = new Vector3(xSize, y, z);
        for (int x = xSize - 1; x >= 0; x--) m_vertices[v++] = new Vector3(x, y, zSize);
        for (int z = zSize - 1; z > 0; z--) m_vertices[v++] = new Vector3(0, y, z);
    }

    // 윗면(y=ySize)과 아랫면(y=0)의 내부 격자 정점을 배열 끝에 추가합니다.
    for (int z = 1; z < zSize; z++) { /* ... 윗면 내부 정점 ... */ }
    for (int z = 1; z < zSize; z++) { /* ... 아랫면 내부 정점 ... */ }
    
    m_mesh.vertices = m_vertices;
}

2. CreateTriangles() - 삼각형 생성

CreateVertices()에서 만든 정점들을 올바른 순서로 연결하여 실제 눈에 보이는 면(삼각형)을 만듭니다.

옆면 생성: Y축을 따라 한 층씩 올라가며, 현재 층의 정점 링과 바로 위층의 정점 링을 "지퍼처럼" 엮어서 사각형 벽을 만듭니다. ring 변수는 한 층의 정점 개수를 의미하며, v + ring은 바로 위층의 같은 위치에 있는 정점을 가리킵니다.

뚜껑 생성: 옆면을 모두 만든 후, CreateTopFace와 CreateBottomFace 메서드를 호출하여 위아래 뚜껑을 덮습니다.

private void CreateTriangles()
{
    // ... triangles 배열 초기화 ...
    int ring = (xSize + zSize) * 2;
    int t = 0, v = 0;

    // Y축으로 한 층씩 올라가며 옆면을 만듭니다.
    for (int y = 0; y < ySize; y++, v++)
    {
        // 링을 따라 돌면서 사각형 벽을 만듭니다.
        for (int q = 0; q < ring - 1; q++, v++)
        {
            t = SetQuad(triangles, t, v, v + ring, v + 1, v + ring + 1);
        }
        // 링의 마지막 조각을 이어 붙여 링을 닫습니다.
        t = SetQuad(triangles, t, v, v + ring, v - ring + 1, v + 1);
    }

    // 위아래 뚜껑을 만듭니다.
    t = CreateTopFace(triangles, t, ring);
    t = CreateBottomFace(triangles, t, ring);

    m_mesh.triangles = triangles;
    m_mesh.RecalculateNormals(); // 조명 처리를 위한 법선 벡터 자동 계산
}

3. CreateTopFace() & CreateBottomFace() - 뚜껑 생성 🧩

이 메서드들은 큐브의 위아래를 덮는 복잡한 로직을 담당합니다.

전략: 옆면의 가장 위/아래 테두리 정점들과, CreateVertices에서 따로 만들었던 내부 격자 정점들을 함께 사용하여 평평한 면을 "짜깁기(stitching)"합니다.

CreateBottomFace의 특징: 3D 그래픽에서 면의 앞면과 뒷면은 정점을 연결하는 순서(Winding Order)에 따라 결정됩니다. 아랫면이 아래를 보도록(법선 벡터가 -Y 방향을 향하도록) SetQuad에 넘겨주는 정점 순서를 CreateTopFace와 다르게 조정합니다.

4. SetQuad() & OnDrawGizmos() - 헬퍼 및 디버깅

SetQuad(): 4개의 정점 인덱스를 받아서 사각형(삼각형 2개)을 만드는 반복적인 작업을 처리하는 유용한 헬퍼(Helper) 함수입니다. 코드를 간결하게 만들어줍니다.

OnDrawGizmos(): 에디터에서만 작동하며, 생성된 모든 정점의 위치에 검은색 구체를 그려주어 메시가 올바르게 생성되고 있는지 시각적으로 디버깅할 수 있게 돕습니다.

0개의 댓글