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);
}
}
}
이 문서는 Unity에서 정점을 공유하는 최적화된 방식의 큐브 메시를 절차적으로 생성하는 Cube.cs 스크립트의 작동 원리를 설명합니다.
단순히 6개의 평면을 만들어 붙이는 방식이 아닙니다. 이 스크립트는 모든 모서리와 꼭짓점의 정점을 공유하여 중복을 없애고 메모리를 효율적으로 사용하는 고급 기법을 사용합니다.
생성 과정은 크게 세 단계로 나뉩니다.
옆면(Side Walls) 생성: 사각형의 정점 띠(Ring)를 아래에서 위로 쌓아 올려 속이 빈 사각기둥을 만듭니다.
윗면/아랫면 내부 정점 생성: 뚜껑에 해당하는 평평한 격자(Grid) 정점들을 만듭니다.
면(Triangle) 생성: 생성된 정점들을 규칙에 따라 연결하여 옆면, 윗면, 아랫면을 완성합니다.
이 메서드는 큐브를 구성하는 모든 점(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;
}
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(); // 조명 처리를 위한 법선 벡터 자동 계산
}
이 메서드들은 큐브의 위아래를 덮는 복잡한 로직을 담당합니다.
전략: 옆면의 가장 위/아래 테두리 정점들과, CreateVertices에서 따로 만들었던 내부 격자 정점들을 함께 사용하여 평평한 면을 "짜깁기(stitching)"합니다.
CreateBottomFace의 특징: 3D 그래픽에서 면의 앞면과 뒷면은 정점을 연결하는 순서(Winding Order)에 따라 결정됩니다. 아랫면이 아래를 보도록(법선 벡터가 -Y 방향을 향하도록) SetQuad에 넘겨주는 정점 순서를 CreateTopFace와 다르게 조정합니다.
SetQuad(): 4개의 정점 인덱스를 받아서 사각형(삼각형 2개)을 만드는 반복적인 작업을 처리하는 유용한 헬퍼(Helper) 함수입니다. 코드를 간결하게 만들어줍니다.
OnDrawGizmos(): 에디터에서만 작동하며, 생성된 모든 정점의 위치에 검은색 구체를 그려주어 메시가 올바르게 생성되고 있는지 시각적으로 디버깅할 수 있게 돕습니다.