Game Develop

[DirectX11] Normal Mapping 본문

ComputerGraphics/DirectX

[DirectX11] Normal Mapping

MaxLevel 2021. 5. 24. 21:22

Normal Mapping 이란?

                                                                                                                                                      

노멀 매핑(Normal Mapping)은 폴리곤의 법선 벡터(Normal Vector)의 값을 사용하여 로우 폴리곤의 그래픽 환경에서 하이 폴리곤의 입체감 및 질감을 구현하는 방법이다. 물체의 질감을 있는 그대로 폴리곤으로 표현하면 삼각형의 양이 많아지고 결국 연산이 많이 필요하게 되는데 폴리곤을 무척 적게 사용하고도 비슷한 효과를 낼 수 있는 기술이다. 90년대까지는 논문에서나 존재하던 기술이었으나 하드웨어의 속도가 빨라지고 프로그래머블 셰이더가 일반화되는 2000년대 중반부터 널리 사용되기 시작하였다. - 나무위키 -

 

정의에 대해서는 나무위키가 설명을 잘해놔서 복붙했다.

좀 더 설명을 덧붙이자면 기존에는 디퓨즈연산을 할 경우, 각 폴리곤의 법선벡터로만 라이팅 연산을 하기 때문에 폴리곤안의 픽셀들은 같은 라이팅연산을 하게된다(즉, 폴리곤 단위의 라이팅연산).

물론 폴리곤이 많으면 이정도만 해도 고퀼리티로 보이지만 폴리곤이 많을 경우 성능이 매우 많이 떨어진다.

그래서 폴리곤개수는 그대로 유지하면서 고퀼리티로 보이게 하기 위해서 법선맵을 이용한 법선매핑이라는것을 도입하게된다. 최종적으로 폴리곤단위로 라이팅연산이 아니라 픽셀단위로 라이팅연산이 되기 때문에 고퀼리티처럼 보인다.

먼저 각 폴리곤에 접선공간(Tangent Space)라는 3D공간을 만들어야한다. 
만드는 방법으로는 일단 각 폴리곤(정확히는 버텍스)들이 갖고 있는 법선벡터를 Z축(N)으로 삼고, UV좌표중 하나를 법선(Tangent)로 한다. 그리고 두 축 N,T를 외적해서 종법선(Binormal)을 구한다.
물론 종법선을 구하기위한 외적은 버텍스셰이더에서 수행해줘야한다. 버텍스단위의 새로운 월드를 구성해주는 것이기 때문에 버텍스에서 TBN을 구하고 그대로 픽셀셰이더에서 쓰면 된다.픽셀셰이더에서는 했다가는 불필요한 연산이 너무 많이 수행된다.

그리고 버텍스셰이더에서 각 T,B,N 값에 worldMatrix를 곱해준다. 
T,B,N을 이용해 월드->접선공간으로 변환시켜주는 과정을 거치는데, 그럴려면 현재 로컬좌표로 되어있는 T,B,N 벡터들을 월드좌표로 변환시켜줄 필요가 있기 때문이다.

그 다음 아래 과정을 수행하면 접선공간으로의 변환행렬을 얻을 수 있다.

  float3x3 TBN = float3x3(T, B, N); // 법선맵에서 가져온 벡터를 접선공간에서의 법선벡터로 사용하게할 변환행렬.

 

 

이해를 위한 HLSL 코드.

1
2
3
4
5
6
7
8
if(hasNormalMap)
{
    float4 normalMapping = normalMap.Sample(samp, input.uv); // 법선맵 RGB값 가져오기.
    float3x3 TBN = float3x3(T, B, N); // 법선맵에서 가져온 벡터를 접선공간으로의 법선벡터로 매핑할 변환행렬.
    normal = normalMapping * 2.0f - 1.0f; // 법선맵의 RGB값(0~1)을 벡터단위(-1~1)로 변환.
    normal = normalize(mul(normal, TBN)); // 접선공간의 법선벡터로 변환.
}
cs

 

위 코드에서는 각 T,B,N에 이미 worldMatrix가 곱해져있기 때문에 바로 normal값에 적용해서 라이팅연산을 해도 되지만,

worldMatrix를 미리 곱하지 않고 TBN 변환행렬 생성 후, 전치행렬로 바꿔줘도 같은 변환행렬을 얻을 수 있다.

worldMatrix를 곱하지 않고 TBN행렬을 만들 경우 이 변환행렬은 월드공간에서 접선공간으로의 변환행렬이다.

하지만 우리가 필요한건 접선공간에서 월드공간으로의 변환행렬이 필요하다. 왜냐하면 결국 라이팅연산은 월드좌표를 기준으로 진행되기 때문에 라이팅연산에 쓰일 벡터들은 모두 월드좌표기준으로서의 벡터들로 존재해야하기 때문이다.

그렇다면 이 TBN행렬의 역행렬을 구해야하고, 결론적으로 말하자면 TBN의 전치행렬이 곧 역행렬이기 때문에 HLSL에서지원하는 transpose함수를 이용해서 전치행렬을 구하면 된다.

 

TBN의 전치행렬이 역행렬인 이유는, TBN은 직교행렬이기 때문이다. 직교행렬의 특징으로는 다음과 같다.

 

1. 각 행벡터의 크기는 1이다.

2. 각 행벡터의 내적값은 0이다.

3. 전치행렬과 역행렬이 같다.

 

T,B,N은 각각 정규화를 거치고 오기 때문에 1번의 조건을 만족하고, T와 N은 CPU에서 직교인상태로 넘어와, 셰이더에서 T와 N을 외적해서 B를 구하기때문에 세 벡터는 모두 직교하기에 2번도 만족한다. 

내적값이 0이란 의미는, 두 벡터를 내적했을 경우 cos세타 값이 나오게 되는데 cos세타값이 0이란 의미는 90도란 의미이다. (cos90degree는 0이기 때문)

 

어쨌든 직교행렬이기 때문에 역행렬을 따로 구할필요없이 바로 전치행렬로 연산에 사용하면된다.

물론 HLSL내에서 D3DXMatrixInverse()라는 역행렬을 구해주는 함수를 지원해주지만, 전치행렬을 구하는것보다 비용이 더 많기 때문에 직교행렬에선 굳이 비용이 더 많은 역행렬함수를 사용할 이유가 없다.