( -..- )
이번 에피소드는 정말 어려웠습니다...
우선 이전 에피소드에서 만든 동굴모양의 메쉬에서 가장자리(outline)를 찾아야합니다. 가장자리는 두개의 정점이 정확히 하나의 삼각형을 공유하고 있을 때 가장자리라고 확신할수있습니다.
그리고 메쉬 정점의 순서는 아웃라인쪽이 셀수있도록 배치되어야 합니다. 그럼 이제 코드를 간단히 살펴봅니다.
1. 트라이앵글(삼각면) 정의
우선 앞서 만들었던 메쉬들은 모두 3개의 정점을 가지고 있습니다. 이를 triangle (삼각면) 이라는 이름의 구조체로 다시 정의를 해줍니다.
//삼각메쉬에 대해 정의를 한다
struct Triangle {
public int vertexIndexA;
public int vertexIndexB;
public int vertexIndexC;
//정점에 대한 인덱스를 저장하기 위한 변수
int[] vertices;
public Triangle (int a, int b, int c) {
vertexIndexA = a;
vertexIndexB = b;
vertexIndexC = c;
}
생성자를 통해서 각 노드들의 정보를 구조체에 담아(?)냅니다.
2. 트라이앵글(삼각면) 딕셔너리 생성하고 정보 담기
트라이앵글 구조체를 저장할 자료구조로 딕셔너리를 하나 선언합니다.
Dictionary<int, List<Triangle>> triangleDictionary = new Dictionary<int, List<Triangle>>();
그리고 트라이앵글도 만들어줍니다.
void CreateTriangles(Node a, Node b, Node c) {
//정점 저장해 삼각형 정의, 맵상의 모든 정점을 삼각점으로 순서대로 저장
triagles.Add(a.vertexIndex);
triagles.Add(b.vertexIndex);
triagles.Add(c.vertexIndex);
//그리고 설정해둔 삼각형 구조체를 통해 삼각형을 정의해주고
//딕셔너리에 저장해준다.
Triangle triangle = new Triangle(a.vertexIndex, b.vertexIndex, c.vertexIndex);
}
CreateTriangles()는 앞선 에피소드에서 작성됐는데요. 이 함수는 동굴의 모든 정점을, Int타입의 triangles라는 리스트에 삼각형 단위로 저장해주고 있었습니다. 이때 triangle이라는 구조체에 각 정점의 값을 인자로 넘겨줘서 관리하기 편하게합니다.
그리고 이 삼각면을 앞에서 생성한 triangleDictionary(삼각면 딕셔너리) 에 저장을 해야하는 일이 남습니다. 이를 함수로 구현합니다.
void AddTriangleToDictionary(int vertexIndexKey, Triangle triangle) {
if (triangleDictionary.ContainsKey(vertexIndexKey)) {
triangleDictionary[vertexIndexKey].Add(triangle);
}
else {
List<Triangle> triangleList = new List<Triangle>();
triangleList.Add(triangle);
triangleDictionary.Add(vertexIndexKey, triangleList);
}
}
삼각면을 딕셔너리에 저장하기 위한 AddTriangleToDictionary()라는 함수를 만들었습니다.
첫번째 if 문은 딕셔너리가 key 값을 가지고 있는지 아닌지 검사합니다. 이는 삼각면의 경우, 하나의 면에 대해서 같은 정점을 공유하는 일이 있기 때문에 특정 정점이 이미 삼각형을 가지고 있을 경우 다른 삼각형을 추가시켜줄겁니다.
그리도 else 문에선 triangleList를 선언하여 삼각면에 대한 정보를 담습니다. 이를 다시 삼각면딕셔너리에 더해준다면, 이 자료에는 정점과 이어지는 모든 삼각형이 저장되게 됩니다. (List를 선언해서 받는 이유는 정점과 이어지는 삼각형이 하나가 아니기 때문입니다.
void CreateTriangles(Node a, Node b, Node c) {
... (이전내용)
Triangle triangle = new Triangle(a.vertexIndex, b.vertexIndex, c.vertexIndex);
AddTriangleToDictionary(triangle.vertexIndexA, triangle);
AddTriangleToDictionary(triangle.vertexIndexB, triangle);
AddTriangleToDictionary(triangle.vertexIndexC, triangle);
}
그리고 삼각면을 생성하는 함수에서 삼각면을 딕셔너리에 넣어주는 함수를 그대로 이용하면 됩니다.
그럼 지금까지 구한 내용을 정리하자면
vertices: Vector3 에는 모든 정점에 대한 정보(Vector3)가 들어갑니다.
triagles: int 에는 모든 삼각면에 대한 정점에 대한 인덱스 정보가 차례대로 들어갑니다.
triangleDIctionary: Dictionary 에는 모든 정점과 정점과 맞붙어있는 삼각형에 대한 정보가 들어갑니다.
3. 외각선인지 아닌지 알아내기
그럼 이제 벽을 생성하기 위해서 해야할것은 정점을 잇는 선이 외각선(outline)인지 알아내는 일입니다. 이를 아래 함수로 처리합니다.
IsOutlineEdge(int vertexA, int vertexB)
bool IsOutlineEdge(int vertexA, int vertexB) {
List<Triangle> trianglesContainingVertexA = triangleDictionary[vertexA];
int sharedTriangleCount = 0;
for (int i = 0; i < trianglesContainingVertexA.Count; i++) {
if (trianglesContainingVertexA[i].Contains(vertexB)) {
sharedTriangleCount++;
if (sharedTriangleCount > 1) {
break;
}
}
}
return sharedTriangleCount == 1;
}
먼저 trianglesContainingVertexA(정점A을 포함하고 있는 삼각형들) 라는 리스트를 만들어줍니다. 이 리스트에는 vertexA(정점A) 에 대해 겹쳐있는 모든 삼각형이 들어가게 됩니다. 그리고 sharedTriangleCount 라는 인트형 변수를 선언해줍니다. 이 변수는 하나의 선이 외곽선인지 아닌지 판단하기 위해 선언됐습니다. (개념적으로 정의했습니다. 아웃라인인지 아닌지를 판단하는 근거는 하나의 선이 공유하고있는 삼각면이 한개인지 아닌지 였습니다.)
그리고 for문을 통해 한 정점에 대해 겹쳐있는 모든 삼각형을 카운트합니다. 그리고 조건문을 통해 정점A를 공유하는 i번째 삼각형이 다음으로 이어지는 vertexB 를 포함하고 있는지 확인합니다. 글로는 이해가 어려우니 아래 그림을 보겠습니다.
정점A는 양쪽 삼각형에 대한 정보를 가지고 있습니다. for문을 통해서 A,B 삼각면의 수만큼 검사를 실시합니다. 그리고 if문을 통해서 정점B를 양쪽의 삼각형이 공유하고 있는지 확인합니다. 만약 A삼각형과 B삼각형에서 정점B를 가지고 있었다면 sharedTriangleCount 변수는 2가 됨으로 이는 외곽선이 아님을 알수있습니다.
4. 특정 정점에 대해 이어진 (외곽선이 될)정점을 가져오기
우선 코드를 보겠습니다.
GetConnectedOutlineVertex(int vertexIndex)
int GetConnectedOutlineVertex(int vertexIndex) {
List<Triangle> trianglesContainingVertex = triangleDictionary[vertexIndex];
for (int i = 0; i < trianglesContainingVertex.Count; i++) {
Triangle triangle = trianglesContainingVertex[i];
for (int j = 0; j < 3; j++) {
int vertexB = triangle[j];
if(vertexB != vertexIndex && !checkedVertices.Contains(vertexB)) {
if (IsOutlineEdge(vertexIndex, vertexB)) {
return vertexB;
}
}
}
}
return -1;
}
여기서도 마찬가지로 특정 정점을 공유하는 모든 삼각형들을 리스트로 받아놓습니다. 그리고 삼각형들의 수만큼 for문을 돌립니다. 반복문이 돌아갈때마다 반복하는 시점에 해당하는 삼각면을 생성합니다. 그리고 이 삼각면의 정점수만큼 다시 반복문을 돌리면서 각 정점을 정점B로 정의하면서 if 문으로 정점B를 이었을때 외곽선인지 아닌지 검사합니다.
그림으로 보면 쉽게 이해할수있을것입니다. 삼각형 A,B,C 를 공유하는 vertexIndex 에 대해서 아웃라인인 정점을 찾아냅니다.
5. 아웃라인선을 저장하고 아웃라인인지 체크한 정점을 표시하기
List<List<int>> outlines = new List<List<int>>();
HashSet<int> checkedVertices = new HashSet<int>();
우선 아웃라인을 저장할 인트형 리스트와 체크된 벌텍스를 저장할 HashSet 자료구조를 선언합니다. (outlines가 List<int>타입의 리스트로 선언된 이유는 outlines는 두개의 정점이 이어지기 때문입니다.)
void CalculateMeshOutlines()
void CalculateMeshOutlines() {
for (int vertexIndex = 0; vertexIndex < vertices.Count; vertexIndex++) {
if (!checkedVertices.Contains(vertexIndex)) {
int newOutlineVertex = GetConnectedOutlineVertex(vertexIndex);
if(newOutlineVertex != -1) {
checkedVertices.Add(vertexIndex);
List<int> newOutline = new List<int>();
newOutline.Add(vertexIndex);
outlines.Add(newOutline);
FollowOutline(newOutlineVertex, outlines.Count - 1);
outlines[outlines.Count - 1].Add(vertexIndex);
}
}
}
}
그리고 위와 같은 함수를 작성하는데요. 이 함수는 맵상의 모든 정점들을 대상으로 for문을 실행합니다.
첫번째 if문에선 checkedVertices 가 vertexIndex 를 아직 가지고 있지 않는다면 (즉, 검사한 정점이 아니라면) 본 내용이 실행됩니다. 그리고 이 벌텍스와 외곽선으로 이어진 newOutlineVertex 또한 가져옵니다. 성공적으로 이웃하는 아웃라인 정점을 받아왔다면 해당 vertexIndex를 검사한 checkedVertices 목록에 집어넣서 불필요한 재검사를 방지합니다.
그리고 newOutline이라는 리스트를 만들어 주고 이곳에 해당하는 정점번호를 넣어주고, outlines 라는 리스트에는 newOutline을 리스트로 넣어줍니다. (반복문이 돌때마다 newOutline이 새로 생성되면서 아웃라인의 정점에 대한 정보를 가지게 될겁니다)
FollowOutline() 에선 아직 생성되지 않은 아웃라인을 이어서 계속 가져옵니다.
마지막 outline[outlines.Count - 1].Add(vertexIndex) 에 스타팅 인덱스를 넣어줌으로써 아웃라인 정점이 제자리로 돌아왔을때의 정점을 추가해줍니다.
void FollowOutline(int vertexIndex, int outlineIndex)
private void FollowOutline(int vertexIndex, int outlineIndex) {
outlines[outlineIndex].Add(vertexIndex);
checkedVertices.Add(vertexIndex);
int nextVertexIndex = GetConnectedOutlineVertex(vertexIndex);
if(nextVertexIndex != -1) {
FollowOutline(nextVertexIndex, outlineIndex);
}
}
FollowOutline은 재귀함수인데, 동굴에서 외곽선은 계속 이어지기 때문에 재귀함수를 통해 이어지지 않을때까지 outline 정점을 추가해줄껍니다. 탈출조건은 if(nextVertexIndex != 1){} 인데요. 이는 더이상 이어질 외곽선 정점이 없으면 끝나게 됩니다.
6. 벽 메쉬를 생성하기
private void CreateWallMesh()
private void CreateWallMesh() {
CalculateMeshOutlines();
List<Vector3> wallVertices = new List<Vector3>();
List<int> wallTriangles = new List<int>();
Mesh wallMesh = new Mesh();
float wallHeight = 5;
foreach (List<int> outline in outlines) {
for (int i = 0; i < outline.Count -1; i++) {
int startIndex = wallVertices.Count;
wallVertices.Add(vertices[outline[i]]); //left
wallVertices.Add(vertices[outline[i+1]]); //right
wallVertices.Add(vertices[outline[i]] - Vector3.up * wallHeight); //bottom left
wallVertices.Add(vertices[outline[i + 1]] - Vector3.up * wallHeight); //bottom right
wallTriangles.Add(startIndex + 0); //left
wallTriangles.Add(startIndex + 2); //bottom left
wallTriangles.Add(startIndex + 3); //bottom right
wallTriangles.Add(startIndex + 3); //bottom right
wallTriangles.Add(startIndex + 1); //right
wallTriangles.Add(startIndex + 0); //left
}
}
wallMesh.vertices = wallVertices.ToArray();
wallMesh.triangles = wallTriangles.ToArray();
walls.mesh = wallMesh;
wallMesh.RecalculateNormals();
}
마지막으로 5번까지 진행한 내용의 정보를 가지고 벽 메쉬를 생성할겁니다.
List<Vector3> wallVertices = new List<Vector3>();
List<int> wallTriangles = new List<int>();
Mesh wallMesh = new Mesh();
float wallHeight = 5;
먼저 위와같이 벽을 생성하기 위한 변수를 선언해줍니다.
foreach (List<int> outline in outlines) {
for (int i = 0; i < outline.Count -1; i++) {
int startIndex = wallVertices.Count;
wallVertices.Add(vertices[outline[i]]); //left
wallVertices.Add(vertices[outline[i+1]]); //right
wallVertices.Add(vertices[outline[i]] - Vector3.up * wallHeight); //bottom left
wallVertices.Add(vertices[outline[i + 1]] - Vector3.up * wallHeight); //bottom right
wallTriangles.Add(startIndex + 0); //left
wallTriangles.Add(startIndex + 2); //bottom left
wallTriangles.Add(startIndex + 3); //bottom right
wallTriangles.Add(startIndex + 3); //bottom right
wallTriangles.Add(startIndex + 1); //right
wallTriangles.Add(startIndex + 0); //left
}
}
그리고 foreach를 통해 모든 outline 그룹에 대해 접근하고 각 outline 별로 for문을 통해 반복해주면서 벽 메쉬에 대한 정보를 추가합니다.
wallVertices.Add()... 를 통해서 i번째에 해당하는 outline을 사각면으로 연결한 정점을 저장하고, wallTriangles.Add()... 를 통해서 사각면의 삼각형 메쉬 번호를 넣어줍니다. (사각면은 두 개의 삼각면으로 돼있습니다)
wallMesh.vertices = wallVertices.ToArray();
wallMesh.triangles = wallTriangles.ToArray();
walls.mesh = wallMesh;
wallMesh.RecalculateNormals();
마지막으로 Mesh에 정보들을 넣어주면 벽 생성이 완료됩니다.
정말 생각보다 이해하기 어려운 에피소드였습니다. 그래도 다음부턴 이해가 훨씬 쉬울 것 같네요. 이상입니다.