normal mapping (법선 매핑)

반응형

specular map : 각 텍셀에 정반사광을 보여줄 정도를 정의
normal map : 법선벡터의 방향을 나타낸다.
                    높이맵(height map)에서 기복을 계산하고, 그 높이의 기복으로부터 법선벡터를
                    계산하여 텍스처에 기록.
                    이렇게 각 픽셀에 사용할 법선 정보를 담고 있는 텍스처를
                    법선맵(normal map)이라고 하고 법선맵을 이용해서
                    조명을 계산하는 기법을 법선매핑(normal mapping)이라고 한다.                   
height map : 법선방향의 기복 크기를 기록한 텍스처, 높은곳이면 하얗게,
                      낮은곳이면 검게 그려진다.

법선벡터의 중요성 : 
확산광에서는 법선과 광원벡터만 사용해서 조명의 강도를 계산.
   ex : Lambert( N · L )
반영반사광에서도 법선벡터가 중요.
   ex : Phong, Blinn Phong ( N · L )

문제점 : 
일반적으로 법선은 정점마다 있어서 픽셀단위 조명에서 이러한 정점에 정의된
법선 벡터를 보간해서 사용
.
하지만 이 방법은 솟아 오른곳이나 울퉁불퉁한 곳에는 폴리곤 수를 늘려
하나하나의 정점과 법선을 
모델러가 조절해 줘야 한다는 단점이 있다.
결론은, 아무리 하이폴리곤 오브젝트를 사용하더라도 정점과 정점 사이는 선형으로 보간되기 떄문에
굴곡의 표현이 어려운데 이 부분을 픽셀셰이더에서 매핑을 이용해 해결.
변경된 법선을 이용하여 정점과 정점 사이에 주름과 돌기같은 음영표현을 한다. 

대안 : 
텍스쳐를 이용해서 간편하게 폴리곤의 삼각형 크기보다 상세한 기복을 표현할 수 있도록 한다. 

즉, 법선맵핑이란 거친질감을 표현하기 위해 특수한 텍스쳐를 사용해 법선벡터를 섭동시켜 픽셀마다 조명계산을 
하게 하여 기복이 있는 것처럼 보이게 한다. 

만들어 보기.
텍스처는 R,G,B채널이 있으므로 각 X,Y,Z값을 대입하여 저장한다.
하지만 텍스처의 각 채널에 가질 수 있는 값의 범위는 0~1 이다.
이곳에 정규화한 단위벡터를 넣어주면 된다.
단위벡터는 크기값이 1인 벡터인데 크기값이 1이라는 것은 (1,0,0)일 수도 있지만
(-1,0,0)이 될 수도 있다는 말이 된다.
즉, 정규화된 법선벡터의 범위가 0~1이 아니라 -1~1이 되어 버리는 것이다.

그렇다면 0~1의 범위만을 포용하는 텍스처에 -1~1의 값을 가지는 벡터값을 어떻게 넣을 것인가?
담을 바구니를 크게 만들지 못한다면 넣을 물건을 작게 만들면 되지 않을까?
즉, -1~1을 0~1의 값으로 변환시키면 된다.
법선맵 RGB = 법선벡터 XYZ * 0.5 + 0.5
이렇게 하면 -1은 0에 맞추어지고 1은 1에 맞추어 진다.

반대로 법선맵 RGB로부터 법선벡터 XYZ를 구하고자 한다면?
위의 식 중 *0.5 + 0.5를 넘겨주면 된다.
법선맵 RGB - 0.5 = 법선벡터 XYZ * 0.5
2*(법선맵 RGB - 0.5) = 2*(법선벡터 XYZ * 0.5)
2*법선맵 RGB - 1.0 = 법선벡터 XYZ * 1.0 = 법선벡터 XYZ
법선맵 RGB * 2 - 1.0 = 법선벡터 XYZ

Toon Shading에서는--------------------------------------------------------
Toon Shading에도 이와 비슷한 방법을 사용했었는데 결과 diffuse값이 0~1인 값을
0, 0.2, 0.4, 0.6, 0.8, 1.0 과 같이 0.2 단위의 값만을 가지도록 만들기 위해
diffuse*5를 곱해서 0~5의 값을 가지게 만들고 ceil()을 사용하여
딱 맞게 0,1,2,3,4,5 의 값중 하나만 가지도록 올림연산 한다.
그리고 이 값들을 5로 나누어 주면 0, 0.2, 0.4, 0.6, 0.8, 1.0의 값으로 만들 수 있다.
----------------------------------------------------------------------------

