노멀 맵핑

노멀 맵핑은 법선 맵핑이라고도 한다.
노멀맵은 텍스쳐의 RGB 정보에 Normal Vector XYZ를 인코딩한 맵이다.
픽셀당 조명(Per-Pixel Lighting)을 구현하기 위해서이다.

노멀 맵핑 하는 방법은 두가지가 있다.

오브젝트 공간 노멀 맵과 탄젠트 공간 노멀 맵이 있다.

< 오브젝트 공간 노멀 맵  >

  • 노멀 벡터가 오브젝트 공간을 기준으로 되어 있음.
  • 구현이 쉽고  외곽 실루엣을 제외하면 하이 폴리곤과 동일한 효과
  • 스키닝(정점 변환) 되지 않는 건물, 자동차에 효과적
  • 노멀맵이 무지개 색깔로 보인다.

< 탄젠트 공간 노멀 맵 >

  • 탄젠트 공간을 텍스쳐 좌표계 기준으로 구한 다음 노멀 계산
  • 오브젝트 공간 노멀 맵보다 퀄리티는 떨어짐
  • 스키닝 되어도 적용 가능하기 때문에 캐릭터에 효과적
  • 노멀맵이 푸르게 보인다. ( 탄젠트 공간을 사용하고 있기 때문에 노멀맵이 위로 향한다 )
  • Z값이 0보다 크기 때문에 텍스쳐에 저장하면 0.5~1의 값을 가진다.

 

1. 노멀 맵( 텍스쳐 ) 만들기

