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 |