이렇게 법선 벡터를 텍스처에 저장하는데 이 법선벡터가 가지는 x,y,z의 의미를 생각해 보자.
법선벡터가 (-1,0,0)의 값을 가진다면 이 벡터는 어딜 향하고 있는 것일까?
x,y,z의 값이 의미가 있으려면 이 값을 표현해 줄 수 있는 공간이 있어야 한다.


위 그림은 법선매핑에서 사용되는 공간의 xyz 좌표계다.
표면에 수직인 방향이 z이며 오른쪽이 x고 y는 이 두 방향에 직각인 방향이다.
그런데 여태까지 해오던 로컬공간, 월드공간, 뷰공간, 프로젝션 공간같은 것중 이 공간은 무슨 공간일까?


육면체의 왼쪽과 오른쪽 면에 동일한 법선맵을 사용한다고 가정한다면 왼쪽면의 z방향(0,0,1)은 왼쪽을 향하고
오른쪽면의 z방향(0,0,1)은 오른쪽을 향한다.
동일한 벡터이지만 가리키는 방향이 다르다?
현재까지 사용한 공간이랑 개념이 조금 다른데 이전 공간들은 물체마다 정의된 것이라면 이 공간은
표면마다 정의
되어 있는 것이다.
"표면의 바깥쪽 방향을 법선의 +z로 한다"는 명제를 사용하여 법선맵을 만들기 때문에
표면마다 다른 공간이 존재하는데 이 공간을 접선공간(tangent space) 또는 표면공간이라고 한다.

이제 이 법선맵에서 법선벡터를 가져와 라이팅을 계산하면된다.
그런데 문제가 하나 있다.
이전 툰셰이딩에서도 말했듯이 같은 공간의 것들끼리 연산을 해주어야 하는데
법선정보가 접선공간에 존재한다는 것이다.
즉, 법선정보를 사용하고 한다면 공간변환을 해 줘야 한다는 것이다.
그렇다면 공간변환을 위해 필요한 행렬연산은 어떻게 만들어줘야 하는가?
접선공간을 구성하는 행렬은 좌표축 3개만 있다면 쉽게 만들 수 있는데
우선 정점의 법선인 Z, 표면위를 달리는 축인 X, 그리고 X와 Z에 직각인 Y를 구해야 한다.
Z는 법선자체 이므로 그대로 사용하면 되고 X는 표면위의 정보에서 구해올 수 있는데
UV좌표가 표면위에 정의되어 있기 때문에 U와 V중 하나를 가져와 그것을 X로 사용하면 된다.
그래서 이 X를 접선(tangent)라고 한다.
X와 Z에 동시에 직각인 Y는 외적을 통해 쉽게 구할 수 있는데 이 축을 종법선(binormal)이라고 한다.
접선 T = (Tx, Ty, Tz)
종법선 B = (Bx, By, Bz)
법선 N = (Nx, Ny, Nz)
로 표시할 수 있는데 접선공간변환에 사용하는 행기준 행렬은 (d3d)
| Tx Ty Tz |
| Bx By Bz |
| Nx Ny Nz |
이렇게 표시 할 수 있고 열기준 행렬이라면 (open gl)
| Tx Bx Nx |
| Ty By Ny |
| Tz Bz Nz |
요렇게 표현할 수 있다.

이제 말로만 계속 언급했던 normal map(법선맵)을 보도록 하자.

법선맵은 왜 항상 푸른계열의 색상을 유지할까?
RGB채널중 B 채널은 법선벡터의 Z와 매칭이 되는데 법선벡터의 Z는 항상 표면의 바깥쪽을 가리키는 방향이기 때문에
벡터의 범위인 -1~1의 범위가 아니라 0~1의 범위를 가진다.
그런데 이전에도 말했듯이 벡터범위인 -1~1를 텍스처 범위인 0~1로 우겨넣기 위해 *0.5, +0.5를 시행하므로
Z값은 범위가 0.5~1.0이 되어 항상 Z값은 최소한 절반이상 들어가 있게 된다.
그래서 B값이 유독 크기 때문에 파란색을 띄게 된다.


