그림자 매핑2

렌더링 출력이 지글그리고 그림자가 격자이다. 이 문제는 다음장에서 수정한다.

CreateShadow 패스에서 만든 결과물을 렌더러텍스쳐로 설정한다.

( CreateShadow 패스 ) tutorial05.html

렌더 타겟 설정

renderTexture를 ShadowMap으로 이름을 수정한다.

ShadowMap을 더블클릭해서 포맷을 변경한다.

A8R8G8B8에서 R32F으로 변경

"Auto-Generate Mip Map" 체크 박스는 해제 한다.

참고)
D3DFMT_R32F :
지정 32 비트 붉은 부동 소수점 픽셀 포맷

깊이값을 저장 할려면 실수값을 저장 할 수 있는 R32F 포맷으로 저장한다.
 

렌더타겟을 만든다.

CreateShadow 패스위에서 오른쪽 마우스 버튼을 누른다.

CreateShadow의 렌더타겟을 화면에서 텍스쳐로 지정한다.

Add Render Target 메뉴선택

ShadowMap 메뉴선택

 

ShadowMap이 추가되면 추가된 ShadowMap을 더블 클릭한다.

칼라를 검은색에서 흰색으로 바꾼다.

(렌더링을 하기전에 렌더타겟을 지울색을 지정한다.
아무것도 그려지지 않는 부분의 값을 흰색으로 설정한다.)

그림자 적용 - 새로운 Pass 추가

그림자를 그리기 위한 새로운 Pass를 추가한다.

ShadowMap을 메시에 입히기 위해서 패스를 추가한다.
ShadowMapping을 마우스 오른쪽 버튼을 클릭 후 AddPass 선택

생성된 Pass1의 이름을 ApplyShadowTorus로 바꾼다.

Model을 지정한다.

Model의 Reference Node-Torus를 선택한다.

그림자 적용 - 정점 셰이더

정점 셰이더 입력 구조체는 다음과 같다.

struct VS_INPUT
{
   float4 pos : POSITION;
   float3 normal : NORMAL;   
};

입력 구조체에 NORMAL을 추가 했으므로 Steam Mapping에도 추가 한다.

ShadowMapping 이펙트 아래 Steam Mapping을 더블 클릭한다.
Index는 0, Data Type은 FLOAT3이다.

정점 셰이더 출력 구조체는 다음과 같다.

struct VS_OUTPUT
{
   float4 pos : POSITION0;
   float4 clipPos : TEXCOORD1;
   float diffuse : TEXCOORD2;
};

레스터라이저가 픽셀을 찾기 위해 기본적으로 위치값은 넘겨준다
그림자를 그릴때도 그림자 텍스쳐를 만들때처럼 광원으로부터 깊이를 구한다.
--> 그림자맵 깊이와 비교하기 위해서이다.

그래서, 그림자를 만들때와 마찬가지로 clipPos 필요
난반사광 결과 필요
 

필요한 전역변수는 다음과 같다.

float4x4 gWorldMat;
float4x4 gLightViewMat;
float4x4 gLightProjMat;

float4 gWorldLightPos;

광원으로부터 깊이를 구해야 하므로 그림자를 만들때 사용했던 변수들이 전부 필요하다.

물체를 그릴때 사용하는 행렬이 필요하다

float4x4 gViewProjMat;

렌더몽키의 ShadowMapping 이펙트에도 이 행렬을 추가한다. 시멘틱을 ViewProjection으로 한다.

광원-뷰행렬 구하기

이전 그림자와 비교를 위해 현재 광원으로부터 깊이를 구한다.

 VS_OUTPUT output;
 
 float4x4 lightViewMat = gLightViewMat;  
 
 float3 dirZ = -normalize( gWorldLightPos);
 float3 up = float3( 0, 1, 0);
 float3 dirX = cross( up, dirZ);
 float3 dirY = cross( dirZ, dirX);

 lightViewMat = float4x4(
    float4( dirX, -dot( gWorldLightPos.xyz, dirX)),
    float4( dirY, -dot( gWorldLightPos.xyz, dirY)),
    float4( dirZ, -dot( gWorldLightPos.xyz, dirZ)),
    float4( 0, 0, 0, 1));   
 
 lightViewMat = transpose( lightViewMat);   

