Normal, Tangent, BiTagent Vector 그리고 Unity Normal Map

3D 그래픽스에서 Normal Vector(노멀 벡터)는 자주 쓰이는 개념이다.

물체 표면을 이루고 있는 Polygon의 Vertex에서 수직으로 뻗어 나오는 벡터가 Normal Vector이다.
그림으로 표현하자면 이렇다.

NormalVector

Normal Vector에는 Face(Surface) Normal과 Vertex Normal을 구분하기도 하는데 이것도 그림으로 표현하면 이렇다.

3021.normals_03714833

그리고 Vertex Normal은 각 Triange들이 이루고 있는 세 Vertex가 동일한 Normal Vector를 가지도록 엔진을 제작할 수도 있고, Vertex가 Merge된 경우 아예 위의 그림에서 마지막에 보이는 것 처럼 인접 Polygon들의 Normal을 평균내어 Normal Vector를 구해 내기도 한다.

그럼 3D Graphics에서 말하는 Tangent Vector와 BiTangent(BiNormal) Vector란 무엇인가?
Tangent Vector는 Normal Vector와 수직인 벡터이다. 그림으로 표현하면 아래와 같다.

TangentVectors

위의 그림에서 보듯이, 파란색 끝을 가지는 화살표가 Normal Vector이고, 나머지 검은색 Vector들이 바로 Tangent Vector들이다.
이 Tangent Vector들을 구하는 방법은 여러가지가 있겠지만, 파란색 끝을 가지는 화살표 Normal Vector와 아무 Vector와 Cross Product 연산을 해주면, 1개의 Tangent Vector가 튀어 나온다. (한 Vector에 수직인 하나의 Vector를 구하는 방법, Refs)

자, 그런데 문제는 Tangent Vector가 한두 녀석이 아니라는 것이다. Normal 에 수직인 벡터는 사실상 Normal Vector가 만들어내는 평면 위의 모든 벡터들이 해당된다.

따라서 그래픽스에서는 통상적으로 텍스쳐 좌표인 UV 좌표와 비교하여 U좌표와 일치하는 Vector를  Tangent, V 좌표와 일치하는 Vector를 BiTangent Vector라고 일컫는다.
그림으로 표현하면 아래와 같다. (빨간색이 Tangent, 연두색이 BiTangent)
NTBFromUVs

이 Normal Vector와 Tangent, BiTangent Vector 개념은 그래픽스 프로그래밍을 하다보면 꽤 자주 쓰인다.

Normal Vector가 중요한 이유는 바로 우리가 익히 들어서 알고 있는 Normal 맵과 밀접한 관련이 있다.
Normal Map은 다들 알다시피 물체 표면의 작은 굴곡들을 Polygon을 추가로 작업하지 않고 표현해 내는 렌더링 방식이다.

간단한 Wikipedia 이미지 한장으로 이해가 쉽게!
Normal_map_example

4백만개 폴리곤으로 만들어진 오브젝트를 500개 폴리곤 + Normal Map으로 비슷한 효과를 낼 수 있으니 -_-b.
노멀 맵을 적용 위해서는 랜더링 파이프라인 과정에서 오브젝트의 픽셀을 조정해야하기 때문에 Fragment Shader를 사용하여 적용한다.

이 때 노멀 맵을 만들어 주게 되는데, 이 노멀맵은 해당 오브젝트의 표면의 Normal Vector값을 통으로 저장하고 있는 맵이다. 노멀 맵의 저장 방식은 두가지가 있다. Model Space(Object Space) Normal Map과  Tangent Space Normal Map 인데, 그림으로 보면 명확해 진다.

gargoyle-uv_world_space

gargoyle-uv_tangent_space

