DirectX11

Shadow Mapping

RuNaPi 2022. 9. 27. 13:35

Shadow Mapping이란 빛의 시점으로 봤을때의 깊이 값과 카메라 시점으로 봤을때의 깊이 값의 차이를 이용하여 그림자를 표현하는 방법이다.

광원 시점으로 봤을 경우의 깊이 값 보다 카메라 시점으로 보았을때의 깊이 값이 클 경우 그림자 안에 있는 것이고

작거나 같을 경우에는 그림자 안에 있지 않는것이다.

그러기 위하여 빛의 시점으로 깊이 버퍼를 그려야한다.

빛을 기준으로 View Projection Matrix를 계산하여야한다.

문제는 Directional Light의 경우는 직교투영을 해주어야 하고 Spot Light와 Point Light의 경우 원근투영으로 Projection Matrix를 계산하여야 한다.

// 직교 투영
m_viewTM = XMMatrixLookAtLH(m_LightPos, _targetPos, _up);
m_projTM = XMMatrixOrthographicOffCenterLH(_viewLeft, _viewRight, _viewBottom, _viewTop, _viewNear, _viewFar);
m_shadowTM = m_viewTM * m_projTM;

// 원근 투영
m_viewTM = XMMatrixLookAtLH(_p, _p + _l, _u);
float screenAspect = 1.0f;
m_projTM = XMMatrixPerspectiveFovLH(m_spotAngle, screenAspect, 0.1f, 1000.0f);
m_shadowTM = m_viewTM * m_projTM;

Light의 ViewProjection Matrix를 구했으면 오브젝트를 빛의 ViewProjTM을 이용해 그려준다.

auto lightViewProj = light->GetViewProjTM();

for (size_t i = 0; i < renderObjects.size(); i++)
{
	cbs.clear();
	XMMATRIX world = XMLoadFloat4x4(&renderObjects[i].m_World);

	PerObject_CB _perObjectCB;

	XMStoreFloat4x4(&_perObjectCB.worldViewProj, world * lightViewProj);

	switch (renderObjects[i].m_RenderType)
	{
		case RenderObject::RenderType::Mesh:
		{
			vs = m_Shadow_Static_Mesh_VS.get();

			break;
		}
		case RenderObject::RenderType::SkinnedMesh:
		{
			vs = m_Shadow_Skin_Mesh_VS.get();
			memcpy(_perObjectCB.boneTransforms, renderObjects[i].m_BoneTMList, sizeof(XMFLOAT4X4) * renderObjects[i].m_Size);
			break;
		}
		case RenderObject::RenderType::ProjectedMesh:
		case RenderObject::RenderType::OutLine:
		{
			continue;
			break;
		}
		default:
		{
			assert(false);
			break;
		}
	}
    
	cbs.push_back(&_perObjectCB);
	vs->Update(cbs.data(), nullptr);
	deviceContext->VSSetShader(vs->GetShader<ID3D11VertexShader>(), nullptr, 0);
    
	auto _indexBuffers = renderObjects[i].m_Mesh->GetIndexBuffers();
	for (size_t index = 0; index < _indexBuffers.size(); index++)
	{
		renderObjects[i].m_Mesh->Bind(deviceContext, (int)index);
		deviceContext->DrawIndexed(_indexBuffers[index]->GetIndexCount(), 0, 0);
	}
}

그러면 다음과 같이 깊이맵이 생성이 된다 아래는 SpotLight의 깊이 맵을 구한 것이다.

Spot Light의 깊이 맵

Shawdow Mapping를 하기 위한 빛의 시점으로 한 깊이 맵을 구했으면 이제 빛을 계산할때 빛마다 깊이 텍스처를 샘플링해

Shadow Factor를 계산해 빛을 계산할때 곱해준다.

Shadow Factor를 계산하기 위해선

WorldPosition에 빛의 ViewProjection Matrix를 곱한후 텍스처 좌표계로 변환시켜 나온 UV 값으로 ShadowMap를 샘플링한다. 

샘플링을 할때 PCF을 적용 시켜주어야하는데, 그림자 맵을 샘플링 할때 쓰이는 투영 텍스처 UV가 그림자 맵의 한 텍셀과 정확히 일치하지 않기 때문에 보간하여서 샘플링을 해주어야한다.

(U, V), (U + dx, V), (U, V + dy), (U + dx, V + dy)를 샘플링 하여 보간을 해주는데 이때의  dx, dy는 쉐도우 맵의 텍셀 값이다. 

float s0 = (U, V 샘플링)
float s1 = (U + dx, V 샘플링)
float s2 = (U, V + dy 샘플링)
float s3 = (U + dx, V + dy 샘플링)

float r0 = depth <= s0
float r1 = depth <= s1
float r2 = depth <= s2
float r3 = depth <= s3

float2 texelPos = SMAP_SIZE * UV

float2 t = frac(texelPos)

return lerp(lerp(r0, r1, t.x), lerp(r2, r3, t.x), t.y)

 그러나 이 방법은 샘플링을 네번 추출해야하는 문제점이 있다.

그래서 DX11에는 SampleSmpLevelZero 라는 메서드를 제공하고 있다.

shadowH = mul(float4(worldPos.xyz, 1.0f), LightInfo[i].ShadowTransform);
shadowH.xyz /= shadowH.w;

shadows = CalcShadowFactor(samShadow, gShadow[i], float3(shadowH.xyz));

// 그림자 수치를 계산
float CalcShadowFactor(SamplerComparisonState samShadow,
	Texture2D shadowMap,
	float3 shadowPosH)
{
	// Depth in NDC space.
	float depth = shadowPosH.z - 0.001f;

	float percentLit = 0.0f;

	shadowPosH.x = shadowPosH.x * 0.5f + 0.5f;
	shadowPosH.y = -shadowPosH.y * 0.5f + 0.5f;

	[unroll]
	for (int i = 0; i < 9; ++i)
	{
		percentLit += shadowMap.SampleCmpLevelZero(samShadow,
			shadowPosH.xy, depth, offset[i]).r;
	}
	return percentLit /= 9.0f;
}


// 빛을 계산한후 곱해준다.
_finColor += CalLight(LightInfo[i], specularColor, diffuseColor, worldPos.xyz, normal.xyz, toEye, roughness2, metallic) * shadows;

 

벽면에 그림자가 그려진 상태

'DirectX11' 카테고리의 다른 글

Texture Compression(Block Compression)  (0) 2023.06.27
GPU Instancing  (0) 2023.06.22
Voxel GI - Cone Tracing  (0) 2023.05.25
Voxel GI - Voxelize  (0) 2023.05.25
Cascade Shadow  (0) 2023.02.16