본문 바로가기

Unity/3D

Unity C# Tutorials - Rounded Cube(Building in 3D) 번역본

반응형

원본 : https://catlikecoding.com/unity/tutorials/rounded-cube/

 

Rounded Cube, a Unity C# Tutorial

A Unity C# scripting tutorial in which we'll create a rounded cube.

catlikecoding.com

참고 : 이 글은 어디까지나 번역기를 이용해서 부자연스러운 부분을 최대한 바꿔본 것이므로 정확하지 않을 수 있습니다. 수정 요청은 댓글로 부탁드립니다.

 

제목 : 유니티 둥근 모서리 큐브 만들기

 

Rounded Cube

 

Building in 3D

  • 완벽한 메쉬(seamless mesh/완전히 이어져 있는 메쉬)로 큐브를 만듭니다.
  • 큐브에 둥근 모서리를 추가합니다.
  • 법선을 정의합니다.
  • 하위 메쉬를 사용합니다.
  • 커스텀 셰이더를 생성합니다.
  • 원시 충돌자를 결합합니다.

이 튜토리얼에서는 하나의 메쉬로 둥근 큐브를 생성합니다.

이 자습서는 Procedural Grid(절차적 격자)를 따르며, 유니티 5.0.1 이상을 위해 만들어졌습니다.

 

다양한 모양의 Rounded Cube

 

1. 큐브합성

(읽기전 참고 : xSize는 가로, ySize 높이, Ring'링'은 완전히 닫혀서 이어진 옆면을 의미합니다.)

2D 그리드를 처리한 후, 다음 논리적 단계는 절차 적으로 3D 구조를 생성하는 것 입니다. 큐브를 개념적으로 살펴봅시다. 3D 볼륨을 둘러싸도록 위치 및 회전되는 6개의 2D면으로 구성되빈다. 그리드의 6가지 인스턴스로 이 작업을 수행 할 수 있습니다.

 

Procedural Grid에서 만든 그리드는 XY(축을 기반으로 한) 평면에 있고 -Z 방향으로 보이도록 되어있습니다. 이것이 큐브의 -Z면입니다. +Z면을 복제하고, Y축으로 180 °로 설정하고, 양쪽면이 일직선상에 있도록 위치하여 +Z면을 생성 할 수 있습니다.

 

-X 및 +X면도 같은 방법으로 생성되지만 Y회전은 90 °및 270 °입니다. 원하는 경우 이러한 면에 Z면과 다른 xSize를 지정할 수도 있지만 ySize는 일치해야합니다.(단, xSize가 다르다면  +Z, -Z면의 간격을 조절해야합니다.) 4개의 면이 정렬되면 닫힌 링(고리)를 형성하게 됩니다. (Ring, 고리는 기억하십시오. 코드에서도 등장합니다.)

 

-Y 및 +Y면은 270 ° 및 90 °의 X회전으로 만들어집니다. 그들의 xSize는  Z면과 일치해야 하며, Ysize는 X의 xSize와 동일해야합니다. (위,아랫면이므로 좌우면의 가로길이가 이 면의 세로길이, 앞뒷면의 가로길이가 이 면의 가로길이가 된다는 것 입니다.)

 

6개의 개별 그리드로 만들어진 큐브

아래 파일은 6개의 분리 된 메쉬로 구성된 큐브를 제공합니다. 잘 보이고 좋은 참고 자료이지만, 별로 실용적이지 않습니다. 우리는 메시를 Mesh.CombineMeshes()를 통해 결합 할 수 있지만, 전체 큐브를 한 번에 만들 수도 있습니다. (이 자습서가 그러합니다.)

파일 : 유니티 패키지

불러오는 중입니다...

2. 큐브 정점 만들기

(읽기 전 참고사항 : zSize는 3차원 세로, Vertex는 정점, Edge는 모서리, Normal은 법선을 의미합니다.)

자체 큐브를 만들려면 새 컴포넌트 스크립트를 만들어야 합니다. 이전 튜토리얼의 일부 코드를 재활용하여 부트 스트랩 해 보겠습니다. 지금은 새로운 차원이 3 차원이므로 zSize를 추가해야 합니다. coroutine과 gizmos를 다시 사용하여 진행 상황을 시각화 합니다.

using UnityEngine;
using System.Collections;

[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";
		WaitForSeconds 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);
		}
	}
}

이제 새로운 GameObject를 Scene에 추가하고 이 컴포넌트를 추가하여 큐브로 만들 수 있습니다. 또는 기존 객체의 그리드 구성 요소를 대체해도 됩니다.

 

세 번째 차원 추가

큐브의 정점을 추가하기 전에 큐브에 필요한 정점 개수를 알아야 합니다. 이미 한면에 필요한 정점의 양을 알고 있습니다.

