Shadow Mapping은 빛의 위치에 카메라를 두고 그 화면 기준 depth를 저장한다. 그러기 위해 물체들을 대상으로 WVPmatrix를 곱하면 ClipPos가 나오는데 w로 나누면 Projection 좌표계를 구할 수 있고 그 z값이 깊이 값이다.
그리고 장면을 찍는 카메라 기준으로 계산해야 하는데 viewPos에 viewInverseMatrix를 곱해 WorldPos를 구한다.
거기다 빛의 위치 카메라 기준 ViewProjMatrix를 곱하면 또 ClipPos가 나오고 w로 나눠주면 Projection 좌표가 나온다.
범위는 -1~1이므로 0~1인 uv좌표계로 바꿔준다.

첫번째는 Projection 좌표 기준 우리가 찍은 좌표에 대한 빛 위치의 카메라 기준 depth값이고 두번째 uv좌표계는 빛 위치의 카메라 기준 가장 가까이에 있는 물체의 depth 값이다. 1번의 깊이값이 2번의 깊이값보다 크다면 1번 좌표에 그림자를 그려줘야 한다.
정리하면 처음에 빛 위치 카메라 기준 depth를 RenderTarget에 저장하고 장면 카메라로 물체를 그릴 때 (라이팅 연산할 때)
uv좌표와 depth를 비교해서 그림자를 그린다. 그리고 Shadow map은 screen 크기와 같게하면 부자연스럽게 표현되기 때문에 screen 크기보다 더 크게 해야 한다.
struct VS_IN
{
float3 pos : POSITION;
};
struct VS_OUT
{
float4 pos : SV_Position;
float4 clipPos : POSITION;
};
VS_OUT VS_Main(VS_IN input)
{
VS_OUT output = (VS_OUT)0.f;
output.pos = mul(float4(input.pos, 1.f), g_matWVP);
output.clipPos = output.pos;
return output;
}
float4 PS_Main(VS_OUT input) : SV_Target
{
return float4(input.clipPos.z / input.clipPos.w, 0.f, 0.f, 0.f);
}
shadow.hlsl의 전체코드인데 pos에는 matWVP를 곱해주고 clipPos를 구한다. 그리고 clipPos.z / clipPos.w 로 깊이 값을 구해 SV_Target으로 넘긴다.
//lighting.hlsl
if (length(color.diffuse) != 0)
{
matrix shadowCameraVP = g_mat_0;
float4 worldPos = mul(float4(viewPos.xyz, 1.f), g_matViewInv);
float4 shadowClipPos = mul(worldPos, shadowCameraVP);
float depth = shadowClipPos.z / shadowClipPos.w;
// x [-1 ~ 1] -> u [0 ~ 1]
// y [1 ~ -1] -> v [0 ~ 1]
float2 uv = shadowClipPos.xy / shadowClipPos.w;
uv.y = -uv.y;
uv = uv * 0.5 + 0.5;
if (0 < uv.x && uv.x < 1 && 0 < uv.y && uv.y < 1)
{
float shadowDepth = g_tex_2.Sample(g_sam_0, uv).x;
if (shadowDepth > 0 && depth > shadowDepth + 0.00001f)
{
color.diffuse *= 0.5f;
color.specular = (float4) 0.f;
}
}
}
lighting.hlsl에서는 directional light일 경우에만 그림자를 그리기로 한다. matViewInv를 곱해 WorldPos로 변환하고 빛위치 카메라 기준으로 변환하기 위해 shadowCameraVP를 곱해 shadowClipPos를 구한다. depth는 shadowClipPos.z / shadowClipPos.w 로 그리려는 해당 지점의 depth이다. 그 지점에 그림자가 있어야 할지 계산하기 위해 uv좌표를 구한다.