돌판의 오른쪽을 보면 붉은색의 하이라이트가 있고 왼쪽은 어두워 진다.
실제 이 돌판은 분명 튀어나와 있는데 오른쪽 붉은색 하이라이트 부분의 경우는 오른쪽을 향하는
법선을 가지고 있을 텐데 이 값이 (1,0,0)이라서 변환 후 (1, 0.5, 0.5)가 되서 붉은색을 띄고
왼쪽은 당연히 왼쪽을 향하고 있을 것이며 왼쪽은 (-1,0,0)이 되고 이는 변환시 (0, 0.5, 0.5)가 되는 것이다.

위 아래도 마찬가지로 위쪽은 당연히 위쪽으로 법선이 향할것이고 아래는 아래쪽 방향으로 향할 텐데
아래쪽 법선의 값은(0,1,0)이라서 변환하면 (0.5, 1.0, 0.5)가 되어 녹색을 띄고 위쪽은 (0,-1,-0)이라서
변환하면 (0.5, 0, 0.5)가 되어 녹색이 사라지면서 어둡게 된다.

이제 렌더몽키로 실제 구현을 해 보자.
중요한건 이전 diffuse를 구하던 것을 vertex shader가 아닌 pixel shader에서 해야 한다는 것이다.
diffuse를 구한 다는 것은 light dir과 normal을 내적한다는 말인데 여기에서는 normal을
texture에서 구해서 사용하기 때문에 texture의 텍셀값을 얻어올 수 있는 pixel에서 diffuse를 구해야 한다.
즉, 법선매핑을 사용할 때 정점에서 가져오는 법선은 조명에 사용하지 않는다.
그리고 Reflection값도 http://mgun.tistory.com/1337 요기 링크를 보면 알겠지만 E와 N의 내적으로 구해진다.
N을 정점의 노말로 사용하지 않고 텍스처에서 사용하므로 Reflection값도 픽셀에서 구할 수 밖에 없다.

그리고 접선공간을 변환하기 위해 지정한 세 데이터(법선, 접선, 종법선)도 월드 공간에 있어야 한다.
조명계산에 필요한 것은 결국 Light Dir(입사광 벡터), View(카메라 벡터), Normal(법선벡터)다.
그런데 올바른 법선이 텍스처에서 읽어와야 하는데 텍스처는 접선공간에 존재하므로
이를 월드공간으로 변환해 줘야 조명계산하는데 필요한 모든 요소들을 한 공간에 묶을 수 있다.
그러려면 픽셀 셰이더에서 법선/접선/종법선 벡터를 사용해서 접선공간에서 월드공간, 또는
월드공간에서 접선공간으로 변환하는 행렬을 만들 수 있다는 말이 된다.

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mUV: TEXCOORD0;
   float3 mLightDir : TEXCOORD1;  
   float3 mViewDir: TEXCOORD2;
   float3 T: TEXCOORD3;
   float3 B: TEXCOORD4;
   float3 N: TEXCOORD5;

};

픽셀셰이더에서 법선텍스처에서 법선을 읽는다.
float3 tangentNormal = tex2D(NormalSampler, Input.mUV).xyz;

텍스처에서 읽어온 값은 0~1이다.
하지만 실제 법선벡터의 범위는 -1~1이다.
0~1의 범위의 값을 -1~1로 다시 바꿔주기 위해서는 *2를 하여 0~2로 만든 후 -1을 하여 -1~1로 설정한다.
tangentNormal = normalize(tangentNormal*2-1);

접선공간 법선을 구했는데 이 녀석은 말 그대로 접선공간에서의 법선값이다.
이를 월드공간에서의 값으로 변환 해 줘야 라이팅 계산시 사용할 수 있다.
그래서 T,B,N벡터로 행렬을 하나 만든다.
float3x3 TBN = float3x3(normalize(Input.T), normalize(Input.B), normalize(Input.N));

그런데 이 TBN 행렬은 월드공간을 접선 공간으로 변환하는 행렬이다.
지금 필요한건 접선공간을 월드공간으로 변환시켜줄 행렬이다.
TBN의 역행렬을 사용해야 하는데 직교행렬의 역행렬(Inverse)은 전치행렬(Transpose)과 같다.
그렇다면 직교행렬이란 무었인가?


정리해 보면 직교행렬이란..
1. 행렬의 각 행벡터가 서로 직교해야 한다.
2. 각 행벡터의 크기가1이어야 한다.
3. 두 행벡터의 내적은 항상 0이어야 한다.

