본문 바로가기
셰이더 (Shader)/셰이더 프로그래밍 입문 - Pope Kim (완)

[HLSL] 챕터7 - 법선매핑

by Minkyu Lee 2023. 3. 19.

TANGENT : 접선정보 시맨틱

BINORMAL : 종법선정보 시맨틱

transpose() : 전치행렬 구하는 함수

 

접선공간 : 법선맵 안에 법선이 존재하는 공간

 

// 법선매핑의 필요성

폴리곤 수 늘리면 속도가 느려지고, 메모리 소모가 커진다.

픽셀마다 법선을 정해준다.

각 텍셀에 법선을 저장하는 방식이다.

이 값을 픽셀셰이더에서 불러와 조명계산에 사용한다.

 

법선정보 담고 있는 텍스쳐법선맵이라고 한다.

이를 이용한 조명계산법선매핑이라고 한다. (normal mapping)

 

// 텍스처에 법선정보를 저장하는 방식

XYZ를 RGB에 대입한다.

값의 범위를 0~1로 만든다.

단위벡터는 -1~1이다.

 

변환 공식은 다음과 같다.

- 벡터 -> 맵

법선맵 RGB = 법선벡터 XYZ * 0.5 + 0.5

 

- 맵 -> 벡터

법선벡터 XYZ = 법선맵 RGB * 2 - 1

 

// 접선공간이란?

법선벡터 x,y,z의 의미는 무엇인가?

공간 개념이 있어야 xyz 가 의미를 가진다.

 

법선매핑에서 가장 널리 사용되는 공간의 XYZ 좌표계는 다음과 같다.

표면에 수직인 방향을 z로 삼는다.

오른쪽을 x로 삼는다.

y는 이 두 방향에 직각이 된다.

 

이 공간은 표면마다 정의되어 있다.

표면의 바깥쪽을 +z로 둔다. 라는 명제를 사용하여 법선맵을 만들기 때문이다.

이를 접선공간 (tangent space)라고 한다.

표면공간이라고 이해해도 무방하다.

 

접선공간에 있는 것을 조명 계산하려면 공간변환이 필요하다.

 

접선공간을 구성하는 행렬은 좌표축 3개만 있으면 된다.

Z는 정점의 법선과 같다.

X는 표면 위를 달리는 축이다.

따라서 표면 위 정보에서 구해온다.

표면위에 정의된 것은 UV 좌표가 있다.

 

U와 V 중 하나를 가져와 X로 삼는다.

이 축을 접선이라고 한다. (tangent)

 

나머지 축은 외적을 사용해 구한다.

이렇게 구한 축을 종법선이라고 한다. (binormal)

 

실무에서 법선, 접선, 종법선 직접계산 할 일 많지 않다.

프로그램에서 메쉬 저장시 정점정보의 일부로 저장하는 옵션이 있기 때문이다.

저장하였다면, NORMAL, TANGENT, BINORMAL 시맨틱을 사용하여 가져올 수 있다.

 

// 접선 T, 종법선 B, 법선 N을 이용한 행렬 만들기

- 행기준 행렬

Tx Ty Tz

Bx By Bz

Nx Ny Nz

 

- 열기준 행렬

Tx Bx Nx

Ty By Ny

Tz Bz Nz

 

// 법선맵이 파란색인 이유

표면 바깥을 향하기 때문이다.

안쪽으로 역류하는 경우는 없다.

z값이 최소 0보다는 크다.

이를 텍스쳐로 저장하면 0.5~1 범위가 되므로 항상 푸르다.

 

// 실무에서 법선맵 g 채널 반전하는 이유

아래쪽 향하는 법선은 0, 1, 0

위쪽 향하는 법선은 0,-1, 0 이기 때문에,

아래에 하이라이트가 있어 혼동이 있을 수 있다.

그래서 눈으로 봤을 때, 이해가 쉽도록 반전한다.

 

// 이번 장에서 물체 공간에서 조명 계산하지 않는 이유

월드 공간에서 조명 계산한다.

다음장에서 환경매핑시 월드공간이 필요하기 때문이다.

 

정점셰이더

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;
   
   // base
   Output.mPosition = mul(Input.mPosition, gWorldViewProjectionMatrix);
   Output.mUV = Input.mUV;
   
   // world pos vector
   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;
   
   // object pos vector to world pos vector
   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;
}

 

픽셀셰이더에서 법선 텍스쳐를 불러와, 법선을 구해야한다.
따라서 정점 셰이더에서 조명계산할 수 없다.

 

 

입사광의 방향벡터를 전달한다.

 

