Normal Mapping 과 TBN 행렬

2020-02-16

preview World 공간에서 Tangent 공간으로!

Normal Mapping

Normal Mapping Normal Maaping 을 하는 이유

Normal 텍스쳐는 ‘덜’ 복잡한 메쉬를 표면이 복잡한 것처럼 그려내도록 할 때 사용합니다. Normal 벡터가 들어 있는 텍스쳐를 사용해서 빛을 계산하는 데 사용하죠. 이때 쉐이더에서 텍스쳐를 샘플링해서 가져온 값을 Normal 벡터로 사용하는데, 이는 Tangent 공간의 Normal 벡터라는 것을 주의하면서 사용해야 합니다.

Tangent Space

Tangent Space Noraml n과 Tangent t 그리고 Bitangent B를 축으로 하는 공간

Normal 벡터는 단지 어떤 방향을 가리키고 있는 단위 벡터입니다. 이는 Normal 텍스쳐의 색상 값을 샘플링해서 얻을 수 있죠. 만약 어떤 물체가 오른쪽으로 회전하고 위로 움직였다고 생각합시다. 하지만 Normal 벡터는 단지 텍스쳐를 샘플링해서 얻어온 값이기 때문에 변함이 없습니다. 그렇지만 물체가 움직인 만큼 Normal도 변화해야 말이 맞겠죠. 그 대신 Normal 벡터는 그대로 두고 다른 계산에 필요한 View 벡터나 Light 벡터를 Normal 벡터가 존재하는 Tangent 공간으로 이동시키면 됩니다. 쉽게 말해서 Tangent 공간은 매 표면마다 정의되어 있고, 이는 Normal 벡터가 존재하는 공간입니다.

Model Space To World Space

Why Transpose of Inverse of Model Matrix Scale을 변경했더니 Normal 벡터가 이상해졌다!

TBN 행렬로 Tangent 공간으로 이동시키기 전에 Normal을 World 공간으로 이동시킬 때 주의할 점을 알아봅시다. 위의 첫 번째 그림처럼 원을 표현하는 정점이 있을 때 이를 y축을 기준으로 절반으로 줄이는 변환을 했다고 합시다. 의도하던 변화는 오른쪽 그림이지만 Model 행렬을 그대로 곱하면 Scale이 Normal 벡터에 적용이 되어 가운데 그림처럼 잘못되게 됩니다. 이를 해결하기 위해서는 Model 행렬을 그대로 곱하지 않고, Transpose(Inverse(Model))을 곱해주어야 합니다.

Normal 벡터는 방향만 의미를 가지므로 Model 행렬을 회전과 스케일링만 가지고 있는 행렬이라고 가정합시다.

그렇다면 우리가 원하는 행렬은 가운데 스케일만 역행렬이 된 행렬일 것입니다.

이때 회전행렬의 역행렬은 회전행렬의 전치행렬과 같습니다. 이는 회전행렬이 직교행렬이기 때문입니다. 그리고 스케일 행렬은 대각행렬이기 때문에 전치에 아무런 영향을 받지 않습니다.

전치를 밖으로 꺼내 정리하면 다음과 같이 됩니다.

이를 다시 쓰면 우리가 원하는 식이 됩니다.

TBN Matrix

Gram-Shmidt n과 t는 거의 수직이므로 만큼 -n 방향으로 t를 밀어 넣습니다.

void main() // vertex shader
{
    //...
    mat3 normalMatrix = transpose(inverse(mat3(M)));
    vec3 T = normalize(normalMatrix * tangent_modelSpace);
    vec3 N = normalize(normalMatrix * vertexNormal_modelSpace);
    T = normalize(T - dot(T, N) * N); // 그람-슈미트 직교화
    vec3 B = cross(N, T);

    mat3 TBN = transpose(mat3(T, B, N));
    //...
}

TBN 행렬을 만들기 위해서 우선 Model 행렬의 역행렬을 전치한 행렬을 구합니다. 그리고 이를 이용해 Normal과 Tangent를 World 공간으로 이동시킨 후 Bitangent 까지 만듭니다. 이때 Bitangent를 만들 때 T, B, N이 서로 수직이 아니게 될 수 있고, 결국 TBN 행렬이 직교행렬이 안될 수 있습니다. 따라서 그람-슈미트 직교화를 이용해 각각의 벡터가 다른 벡터에 수직이 되도록 합니다.

World Space To Tangent Space

void main() // vertex shader
{
    //...
    fragPosition_tangentSpace = TBN * fragPosition_worldSpace;
    cameraPosition_tangentSpace = TBN * cameraPosition_worldSpace;
    lightPosition_tangentSpace = TBN * lightPosition_worldSpace;
}

마지막으로 Fragment Shader에서 계산을 위해 World 공간에 있는 벡터들을 Tangent 공간으로 이동시켜 줍니다. 이는 Normal 벡터는 Tangent 공간에 있기 때문에 Tangent 공간으로 이동시켜야 Normal 벡터와 계산이 가능하기 때문입니다.

Normal 벡터는 Tangent 공간에 있습니다. Normal 벡터를 World 공간으로 이동시킨 후 계산을 하거나, 다른 벡터들을 Tangent 공간으로 이동시켜야 합니다. Normal 벡터를 World 공간으로 이동시키기 위해서는 TBN 행렬을 Fragment Shader로 넘겨야 하는데 그러면 Fragment Shader에서 계산이 많아져 더 비쌉니다. 그래서 다른 벡터들을 Vertex Shader에서 Tangent 공간으로 이동시키는 것입니다.

마무리

이번 포스팅에서는 실제로 Tangent 벡터를 구하는 방법이나 그래서 Tangent 공간으로 이동시킨 벡터들을 가지고 어떻게 Shading을 할지는 다루지 않았습니다. 앞으로 Phong Shading으로 시작해 PBR까지 공부하면서 천천히 다루어 보도록 하겠습니다.

유용한 링크

코드 (Git Repository)

learnopengl - Advanced-Lighting/Normal-Mapping

opengl-tutorial - intermediate-tutorials/tutorial-13-normal-mapping