04
09

게임에서의 빛 연산, 그림자 연산의 완벽한 연산 알고리즘은 아직 개발되지 않았다. 

특히 3D 모델링을 해봤다면, VRay나 Cycles 같은 렌더러에서

한 장의 이미지를 렌더링 하기 위해서 한 시간을 기다렸던 경험이 있을지도 모른다. 

출처 : https://www.chaos.com/vray/sketchup/free-trial

레이 트레이싱 방식은 픽셀 하나하나 마다 통과하는 광선으로 계산한다. 요구 연산량이

상당하기 때문에, 실시간 렌더링이 불가능하며, 현재 게임에서 쓰이는 레이 트레이싱은

모든 픽셀을 계산하지 않고, AI연산으로 그 많은 연산을 커버한다고 한다. 

(레이 트레이싱 전용 가속 장치 RT 코어 탑재된 RTX 20 시리즈부터 지원함)

 

- Shadow Mapping

그림자란 광원의 경로 상에서 불투명한 물체가 있을 때, 빛의 직전성 때문에,

물체에 빛이 통과하지 못하여 생기는 어두운 부분을 말한다.

출처 :https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

왼쪽 사진에서는 그림자가 없을 때의 모습인데, 3D 공간에서 

그림자가 없으면 물체가 떠있는지 바닥에 있는지 구별하기가 어렵다. 

최소한 3D 공간라면, 원형 그림자이라도 깔아놔야 한다. 

원형 평면 그림자

게임에서는 그림자를 흉내내기 위한 기법이 여러 가지 있지만, 

대표적으로 평면 투영 쉐도우, 투영 쉐도우, 깊이 맵 쉐도우가 있다. 

 

1. 평면 투영 쉐도우 (Planar Projected Shadow)

그림자를 만들 오브젝트를 빛 방향에서 바닥으로 납작하게 눌러서 바닥에 그려주는 방식이다.

고려할 점은 평면 투영 행렬을 구해서 곱한다는 것이다.

(D3DXMatrixShadow() - 평면 투영 행렬 구하는 Direct함수) 

오브젝트를 직접 평면 투영 행렬을 월드 행렬로 사용하여 그림자를 구현한다.

구현한 평면 투영 쉐도우

 

오브젝트를 바닥으로 짓누른 모양으로 한번 더 렌더링 하기 때문에,

와이어프레임으로 보면 똑같은 매쉬로 그려지는 것을 볼 수 있다.  

단점으로는, 평면이 아니라면 그림자가 드리워질 수 없다.

 

2. 투영 텍스쳐 쉐도우 (Projection Shadow) 

위는 오직 평면 지형에서만 그림자가 그려진다.,

울퉁불퉁한 지형에 그림자를 그리기 위해 투영 텍스쳐 쉐도우를 사용한다.

광원 위치에서 바라본 장면, 빈 텍스쳐에 그 장면을 텍스쳐로 저장한다.

텍스쳐를 생성해, 빈 텍스쳐에 렌더 타깃 RenderTargetView()을 그려서 리소스에 전달해야 한다.

위 그림의 왼쪽 아래에 있는 텍스쳐가 렌더 타깃의 모습이다. 

지형이 울퉁불퉁하더라도, 텍스쳐 좌표에 입히기 때문에, 

지형에 맞게 뿌려진다. 

단, 자기 그림자를 그릴 수 없다.  쉐도우 여부만 판정하고,

깊이의 정보가 없기 때문이다.

 

3. 깊이 맵 쉐도우 (Depth Map Shadow)

이전에, 렌더 몽키를 이용해 구현한 적이 있다.

https://dlemrcnd.tistory.com/41?category=525775 

 

셰이더 프로그래밍 - AMD RenderMonkey 사용해서 쉐이더 공부를 해보자8 (그림자 매핑)

<본 포스팅은 강좌가 아닙니다.> 이전 다렉 프로젝트할때 뎁스맵 쉐도우를 적용했었다. 그러면서 3가지의 그림자 기법인 평면쉐도우, 프로젝션 쉐도우, 뎁스맵 쉐도우 대해 알고있었다. 1. 평면

dlemrcnd.tistory.com

 

전방향 쉐도우(Omnidirectional Shadow Maps), 계단식 쉐도우(Cascaded Shadow)등

기초가 되는 그림자 기법이며 자기 그림자인, Self-Shadow가 가능하다.

 

과정은 다음과 같다. 

1. 광원 위치에서 바라보는 뷰 및 투영 행렬 연산

2. 깊이 맵 저장할 텍스쳐 생성

3. 깊이 맵 텍스쳐에 깊이 값 렌더링

4. 오브젝트 렌더링 시 깊이 값 이용해 그림자 판정

 

VSPS_DepthShadow.hlsl

더보기
cbuffer CBuf : register(b0)
{
	matrix g_matWorld : packoffset(c0);
	matrix g_matView : packoffset(c4);
	matrix g_matProj : packoffset(c8);
	matrix g_matNormal : packoffset(c12);
	float4 g_lightPos : packoffset(c16);	//라이트 방향
	float4 g_lightColor : packoffset(c17);  //라이트 색상
	float4 g_camPos : packoffset(c18);		//카메라 방향
	float4 g_value : packoffset(c19);		//기타 시간 값등
};
cbuffer cbDataShadow: register(b2)
{
	matrix g_matShadow	: packoffset(c0);
};
struct VS_INPUT
{
	float3 p : POSITION;
	float3 n : NORMAL;
	float4 c	: COLOR;
	float2 t	: TEXCOORD;

