해당 글은 CatlikeCoding의 튜토리얼을 실습하며 번역해놓은 글입니다. 번역 과정에서 제가 내용을 덧붙이거나 삭제한 부분이 존재합니다. 원본의 링크는 글 하단에 있습니다.
원본 글에 적혀있지 않은 내용을 추가할 때에는 (작성자 추가: ..내용)과 같은 형태로 작성하겠습니다.
요약
- seamless mesh로 큐브를 만듭니다
1. 큐브 합성하기
2D 격자 문제를 해결한 다음 단계는 3D 구조를 절차적으로 생성하는 것입니다. 큐브를 살펴보겠습니다. 개념적으로 큐브는 3D 볼륨을 둘러싸도록 배치되고 6개의 2D면으로 구성됩니다. 그리드의 6개의 인스턴스로 이 작업을 수행할 수 있습니다. 이렇게 하면 6개의 개별 메쉬로 구성된 큐브가 생깁니다. 보기에는 괜찮지만 실용적이지는 않습니다. Mesh.CombineMeshes를 통해 메쉬를 결합할 수 있지만, 전체 큐브를 한 번에 만드는 것이 더 낫습니다.
2. 큐브 정점 생성하기
큐브를 만들려면 새 컴포넌트 스크립트를 만들어야 합니다. 이전 튜토리얼의 코드를 재활용하여 만들어보겠습니다. 이전과 다른 점은 3차원이므로 zSize를 추가해야 합니다. 다시 한번 코루틴과 기즈모를 사용하여 큐브가 생성되는 과정을 시각화해보겠습니다.
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Cube : MonoBehaviour
{
public int xSize, ySize, zSize;
private Mesh mesh;
private Vector3[] vertices;
private void Awake ()
{
StartCoroutine(Generate());
}
private IEnumerator Generate()
{
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Cube";
var wait = new WaitForSeconds(0.05f);
yield return wait;
}
private void OnDrawGizmos ()
{
if (vertices == null) return;
Gizmos.color = Color.black;
for (int i = 0; i < vertices.Length; i++)
Gizmos.DrawSphere(vertices[i], 0.1f);
}
}
이제 이 컴포넌트를 추가하여 씬에 새 게임 오브젝트를 추가하고 이를 정육면체로 바꿀 수 있습니다. 또는 기존 오브젝트의 그리드 컴포넌트를 교체할 수도 있습니다.
정육면체의 곡지점을 추가하기 전에 먼저 정육면체에 필요한 점의 갯수를 알아야 합니다. 우리는 이미 하나의 면에 필요한 정점의 수를 알고 있습니다.
한 면의 버텍스의 수 = (#x + 1)(#y + 1)
따라서 여섯 개의 면의 정점의 수를 더하면 총 필요한 정점의 수를 구할 수 있습니다.
큐브의 버텍스의 수 = 2((#x + 1)(#y + 1) + (#x + 1)(#z + 1) + (#y + 1)(#z + 1))
그러나 면의 가장자리가 서로 닿으면 정점이 겹치게 되어 정점이 중복됩니다. 큐브의 각 모서리 정점은 세 배가 되고 가장자리를 따라 있는 모든 정점은 두배가 됩니다. 이것은 문제가 되지 않습니다. 사실 버텍스가 주옥되는 것은 매우 일반적인 현상입니다. 이것은 법선이 있는 메시에서 날카로운 모서리(sharp edges)를 만드는 데 사용되기 때문에 매우 일반적입니다. 따라서 6개의 면을 서로 완전히 분리하여 단일 배열로 결합하여 만들 수 있습니다.
(작성자 추가: 큐브와 같이 각진 오브젝트에서는 동일한 위치에 중복되는 정점 정보가 없으면, 각 모서리와 맞닿아있는 면의 라이팅 계산을 자연스럽게 하기 힘듭니다.)
하지만 우리는 이미 격자를 만드는 방법을 알고 있기 때문에 그렇게 하지 않을 것입니다. 우리의 큐브에는 중복이 되는 정점이 없습니다. 그렇게 하는 것이 더 흥미롭습니다. 그렇다면 얼마나 많은 버텍스가 필요할까요? 유형별로 나눠보겠습니다. 먼저 모서리 정점이 8개 필요합니다. 그런 다음 각 방향(x, y, z)에 4개씩 12개의 엣지가 있습니다. 모서리를 포함하지 않으므로 각 가장자리에는 지정된 크기에서 1을 뺀 수만큼의 정점이 있습니다.
(작성자 추가: 세 점이 만나는 부분을 모서리, 두 점이 만나며 삼각형의 변을 이루는 부분을 엣지라고 하였습니다.)
큐브의 엣지의 버텍스의 수 = 4(#x + #y + #z - 3)
나머지 꼭지점은 면 안에 있는 꼭지점입니다. 이는 정점이 중복된 정육면체의 크기가 2만큼 줄어든 것과 같습니다.
큐브 면 안의 버텍스의 수 = 2((#x−1)(#y−1)+(#x−1)(#z−1)+(#y−1)(#z−1))
이제 필요한 버텍스의 수를 알 수 있습니다.
private IEnumerator Generate()
{
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Cube";
var wait = new WaitForSeconds(0.05f);
int cornerVertices = 8;
int edgeVertices = (xSize + ySize + zSize - 3) * 4;
int faceVertices = (
(xSize - 1) * (ySize - 1) +
(xSize - 1) * (zSize - 1) +
(ySize - 1) * (zSize - 1)) * 2;
vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];
yield return wait;
}
private IEnumerator Generate()
....
int v = 0;
for (int y = 0; y <= ySize; y++)
{
for (int x = 0; x <= xSize; x++)
{
vertices[v++] = new Vector3(x, y, 0);
yield return wait;
}
for (int z = 1; z <= zSize; z++)
{
vertices[v++] = new Vector3(xSize, y, z);
yield return wait;
}
for (int x = xSize - 1; x >= 0; x--)
{
vertices[v++] = new Vector3(x, y, zSize);
yield return wait;
}
for (int z = zSize - 1; z > 0; z--)
{
vertices[v++] = new Vector3(0, y, z);
yield return wait;
}
}
for (int z = 1; z < zSize; z++)
{
for (int x = 1; x < xSize; x++)
{
vertices[v++] = new Vector3(x, ySize, z);
yield return wait;
}
}
for (int z = 1; z < zSize; z++)
{
for (int x = 1; x < xSize; x++)
{
vertices[v++] = new Vector3(x, 0, z);
yield return wait;
}
}
}
사각형의 버텍스를 추가하였습니다.
3. 삼각형 생성하기
이제 정점이 올바르게 배치되고 정점이 배치되는 순서에 익숙해졌으므로 삼각형으로 넘어갈 수 있습니다. 이를 준비하기 위해 코루틴을 제거하고 정점과 삼각형을 생성하는 별도의 메서드를 추가했습니다. 물론 정점은 메쉬에 할당해야 합니다.
private void Generate()
{
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Cube";
CreateVertices();
CreateTriangles();
}
private void CreateVertices()
{
...
mesh.vertices = vertices;
}
private void CreateTriangles() { }
단일 쿼드를 생성하는 것은 격자를 생성할때와 완전히 동일합니다. 여러 곳에서 쿼드를 만들게 될 것잉므로 이에 대한 메서드를 만드는 것이 좋습니다.
private static int SetQuad (IList<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;
}
정점과 달리 삼각형의 수는 단순히 여섯 개의 면을 합친 수와 같습니다. 공유 정점을 사용하는지 여부는 중요하지 않습니다. 첫 번째 삼각형 행을 만드는 것은 그리드와 다시 한 번 동일합니다. 지금까지 유일한 차이점은 다음 행의 정점에 대한 오프셋이 정점의 전체 링과 같다는 것입니다. (링은 아래 코드참조)
private void CreateTriangles()
{
int quads = (xSize * ySize + xSize * zSize + ySize * zSize) * 2;
int[] triangles = new int[quads * 6];
int ring = (xSize + zSize) * 2;
int t = 0, v = 0;
for (int y = 0; y < ySize; y++, v++)
{
for (int q = 0; q < ring - 1; q++, v++)
{
t = SetQuad(triangles, t, v, v + 1, v + ring, v + ring + 1);
}
t = SetQuad(triangles, t, v, v - ring + 1, v + ring, v + 1);
}
mesh.triangles = triangles;
}
모든 링의 쿼드를 렌더링하기 위한 코드입니다. (작성자 추가: 링은 y축의 수직인 방향의 평면을 가지고 있으 같은 y값을 가진 쿼드의 집합을 의미합니다)
링의 마지막 쿼드는(q = ring-1) 이전 쿼드들과 다르게 다음 쿼드의 두 정점이 아닌, 링의 시작 부분의 정점과 연결되야 하기 때문에 별도로 처리하였습니다.
아쉽게도 윗면과 아랫면은 링을 렌더링할 때와 달리 그렇게 간단하지 않습니다.이 버텍스 레이아웃들은 링에 둘러쌓인 그리드와 같습니다.
윗면부터 시작하여 각 면에 고유한 메서드를 구현하겠습니다.
private void CreateTriangles()
{
...
t = CreateTopFace(triangles, t, ring);
mesh.triangles = triangles;
}
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);
return t;
}
첫 번째 행은 익숙한 패턴을 따릅니다. 내부 격자의 첫 번째 행이 나선이 끝난 직후에 추가되었기 때문에 작동합니다. 하지만 마지막 쿼드의 네 번째 꼭지점은 고리가 위쪽으로 구부러지는 곳이기 때문에 다릅니다.
다음 행부터는 더 복잡해집니다. 행의 첫번쨰 쿼드를 렌더링하기 위해 링 위에 있는 행의 최소 정점 인덱스를 추적해서 사용해야 합니다. 추적해야 할 다른 인덱스는 그리드의 중간 부분에 대한 것입니다. 행의 중간 부분은 그리드를 구성할때와 매우 비슷합니다. 행의 마지막 쿼드는 다시 한번 바깥쪽 링을 처리해야 하므로 최대 정점도 추적해야 합니다.
이를 루프로 전환하여 마지막 행을 제외한 모든 행을 처리할 수 있습니다. 반복할 때마다 중간 및 최대 정점 인덱스가 증가해야 합니다. 대신 최소 정점 인덱스는 링의 방향 때문에 감소합니다.
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 < zSize - 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);
}
return t;
}
제일 아랫줄의 행은 다음과 같이 생성할 수 있습니다.
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);
아랫 면은 동일한 접근 방식을 사용하면서 설정값만 바꿔주면 됩니다. 아랫면은 윗면과의 몇 가지 차이점이 있습니다. 정점 인덱스가 달라서 첫 번째 행이 약간 더 복잡해집니다. 또한 쿼드 정점의 방향을 변경하여 위쪽이 아닌 아랫쪽을 향하도록 해야 합니다.
private void CreateTriangles()
{
...
t = CreateTopFace(triangles, t, ring);
t = CreateBottomFace(triangles, t, ring);
mesh.triangles = triangles;
}
private int CreateTopFace (int[] triangles, int t, int ring)
{
...
}
private int CreateBottomFace (int[] triangles, int t, int ring)
{
int v = 1;
int vMid = 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++)
t = SetQuad(triangles, t, vMid, vMid + 1, v, v + 1);
t = SetQuad(triangles, t, vMid, v + 2, v, v + 1);
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, vMid + xSize - 1, vMin + 1, vMid);
for (int x = 1; x < xSize - 1; x++, vMid++)
t = SetQuad( triangles, t, vMid + xSize - 1, vMid + xSize, vMid, vMid + 1);
t = SetQuad(triangles, t, vMid + xSize - 1, vMax + 1, vMid, vMax);
}
int vTop = vMin - 1;
t = SetQuad(triangles, t, vTop + 1, vTop, vTop + 2, vMid);
for (int x = 1; x < xSize - 1; x++, vTop--, vMid++)
t = SetQuad(triangles, t, vTop, vTop - 1, vMid, vMid + 1);
t = SetQuad(triangles, t, vTop, vTop - 1, vMid, vTop - 2);
return t;
}
'유니티 > Catlikecoding' 카테고리의 다른 글
[메시 기초] 메시 변형 (Catlikecoding) (0) | 2023.08.11 |
---|---|
[메시 기초] 큐브 구체 (Catlikecoding) (0) | 2023.08.07 |
[메시 기초] 둥근 큐브 렌더링 (Catlikecoding) (0) | 2023.08.06 |
[메시 기초] 절차적 그리드 (Catlikecoding) (0) | 2023.08.04 |
[의사난수노이즈] 해시 - 시각화(Catlikecoding) (0) | 2022.04.21 |