본문 바로가기

Graphics_Shader/HLSL

법선매핑(실습)


1. 법선매핑(Normal Mapping)

일반적으로 물체가 실체와 비슷에 보이기 위해서는 폴리곤의 수를 늘려서 실체와 비슷한 모델로 만듭니다.

테셀레이션(tesellation) 기법 :  실제 폴리곤 수를 높이는 방법

하지만, 정점의 수가 많아질수록 처리 속도가 느려지고, 정점버퍼의 메모리 소모도 커집니다. 따라서, 폴리곤의 수를 늘리지 않고도 디테일을 추가할 수 있는 방법으로 법선매핑이 있습니다.

법선이 어떻게 정의되느냐에 따라서 표면의 굴곡을 나타낼 수 있습니다.  각 텍셀에 법선을 저장시킴

법선맵(normal map) : 각 픽셀이 사용할 법선정보를 담고 있는 텍스처

법선매핑(normal mapping) :  법선맵을 이용해서 조명을 계산하는 기법

2. 법선맵(Normal Map)

법선(Normal) : 3차원 공간에 존재하는 방향벡터 $(x, y, x)$

텍스처는 RGB 채널에 존재하므로 법선의 각 채널  X, Y, Z를 RGB에 대입하면 됩니다.

이때, 텍스터의 RGB 각 채널이 가질 수 있는 값의 범위는 0~1입니다.

정규화된 단위벡터를 사용한다고 가정한 법선벡터의 값의 범위는 -1~1입니다.

따라서, 법선벡터XYZ = 법선맵RGB * 2 - 1을 사용합니다.

3. 접선공간(Tangent Space)

여기서 사용하는 공간은 각 표면마다 정의되어 있습니다. 법선맵을 만들 때 표면의 바깥쪽 방향을 법선의 $+z$로 둡니다. 이때 각 표면마다 다른 공간이 존재합니다. 이것을 접선공간이라고 합니다.

접선공간(tangent space) :  각 표면마다 존재하는 공간(표면공간)

접선공간을 구성하는 행렬을 구할 때, Z축은 정점의 법선과 같은 값을 가집니다. X축은 표면 위를 달리는 축으로, 표면 위에 있는 정보에서 구하게 됩니다.(UV좌표가 표면위에 정의되어 있습니다. U또는 V 중 하나의 값을 가져와 X값으로 설정합니다.) 이것을 접선(tangent)라 합니다. 마지막 Y축은 외적을 통해서 구할 수 있습니다. 이 축을 종법선(Binormal)이라 합니다.

더보기

X축 : 접선(tangent) : TANGENT → UV좌표

Y축 : 종법선(Binormal) : BINORMAL → Z축과 Y축의 외적

Z축 : 법선(normal) : NORMAL → 정점의 법선벡터

접선공간변환 행렬

$\left[ \begin{matrix} Tx & Ty & Tz \cr Bx & By & Bz \cr Nx & Ny & Nz \end{matrix} \right]$ 열벡터

$\left[ \begin{matrix} Tx & Bx & Nx \cr Ty & By & Ny \cr Tz & Bz & Nz \end{matrix} \right]$ 행벡터


① 법선매핑을 이용하기위한 NormalMap을 등록한다.

    >> [Add Texture - Add 2D Texture - FieldstoneBumpDOT3.tga] (rename FieldstoneBumpDOT3 to NormalMap)

② NormalMap을 Pass 0에 추가한다. 

    >> [Add Texture Object - NormalMap] (rename NormalMapSampler)

③ 접선(TANGENT)과 종법선(BINORMAL)을 추가해 준다.

// Vertex Shader
float4x4 gWorldMatrix;
float4x4 gWorldViewProjectionMatrix;

float4 gWorldLightPosition;
float4 gWorldCameraPosition;

struct VS_INPUT 
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
   float3 mTangent : TANGENT;
   float3 mBinormal : BINORMAL;
   float2 mUV: TEXCOORD0;
};

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;
   
   float4 worldPosition = mul(Input.mPosition, gWorldMatrix);
   float3 lightDir = worldPosition.xyz - gWorldLightPosition.xyz;
   Output.mLightDir = normalize(lightDir);
   
   float3 viewDir = normalize(worldPosition.xyz - gWorldCameraPosition.xyz);
   Output.mViewDir = viewDir;
   
   float3 worldNormal = mul(Input.mNormal, (float3x3)gWorldMatrix); // 월드 공간으로 변환
   Output.N = normalize(worldNormal);
   
   float3 worldTangent = mul(Input.mTangent, (float3x3)gWorldMatrix); // 월드 공간으로 변환
   Output.T = normalize(worldTangent);
   
   float3 worldBinormal = mul(Input.mBinormal, (float3x3)gWorldMatrix); // 월드 공간으로 변환
   Output.B = normalize(worldBinormal);
  
   return Output;
}

픽셀마다 법선을 읽어오기 위해서 픽셀셰이더에서 법선을 구해와야 합니다. 그러니 정점셰이더 단계에서는 조명계산에 유효한 법선데이터가 없습니다. 대신 픽셀셰이더에서 조명을 계산할 수 있도록 입사광의 방향벡터(mLightDir)를 전달해 줘야합니다.


// Pixel Shader
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);
   
   float4 albedo = tex2D(DiffuseSampler, Input.mUV);
   float3 lightDir = normalize(Input.mLightDir);
   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.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);
}

위의 코드를 각각 실행(F5)시키면 다음과 같은 결과 화면이 보입니다.


결과화면을 확대하면 위와 같이 보입니다. 이것은 작은 사진을 확대할 때 깨지는 것과 비슷한 현상입니다. 이것을 방지하기 위해서는 그래픽 하드웨어에 선형필터를 적용하면 됩니다.

Texture State Editor에서 아래의 두 State의 Value를 다음과 같이 설정합니다.

모든 Sampler에서 설정을 마치면 다음과 같이 선명해진 결과값을 볼 수 있습니다.

'Graphics_Shader > HLSL' 카테고리의 다른 글

환경매핑(실습)  (0) 2019.12.24
툰 셰이더(실습)  (0) 2019.11.27
디퓨즈/스페큘러 매핑(실습)  (0) 2019.11.27
기초 조명셰이더(실습)  (0) 2019.11.27
셰이더의 기초적인 문법과 텍스처 매핑(실습)  (0) 2019.11.27