헤더파일

셰이더 공부 - 법선 매핑 본문

DirectX

셰이더 공부 - 법선 매핑

헤더파일 2018. 2. 26. 18:45

셰이더를 진행하기 전에 알아야할 몇가지 수학 지식이 있습니다.


셰이더에서 mul 함수를 써서 행렬과 벡터를 곱할 때 

mul(행렬, 벡터) - 행기준 행렬

mul(벡터, 행렬) - 열기준 행렬


DirectX는 행우선 행렬힙니다.

따라서 셰이더에서 mul(벡터, 행렬) 로 계산하려면 tranpose해서 셰이더에 전달해야 합니다.


직교행렬

직교행렬이란 모든 열벡터 즉, x, y , z 축 벡터가 자기 자신을 제외한 나머지 모든 열벡터들과 직교이면서 크기가 1인 단위 벡터들로 구성된 행렬을 의미합니다. 직교행렬의 중요한 성질중 하나는 직교행렬의 역행렬은 전치행렬과 같다는 것입니다. 이 성질은 접선공간에서 월드공간으로 변환하는 행렬을 만들 때 유용합니다.


접선공간

법선 매핑을 할 때 텍스쳐로부터 법선을 받아오는데 이 법선은 월드공간이 아닌 접선공간이라는 다른 공간에 있습니다. 

접선공간에 대한 이미지 검색결과

위 그림 처럼 z 축의 양의 방향이 항상 표면의 바깥쪽을 향하는 공간을 말합니다. z축이 물체의 깊이를 나타내는 월드공간과 분명히 다른 표면마다 정의 된 공간입니다. 월드 공간에서 접선공간으로 변환하는 행렬을 3가지 벡터를 합쳐서 만들 수 있습니다. 먼저 z축은 항상 바깥쪽을 향하는 정점의 법선(Normal)과 같습니다. x 축 그림에서 u나 v는 표면을 달리는 축이므로 텍스처의 uv좌표 중 u나 v 중에 하나로 정해서 이걸 접선(Tangent)로 삼으면 됩니다. 

접선공간의 축은 직교행렬이여야 하므로 나머지 한 축은 모든 축과 직교여야 합니다. 이 축을 구하는 건 벡터의 외적을 이용하면 쉽습니다. 법선과 접선을 외적해 마지막 한 축인 종법선(Binormal)을 구해줍니다.


월드 공간-> 접선공간 변환


위에서 구한 세가지 벡터로 변환행렬을 만들면 됩니다.

| Tx Ty Tz |

| Bx By Bz |

| Nx Ny Nz|   - 열 기준 변환행렬


위 행렬을 전치하면 행 기준 변환행렬도 만들 수 있습니다.


법선(노멀) 맵



법선 맵은 법선의 x,y,z 값을 텍스처의 색상값인 (r,g,b)에 넣은 것입니다. 이런 귀찮은 과정을 사용하는 이유는 정점마다 있는 법선으로는 울퉁불퉁한 표면을 모두 표현하기 부족하기 때문입니다. 그렇다고 정점을 픽셀 수 만큼 늘려버리면 엄청난 리소스를 차지할 것입니다. 그래서 대안으로 텍스처형태로 법선을 저장해 놓는 것입니다.

먼저 법선벡터는 -1~1사이 값을 가지므로 색상벡터 범위인 0~1로 변환해야 합니다. 

이 변환된 값을 다시 변환하는 것은 색상벡터에 2를 곱한 뒤 1을 빼주면 됩니다.

법선 맵이 전체적으로 파란 이유는 (r,g,b)값 중 b값이 z값 즉 표면의 바깥을 가리키는 값인데 이 값은  항상 0보다 큽니다. 따라서 파란색이 최소한 0.5 이상은 들어가 있기 때문에 법선맵은 항상 파란색을 띕니다. 


1. 정점셰이더


float4x4 gWorldViewProjectionMatrix;

float4x4 gWorldMatrix;



float4 gWorldLightPosition;

float4 gWorldCameraPosition;



struct VS_INPUT

{

   float4 mPosition : POSITION;

   float3 mNormal: NORMAL;

   float2 mUV:TEXCOORD0;

   float3 mBinormal:BINORMAL;

   float3 mTangent:TANGENT;

};



struct VS_OUTPUT

{

   float4 mPosition : POSITION;

   float2 mUV:TEXCOORD0;

   float3 mLightDir: TEXCOORD1;

   float3 mViewDir: TEXCOORD2;

   float3 T:TEXCOORD3;

   float3 B:TEXCOORD4;

   float3 N:TEXCOORD5;

};



VS_OUTPUT vs_main( VS_INPUT Input )