라이트 타겟이 (0, 0, 0)이므로 gWorldLightPos만 넣는다.

float3 dirZ = -normalize( gWorldLightPos);

2번의 공간 변환

1) 실제로 물체를 그리기 위한 공간 변환

물체공간  --->  월드공간  --->  뷰공간  --->  투영공간   
//render world-view-projection position
float4 worldPos = mul( input.pos, gWorldMat);
output.pos = mul( worldPos, gViewProjMat);

2) 물체의 깊이(광원으로부터 깊이)를 구하기 위한 변환

물체공간  --->  월드공간  -->  광원-뷰공간  -->  광원-투영공간
//light world-view-projection position
output.clipPos = mul( worldPos, lightViewMat);
output.clipPos = mul( output.clipPos, gLightProjMat);

난반사광 계산

난반사광을 구할려면 입사광 벡터와 법선이 필요하다
내적을 하여 난반사광을 구한다.

float3 lightDir = normalize( worldPos.xyz - gWorldLightPos.xyz);
float3 worldNormal = normalize( mul( input.normal, (float3x3)gWorldMat));

output.diffuse = dot(-lightDir, worldNormal);

 

그림자 적용 - 픽셀 셰이더

픽셀셰이더에서 현재 깊이와 그림자 맵의 깊이를 비교해 그림자를 보여준다.

텍스쳐 샘플러 선언

그림자맵 사용을 위해 텍스쳐 샘플러를 선언한다.
렌더몽키의 ApplyShadowTorus 패스에서 ShadowMap을 추가한다.

"Add Texture Object" ->

ShadowMap 추가

이름을 Texture0에서

ShadowSampler로 바꾼다.

* CreateShadow 패스에서 생성한 텍스쳐를 ApplyShadowTorus 패스에서 이용 할려면 CreateShadow 패스가 ApplyShadowTorus 패스 위에 있어야 한다.

sampler2D ShadowSampler

픽셀 셰이더 전역 변수 

물체의 색을 위한 전역변수를 지정한다.

float4 gObjectColor;

ApplyShadowTorus 패스에
Color 변수를 추가한다.

추가한 변수 이름을
gObjectColor로 바꾼다.

픽셀 셰이더 입력 구조체

정점 셰이더의 출력 구조체와 일치 시킨다.

struct PS_INPUT
{
   float4 clipPos: TEXCOORD1;
   float diffuse: TEXCOORD2;
};

난반사광에 물체의 색상을 곱한다.

그림자가 없으면 이 색상이 들어올 것이고 그림자가 필요하다면 이 값을 적당히 줄여준다.

float3 rgb = saturate( input.diffuse) * gObjectColor;

현재 픽셀의 깊이값

//current pixel depth
float currentDepth = input.clipPos.z / input.clipPos.w;

그림자맵 픽셀의 깊이값

텍스쳐 UV 좌표와 투영공간의 좌표는 위치나 크기가 다르다.
투영공간의 좌표계 텍스쳐 좌표계
투영 공간에서 텍스쳐 좌표계로 픽셀의 크기는 2에서 1 비율로 줄어 든다.

1) 투영 좌표계 ---> UV 좌표계로 변환

,u = x / 2 + 0.5     (크기가 1/2이므로 0.5를 더해준다.)
v = -y / 2 + 0.5

2) UV 좌표계 ---> 투영 좌표계

x = u * 2 - 1
y = -v * 2 - 1

투영 좌표계의 xy 좌표로 UV 좌표를 구한다.

//shadowmap pixel depth
float2 uv = input.clipPos.xy / input.clipPos.w;
uv.y = -uv.y;
uv = uv * 0.5 + 0.5;

float shadowDepth = tex2D( ShadowSampler, uv).r;

CreateShadow 패스의 픽셀 셰이더에서 다음과 같이 깊이 값을 저장 했었다.

float depth = input.clipPos.z / input.clipPos.w;
R32F 포맷으로 텍스쳐를 저장 했기 때문에 r 채널로 깊이값을 읽어 온다.

현재 깊이가 그림자맵 보다 더 깊으면 그림자를 표시한다.
그림자가 있으면 조명을 50% 줄였다.

if( currentDepth > shadowDepth + 0.0000125f)
{
   rgb *= 0.5f;
}

