본문 바로가기

그래픽스

[그래픽스] Normal Mapping

Normal Mapping은 PBR에서 필수적으로 사용되는 기술이다.

Normal Mapping을 사용하지 않으면 빛에 따라 음영이 지긴 하지만 Texture가 평면이라는 느낌이 든다.

하지만 Normal Mapping을 사용한다면 빛에 따라 훨씬 더 사실적인 질감을 Texture가 가지게 된다.

 

 

기존 Texture는 픽셀들이 Interpolation되어 Vertex 4개와 모두 같은 Normal을 가지고 있는데 Normal Mapping된

Texture는 픽셀들이 모두 자신만의 Normal Vector을 가지고 있다. Normal Mapping은 Texture로부터 Normal

Vector을 샘플링해오는 것이다.

 

Normal Mapping을 할 때 주의해야 할점은 텍스쳐 좌표계와 렌더링하는 월드 좌표계가 다르다는 것이다.

텍스쳐 좌표계는 아래가 y축의 양의 방향이지만 월드 좌표계는 위가 y축의 양의 방향이므로 텍스쳐 좌표계에서

월드 좌표계로 변환하기 위해 normal vector을 구할 때 y와 z에 -1을 곱해줘야 한다.

 

 

Texture에서 Normal Vector을 가져올 때 Normal Vector을 Texture 좌표계에서 월드 좌표계로 변환시켜줘야 한다.

이 Normal Vector가 어떤 좌표계에 정의되어 있는지를 알아야 하기 때문에 Vertex Shader의 ConstantData에

Vector3 tangentModel을 추가한다. input_element_desc에도 "tangent"라는 이름으로 추가한다. 

 

Texture 좌표계에서 x축 방향을 tangent, y축 방향을 bi-tangent, z축 방향을 normal이라고 한다. 

 

 

  float3 normalTex = g_normalTexture.SampleLevel(g_sampler, input.texcoord, lod).rgb;
  normalTex = 2.0 * normalTex - 1.0;

 

Pixel Shader에서 Normal Map Texture을 샘플링한다. 이 때 mipmap을 이용한 samplelevel을 사용한다.

그리고 우리가 사용하고 있는 픽셀 포맷은 UNORM, 0~255를 0~1로 변환해서 사용한다. 그런데 양수만 가지고는

모든 normal vector을 표현할 수 없기 때문에 2를 곱하고 1을 빼서 -1~1의 범위로 만들어준다.

 

월드 공간의 Texture가 Texture 좌표계에서 Texture와 같은 방향을 바라보고 있다고 할 때, 

T(Tangent) = (1, 0, 0)  B(Bi-Tangent) = (0, -1, 0)  N(Normal) = (0, 0, -1) 이다.

그 다음으로 텍스쳐 -> 월드 로 변환하는 변환행렬을 만들어야 한다.

 

 

        float3x3 TBN = float3x3(T, B, N);
        normalWorld = normalize(mul(normalTex, TBN));

 

변환행렬을 만드는 법은 x축을 변환한 것을 1행에 넣고, y축을 변환한 것을 2행에 넣고 z축을 변환한 것을 3행에 넣는다.

원래는 4x4 행렬을 만들어야 되는데 지금은 이동이 필요없으므로 3x3행렬로 만들도록 하겠다.

변환행렬은 위의 벡터 T, B, N을 1행, 2행, 3행에 넣으면 된다. 그리고 아까 샘플링해온 normalTex 벡터에

TBN을 곱하고 normal vector니까 normalize해준다. 결과적으로 월드 좌표계에서  normal 벡터를 구할 수 있다.

하지만 이것은 모델이 Texture와 정확히 같은 방향을 보고 있을 때이고 일반적인 상황에선 적용할 수 없다.

 

 

 

Texture 좌표계의 축들을 삼각형 입장에서 어떤 방향인지 계산해서 변환 행렬을 구할 수 있다. 삼각형 각각 vertex가

texture 좌표계에서 좌표와 world 좌표계에서 좌표를 함께 가지고 있으므로 이것을 이용해서 texture 좌표계의 축들을 

world 좌표계로 변환할 수 있다. 

 

물체가 변환됨에 따라서 normal 벡터도 invTranspose를 곱해줘서 변환을 했다. tangent벡터도 함께 변환을 시켜줘야 하는데, tagent벡터는 물체 표면에 있는 벡터이다.

 

 

tangent 벡터를 p2 - p1이라 할 때 물체에 M행렬을 곱해 변환되었을 때 변환된 tangent 벡터는 (p2 - p1)M이다.

즉, 변환전 tangent 벡터 t에 행렬 M을 곱하면 tangent 벡터도 변환된다. 그래서 tangent 벡터에 modelToWorld 행렬을

곱하면 tangent 벡터를 변환할 수 있다. 

 

 

 

  이제 texture 좌표계를 다시 world 좌표계로 바꿔보자.

 

 

        float3 N = normalWorld;
        float3 T = normalize(input.tangentWorld - dot(input.tangentWorld, N) * N);
        float3 B = cross(N, T);

 

normal 벡터는 normalWorld 그대로, T는 tangent 벡터와 normalWorld 벡터가 수직이라고 확정지을 수 없으므로

그람 - 슈미츠 직교화를 통해 normalWorld벡터와 수직인 벡터를 만든 것이다. (나도 잘모름, 추가적인 공부 필요)

B벡터는 N과 T를 외적해서 구할 수 있다. (외적 순서 주의)