{

   VS_OUTPUT Output;



   Output.mPosition = mul( Input.mPosition, gWorldViewProjectionMatrix );

   Output.mUV = Input.mUV;


   float3 worldPosition = mul(Output.mPosition, (float3x3)gWorldMatrix);

   Output.mLightDir = worldPosition.xyz - gWorldLightPosition.xyz;

   Output.mLightDir = normalize(Output.mLightDir);


   float3 viewDir = normalize(worldPosition.xyz - gWorldCameraPosition.xyz);

   Output.mViewDir = viewDir;


   float3 worldNormal =  mul(Input.mNormal,(float3x3)gWorldMatrix);

   float3 worldBinormal =  mul(Input.mBinormal,(float3x3)gWorldMatrix);

   float3 worldTangent =  mul(Input.mTangent,(float3x3)gWorldMatrix);

   

   Output.B = normalize(worldBinormal);

   Output.N = normalize(worldNormal);

   Output.T = normalize(worldTangent);

   

   return Output;

}


float4x4 gWorldMatrix;


먼저 월드행렬의 역행렬 대신 그냥 월드행렬을 가져왔습니다. 이유는 월드공간에서 접선공간으로 변환하기 때문에 지역공간에서 받아온  접선, 법선, 종법선을 월드공간으로 바꿔줄 필요가 있기 때문입니다.


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 worldNormal =  mul(Input.mNormal,(float3x3)gWorldMatrix);

   float3 worldBinormal =  mul(Input.mBinormal,(float3x3)gWorldMatrix);

   float3 worldTangent =  mul(Input.mTangent,(float3x3)gWorldMatrix);

   

   Output.B = normalize(worldBinormal);

   Output.N = normalize(worldNormal);

   Output.T = normalize(worldTangent);


법선 맵을 이용해서 난반사와 정반사광을 계산 할 것이므로 diffuse값은 여기서 계산 할 수 없습니다. 따라서 월드 공간으로 변환한 법선, 접선, 종법선과 카메라 벡터, 조명벡터를 픽셀셰이더로 넘겨줍니다.


2.픽셀셰이더


struct PS_INPUT

{

   float2 mUV : TEXCOORD0;

   float3 mLightDir : TEXCOORD1;

   float3 mViewDir: TEXCOORD2;

   float3 T:TEXCOORD3;

   float3 B:TEXCOORD4;

   float3 N:TEXCOORD5;

};


sampler2D DiffuseSampler;

sampler2D SpecularSampler;

sampler2D NormalSampler;


float3 gLightColor;


float4 ps_main(PS_INPUT Input) : COLOR

{

   float3 tangentNormal = tex2D(NormalSampler, Input.mUV).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);

   

   float3 lightDir = normalize(Input.mLightDir);

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

   

   float4 albedo = tex2D(DiffuseSampler, Input.mUV);

   diffuse = gLightColor*albedo.rgb*diffuse;

   

   float3 specular = 0;

   if(diffuse.x>0)

   {

      float3 reflection = reflect(lightDir,worldNormal);

      float3 viewDir = normalize(Input.mViewDir);

      

      specular = saturate(dot(reflection,-viewDir));

      specular = pow(specular,20.0f);

      

      float4 specularIntensity = tex2D(SpecularSampler, Input.mUV);

      specular *=specularIntensity.rgb * gLightColor;

   }

   

   float3 ambient = float3(0.1f,0.1f,0.1f)*albedo;

   

   return float4(ambient + diffuse + specular, 1);

}



sampler2D NormalSampler;


법선 맵 텍스처 오브젝트를 새로 받아옵니다.


float3 tangentNormal = tex2D(NormalSampler, Input.mUV).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);


먼저 법선맵에서 텍셀을 구해옵니다. 이 텍셀을 법선 벡터로 변환하기 위해 2를 곱한 뒤 1을 빼줍니다. -1~1 사이 범위로 변환하기 위해섭니다.


이 법선은 접선 공간에 있으므로 위해서 말한 월드공간->접선공간으로 변환하는 행렬을 만든 뒤 이행렬의 역행렬을 구해서 접선공간->월드공간으로의 변환을 해야합니다.

위에 있는 TBN 행렬은 월드공간 -> 접선공간으로 변환하는 행렬로 이 행렬은 직교행렬입니다. 직교행렬의 역행렬은 전치 행렬이므로 transpose 함수를 써서 역행렬을 구해줍니다.


mul(행렬, 벡터) 형태로 곱한 것은 TBN이 행우선 행렬이기 때문입니다. 이렇게 변환하면 월드공간에서의 법선을 구할 수 있습니다.


이렇게 구한 법선으로 스페큘러 맵핑에서 했듯이 빛의 양을 구하면 됩니다.

완성하면 울퉁불퉁한 표면을 잘 살린 법선 매핑이 완성됩니다.



'DirectX' 카테고리의 다른 글

HLSL 프로그래밍 - DXUT프레임워크  (0) 2018.02.28
셰이더 공부 - 환경매핑  (0) 2018.02.27
셰이더 공부 - 스페큘러 맵핑  (0) 2018.02.26
셰이더 공부 - 툰셰이더  (0) 2018.02.25
셰이더공부 - 정반사광  (0) 2018.02.25
Comments