( # x + 1 ) ( # y + 1 )

->#축은 위의 세 차원 변수인 축Size와 동일합니다. 즉 xSize * ySize = 한 면의 X축과 Y축을 기반으로 만들어진 면 입니다.

따라서 이 6개의 얼굴을 합쳐 합계를 구할 수 있습니다.

2((#x+1)(#y+1)+(#x+1)(#z+1)+(#y+1)(#z+1))

-> XY, XZ, YZ 면을 구했다면 -XY, -XZ, -YZ는 각각 대응되는 면과 같은 크기입니다. 따라서 *2를 하면 전체 합계입니다.

그러나 면의 모서리가 서로 닿으면 꼭지점이 겹쳐져 중복 된 꼭지점이 생깁니다. 각 큐브의 모서리 꼭지점은 3배가 되고 가장자리를 따라 모든 꼭지점은 두배가 됩니다.

(그림을 그려서 해보시면 쉽게 이해 됩니다.)

 

겹친면 정점

실질적으로 이렇게 겹친다 해서 아무 문제도 되지 않습니다. 실제로 정점 복제는 법선이 있는 메쉬에서 날카로운 모서리를 만드는데 사용되므로 매우 일반적입니다. 따라서 우리는 하나의 배열로 결합 된 서로 완전히 다른 6개의 면을 만들 수 있습니다.

 

하지만 그리드를 만드는 방법을 이미 알고 있으므로 여기서 우리가 할 일은 아닙니다. 우리 큐브는 정점이 중복되지 않습니다.

 

얼마나 많은 접점이 필요할까요? 유형별로 분류해봅시다. 먼저 8개의 꼭지점이 필요합니다. 그런 다음 각 방향으로 4개의 가장자리가 있습니다. 모서리를 포함하지 않기 때문에 각 모서리에는 해당 크기에서 1을 뺀 것과 같은 양의 정점이 있습니다. 또는 X,Y,Z 가장자리의 네 세트로 생각하면 됩니다.

4 ( # x + # y + # z - 3 )

-> 겹쳐버린 꼭지점은 이미 뺏으므로 축의 갯수만큼 마지막에 뺀 것 입니다. 그리고 이어진 3개의 모서리가 4개 있으면 큐브를 그릴 수 있습니다.

 

나머지 정점은 면 내부에 있는 정점입니다. 크기가 2로 줄어든 복제된 꼭지점이 잇는 큐브와 같습니다.

 

2((#x1)(#y1)+(#x1)(#z1)+(#y1)(#z1))

-> 크기가 2로 줄어들기 전엔 각 축의 +1이었으므로  +1 - 2 = -1 입니다. 

 

이제 마침내 필요한 정점의 수를 알게 되었습니다.

	private IEnumerator Generate () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Procedural Cube";
		WaitForSeconds 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;
	}

첫 번째면 행의 정점 배치는 그리드의 첫 번째 행을 배치하는 것과 같습니다.

		vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];

		int v = 0;
		for (int x = 0; x <= xSize; x++) {
			vertices[v++] = new Vector3(x, 0, 0);
			yield return wait;
		}
...더보기

v가 배열에 엑세스 할 때 왜 증가할까요?

이 아이디어는 배열에 정점을 지정할 때 마다 정점 인덱스가 증가해야한다는 것에서 기인하였습니다. 규칙은 위치가 설정 될 때마다 vertices[v++]가 수행되어야 한다는 것 입니다. 이렇게 하면 v가 다른 곳에서 증가하는 것에 대해 걱정할 필요가 없습니다.

 

물론 v를 for문의 한 곳에서 증가시킬 수 있습니다. 이를 할 경우 일관성을 지켜야 합니다.

두 번째 면의 첫 번째 줄을 계속 진행하고, 정사각형 반지를 만들어 봅시다. 이것은 다른 범위와 위치를 사용하여 네 번 반복하여 수행됩니다.

 

		for (int x = 0; x <= xSize; x++) {
			vertices[v++] = new Vector3(x, 0, 0);
			yield return wait;
		}
		for (int z = 1; z <= zSize; z++) {
			vertices[v++] = new Vector3(xSize, 0, z);
			yield return wait;
		}
		for (int x = xSize - 1; x >= 0; x--) {
			vertices[v++] = new Vector3(x, 0, zSize);
			yield return wait;
		}
		for (int z = zSize - 1; z > 0; z--) {
			vertices[v++] = new Vector3(0, 0, z);
			yield return wait;
		}

https://thumbs.gfycat.com/GrayIdealisticAlbino-mobile.mp4

불러오는 중입니다...

바닥 정점 링이 나타납니다.

 

 

Y축을 따라 링을 배치하여 반복하면 큐브의 높이 주변을 완전히 감쌀 수 있습니다.

 

		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;
			}
		}

https://thumbs.gfycat.com/DaringIllustriousKronosaurus-mobile.mp4

불러오는 중입니다...

큐브 배치

 

 

그 후에 우리는 꼭대기와 바닥을 덮어야 만 합니다. 규칙적인 격자처럼 구멍을 채웁니다.

		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. 삼각형 추가하기

(읽기 전 참고 : triangle은 삼각형을 의미합니다.)

접점을 올바른 위치에 배치했고, 배치된 순서를 알고 있기에 바로 삼각형을 할 수 있습니다. 이를 위해 필자는 코루틴을 제거하고 접점과 삼각형 생성을 위한 별도의 메소드를 추가했습니다. 물론 접점을 메쉬에 할당해야합니다.

 

	private void Awake () {
		Generate();
	}

	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 (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;
	}

 

쿼드의 해부학

...더보기

삼각형 매개 변수가 필요한 이유는 무엇일까요?

큐브 객체에 vertices 필드를 주었지만 삼각형에 대해서는 그렇게 하지 않습니다. 그래서 우리는 그것을 SetQuad인수로 메서드에 전달해야합니다. 이것이 방법이 정적 일 수 있는 이유입니다.

 

물론 객체 레벨에서도 삼각형을 저장할 수 있지만 나중에 매개 변수 접근법을 활용할 것임을 알고 있어야 합니다.

 

...더보기

삼각형 인덱스를 반환하는 이유는 무엇입니까?

동일한 이유로 배열에 액세스 할 때 접점 인덱스를 증가시킵니다. 쿼드를 설정할 때마다 이 방법으로 결과를 인덱스에 다시 지정하면 완료됩니다.

정점과 달리 삼각형의 수는 단순히 결합 된 6개의 면 수와 같습니다. 공유 접점을 사용하는지 여부는 중요하지 않습니다.

 

	private void CreateTriangles () {
		int quads = (xSize * ySize + xSize * zSize + ySize * zSize) * 2;
		int[] triangles = new int[quads * 6];
		mesh.triangles = triangles;
	}

첫 번째 삼각형 행을 만드는 것은 다시 한번 그리드와 같습니다. 지금까지의 유일한 차이점은 다음 행의 꼭지점에 대한 오프셋이 전체 접점 링과동일하다는 것 입니다.

	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 q = 0; q < xSize; q++, v++) {
			t = SetQuad(triangles, t, v, v + 1, v + ring, v + ring + 1);
		}
		
		mesh.triangles = triangles;
	}