	float3 mTangent	: TANGENT;
	float3 mBinormal : BINORMAL;
};
struct VS_OUTPUT
{
	float4 p : SV_POSITION;
	float2 t : TEXCOORD0;
	float4 c : COLOR0;
	float3 mLightDir : TEXCOORD1; //방향
	float3 mViewDir  : TEXCOORD2; //방향
	float4 mShadow	 : TEXCOORD3; //뎁스 맵 쉐도우 추가
	float3 mT        : TEXCOORD4;
	float3 mB        : TEXCOORD5;
	float3 mN        : TEXCOORD6;
};

VS_OUTPUT VS(VS_INPUT Input)
{
	//난반사광의 계산 동일한 계산을 PS VS 둘다 할 수 있음. 
   //하지만 픽셀단위 계산보다 정점단위 계산이 더 연산량이 적음
	VS_OUTPUT Output = (VS_OUTPUT)0;
	float4 vLocal = float4(Input.p, 1.0f);
	float4 vWorld = mul(vLocal, g_matWorld);

	//라이트 방향 월드 행렬 곱함, 월드 공간에서의 위치여서 여기서 광원의 위치를 뺀다.
	float3 lightDir = vWorld.xyz - g_lightPos.xyz;
	Output.mLightDir = normalize(lightDir);
	//보는 방향
	float3 viewDir = vWorld.xyz - g_camPos.xyz;
	Output.mViewDir = normalize(viewDir);
	//쉐도우 행렬곱
	Output.mShadow = mul(vWorld, g_matShadow);

	float4 vView = mul(vWorld, g_matView);
	float4 vProj = mul(vView, g_matProj);
	float3 worldTangent = mul(Input.mTangent, (float3x3)g_matWorld);
	float3 worldBinormal = mul(Input.mBinormal, (float3x3)g_matWorld);
	float3 worldNormal = mul(Input.n, (float3x3)g_matWorld);
	Output.p = vProj;
	Output.t = Input.t;
    //깊이 연산, 거리만큼 0~1 0이면 쉐도우가 없고 1이면 쉐도우가 있는 것
	float depth1 = vProj.z * 1.0f / (1000.0f - 1.0f) + -1.0f / (1000.0f - 1.0f);
	Output.c = float4(depth1, depth1, depth1, 1);
	Output.mT = normalize(worldTangent);
	Output.mB = normalize(worldBinormal);
	Output.mN = normalize(worldNormal);

	return  Output;
}
Texture2D		g_txDiffuse : register(t0);
Texture2D		g_txSpecular : register(t1);
Texture2D		g_txNormal : register(t2);
Texture2D		g_txShadow  : register(t3);
SamplerState	g_Sample : register(s0);
SamplerState	 g_SamplerClamp : register(s1);;
float4 PS(VS_OUTPUT Input) : SV_TARGET
{
	//텍스쳐에서 노말, 법선 좌표 구해옴
   float3 tangentNormal = g_txNormal.Sample(g_Sample, Input.t).xyz;
   tangentNormal = normalize(tangentNormal * 2 - 1);

   float3x3 TBN = float3x3(normalize(Input.mT), normalize(Input.mB),normalize(Input.mN));
   TBN = transpose(TBN);
   float3 worldNormal = mul(TBN, tangentNormal);
   float4 albedo = g_txDiffuse.Sample(g_Sample, Input.t); //알베도 기본 색상 텍스쳐
   //쉐도우
   float3 vShadowProj;
   vShadowProj.xy = Input.mShadow.xy / Input.mShadow.w;
   float shadow = g_txShadow.Sample(g_SamplerClamp, vShadowProj.xy);
   float depth = Input.mShadow.z * 1.0f / (1000.0f - 1.0f) + -1.0f / (1000.0f - 1.0f);
   if (shadow + 0.005f <= depth)
   {
	   albedo = albedo * float4(0.5f
		   , 0.5f, 0.5f, 1.0f);
   }
   //디퓨즈 텍스쳐
   float3 lightDir = normalize(Input.mLightDir);
   float3 diffuse = saturate(dot(worldNormal, -lightDir)); // 빛드리우는 디퓨즈
   diffuse = g_lightColor.rgb * albedo.rgb * diffuse;

   //스페큘러맵
   float3 specular = 0;
   if (diffuse.x > 0.0f)
   {
	  float3 reflection = reflect(lightDir, worldNormal);
	  float3 viewDir = normalize(Input.mViewDir);

	  specular = saturate(dot(reflection, -viewDir));
	  specular = pow(specular,20.0f);

	  //스페큘러 텍스쳐
	  float4 specularInten = g_txSpecular.Sample(g_Sample, Input.t);
	  specular *= specularInten.rgb * g_lightColor;
   }
   float3 ambient = float3(0.05f, 0.05f, 0.05f) * albedo;
   return float4(ambient + diffuse + specular, 1);
}
float4 PSDepth(VS_OUTPUT Input) : SV_TARGET
{
	return Input.c;
}

 

 

깊이 맵에서, 멀리 있으면 흰색, 가까울수록 검은색으로 연산해서

텍스쳐의 배경은 흰색으로 해야 한다. 

깊이 맵 텍스쳐

 

깊이 맵을 만들 때, 고려해야 할 것이 깊이 바이어스(편향치)이다.

저장 시에 약간의 실수 값의 오차가 발생되어서 나타난다.

 

https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

 

광원의 각도로 텍스쳐를 저장해서 기울어진 깊이 텍스쳐가 되었기 때문이다. 

 

 

이 문제를 깊이 바이어스, 깊이 값을 오프셋함으로써 문제를 해결할 수 있다.

 

결과

COMMENT