Search

Linq로 부드러운 노멀 벡터 구하기에 도전

date
2019/09/24
tags
아들과 2인개발
유니티3D
Linq
렌더링
4 more properties
아들과 2인 개발 모음
Search
왼쪽: 부드러운 노멀 벡터를 사용한 외곽선, 오른쪽: 모델의 노멀 벡터를 사용한 외곽선
툰 셰이딩에 대한 조사를 마치고 나서 다음 스텝으로 건조기 몬스터를 모델링하고 있습니다. 목업 수준의 초안이 나와서 유니티에 띄워보니 각진 모서리의 경계선이 너무 엉망입니다. 몸통은 나중에 Bevel 툴로 부드럽게 다듬을 계획이라서 괜찮지만, 연통처럼 생긴 팔다리는 더는 버텍스를 늘리지 않을 계획이라서 외곽선 셰이딩을 개선하기로 결정했습니다.
지난 글에서도 얘기했지만, 각진 모서리의 경계선이 이상하게 나오는 이유는 같은 위치의 버텍스라도 서로 다른 방향의 노멀 벡터를 가지고 있기 때문입니다. 아래 그림에서 왼쪽처럼 노멀이 다른 방향을 보고 있으면, 외곽선을 그리기 위해서 버텍스를 이동시킬 때 각각의 방향으로 이동해서 외곽선에 빈틈이 생기게 됩니다.
왼쪽: 각진 노멀, 오른쪽: 부드러운 노멀
오른쪽처럼 노멀이 한 곳을 바라보도록 모델링을 하는 방법이 있지만 이렇게 하면 오브젝트 표면의 셰이딩에도 영향을 주기 때문에 외곽선용 노멀을 별도로 보관하는 방법을 사용할 수 있습니다. 저는 모델의 노멀 벡터를 얻어와서 부드러운 노멀 벡터로 변경한 다음에 복사된 모델의 탄젠트 채널에 넣어주는 방식으로 작업했습니다.
우선, 부드러운 노멀 벡터를 계산하는 방법은 아래와 같습니다.
같은 위치를 공유하는 버텍스들을 그루핑합니다.
해당 버텍스들의 노멀 벡터들을 가져와서 평균을 낸 다음, 평균값을 다시 노멀 벡터에 덮어씁니다.
방법도 간단하고 관련 코드도 많이 공유되어 있어서 그냥 가져다 쓰면 되는 일이지만, 제 마음 속의 악마가 Linq를 사용하면 한 스테이트먼트에 구현할 수 있을 것 같다고 속삭이는군요. (하나의 스테이트먼트란 하나의 세미콜론으로 끝나는 문장이라고 보시면 됩니다) 정신을 차리고 보니 결코 짧다고는 말할 수 없는 하나의 스테이트먼트를 만들어냈습니다!
// Mesh mesh = ...; List<Vector3> smoothNormals = ( from avgn_il in ( from v_i in mesh.vertices.Select((v,i) => new {v,i}) join n_i in mesh.normals.Select((n,i) => new {n,i}) on v_i.i equals n_i.i group n_i by v_i.v into v__n_i select new { avgn = new Vector3(v__n_i.Average(e=>e.n.x), v__n_i.Average(e=>e.n.y), v__n_i.Average(e=>e.n.z)), il=v__n_i.Select(e=>e.i), } ) from avgn_i in ( from i in avgn_il.il select new { avgn=avgn_il.avgn, i=i}) orderby avgn_i.i select avgn_i.avgn ).ToList();
C#
분명히 성공은 성공인데, 사과 껍질을 안 끊기고 한 번에 깍아 낸 것처럼 약간의 뿌듯함과 함께 자괴감이 밀려들었습니다.
그래도 관심이 있는 분들이 계실 수도 있고, 저도 나중에 보면 기억이 안 날 것 같아서 아래에 주석을 추가해 보았습니다. (참고로, Mesh.vertices와 Mesh.normals는 같은 인덱스를 사용합니다.)
// 원본 메시를 준비 // Mesh mesh = ...; // 부드러운 노멀 벡터가 저장될 리스트 List<Vector3> smoothNormals = ( // in 뒤쪽의 괄호를 통해서 얻게 될 리스트의 원소에는 부드러운 노멀(avgn)과 해당 노멀 벡터를 다시 덮어쓸 노멀 벡터 리스트상의 인덱스를 가진 리스트(il)가 있음. from avgn_il in ( // 버텍스의 리스트에 인덱스를 붙여주어서 v_i를 얻는다. from v_i in mesh.vertices.Select((v,i) => new {v,i}) // 노멀의 리스트에 인덱스를 붙여주어서 n_i를 얻고, 같은 인덱스의 버텍스와 노멀을 결합한다. join n_i in mesh.normals.Select((n,i) => new {n,i}) on v_i.i equals n_i.i // 버텍스의 위치를 기준으로 노멀과 인덱스를 그루핑해서 v__n_i를 얻는다. // v__n_i는 키가 버텍스 위치, 값은 "노멀과 인덱스"의 리스트가 된다. group n_i by v_i.v into v__n_i // 같은 버텍스 위치에 대한 노멀을 모았으므로, 평균을 내서 부드러운 노멀을 만든다. select new { // 평균낸 노멀. 즉, 부드러운 노멀을 avgn에 저장 avgn = new Vector3(v__n_i.Average(e=>e.n.x), v__n_i.Average(e=>e.n.y), v__n_i.Average(e=>e.n.z)), // 평균 계산에 사용했던 노멀들의 인덱스 리스트를 il에 저장 il=v__n_i.Select(e=>e.i), } ) // 버텍스 기준으로 그루핑 되었던 것을 풀어서, 부드러운 노멀과 인덱스를 가진 avgn_i를 얻는다 from avgn_i in ( from i in avgn_il.il select new { avgn=avgn_il.avgn, i=i}) // 인덱스 기준으로 정렬 orderby avgn_i.i // 부드러운 노멀만 선택 select avgn_i.avgn // List<Vector3> 타입으로 변환 ).ToList();
C#
이렇게 만든 부드러운 노멀은 메시의 탄젠트에 넣으면 됩니다. 아웃라인 셰이더도 노멀이 아닌 탄젠트를 읽도록 수정해줍니다.
struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; #if _OUTLINENORMAL_TANGENT float3 tangentOS : TANGENT; // 탄젠트 채널 추가 #endif }; Varyings vert(Attributes input) { Varyings output = (Varyings)0; VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); #if _OUTLINENORMAL_TANGENT float3 normalOS = input.tangentOS; // 탄젠트를 외곽선용 노멀로 사용 #else float3 normalOS = input.normalOS; #endif }
C
이렇게 완성된 아웃라인을 감상해보시죠! 왼쪽이 부드러운 노멀을 사용하는 아웃라인입니다.

마치며..

사실 지금 시점에서 외곽선이 중요한 것은 아닌데 쓸데없이 도전정신이 생겨서 딴 길로 샜습니다. 세계 최초로 하나의 스테이트먼트를 사용해서 부드러운 노멀 벡터 만들기에 성공했지만, 상처만 남은 느낌입니다.
이제는 빨리 진도를 뽑아야 할 것 같습니다. AD 님이 벌써 두 번째 게임의 아이디어를 내놓고 있으니까요.