첫 번째 삼각형 행

전체 링을 삼각형 화(triangulate)하려면 루프를 길게하여 모든 방향으로 이동하십시오.

		for (int q = 0; q < ring; q++, v++) {
			t = SetQuad(triangles, t, v, v + 1, v + ring, v + ring + 1);
		}

잘못된 고리

이것은 마지막 쿼드를 제외하곤 정상적으로 작동합니다. 두 번재 및 네번째 접점은 링의 시작 부분까지 되감기 해야합니다. 따라서 루프에서 추출하십시오.

 

		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);

완성된 고리

모든 고리를 삼각형화 하려면 Y를 따라 과정을 다시 한번 반복하세요. 고리가 한단계 더 짧기 때문에 각 고리의 다음에 접점 색인을 증가시켜야 합니다.

 

		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);
		}

완전히 감싼 상태

문제는 위아랫면입니다. 접점 레이아웃은 고리로 둘러싸인 그리드와 같습니다.

 

모자에는 고리 안쪽에 격자가 있습니다.

 

윗면부터 시작하여 자체 메서드를 제공합니다.

	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;
	}

윗면의 첫번째 행

다음 행에 대해서는 더 복잡해집니다. 고리에 있는 최소 정점 인덱스를 추적하는 것이 유용합니다. 추적할 다른 색인은 중간 부분에 대한 것이고 그리드입니다.

		…
		
		int vMin = ring * (ySize + 1) - 1;
		int vMid = vMin + 1;

		t = SetQuad(triangles, t, vMin, vMid, vMin - 1, vMid + xSize - 1);
		
		return t;

다음 행의 첫번째 쿼드

행의 중간 부분은 일반 격자와 매우 비슷합니다.

		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);
		}

행의 마지막 쿼드는 다시 외부 고리를 처리해야하므로 최대 정점도 추적합시다.

		int vMin = ring * (ySize + 1) - 1;
		int vMid = vMin + 1;
		int vMax = v + 2;

		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);