ClipPos에서 uv좌표로 변환하는 것은 위 그림과 같다. 변환하면 빛위치카메라 영역 안이라는 것이므로 shadow.hlsl의 PS_Main에서 저장했던 RenderTarget에 uv 좌표를 이용해 빛위치 카메라 기준 가장 먼저 부딪히는 깊이 shadowDepth를 구해주고 그것을 현재 깊이 depth와 비교해 depth가 더 크다면 그림자를 그려줘야 한다. \
shared_ptr<GameObject> _shadowCamera;
void Light::RenderShadow()
{
_shadowCamera->GetCamera()->SortShadowObject();
_shadowCamera->GetCamera()->Render_Shadow();
}
C++코드로가서 Light는 Shadow를 그리기 위한 Shadow Camera를 내부적으로 갖는다. 그리고 RenderShadow가 생겼다.
void Camera::SortShadowObject()
{
shared_ptr<Scene> scene = GET_SINGLE(SceneManager)->GetActiveScene();
const vector<shared_ptr<GameObject>>& gameObjects = scene->GetGameObjects();
_vecShadow.clear();
for (auto& gameObject : gameObjects)
{
if (gameObject->GetMeshRenderer() == nullptr)
continue;
if (gameObject->IsStatic())
continue;
if (IsCulled(gameObject->GetLayerIndex()))
continue;
if (gameObject->GetCheckFrustum())
{
if (_frustum.ContainsSphere(
gameObject->GetTransform()->GetWorldPosition(),
gameObject->GetTransform()->GetBoundingSphereRadius()) == false)
{
continue;
}
}
_vecShadow.push_back(gameObject);
}
}
그림자를 그리기전 그림자를 그릴 물체를 sort하는데 static이 아닌 물체만 그림자를 그려준다 정적인 물체들은 그림자가 정해져있기 때문에 빠른 연산을 위해 shadowmapping을 하지 않는다.
void MeshRenderer::RenderShadow()
{
GetTransform()->PushData();
GET_SINGLE(Resources)->Get<Material>(L"Shadow")->PushGraphicsData();
_mesh->Render();
}
Camera::RenderShadow를 거쳐 MeshRender::RenerShadow를 하는데 shadow를 위한 공용 material을 사용한다.
// Shadow Group
{
vector<RenderTarget> rtVec(RENDER_TARGET_SHADOW_GROUP_MEMBER_COUNT);
rtVec[0].target = GET_SINGLE(Resources)->CreateTexture(L"ShadowTarget",
DXGI_FORMAT_R32_FLOAT, 4096, 4096,
CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);
shared_ptr<Texture> shadowDepthTexture = GET_SINGLE(Resources)->CreateTexture(L"ShadowDepthStencil",
DXGI_FORMAT_D32_FLOAT, 4096, 4096,
CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL);
_rtGroups[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::SHADOW)] = make_shared<RenderTargetGroup>();
_rtGroups[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::SHADOW)]->Create(RENDER_TARGET_GROUP_TYPE::SHADOW, rtVec, shadowDepthTexture);
}
Shadow 타입 RenderTargetGroup을 만든다. 화면 크기보다 더 커야하므로 4096으로 맞춰져 있고 depthstencilTexture도 그 크기에 맞춰야 하기 때문에 기존에 쓰던 것을 쓰는 것이 아닌 새로운 Texture을 만들어 사용한다.
//Light::Render
if (static_cast<LIGHT_TYPE>(_lightInfo.lightType) == LIGHT_TYPE::DIRECTIONAL_LIGHT)
{
shared_ptr<Texture> shadowTex = GET_SINGLE(Resources)->Get<Texture>(L"ShadowTarget");
_lightMaterial->SetTexture(2, shadowTex);
Matrix matVP = _shadowCamera->GetCamera()->GetViewMatrix() * _shadowCamera->GetCamera()->GetProjectionMatrix();
_lightMaterial->SetMatrix(0, matVP);
}
Light::Render에서 만든 Shadow의 Texture을 이용해 렌더링한다.
D3D12_VIEWPORT vp = D3D12_VIEWPORT{ 0.f, 0.f, _rtVec[0].target->GetWidth() , _rtVec[0].target->GetHeight(), 0.f, 1.f };
D3D12_RECT rect = D3D12_RECT{ 0, 0, static_cast<LONG>(_rtVec[0].target->GetWidth()), static_cast<LONG>(_rtVec[0].target->GetHeight()) };
GRAPHICS_CMD_LIST->RSSetViewports(1, &vp);
GRAPHICS_CMD_LIST->RSSetScissorRects(1, &rect);
Shadow의 Texture 크기가 다르므로 SetRenderTarget할 때 Texture의 크기가 달라질 수 있다. 그래서 RenderTargetGroup마다 자신이 세팅할 RenderTarget 크기에 맞게 Viewport, Rect를 설정한다.
전체적인 순서는 light에 shadow camera를 배치해 light::finalupdate 때 shadow camera가 light와 똑같은 방향을 바라보도록 배치하고 Render Shadow를 먼저 한다. 끝나면 Shadow Map RenderTarget에 그려질 것이고 RenderLights를 할 때 RenderTarget으로 Shadow도 함께 넘겨서 그림자에 대하여 계산한다.
'그래픽스' 카테고리의 다른 글
| [DX12] Terrain (0) | 2022.07.31 |
|---|---|
| [DX12] Tessellation (0) | 2022.07.31 |
| [DX12] Instancing (0) | 2022.07.30 |
| [DX12] Particle System (0) | 2022.07.29 |
| [DX12] Compute Shader (0) | 2022.07.29 |