우선 T,B,N은 모두 normalize했으므로 벡터의 길이는 1인것은 당연하고
세 벡터가 서로 직교하므로  내적은 항상 cos90도, 즉 0이 나오므로 TBN은 직교행렬이다.

그렇다면 전치행렬은 무었인가?
전치행렬은 단순하게 행렬의 행과 열을 바꾸는 것을 말한다.

TBN은 직교행렬이고, 월드공간을 접선공간으로 변환시켜준다.
이를 접선공간에서 월드공간으로 바꿔주는 행렬로 바꿔주기 위해
TBN의 역행렬을 구해줘야하는데 TBN은 직교행렬이므로 역행렬과 전치행렬은 같다.
HLSL에서 전치행렬을 구하는 함수 transpose()가 미리 준비되어 있다.
TBN = transpose(TBN);

이제 접선에서 월드공간으로 변환시킬 수 있는 행렬을 구했으니 접선공간에서 구한 normal값을
월드공간으로 변환시켜 주자.
float3 worldNormal = mul(TBN, tangentNormal);

오잉? 먼가 좀 이상한 부분이 있다.
여태까지는 mul(벡터, 행렬)의 방식으로 곱했었다.
ex) Output.mPosition = mul( Input.mPosition, gWorldViewProjectionMatrix );
그런데 여기에서는 왜 mul(TBN(행렬) , tangentNormal(벡터)) 인가?
이유는 TBN을 만들 때 float3x3 생성자를 사용했기 때문인데, 이를 사용하면
행기준 행렬이 나온다.
그와 반대로 SetMatrix()함수를 통해 CPU로부터 건네받은 행렬은 열기준 이다.
행기준 행렬은 mul(행렬, 벡터)의 순서로, 열기준 행렬은 mul(벡터, 행렬)의 순서로 곱한다.

이젠 월드상의 노말값도 구했겠다 나머지는 이전에 하던 것과 같다.
float4 albedo = tex2D(DiffuseSampler, Input.mUV);
float3 lightDir = normalize(Input.mLightDir);
float3 diffuse = saturate(dot(worldNormal, -lightDir));
diffuse = gLightColor * albedo.rgb * diffuse;
diffuse 텍스처로부터 diffuse값을 구하고 normal과 light를 내적하여 조명도 계산하고
거기에 추가적으로 light의 color도 고려해서 최종 diffuse값을 구해주면 된다.

반사값은 미리 제공해 주는 reflect()함수를 사용해서 구하면 되고
float3 reflection = reflect(lightDir, worldNormal);

specular은 반사R과 camera라 V를 내적해서 하이라이트를 구하고
specular = saturate(dot(reflection, -viewDir));
specular = pow(specular, 20.0f);

specular 텍스처를 통해 specular의 강한정도와 light의 color을 고려해서 최종 specular값을 구한다.
float4 specularIntensity = tex2D(SpecularSampler, Input.mUV);
specular *= specularIntensity.rgb * gLightColor; 


Normal.rfx


Normal Mapping : 법선맵 사용, 법선맵이 입혀진 측면에서 보면 입체감이 없다.
Parallax Mapping : 법선 외에 높이맵(height map)을 사용하여 문제점 해결
Parallax Occlusion Mapping : 인접픽셀과의 높이차를 구한 뒤 그에 따라 그림자를 입히는 기법.


위의 사진은 view와 light를 tangent 공간으로 바꾼 후 계산한 diffuse + normal + specular의
결과물이다.

Default.rfx




참고 : 
- Displacement Mapping과 Parallax Mapping의 차이
  http://mgun.tistory.com/122
- Surface Texture Mapping
  http://mgun.tistory.com/140
- mapping
  http://mgun.tistory.com/280
- 사용 리소스
  http://www.bencloward.com/shaders_offset.shtml
- kw x-port
  http://www.kwxport.org/
-탄젠트공간
  http://mgun.tistory.com/1289
-법선 매핑
http://blog.naver.com/sorkelf/40157218010

ps. 문제가 되는 이미지나 링크는 알려주시면 자삭하겠습니다.


'RenderMonkey' 카테고리의 다른 글

Shadow Mapping  (0) 2014.01.11
fresnel  (0) 2013.10.22
기본적인 반사 벡터  (0) 2013.02.02
lambert  (3) 2013.01.31
흐음...렌더몽키라..  (0) 2013.01.29
TAGS.

Comments