이것은 마지막 행을 제외하고 모두 처리하는 루프로 바뀔 수 있습니다. 매 반복마다 중간 및 최대 정점 인덱스를 증가시켜야 합니다. 링의 방향으로 인해 최소 정점 인덱스가 대신 감소합니다.

		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);
		}

마지막 행을 제외하고 모두

최상위 정점 인덱스를 소개 한 다음 마지막 정점 인덱스의 첫 번째 쿼드를 설정하는 데 사용하십시오.,

		int vTop = vMin - 2;
		t = SetQuad(triangles, t, vMin, vMid, vTop + 1, vTop);

그런 다음 행의 가운데를 반복합니다.

	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);
		}

그리고 마침내 마지막 쿼드

int vTop = vMin - 2;
		t = SetQuad(triangles, t, vMin, vMid, vMin - 1, vMin - 2);
		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 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;
	}

파일 : 유니티 패키지

불러오는 중입니다...

4. 큐브 라운딩

바닐라 큐브는 특별하지 않습니다. 둥근 큐브로 바꿔보죠. 스크립트 파일과 클래스 이름의 이름을 바꿉니다. Unity는 컴포넌트를 추적해야하지만, 연결이 끈허진 경우 스크립트를 다시 끌어 놓습니다.

 

또한 큐브의 반올림 되는 양을 제어해야하므로 roundness 필드를 추가하십시오. 그것은 큐브의 가장 작은 차원의 1/2 사이의 값으로 설정되어야 합니다.

 

public class RoundedCube : MonoBehaviour {

	public int xSize, ySize, zSize;
	public int roundness;
	
	…
}

구성 가능한 Roundness

Unity가 법선을 다시 계산하도록 할 수는 있지만, 이번에는 우리가 합시다. 인접한 삼각형을 평균화 하는 대신 입방체의 둥근 정도를 계산할 때 우리의 접근 방식이 개선될 것 입니다. 그래서 normals 배열 필드를 추가하십시오.

 

이번에는 법선을 수동으로 수행하십시오.

 

	private Vector3[] normals;

	private void CreateVertices () {
		…
		vertices = new Vector3[cornerVertices + edgeVertices + faceVertices];
		normals = new Vector3[vertices.Length];

		…

		mesh.vertices = vertices;
		mesh.normals = normals;
	}

이렇게 하면 법선에 대한 기즈모를 그리는데 사용할 수 있기 때문에 우리가 잘 하고 있는지 확인 할 수 있습니다.

	private void OnDrawGizmos () {
		if (vertices == null) {
			return;
		}
		for (int i = 0; i < vertices.Length; i++) {
			Gizmos.color = Color.black;
			Gizmos.DrawSphere(vertices[i], 0.1f);
			Gizmos.color = Color.yellow;
			Gizmos.DrawRay(vertices[i], normals[i]);
		}
	}

아직 접선이 없습니다.

그러면 둥근 큐브의 정점을 어떻게 배치하고 그들의 법선을 계산하십니까? 이를위한 전용 방법을 고안해 보겠습니다.

 

	private void CreateVertices () {
		…

		int v = 0;
		for (int y = 0; y <= ySize; y++) {
			for (int x = 0; x <= xSize; x++) {
				SetVertex(v++, x, y, 0);
			}
			for (int z = 1; z <= zSize; z++) {
				SetVertex(v++, xSize, y, z);
			}
			for (int x = xSize - 1; x >= 0; x--) {
				SetVertex(v++, x, y, zSize);
			}
			for (int z = zSize - 1; z > 0; z--) {
				SetVertex(v++, 0, y, z);
			}
		}
		for (int z = 1; z < zSize; z++) {
			for (int x = 1; x < xSize; x++) {
				SetVertex(v++, x, ySize, z);
			}
		}
		for (int z = 1; z < zSize; z++) {
			for (int x = 1; x < xSize; x++) {
				SetVertex(v++, x, 0, z);
			}
		}

		mesh.vertices = vertices;
		mesh.normals = normals;
	}

	private void SetVertex (int i, int x, int y, int z) {
		vertices[i] = new Vector3(x, y, z);
	}

그러면 정점을 어떻게 배치할까요? 원래의 큐브 안에 작은 큐브가 떠있는 것을 생각해보세요. 이 두 큐브의 면 사이의 거리는 Roundness와 같습니다. 이 작은 입방체의 모서리에 구가 붙어 있고 그 반경이 Roundness와 같다고 생각해보십시오. 그래서 내부 큐브가 단단히 고정되어 있습니다.

 

바깥 쪽 입방체의 모든 점에 대해 내부 큐브에 가장 가까운 점이 있습니다. 이것을 사용하여 둥근 큐브의 법선 벡터를 결정할 수 있습니다.

다른 큐브가 안에 숨어 있습니다.