법선맵(텍스쳐은 0과 1사이의 값을 가진다. ( 0 ~ 1 )
법선벡터는 -1과 1사이의 값을 가진다. ( -1 ~ 1 )

법선벡터에서 텍스쳐로 바꿀려면 사이즈를 반으로 줄여야 한다. ( 0.5 곱하기)
그러면 법선벡터의 값은 -0.5 ~ 0.5의 값을 가진다. ( 0.5 더하기)

0.5를 더하면 0 ~ 1 사이의 값을 가진다.

법선맵 RGB = 법선벡터 XYZ * 0.5 + 0.5     ( 텍스쳐 저장 할 때 )

법선벡터 XYZ = 법선맵 RGB * 2 - 1     ( 텍스쳐에서 벡터로 변환 할 때 )

2. 접선(탄젠트) 공간

Tangent (접선) : 정점 표면위의 정보
Bi-normal(종법선): 접선, 정점의 외적에 의해 연산
Normal(법선): 정점의 법선과 동일

맥스에서 익스포트시 Tangent, Bi-normal 정보는 정점 정보와 함께 익스포트 된다.
Normal 값은 정점의 노멀을 이용한다.

 

행렬로 표시하면 다음과 같다

행기준 행렬( Row major matrix ) - d3d
| Tx Ty Tz |
| Bx By Bz |
| Nx Ny Nz |

열기준 행렬 ( Column major matrix ) - open gl
| Tx Bx Nx |
| Ty By Ny |
| Tz Bz Nz |

3. 셰이더 코드

노멀맵 라이트의 계산은  크게 다음의 과정으로 진행한다.

< 정점 셰이더 >

정점을 월드 공간으로 변환
라이트 벡터 구하기
뷰벡터 구하기
각각의 TBN을  월드공간으로 변환

< 픽셀 셰이더 >

TBN 월드 행렬 구하기
노멀값 구하기
난반광 계산
정반사광 계산
엠비어트 라이트 계산
모든 라이트 더하기

SpecularMapping2.rfx에 코드를 수정해서 사용 할 것이다.

렌더몽키에 있는 샘플 이미지 FieldstoneBumpDOT3.tga를 노멀 맵으로 추가한다.
텍스쳐 오브젝트 이름은 NormalSampler로 한다.

스트림 맵핑을 더블클릭해서 Tangent, Bi-normal을 Float3, 인덱스 0으로 추가한다.

NormalMapping

float4x4 gWorldMat;
float4x4 gWorldViewProjMat;

float4 gLightPos;
float4 gViewPos;

struct VS_INPUT
{
   float4 pos : POSITION;
   float3 normal : NORMAL;
   float2 uv : TEXCOORD0;
   float3 tangent : TANGENT;
   float3 binoraml : BINORMAL;
};

struct VS_OUTPUT
{
   float4 pos : POSITION;
   float2 uv : TEXCOORD0;
   float3 lightDir : TEXCOORD1;
   float3 viewDir : TEXCOORD2;
   float3 T : TEXCOORD3;
   float3 B : TEXCOORD4;
   float3 N : TEXCOORD5;
};

VS_OUTPUT vs_main( VS_INPUT input )
{
   VS_OUTPUT output;

   output.pos = mul( input.pos, gWorldViewProjMat);
   
   output.uv = input.uv;
   
   float4 worldPos = mul( input.pos, gWorldMat);
   
   float3 lightDir = worldPos - gLightPos.xyz;
   output.lightDir = normalize( lightDir);
   
   float3 viewDir = worldPos - gViewPos.xyz;
   output.viewDir = normalize( viewDir);
   
   float3 worldNormal = mul( input.normal, (float3x3)gWorldMat);
   output.N = normalize( worldNormal);
   
   float3 worldTangent = mul( input.tangent, (float3x3)gWorldMat);
   output.T = normalize( worldTangent);
   
   float3 worldBinormal = mul( input.binoraml, (float3x3)gWorldMat);
   output.B = normalize( worldBinormal);
   
   return output;   
}

//----------------------------------------------------------------

sampler2D DiffuseSampler;
sampler2D SpecularSampler;
sampler2D NormalSampler;

struct PS_INPUT
{
   float2 uv : TEXCOORD0;
   float3 lightDir : TEXCOORD1;
   float3 viewDir : TEXCOORD2;
   float3 T : TEXCOORD3;
   float3 B : TEXCOORD4;
   float3 N : TEXCOORD5;

};

float3 gLightColor;

float4 ps_main( PS_INPUT input) : COLOR
{
   float3 tangentNormal = tex2D( NormalSampler, input.uv).xyz;
   tangentNormal = normalize( tangentNormal * 2 - 1);
   
   
//접선공간 --> 월드공간으로 변환
   float3x3 TBN = float3x3( normalize( input.T),
                            normalize( input.B),
                            normalize( input.N));  
//월드 --> 접선행렬로 변환하는 행렬
   TBN = transpose( TBN);                          
//접선 --> 월드
   float3 worldNormal = mul( TBN, tangentNormal);
     
   float4 albedo = tex2D(DiffuseSampler, input.uv);
   float3 lightDir = normalize( input.lightDir);
   float3 diffuse = saturate( dot( worldNormal, -lightDir));
   diffuse = gLightColor * albedo.rgb * diffuse;
      
   float3 specular = 0;
   
   if( diffuse.x > 0)
   {
      float3 reflection = reflect( lightDir, worldNormal);
      float3 viewDir = normalize( input.viewDir);
      
      specular = dot( reflection, -viewDir);
      specular = saturate( specular);
      
      specular = pow( specular, 20);
      
      float4 specularIntensity = tex2D(SpecularSampler, input.uv);
      specular = gLightColor * specularIntensity.rgb * specular;
   }
   
   float3 ambient = float3( 0.1f, 0.1f, 0.1f) * albedo;
   
   return( float4( ambient + specular + diffuse, 1.0f) );
}

< 정점 셰이더 >

struct VS_INPUT
{
   float4 pos : POSITION;
   float3 normal : NORMAL;
   float2 uv : TEXCOORD0;
   float3 tangent : TANGENT;
   float3 binoraml : BINORMAL;
};

TANGENT, BINORMAL이 추가 되었다. NORMAL은 기존에 있던 NORMAL 값을 사용하면 된다.

struct VS_OUTPUT
{
   float4 pos : POSITION;
   float2 uv : TEXCOORD0;
   float3 lightDir : TEXCOORD1;
   float3 viewDir : TEXCOORD2;
   float3 T : TEXCOORD3;
   float3 B : TEXCOORD4;
   float3 N : TEXCOORD5;
};

TBN 행렬을 이용해 픽셀 셰이더에서 노멀값을 계산하기 때문에 이전에 VS_OUTPUT에 포함 되었던 diffuse, reflection의 계산은 픽셀 셰이더에서 계산이 된다.
픽셀셰이더에서 계산하기 위해 lightDir을 추가한다.

T, B, N의 값은 월드행렬로 변환해서 넘긴다.

 

< 픽셸 셰이더 >

스펙큘러 셰이더에노멀맵 샘플러가 추가 되었다.
TBN을 이용해 노멀값 구하는 코드 부분이 추가 되었다.

tangentNormal = normalize( tangentNormal * 2 - 1)

텍스쳐 값을 이용해 법선 벡터로 변환한다. ( -1 ~ 1 ----> 0 ~ 1 )

float3x3 TBN = float3x3( normalize( input.T), normalize( input.B), normalize( input.N))

이 TBN은 직교이고 월드공간을 접선 공간으로 변환하는 행렬이다.

TBN = transpose( TBN)

TBN의 역행렬로 접선 공간에서 월드공간으로 변환하는 행렬이다.

위키 백과 - 직교행렬을 참조하면 다음의 수학식이 있다.

직교행렬의 전치행렬은 원래 행렬의 역행렬과 같다. 즉, 임의의 직교행렬 Q에 대해서

float3 worldNormal = mul( TBN, tangentNormal)

반사광 계산을 위해, worldNormal을 구한다.
행렬의 곱 순서가 mul( 벡터, 행렬)이 아니다.
float3x3 생성자는 행기준 행렬이지만 SetMatrix( ) 함수를 통해 넘어오는 행렬은 열기준이다.
행기준과 열기준이 다를 때는 mul 실행시 벡터와 행렬의 순서를 바꿔준다.

float3 diffuse = saturate( dot( worldNormal, -lightDir))

worldNormal을 이용해 diffuse 값을 구하고 있다.

나머지 셰이더 코드는 스펙큘러 맵에서 본 코드와 비슷하다.

다운로드 : NormalMapping.rfx

참고:

한빛미디어 "셰이더 프로그래밍 입문"

http://zho.pe.kr/view.html?file_name=doc/normalmap.txt

http://www.surlybird.com/tutorials/TangentSpace/

http://marupeke296.com/DXPS_S_No5_NormalMap.html

위의 일본싸이트 해석한 싸이트임

http://blog.naver.com/sorkelf/40157218010