지형을 여러 개의 블록(Chunk) 또는 그리드 단위로 나누어 생성할 때, 인접한 블록 사이의 경계 부분에서 법선(Normal) 데이터가 일치하지 않아 부자연스러운 구분선이 생기는 문제가 빈번히 발생합니다. 각 블록이 독립적으로 메시를 계산하기 때문에 발생하는 이 문제는 인접한 블록끼리 공유하는 정점의 법선 데이터를 합산하고 정규화(Normalize)하여 일치시키는 과정이 필요합니다.
예를 들어 4x4 형태의 그리드 맵에서 각 블록의 변이 3등분(한 변당 4개의 정점)되어 있다고 가정할 때, 다음과 같은 단계로 법선 데이터를 보정할 수 있습니다.
1. 맵 모서리 영역의 기초 법선 참조 설정
가장 먼저 지형 전체의 외곽 모서리에 해당하는 블록들 사이에서 법선 데이터를 상호 참조할 수 있도록 초기값을 설정합니다. 이는 배열 순회 시 데이터 전파의 기초가 됩니다.
// 그리드 크기 체크 후 모서리 법선 데이터 병합
if (gridZ >= 2)
{
// 좌측 하단 블록과 그 위쪽 블록의 접점 처리
terrainCells[0, 0].normals[topL] += terrainCells[0, 1].normals[bottomL];
// 우측 하단 블록과 그 위쪽 블록의 접점 처리
terrainCells[gridX - 1, 0].normals[topR] += terrainCells[gridX - 1, 1].normals[bottomR];
}
if (gridX >= 2)
{
// 하단부 블록 간 좌우 접점 처리
terrainCells[0, 0].normals[bottomR] += terrainCells[1, 0].normals[bottomL];
// 상단부 블록 간 좌우 접점 처리
terrainCells[0, gridZ - 1].normals[topR] += terrainCells[1, gridZ - 1].normals[topL];
}
2. 외곽 경계선을 따라 법선 데이터 전파
맵의 네 가장자리에 위치한 블록들을 순회하며 인접한 블록과 겹치는 정점의 법선 값을 누적합니다. 이 과정을 통해 외곽 테두리의 셰이딩이 부드럽게 연결됩니다.
for (int z = 0; z < gridZ; z++)
{
for (int x = 0; x < gridX; x++)
{
// 최하단 라인 처리
if (z == 0 && x > 0)
{
if (x < gridX - 1)
terrainCells[x, z].normals[bottomR] += terrainCells[x + 1, z].normals[bottomL];
terrainCells[x, z].normals[bottomL] = terrainCells[x - 1, z].normals[bottomR];
}
// 최좌측 라인 처리
if (x == 0 && z > 0)
{
if (z < gridZ - 1)
terrainCells[x, z].normals[topL] += terrainCells[x, z + 1].normals[bottomL];
terrainCells[x, z].normals[bottomL] = terrainCells[x, z - 1].normals[topL];
}
// 최우측 라인 처리
if (x == gridX - 1 && z > 0)
{
if (z < gridZ - 1)
terrainCells[x, z].normals[topR] += terrainCells[x, z + 1].normals[bottomR];
terrainCells[x, z].normals[bottomR] = terrainCells[x, z - 1].normals[topR];
}
// 최상단 라인 처리
if (z == gridZ - 1 && x > 0)
{
if (x < gridX - 1)
terrainCells[x, z].normals[topR] += terrainCells[x + 1, z].normals[topL];
terrainCells[x, z].normals[topL] += terrainCells[x - 1, z].normals[topR];
}
}
}
3. 내부 교차점 정점의 법선 평균화
네 개의 블록이 만나는 내부 교차점 정점들의 법선을 계산합니다. 해당 정점은 4개 블록 모두에 존재하므로, 이를 모두 더한 뒤 정규화하여 모든 블록이 동일한 법선 값을 갖게 합니다.
for (int z = 0; z < gridZ; z++)
{
for (int x = 0; x < gridX; x++)
{
// 우측 상단 정점을 기준으로 주변 4개 블록의 법선 병합
if (x < gridX - 1 && z < gridZ - 1)
{
Vector3 unifiedNormal = (
terrainCells[x, z].normals[topR] +
terrainCells[x + 1, z].normals[topL] +
terrainCells[x, z + 1].normals[bottomR] +
terrainCells[x + 1, z + 1].normals[bottomL]
).normalized;
terrainCells[x, z].normals[topR] = unifiedNormal;
}
// 병합된 값을 인접 블록의 대응되는 위치에 할당 (순차 전파)
if (x >= 1 && z >= 1)
terrainCells[x, z].normals[bottomL] = terrainCells[x - 1, z - 1].normals[topR];
if (x < gridX - 1 && z >= 1)
terrainCells[x, z].normals[bottomR] = terrainCells[x, z - 1].normals[topR];
if (x >= 1 && z < gridZ - 1)
terrainCells[x, z].normals[topL] = terrainCells[x - 1, z].normals[topR];
}
}
4. 에지(Edge) 사이 정점 법선 동기화
블록의 모서리 외에 변을 구성하는 나머지 정점들도 인접 블록과 맞닿아 있습니다. 루프를 돌며 좌우, 상하 블록 간의 법선 데이터를 평균화하여 최종적으로 모든 경계면을 매끄럽게 처리합니다.
for (int z = 0; z < gridZ; z++)
{
for (int x = 0; x < gridX; x++)
{
for (int step = 1; step < segmentCount; step++)
{
int idxTop = (segmentCount + 1) * segmentCount + step;
int idxBottom = step;
int idxLeft = step * (segmentCount + 1);
int idxRight = (step + 1) * (segmentCount + 1) - 1;
// 좌측 경계: 왼쪽 블록의 우측 법선 가져오기
if (x > 0)
terrainCells[x, z].normals[idxLeft] = terrainCells[x - 1, z].normals[idxRight].normalized;
// 하단 경계: 아래쪽 블록의 상단 법선 가져오기
if (z > 0)
terrainCells[x, z].normals[idxBottom] = terrainCells[x, z - 1].normals[idxTop].normalized;
// 우측 경계: 현재 블록과 오른쪽 블록의 법선 합산 및 정규화
if (x < gridX - 1)
{
Vector3 sumR = (terrainCells[x, z].normals[idxRight] + terrainCells[x + 1, z].normals[idxLeft]).normalized;
terrainCells[x, z].normals[idxRight] = sumR;
}
// 상단 경계: 현재 블록과 위쪽 블록의 법선 합산 및 정규화
if (z < gridZ - 1)
{
Vector3 sumT = (terrainCells[x, z].normals[idxTop] + terrainCells[x, z + 1].normals[idxBottom]).normalized;
terrainCells[x, z].normals[idxTop] = sumT;
}
}
// 보정된 법선 데이터를 실제 메시 데이터에 적용
terrainCells[x, z].ApplyFinalNormals();
}
}