둥근 정육면체의 서페이스 점은 내부 점에서 시작하여 둥근 정도의 크기만큼 법선을 따라 이동하여 찾을 수 있습니다. 내부 코드를 아직 찾지 않았다는 것을 제외하고는 코드를 작성해야합니다.

	private void SetVertex (int i, int x, int y, int z) {
		Vector3 inner = vertices[i] = new Vector3(x, y, z);

		normals[i] = (vertices[i] - inner).normalized;
		vertices[i] = inner + normals[i] * roundness;
	}

이제 내부 점을 찾아야 합니다. X 좌표를 확인하여 시작하세요. 둥근 정도보다 작으면 내부 큐브의 왼쪽에 있고 내부의 X 좌표는 단순히 원형 값 입니다. 그리고 우리가 큐브의 X 크기를 넘어서서 Roundness를 뺀다면 오른쪽에 있습니다. 다른 모든 경우에 우리는 내부 입방체의 범위에 있으며 두 점 모두 동일한 X 좌표를 공유합니다.

	private void SetVertex (int i, int x, int y, int z) {
		Vector3 inner = vertices[i] = new Vector3(x, y, z);

		if (x < roundness) {
			inner.x = roundness;
		}
		else if (x > xSize - roundness) {
			inner.x = xSize - roundness;
		}

		normals[i] = (vertices[i] - inner).normalized;
		vertices[i] = inner + normals[i] * roundness;
	}

1차원 진원도는 아직 평평합니다.

지금 까지 결과는 전혀 둥글게 되지 않았지만 이미 양수 및 음수 X방향의 법선을 얻고 있습니다. Y 좌표에 대해 동일한 체크를 합니다.

 

		if (x < roundness) {
			inner.x = roundness;
		}
		else if (x > xSize - roundness) {
			inner.x = xSize - roundness;
		}
		if (y < roundness) {
			inner.y = roundness;
		}
		else if (y > ySize - roundness) {
			inner.y = ySize - roundness;
		}

 

둥글게 된 2차원

더 좋아 보이기 시작했습니다. 라운딩 및 법선은 XY 평면에서 작동합니다. 남은 것은 Z 좌표도 확인하는 것 입니다.

 

		if (x < roundness) {
			inner.x = roundness;
		}
		else if (x > xSize - roundness) {
			inner.x = xSize - roundness;
		}
		if (y < roundness) {
			inner.y = roundness;
		}
		else if (y > ySize - roundness) {
			inner.y = ySize - roundness;
		}
		if (z < roundness) {
			inner.z = roundness;
		}
		else if (z > zSize - roundness) {
			inner.z = zSize - roundness;
		}

완전히 둥글게 되었습니다.

그리고 마침내 완전히 둥근 입방체를 갖게되었습니다.

파일 : 유니티 패키지

불러오는 중입니다...

5. 메쉬 분할하기

중복 된 정점이 없는 단일 메쉬로 생성 된 멋진 둥근 큐브가 있습니다. 어떻게 하면 텍스쳐를 적용할 수 있을까요? 이를 위해 UV 좌표가 필요하지만 완벽한 포장을 만드는 방법은 없습니다. 그리고 이음새는 중복 정점이 필요합니다.

 

중복 정점 사용으로 전환할 수 있지만 다른 방법이 존재합니다.여러 개의 서브 메쉬를 사용하여 동일한 정점을 사용하는 벌도의 삼각형 목록을 만들 수 있습니다. 이렇게 하면 삼각형 집합마다 다른 재질을 사용할 수 있습니다.

 

메쉬를 세 쌍의 반대면으로 나눠 봅시다. 즉 3개의 배열과 3개의 삼각형 인덱스가 필요합니다.

	private void CreateTriangles () {
		int[] trianglesZ = new int[(xSize * ySize) * 12];
		int[] trianglesX = new int[(ySize * zSize) * 12];
		int[] trianglesY = new int[(xSize * zSize) * 12];
		int ring = (xSize + zSize) * 2;
		int tZ = 0, tX = 0, tY = 0, v = 0;
		
		…
	}

이제 우리는 링 루프를 Z와 X의 배열을 번갈아 네 개의 세크먼트로 나눠야 합니다.

		for (int y = 0; y < ySize; y++, v++) {
			for (int q = 0; q < xSize; q++, v++) {
				tZ = SetQuad(trianglesZ, tZ, v, v + 1, v + ring, v + ring + 1);
			}
			for (int q = 0; q < zSize; q++, v++) {
				tX = SetQuad(trianglesX, tX, v, v + 1, v + ring, v + ring + 1);
			}
			for (int q = 0; q < xSize; q++, v++) {
				tZ = SetQuad(trianglesZ, tZ, v, v + 1, v + ring, v + ring + 1);
			}
			for (int q = 0; q < zSize - 1; q++, v++) {
				tX = SetQuad(trianglesX, tX, v, v + 1, v + ring, v + ring + 1);
			}
			tX = SetQuad(trianglesX, tX, v, v - ring + 1, v + ring, v + 1);
		}

