Unity) 하드 엣지에서 발생한 아웃라인 트러블슈팅
툰 셰이딩을 구현하면서 생각지도 못한 벽에 부딪히게 되었다.
툰 셰이딩을 적용하며 중요하게 생각해야할 부분은 바로 아웃라인 이다.
특히 카툰 스타일의 렌더링에서는 캐릭터나 오브젝트의 윤곽선이 스타일링의 핵심이 되기 떄문에, 아웃라인 문제는 전체 퀄리티에 큰 영향을 줄수 있다고 생각한다.
이번 글에서는 하드 엣지 모델에서 아웃라인이 비정상 적으로 출력되던 문제를 다뤄 보려고 한다.
아웃라인 생성의 기본 원리
툰 셰이딩에서 아웃라인을 생성하는 방식은 다양하지만, 이번에는 가장 보편적인 방식으로 사용되는 방식을 사용해봤다.
노말 방향의 반대 방향으로 확장된 실루엣을 백페이스로 렌더링 하는 것이다.
이때 방향 계산을 위해 사용되는 벡터의 개념은 다음과 같다.
- n : 서피스의 노멀 벡터
- v : 뷰(카메라) 벡터
- h: 하프 벡터 (지금은 n + v 값의 normalize된 값이다.)
뷰 방향에 따라 실루엣을 잡아내야 하기 때문에, 아래와 같은 방식이 자주 쓰인다.
outlineDir = normalize(n * viewInverseMatrix)
아니면 간단히
outlineDir = vertexPos + normal * outlineThickness
이런 방식으로 사용하기도 했다.
문제: 하드 엣지 에셋에서 아웃라인 깨짐 현상
기존의 소프트 엣지 기반 캐릭터에서는 문제가 없었던 아웃라인이, 하드 엣지를 포함한 에셋에서 이상하게 깨지는 현상이 생겼다.
다각형 평면 오브젝트에서 각 면마다 다른 노말이 적용되니, 경계 지점에서 아웃라인이 끊겨 보이는 현상이었다.
정상 이미지
문제 이미지
시도해 본 방법들
몇몇 방법들을 검색을 통해 시도해 봤지만, 아직 내게 부족한 부분들이 많아 모든 내용을 적용해 볼 수는 없었다.
-
버텍스 노말을 평균내어 UV에만 적용해보기
- 이론상으로는 노말을 부드럽게 만들어 아웃라인을 매끄럽게 만들 수 있을 것이라 기대했다.
- 하지만 무슨이유인지 모르겠지만, 적용된 UV가 셰이더 코드에서는 변경 되지 않음을 확인했다.
-
소프트 엣지 어셋을 동일한 위치에 배치해보기
- 비교를 위한 테스트. 같은 위치에 소프트 엣지 모델을 놓으니 아웃라인이 부드럽게 출력되었음.
-
즉, 셰이더의 문제라기보다는 입력되는 노말 정보의 차이가 원인이라는 걸 확신할 수 있었다.
- 그러나 두 어셋을 겹쳐 사용하기엔 부적절 하다고 생각했다.
-
기존 어셋을 소프트 엣지로 강제 변경해보기
- 최종적으로 이 방법을 채택.
-
완전한 해결은 아니지만, 아웃라인 출력이 비교적 정상적으로 이뤄졌다.
-
소프트 엣지로 변경 방법은 두가지가 있었다
- DCC프로그램을 활용한다.
- (채택)코드를 이용해 한 버텍스의 노말값들을 강제로 하나의 노말값으로 변경한다.
해결 방법: 코드로 소프트 노말 처리하기
결국 선택한 방법은 코드를 이용해 하드 엣지 모델의 버텍스 노말을 평균을 찾아 소프트 엣지처럼 처리하는 방식이었다. 이렇데 하면 하드 엣지 모델도 소프드 엣지 모델로 변경되며 아웃라인이 자연스럽게 출력되었다.
DCC 툴에서도 수작업으로 엣지를 부드럽게 만들 수 있지만, 런타임 또는 에디터 상에서 자동화가 가능하다는 점에서 코드 방식이 훨씬 유연하다 생각해 이방식으로 처리했다.
구현한 스크립트: SmoothNormalBaker
이 스크립트는 SkinnedMeshRenderer
를 가진 오브젝트를 찾고, 그 안의 공유된 버텍스 위치를 기준으로 노말을 평균화해 새로운 메쉬로 대체하는 구조다.
전체 흐름
private void Awake()
{
SkinnedMeshRenderer[] skinnedMeshRenderer = GetComponentsInChildren<SkinnedMeshRenderer>(true);
foreach (SkinnedMeshRenderer smr in skinnedMeshRenderer)
{
BakeSmoothNormals(smr);
}
}
- 자식 오브젝트들에서
SkinnedMeshRenderer
를 전부 찾고,BakeSmoothNormals()
를 호출한다.
노말 평균화 핵심 로직
void BakeSmoothNormals(SkinnedMeshRenderer smr)
{
// 원본 메쉬 복제
Mesh originMesh = smr.sharedMesh;
Mesh meshInst = Instantiate(originMesh);
// 버텍스 위치 기준으로 그룹화
VertexGrp(meshInst.vertices, ref vertexGroup);
// 각 그룹별 평균 노말 계산
Vector3[] smoothNormals = FindNormalAVG(meshInst, vertexGroup);
// 노말과 UV1에 적용
meshInst.normals = smoothNormals;
meshInst.SetUVs(1, new List<Vector3>(smoothNormals));
smr.sharedMesh = meshInst;
}
- 복제된 메쉬에 평균 노말을 적용하고, 필요하다면 UV1에도 저장한다.
버텍스 그룹화: 위치를 기준으로 묶기
void VertexGrp(Vector3[] vertices, ref Dictionary<Vector3, List<int>> vertexGroup)
{
for (int i = 0; i < vertices.Length; i++)
{
Vector3 key = RoundVec3(vertices[i], 0.0001f); // 위치 오차 보정
if (!vertexGroup.ContainsKey(key))
vertexGroup[key] = new List<int>();
vertexGroup[key].Add(i);
}
}
- 위치가 거의 동일한 버텍스들을 하나의 그룹으로 묶어준다.
- 위치 정밀도를 조정하는
RoundVec3
함수는 오차로 인한 분리를 방지한다.
평균 노말 계산
Vector3[] FindNormalAVG(Mesh mesh, Dictionary<Vector3, List<int>> vertexGroup)
{
Vector3[] normals = mesh.normals;
Vector3[] smoothNormals = new Vector3[normals.Length];
foreach (var grp in vertexGroup.Values)
{
Vector3 avg = Vector3.zero;
foreach (int i in grp)
avg += normals[i];
avg.Normalize();
foreach (int i in grp)
smoothNormals[i] = avg;
}
return smoothNormals;
}
- 각 그룹의 노말을 평균내고, 같은 위치의 모든 버텍스에 동일하게 적용해준다.
- 이렇게 하면 실질적으로 소프트 엣지와 같은 효과를 얻게 된다.
마무리하며: 앞으로의 방향
이번 문제를 통해 아웃라인 셰이딩에서 노말의 일관성이 얼마나 중요한지 다시 한번 깨달았다.
셰이더의 복잡도보다 오히려 입력 데이터의 정리 상태가 더 큰 영향을 줄 수 있다.
배운 점
- 아웃라인 문제는 셰이더보다 모델링 구조와 노말 데이터에 더 밀접하게 연관된다.
- 하드 엣지를 사용하더라도, 아웃라인 셰이딩 시에는 별도의 처리나 예외 케이스가 필요하다.
- Unity에서는 코드 기반 노말 수정이 가능하므로, DCC 툴이 아닌 런타임/에디터 상에서 처리할 수 있는 자동화 방법이 매우 유용하다.
앞으로 해보고 싶은 점
MeshFilter
기반의 정적 메시에도 동일한 방식 적용해보기- UV에 저장한 노말을 활용해 셰이더에서 직접 아웃라인 처리해보기
- 특정 엣지만 하드하게 유지하고 싶은 경우, 그룹화를 더 정밀하게 제어할 수 있는 툴 만들기
- 하드 엣지를 유지한 이후에도 적용가능한 아웃라인 제작해보기
댓글남기기