첫번째 그림이 Model Space Normal Map이고 두번째 그림이 Tangent Space Normal Map이다. 다른점은 Model Space Normal Map은 노멀맵이 오브젝트에 입혀졌을 때 그 때의 모든 표면의 Normal Vector의 x,y,z를 이미지의 RGB 값에 맵핑 시켜 저장한 것이다. 따라서 x, y, z가 0~1사이의 모든 방향으로 적절하게 분배되어 나오게 되므로 색상 또한 다양하게 표현이 된다. 반대로 Tangent Space Normal Map은 노멀맵이 적용된 물체의 표면을 기준으로 Normal Vector값을 연산하여 저장한 이미지이다. 이 때 물체의 표면의 Normal 은 대게 z 방향(표면의 바깥 방향)으로 튀어 나오므로 bluish하게 보이게 된다. Model Space Normal Map에 비해 Tnagent Space Normal을 사용하는 이유는 아래와 같다.
1. 뽑아내기 쉽다
2. 한번 뽑아내서 여러 매쉬에 사용 가능하다.
3. Bone이나 바이패드를 사용하는 에니메이션이 되는 메쉬에서는 항상 Normal이 변하기 때문에 Tangent-Space Normal을 사용해야 한다.
(Model Space Normal에서 Forward Kinematics 연산을 한 후의 Vertex Normal을 구하기 위해서는 에니메이션 객체의 회전과 FK 연산을 모두해줘야 하는 Overhead)
이에 대한 추가적인 설명은 GameDev StackExchange를 참고하면 되겠다.

유니티에서 노멀맵을 적용하기 위한 쉐이더 코드는 Official Document에 잘 나와 있고 아래와 같다.

Surface Shader를 써서 적용했는데 기본 텍스쳐를 Albedo 색상으로 적용한 후에, 간단히 BumpMap의 좌표를 이용하여 BumpMap의 픽셀 값을 읽어와서 Normal Output에 넘겨주면 적용된다.
이 때 UnpackNormal 이라는 Built-in Function 을 쓰는데, Built-in Shader 파일인 UnityCG.cginc 파일 내부를 보면 해당 함수의 정의를 볼 수 있다.

모바일 디바이스의 OpenGL인 OpenGL ES와 모바일 API의 경우 간단한 연산을, 그 외엔 조금 더 정교한 연산을 하도록 하고 있다.
Unity의 노멀맵은 내부적으로 DXT5nm 형식을 쓰고 있다. 이 DXT5nm 포멧은 DXT5와 형식이 같은데 차이점은 압축하기 전에 Red 값을 Alpha값으로 옮기는 것이다.
이는 Data Loss를 최소화 시키기 위함인데 DXT5 포멧의 바이트 배분은 아래와 같다.

24바이트를 고루 나눠 쓰고 있긴하지만 A와 G 채널의 바이트를 가장 많이 할당한다. 그러므로 압축 전에 R 값을 A로 옮기는 작업은 데이터 손실을 최소화 시키는 좋은 방식이다.

위의 코드는 0 ~ 1 로 구성된 좌표계를 -1 ~ 1 좌표계로 이동시키는 작업이다. w 즉 a 채널의 값을 x에, y 즉 G 채널의 값을 y에 저장 시킨다.
그리고, 아래 좌표는 x, y 값을 가지고, Normalized 된 Normal Vector의 z 값을 복원하여 완벽한 Normal Vector를 만들어내는 작업이다.

반면 모바일의 경우엔 DXT(RGB, 5:6:5), PVRTC 등의 포멧의 값을 그대로 읽어서 추가 연산 없이 좌표계 이동만 계산해 낸다.

노멀맵의 적용은 많은 추가 작업과 추가 연산이 들어간다.
1) 아티스트들이 작업할 때 추가적으로 노멀 맵핑 텍스쳐를 만들어 줘야하고,
2) 그로 인해 파츠나 캐릭터가 많을 경우 게임의 용량이 비례해서 커지게 된다.
3) 또한, Shader를 써서 픽셀 단위당 연산을 해줘야 하기 때문에 디바이스 상에서도 추가 부담이다.

그럼에도 불구하고 프로젝트에 맞게 적절하게 쓸 경우 노멀맵이 만들어내는 훌륭한 Visual은 위의 모든 단점을 커버하고도 남는다.

Armor_CU_sky

References :
http://www.surlybird.com/tutorials/TangentSpace/
http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/
http://gamedev.stackexchange.com/questions/88353/why-are-normal-maps-predominantly-blue/88356#88356
http://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html
http://docs.unity3d.com/Manual/android-GettingStarted.html
http://tech-artists.org/wiki/Normal_map_compression
http://www.nvidia.com/object/real-time-normal-map-dxt-compression.html
http://answers.unity3d.com/questions/170985/how-does-unity-handle-normal-maps-internally.html