위쪽 및 아래쪽 면은 단순히 Y 배열을 사용합니다.

		tY = CreateTopFace(trianglesY, tY, ring);
		tY = CreateBottomFace(trianglesY, tY, ring);

그리고 mesh.traingles를 하나에 할당하는 대신 3개의 서브 메쉬로 만듭니다.

		mesh.subMeshCount = 3;
		mesh.SetTriangles(trianglesZ, 0);
		mesh.SetTriangles(trianglesX, 1);
		mesh.SetTriangles(trianglesY, 2);

두 면만 나타납니다.

 

우리의 메쉬는 이제 세 조각으로 자르고 첫 번째 만 실제로 렌더링 됩니다. 메쉬 렌더러에 하나의 서브 메쉬마다 하나씩 추가 머티리얼을 할당해야 합니다. 그래서 머티리얼 배열이 있습니다.

 

...더보기

이 작업에는 더 많은 draw call이 필요하지 않습니까?

실제로 한개 대신 세개나 있습니다. 그렇다면 정점이 적거나 draw call을 덜 받는 것 중에 더 중요한 것은 무엇인가요? 더 유용한 것을 상황에 맞춰 선택하면 됩니다.

 

당신은 일반적으로 가능한 적은 수의 drall call을 원합니다. 하지만 draw call 이 병목현상이 될 때만 문제가 됩니다. 다이나믹 배팅도 도움이 될 수 있다는 것을 상기하셔야 됩니다. 

 

중복 정점을 갖지 않는 것이 무거운 계산에서 사용할 때 유리할 수 있는데, 이 경우 정점을 덜 사용하는 것이 좋습니다. 중복과 심을 다루지 않아도 알고리즘이 훨씬 쉬워집니다.

파일 : 유니티패키지

불러오는 중입니다...

 

6. 그리드 렌더링하기

이제 면을 구별할 수 는 있지만, 텍스처 좌표는 여전히 없습니다. 개별 큐브를 볼 수 있도록 큐브 전체에 격자 패턴을 표시하려고 가정합니다. 어떻게하면 할 수 있을까요?

하나의 쿼드를 덮는 텍스처

메쉬에 UV 좌표를 저장하는 대신 사용자 정의 셰이더를 사용하여 텍스처를 적용하는 방법을 파악할 수 있습니다. 다음은 새로 생성된 쉐이더입니다.