return float4( rgb, 1.0f);

깊이를 계산할 때 0.0000125를 더하여 지글거리는 문제를 해결하고 있다.

currentDepth와 shadowDepth가 모두 동일한 깊이 결과가 나와야 하지만 부동소수점 에러문제도 있고 텍스쳐 저장할 때 생기는 오차도 있어서 이런 문제가 발생한다.

부동소수점 오차를 해결하기 위해 0.000125를 더했다.

지글 거리는 문제는 해결 했지만 여전히 텍스쳐는 각져 보인다.

각져 보이는 문제를 해결하려면 그림자맵의 텍스쳐 크기를 키운다.

ShadowMapping 이펙트에 있는 ShadowMap의 크기를 2048로 만들면 그림과 같이 각져 보이는 문제가 해결된다.

 

 

ApplyShadowTorus 패스의 HLSL 코드이다.

//Vertex Shader
struct VS_INPUT
{
   float4 pos : POSITION;
   float3 normal : NORMAL;   
};

struct VS_OUTPUT
{
   float4 pos : POSITION0;
   float4 clipPos : TEXCOORD1;
   float diffuse : TEXCOORD2;
};

float4x4 gWorldMat;
float4x4 gLightViewMat;
float4x4 gLightProjMat;

float4 gWorldLightPos;

float4x4 gViewProjMat;

VS_OUTPUT vs_main( VS_INPUT input)
{
   VS_OUTPUT output;
   
   float4x4 lightViewMat = gLightViewMat;  
   
   float3 dirZ = -normalize( gWorldLightPos);
   float3 up = float3( 0, 1, 0);
   float3 dirX = cross( up, dirZ);
   float3 dirY = cross( dirZ, dirX);
  
   lightViewMat = float4x4(
      float4( dirX, -dot( gWorldLightPos.xyz, dirX)),
      float4( dirY, -dot( gWorldLightPos.xyz, dirY)),
      float4( dirZ, -dot( gWorldLightPos.xyz, dirZ)),
      float4( 0, 0, 0, 1));   
   
   lightViewMat = transpose( lightViewMat);   
   
   output.pos = mul( input.pos, gWorldMat);
   output.pos = mul( output.pos, lightViewMat);
   output.pos = mul( output.pos, gLightProjMat);
   
   //render world-view-projection position
   float4 worldPos = mul( input.pos, gWorldMat);
   output.pos = mul( worldPos, gViewProjMat);
   
   //light world-view-projection position
   output.clipPos = mul( worldPos, lightViewMat);
   output.clipPos = mul( output.clipPos, gLightProjMat);
   
   float3 lightDir = normalize( worldPos.xyz - gWorldLightPos.xyz);
   float3 worldNormal = normalize( mul( input.normal, (float3x3)gWorldMat));
   
   output.diffuse = dot(-lightDir, worldNormal);
   
   return output;
   
}

//Pixel Shader
sampler2D ShadowSampler;

struct PS_INPUT
{
   float4 clipPos: TEXCOORD1;
   float diffuse: TEXCOORD2;
};

float4 gObjectColor;

float4 ps_main( PS_INPUT input) : COLOR
{   
   float3 rgb = saturate( input.diffuse) * gObjectColor;
   
   //current pixel depth
   float currentDepth = input.clipPos.z / input.clipPos.w;
   
   //shadowmap pixel depth
   float2 uv = input.clipPos.xy / input.clipPos.w;
   uv.y = -uv.y;
   uv = uv * 0.5 + 0.5;
   
   float shadowDepth = tex2D( ShadowSampler, uv).r;
   
   if( currentDepth > shadowDepth + 0.0000125f)
   {
      rgb *= 0.5f;
   }
   
   return float4( rgb, 1.0f);
}

다운로드 : ShadowMapping2.rfx

다운로드 : ShadowMapping3.rfx ( ShadowMap 크기가 2048임 )

( CreateShadow 패스 ) tutorial05.html

참고 ) 셰이더 프로그래밍 입문 (한빛미디어)

Depth를 기록하자
http://ttmayrin.tistory.com/35

Introduction to 3D Game Programming with DirectX 9.0c 그림자매핑
http://coreafive.tistory.com/?page=4

http://cybershin.tistory.com/89