참고
https://learnopengl.com/Advanced-Lighting/Normal-Mapping
https://blog.naver.com/ideugu/221402703960
Normal Mapping
모든 장면들은 셀 수 없이 많은 삼각형들로 이루어진 meshes이다. 이런 평평한 삼각형 2D Texture에게 생기를 불어주는 것이 해당 기술이다.
예를 들어, 벽돌 벽면이 존재한다고 한다면 빛은 벽돌 사이의 작은 크랙들을 인식하지 못한다. 왜냐하면, 표면의 normal vectors가 수직(perpendicular)적이기 때문이다.
위의 사진들을 비교하면 확연한 차이가 나타나는 것을 확인할 수 있다. 노말 맵을 적용하기 위해서는 한 가지 Resource가 필요하다.
DirectX형태의 자료는 Y값을 반전 시킬 필요 없지만, glTf와 같은 형태는 OpenGL 환경에 맞춰있다보니, Y값에다가 -1을 곱해줘야 한다.
노말맵은 normal vector를 (R, G, B) = (x,y,z) 성분으로 나타낸 리소스이다.
즉, 각 brick에서 높은 부분에 위치한 brick이 더 초록색이 되는 경향이 있다. 이것은 y direction이 (0,1,0)에 가깝다는 이야기를 의미한다. 그리고 파란색에 가깝게 보이는 brick들은 (0, 0, 1)과 같은 positive z-dircetion을 가진 normals이다.
기본적으로, 노말맵의 normal vector는 항상 양의 z축을 기준으로 가리킨다. 예를 들어, 큐브 형태의 물체가 있다고 한다면, normal vector 방향이 음수가 되는 경우도 있을 것이다. 이러한 상황에서 노말맵은 올바르게 적용되지 않는다. 이러한 문제를 해결 법 중 하나는 각 면의 방향에 맞는 별도의 노말 맵을 정의하는 것이다. 이것은 현실적으로 힘든 방법이므로, 다른 조명 벡터들이 노말 맵의 방향 벡터를 기준으로 변환한다. 이러한 계산을 돕는 것이 tangent space이다.
노말맵의 normal vector들은 tangent space(양의 z direction을 표현하는)에서 표현되어진다. 특정 행렬 계산을 통해 normal 벡터의 방향을 바꾸는 것은 local tangent space로부터 world->view 좌표 계산 시 이루어진다.
이러한 행렬을 TBN
행렬이라고 부른다. 우리는 3개의 수직 벡터(Right, Forward, Up)가 필요하다.
- Tangent Vector : Right
- Bitangent Vector : Forward
- Normal Vector : Up
tangent와 bitangent를 계산하지는 것은 좋지 않은 방법이다. 그림에서 볼 수 있듯이, 두 벡터는 texture coordinates의 방향과 정렬되어 있는 것을 볼 수 있다. 이것을 이용해서 tangent
와 bitangent
를 계산할 수 있다.
T
: Tangent VectorB
: Bitangent Vector- ΔU, ΔV : 텍스쳐 좌표의 차이를 의미한다.
$$E_1 = \Delta{U_1}T + \Delta{V_1}B \tag{1}$$
$$E_2 = \Delta{U_2}T + \Delta{V_2}B \tag{2}$$
$$(E_{1x},E_{1y},E_{1z}) = \Delta{U_1}(T_{x},T_{y},T_{z}) + \Delta{V_1}(B_{x},B_{y},B_{z}) \tag{3}$$
$$(E_{2x},E_{2y},E_{2z}) = \Delta{U_2}(T_{x},T_{y},T_{z}) + \Delta{V_2}(B_{x},B_{y},B_{z}) \tag{4}$$
두 삼각형 사이의 벡터(E)를 계산할 수 있다. 그렇다면 남는 것은 T, B 벡터이다. 선형대수학을 이용해서 이 두 값을 구할 수 있다.
여기서 행렬(A)와 역행렬(A^-1)을 곱하게 되면 항등행렬(I)이 되어, 계산에 영향을 미치지 않는다. 위와 같은 순서로 최종적으로 T
와 B
의 벡터를 계산할 수 있게 된다.
glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;
tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x); tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y); tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
bitangent
는 향후 쉐이더 계산 시 cross product
를 통해서 도출하면 된다.
float3 normal = normalTex.SampleLevel(linearWrapSampler, input.texcoord, lodBias).rgb; // 범위 [0, 1]
normal = 2.0 * normal - 1.0; // 범위 조절 [-1.0, 1.0]
// OpenGL 용 노멀맵일 경우에는 y 방향을 뒤집어줍니다.
normal.y = invertNormalMapY ? -normal.y : normal.y;
float3 N = normalWorld;
float3 T = normalize(input.tangentWorld - dot(input.tangentWorld, N) * N);
float3 B = cross(N, T);
이렇게 TBN
행렬을 구할 수 있다.
여기서 한가지 특이한 점은 노말맵을 기준으로 T
를 다시 계산할 때 dot(input.tangent, N) * N을 하는 것을 볼 수 있다. 두 길이가 1이므로, cos 값만 남게된다. 이렇게하면 아래 그림처럼 N에 직교하는 T값을 구할 수 있게 된다.
$$T=(tangent\ -\ cos\theta * N)$$
앞서 소개한 직교하는 벡터의 형태로 normal mapping을 구현할 수 있고 비직교적인 방법으로 조명과 카메라의 위치를 통해 TBN 행렬을 도출하는 방법이 있다. 다만, 이 방법은 계산량이 많아진다는 단점이 있다.
'Graphics' 카테고리의 다른 글
7. Screen Space Ambient Occlusion(SSAO) (0) | 2024.07.04 |
---|---|
6. Deferred Lighting (0) | 2024.07.01 |
4. Point Shadow Mapping (1) | 2024.06.24 |
3. Directional Shadow Mapping (0) | 2024.06.21 |
2. Depth Buffer와 안개 효과 (1) | 2024.06.14 |