Shader "Custom/Rounded Cube Grid" {
	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Standard fullforwardshadows
		#pragma target 3.0

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;

		void surf (Input IN, inout SurfaceOutputStandard o) {
			fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}

이것은 기본 표면 쉐이더입니다. 중요한 점은 주 텍스처에 대한 좌표를 예상하는 입력 구조를 정의한다는 것 입니다. 이 좌표는 suff함수에서 사용되며 렌더링되는 각 조각에 대해 호출됩니다. 그러한 uv_MainTex 좌표가 없기 때문에 다른 좌표로 대체해야합니다.

		struct Input {
			float2 cubeUV;
		};

		…

		void surf (Input IN, inout SurfaceOutputStandard o) {
			fixed4 c = tex2D(_MainTex, IN.cubeUV) * _Color;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}

UV는 정점마다 정의되므로 정점마다 호출되는 함수를 추가해야합니다.

		CGPROGRAM
		#pragma surface surf Standard fullforwardshadows vertex:vert
		#pragma target 3.0

		sampler2D _MainTex;

		struct Input {
			float2 cubeUV;
		};

		half _Glossiness;
		half _Metallic;
		fixed4 _Color;
		
		void vert (inout appdata_full v, out Input o) {
			UNITY_INITIALIZE_OUTPUT(Input, o);
		}

		void surf (Input IN, inout SurfaceOutputStandard o) {
			fixed4 c = tex2D(_MainTex, IN.cubeUV) * _Color;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG

쉐이더가 작동하는지 확인하려면 UV로 정점 위치의 XY좌표를 직접 사용하십시오.

		void vert (inout appdata_full v, out Input o) {
			UNITY_INITIALIZE_OUTPUT(Input, o);
			o.cubeUV = v.vertex.xy;
		}

UV를 XY로 사용

이것은 Z면에 대해 합리적으로 작동하지만 다른면은 엉망입니다. 우리는 다른 접점 좌표를 사용해야 합니다.  키워드 열거 쉐이더 속성을 추가하여 선택할 수 있도록 지원할 수 있습니다.

 

	Properties {
		_Color ("Color", Color) = (1,1,1,1)
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
		[KeywordEnum(X, Y, Z)] _Faces ("Faces", Float) = 0
	}

면을 선택하세요

어떤 옵션을 선택하느냐에 따라 Unity는 머티리얼에 맞춤 셰이더 키워드를 사용 가능하게 합니다. 셰이더가 지원하고자하는 각 키워드에 대해 자체 버전을 생성하도록 셰이더에 지시해야합니다.

	CGPROGRAM
		#pragma shader_feature _FACES_X _FACES_Y _FACES_Z
		#pragma surface surf Standard fullforwardshadows vertex:vert
		#pragma target 3.0

어떤 키워드가 정의되어 있는지 확인할 수 있기 때문에 각 옵션마다 다른 코드를 작성할 수 있습니다.

		void vert (inout appdata_full v, out Input o) {
			UNITY_INITIALIZE_OUTPUT(Input, o);
			#if defined(_FACES_X)
				o.cubeUV = v.vertex.yz;
			#elif defined(_FACES_Y)
				o.cubeUV = v.vertex.xz;
			#elif defined(_FACES_Z)
				o.cubeUV = v.vertex.xy;
			#endif
		}

재질마다 다른 좌표 사용

이제 제대로 보이기 시작했지만 그리드 선은 실제 쿼드에 맞지 않습니다. 더 나쁜 것은 우리가 월드-스페이스 버텍스 포지션을 사용함에 따라 큐브를 움직이거나 회전시킬 때 이상하게 보입니다.

 

둥글게 되기 전에 원래 큐브의 정점 위치가 필요합니다. 어떻게든 메쉬에 저장할 수 있다면 쉐이더에 전달할 수 있습니다. 정점 색상을 사용하지 않으므로 이 목적으로 정점 색상 채널을 사용할 수 있습니다.

...더보기

정점 색상을 사용해야하는 이유는 무엇일까요?

UV채널을 사용하는 것이 더 적합할 수 있지만 3D벡터를 저장할 때만 2D 벡터를 넣을 수 있습니다. 우리는 첫 번째 및 두번째 UV 채널을 모두 사용할 수 잇찌만 두 채널간에 데이터를 분할해야합니다.

 

그러나 접선 벡터는 4D이며 우리는 어쨌든 이것을 사용하지 않을 것 입니다. 왜 탄젠트 채널에 데이터를 넣지 않을까요? 문제는 위치와 법선과 같은 접선이 객체 공간에 정의되어 있다는 것 입니다. 이는 일괄 처리가 여러 객체를 단일 객체로 결합하기 때문에 일괄 처리 형식을 사용할 때 Unity에 의해 거의 변경될 수 있음을 의미합니다. 처음에는 효과가 있는 것 처럼 보일 수 있지만 장면에 하나 이상의 둥근 큐브가 있으면 이상한 일이 발생합니다.

	private Color32[] cubeUV;

	private void CreateVertices () {
		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];
		normals = new Vector3[vertices.Length];
		cubeUV = new Color32[vertices.Length];

		…

		mesh.vertices = vertices;
		mesh.normals = normals;
		mesh.colors32 = cubeUV;
	}

	private void SetVertex (int i, int x, int y, int z) {
		…

		normals[i] = (vertices[i] - inner).normalized;
		vertices[i] = inner + normals[i] * roundness;
		cubeUV[i] = new Color32((byte)x, (byte)y, (byte)z, 0);
	}

정점 색상 컴포넌트는 단일 바이트로 저장되기 때문에 여기서 Color32를 일반적인 Color 유형 대신 사용해야 합니다. 전체 색상은 단일 플로트와 동일한 크기인 4바이트입니다.

 

정규 색상을 사용하면 Unity는 0-1부동 소수점을 0-255 바이트로 변환하여 해당 범위를 벗어나는 모든 내용을 잘라냅니다. 직접 바이트로 변환하면 큐브 크기를 최대 255개 까지 처리할 수 있습니다.

 

셰이더 측에서, 이제 위치 대신 정점 색상을 사용할 수 있습니다. 셰이더는 정점 색상 채널을 0-1 범위의 값으로 해석하므로 255를 곱하여 이 변환을 되돌려야 합니다.

		void vert (inout appdata_full v, out Input o) {
			UNITY_INITIALIZE_OUTPUT(Input, o);
			#if defined(_FACES_X)
				o.cubeUV = v.color.yz * 255;
			#elif defined(_FACES_Y)
				o.cubeUV = v.color.xz * 255;
			#elif defined(_FACES_Z)
				o.cubeUV = v.color.xy * 255;
			#endif
		}

원래 큐브 위치를 사용하는 겨자

그리고 마침내 기능적 격차 텍스처를 갖게되었습니다. 각 쌍의면 중 하나의 UV좌표는 미러링되지만 대칭 텍스처를 사용하기 때문에 문제가 되지 않습니다.

 

파일 : 유니티 패키지

불러오는 중입니다...

7. 충돌체 추가

큐브는 주변으로 던져 버릴 수 있을 때만 재미 있습니다. 물리와 충돌에 관한 기능을 필요로 합니다. 불행히도 메쉬 콜라이더는 실질적 작동이 어렵습니다. 왜냐하면 블록 콜라이더의 폴리곤 수 제한에 빠르게 빠지기 때문입니다. 하지만 걱정하지 마세요. 원시 콜라이더를 사용하여 완벽한 둥근 큐브를 만들 수 있습니다. 이를 위한 메소드를 추가합니다.

 

	private void Generate () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Procedural Cube";
		CreateVertices();
		CreateTriangles();
		CreateColliders();
	}
	
	private void CreateColliders () {
	}