// T, B, N을 월드공간으로 변환해서 내보내기

월드공간에서 조명계산을 하려면, 모든 변수가 월드공간에 있어야한다.

입사광 벡터, 카메라 벡터는 월드공간에 있다.

 

법선도 마찬가지로 월드공간에 있어야한다.

법선은 텍스쳐에서 읽어오면 접선공간에 위치해있다.

이것을 월드공간으로 변환해야한다.

따라서 픽셀셰이더에서 법선, 접선, 종법선 벡터를 사용할 수 있게 해주어야한다.

T,B,N을 출력데이터에 추가한다.

 

T,B,N은 정점버퍼에서 입력받아 가져온다.

그래서 물체공간에 위치해있다.

따라서 월드공간으로 변환해서 내보내준다.

그래야만 월드공간 <-> 접선공간 변환이 가능한 행렬로 만들 수 있다.

 

픽셀셰이더

sampler2D DiffuseSampler;
sampler2D SpecularSampler;
sampler2D NormalSampler;

float3 gLightColor;

struct PS_INPUT
{
   float2 mUV : TEXCOORD0;
   float3 mLightDir : TEXCOORD1;
   float3 mViewDir : TEXCOORD2;
   
   float3 T : TEXCOORD3;
   float3 B : TEXCOORD4;
   float3 N : TEXCOORD5;
};

float4 ps_main(PS_INPUT Input) : COLOR
{
   // World Normal
   float3 tangentNormal = tex2D(NormalSampler, Input.mUV).xyz; // don't use alpha.
   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); //float3x3 is rows major matrix.
   
   // Lighting
   float4 albedo = tex2D(DiffuseSampler, Input.mUV);
   float3 lightDir = normalize(Input.mLightDir);
   float3 diffuse = saturate(dot(-lightDir, worldNormal));
   diffuse = gLightColor * albedo.rgb * diffuse;
   
   float3 specular = 0;
   if(diffuse.x > 0)
   {
      float3 reflection = reflect(lightDir, worldNormal); // important argument order.
      float3 viewDir = normalize(Input.mViewDir);
      specular = saturate(dot(reflection, -viewDir));
      specular = pow(specular, 20.0);
      
      float4 specularIntensity = tex2D(SpecularSampler, Input.mUV);
      specular *= specularIntensity.rgb * gLightColor ;
   }
   
   float3 ambient = float3(0.1, 0.1, 0.1) * albedo;
   return float4(ambient + diffuse + specular, 1.0);
}

픽셀셰이더에서 조명계산한다.

 

// 법선 읽어오기

법선맵 텍스쳐로부터 법선을 읽어온다.

읽어온 텍셀 값의 0 ~ 1 범위를 -1 ~ +1 범위로 바꾸어준다.

*2 -1이 그것이다.

 

// 법선 월드공간으로 변환

T, B, N을 행렬로 만들어서 적용한다.

만들면 월드공간 -> 접선공간으로 변환하는 행렬이 된다.

역행렬을 구하면 접선공간 -> 월드공간으로 변환하는 행렬로 쓸 수 있다.

 

직교행렬의 역행렬은 전치행렬과 같다.

따라서 transpose 함수를 사용한다.

이를 접선공간 노말 (tangent normal)에 곱하면 월드공간 노말이 나온다.

 

// mul(행렬, 벡터) 순서로 곱한 이유

float3x3 생성자 이용하면 행기준 행렬이 나오기 때문이다.

그와 반대로 setMatrix 함수를 이용해 CPU로 건네받은 행렬은 열기준 이다.

 

행기준 행렬은 mul(행렬, 벡터)의 순서로 곱한다.

열기준 행렬은 mul(벡터, 행렬)의 순서로 곱한다.

 

// 이후 과정

이전 챕터와 원리 동일하다.

 

  • 법선 매핑과 비슷한 기타 고급기법

// 패럴랙스 매핑

법선매핑의 문제점은 측면에서 보면 입체감이 없다.

해결위해 고안된 기법이 패럴랙스 매핑이다.

법선 외에 높이맵을 이용한 기법이다.

 

// 패럴랙스 오클루젼 매핑

여기서 한걸음 더 나아간 것이 인접픽셀과의 높이차를 구한 뒤

그에 따라 그림자 입히는 패럴랙스 오클루젼매핑이다.

GPU 젬스와 셰이더 X 책에서 여러번 소개된 기법이다.

 

// 테셀레이션

법선매핑 대신 실제 폴리곤 수 높이는 방법이 테셀레이션이다.

DirectX 10에서 등장하였다.

혼용해서 사용도 가능하다.

 

 

댓글