그리고 첫번째 단계, 박스 콜라이더를 추가합니다.

	private void CreateColliders () {
		gameObject.AddComponent<BoxCollider>();
	}

하나의 박스 콜라이더

Unity가 우리의 메시 바운딩 박스와 일치하도록 콜라이더를 위치시키고 스케일 할 만큼 유능합니다. 이제 우리는 반대쪽 면의 평평한 면과 일치하도록 충돌자의 크기를 조정해야합니다. 세면 모두에 대해 이 작업을 수행해야 하므로 세 개의 교차 블록이 생깁니다.

 

	private void CreateColliders () {
		AddBoxCollider(xSize, ySize - roundness * 2, zSize - roundness * 2);
		AddBoxCollider(xSize - roundness * 2, ySize, zSize - roundness * 2);
		AddBoxCollider(xSize - roundness * 2, ySize - roundness * 2, zSize);
	}
	
	private void AddBoxCollider (float x, float y, float z) {
		BoxCollider c = gameObject.AddComponent<BoxCollider>();
		c.size = new Vector3(x, y, z);
	}

플랫 영역의 박스 콜라이더

캡슐을 사용하여 모서리와 모서리를 채울 수 있습니다. 우리는 그들에게 올바른 방향을 주고 각 가장자리의 중심에 위치시켜야합니다.

 

	private void AddCapsuleCollider (int direction, float x, float y, float z) {
		CapsuleCollider c = gameObject.AddComponent<CapsuleCollider>();
		c.center = new Vector3(x, y, z);
		c.direction = direction;
		c.radius = roundness;
		c.height = c.center[direction] * 2f;
	}

우리는 가장자리 당 하나의 캡슐이 필요하므로 총 12개가 필요합니다. 배치를 쉽게하기 위해 최소, 절반 및 최대 벡터를 만들었습니다.

 

	private void CreateColliders () {
		AddBoxCollider(xSize, ySize - roundness * 2, zSize - roundness * 2);
		AddBoxCollider(xSize - roundness * 2, ySize, zSize - roundness * 2);
		AddBoxCollider(xSize - roundness * 2, ySize - roundness * 2, zSize);

		Vector3 min = Vector3.one * roundness;
		Vector3 half = new Vector3(xSize, ySize, zSize) * 0.5f; 
		Vector3 max = new Vector3(xSize, ySize, zSize) - min;

		AddCapsuleCollider(0, half.x, min.y, min.z);
		AddCapsuleCollider(0, half.x, min.y, max.z);
		AddCapsuleCollider(0, half.x, max.y, min.z);
		AddCapsuleCollider(0, half.x, max.y, max.z);
		
		AddCapsuleCollider(1, min.x, half.y, min.z);
		AddCapsuleCollider(1, min.x, half.y, max.z);
		AddCapsuleCollider(1, max.x, half.y, min.z);
		AddCapsuleCollider(1, max.x, half.y, max.z);
		
		AddCapsuleCollider(2, min.x, min.y, half.z);
		AddCapsuleCollider(2, min.x, max.y, half.z);
		AddCapsuleCollider(2, max.x, min.y, half.z);
		AddCapsuleCollider(2, max.x, max.y, half.z);
	}

준비 완료

남은 것은 둥근 입방체에 단단한 물건을 넣고 친구를 사귀고 견과류를 먹는 것 입니다. 일단 큐브를 완성하면 Cube Sphere 튜토리얼을 살펴보십시오.

 

https://thumbs.gfycat.com/RingedCornyBlesbok-mobile.mp4

불러오는 중입니다...

Rigidbodies 주위에 바운싱

파일 : 유니티 패키지

불러오는 중입니다...

파일 : PDF

반응형