1. 장면 깊이 렌더링
광원의 시점에서 본 장면의 깊이를 렌더링하는 과정이 있다. 이는 텍스처 대상 렌더링 ( render - to - texture )의 일종이며, 광원의 시점에서 본 장면의 깊이 값들을 깊이 버퍼에 기록하는 것을 뜻한다.
그림자 매핑 알고리즘에는 두 번의 렌더링 패스가 필요하다.
- 광원의 시점에서 본 장면 깊이를 그림자 맵에 렌더링
- 플레이어 카메라에서 본 장면을 후면 버퍼에 렌더링
2. 직교 투영/정사영 (Orthographic_Projection)
그림자 매핑에서 평행 광이 만들어 내는 그림자를 본뜰때 직교 투영이 필요하다.
(2.1). 직교 투영 행렬 구하기 (orthogonal_projection_matrix)
벡터를 특정 벡터 공간 또는 부분 공간에 직교적으로 투영할 떄 사용하는 행렬이다. 카메라가 보여주는 일부 공간을 절두체(frustum)라고 하고, 카메라는 절두체 내부의 좌표들을 화면에 보여준다. 우리가 화면 속에서 보는 해상도 영역을 해상도에 따라 다르기 때문에 정규화할 필요가 있다. 이를 정규화한 공간을 NDC(Normalized Display Coordinate) 공간이라고 한다.
3. 그림자 매핑 알고리즘
(3.1). 그림자 맵 리소스를 생성한다. -- Build Resource( )
D3D12_RESOURCE_DESC 를 채운 후 CreateCommittedResource 함수 호출
그림자 맵을 저장할 텍스처 리소스와 깊이 스텐실 뷰를 생성한다.
(3.2). Depth Stencil View (DSV) 생성한다. -- Build Descriptors( )
D3D12_DEPTH_STENCIL_VIEW_DESC 채운 후 CreateDepthStencilView 함수 호출
(3.3) 빛의 관점에서 깊이맵 렌더
기존 오브젝트 렌더는 '카메라' 기준으로 렌더링을 했다면, 그림자를 그리기 위해서는 '빛'의 관점에서 장면의 깊이 정보를 렌더링하여 그림자 맵을 만든다
- 직교 투영(Orthographic Projection)은 태양과 같은 평행광에 적합한 투영
- 원근 투영 (Perspective Projection)은 점광에 적합한 투영
(3.4) 깊이맵 렌더링
- 기존 렌더 타겟을 그림자 맵의 깊이 스탠실 뷰로 설정한다.
- 빛의 뷰-투영 행렬을 기반으로 장면을 렌더링 후 깊이 정보를 그림자맵에 저장한다.
(3.5) '카메라' 기준으로 다시 렌더링
- 메인 카메라의 관점에서 장명을 렌더링한다
- 그림자 맵을 샘플링하여 그림자를 계산한다.
- 그림자 정보를 조명 모델과 통합하여 최종 이미지를 생산한다.
4. 그림자 맵 판정
'빛'의 관점에서 장면을 렌더링해서 그림자 맵을 만든 후, '카메라' 관점 렌더링에서 그림자 맵에서 표본을 추출하여 현재 픽셀이 그림자 안에 있는지를 여부를 판정한다.
- 카메라 관점에서 바라보는 공간의 점을 P1, P2
- 빛의 관점에서 P1, P2까지의 거리를 d1, d2
- 그림자맵에 저장된 P1, P2 위치의 깊이 값 z1, z2
위와 같은 상황에서
d와 z 관계가 d <= z 이면 참(그림자를 그리지 않는다.), d > z (그림자를 그려야 한다.)이면 거짓으로 판정할 수 있다.
코드를 하나하나 보자.
shadowPosH.xyz /= shadowPosH.w;
shodowPosH는 빛의 뷰프러스텀행렬을 적용한 좌표이다. 이를 이용하여 빛에서 p1,p2를 바라보는 d1, d2를 알아낸다.
float depth = shadowPosH.z;
현재 픽셀의 깊이값 d1, d2
uint width, height, numMips;
gShadowMap.GetDimensions(0, width, height, numMips);
그림자맵 : gShadowMap
float dx = 1.0f / (float)width;
텍셀 크기: 1.0/ 텍스처 폭
const float2 offsets[9] =
{
float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
};
오프젯 구하기 이는 그림자 맵 샘플링시 그림자 경계를 부드럽게 처리하기 위한 중심 텍셀을 기준으로 주변 텍셀을 함께 샘플링하는 코드이다.
[unroll]
for(int i = 0; i < 9; ++i)
{
percentLit += gShadowMap.SampleCmpLevelZero(gsamShadow,
shadowPosH.xy + offsets[i], depth).r;
}
SampleCmpLevelZero 비교 샘플링 함수이다.
shadowPosH.xy + offsets[i] depth, depth라고 되어있는데 이는 gShadowMap에 이미 저장된 shadowPosH.xy + offsets[i]이 위치의 깊이 값과 텍셀 depth을 비교한다는 뜻이다.
※ shadowmap과 depth 차이점
- shadowmap : 빛의 관점에서 렌더링 된 깊이맵
- depth 현재 화면 픽셀 빛의 관점 깊이값
return percentLit / 9.0f;
위에서 9개의 오프셋 위치에서 값을 구했기 때문에 평균 값을 위해 9.0f를 나눠준다.
5. PSO와 Draw에서 신경 써야 할 부분 정리
그림자 그릴 때는 그림자 맵을 따로 그려줘야 하므로 신경 써줄 게 조금 많았다. 정리해 보자 ~
(5.1). DSV DescriptorHeap 개수 증가
그림자 맵용 버퍼를 추가로 만들어줘야 한다. 기존에는 깊이/스텐실 버퍼 하나만 가지고 있었다.
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 2;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
(5.2) 그림자 맵은 렌더 타켓을 사용하지 않음
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(),
D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_DEPTH_WRITE));
UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants));
mCommandList->ClearDepthStencilView(mShadowMap->Dsv(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
mCommandList->OMSetRenderTargets(0, nullptr, false, &mShadowMap->Dsv());
OMSetRenderTargets(0, nullptr, false, &mShadowMap->Dsv());
렌더 타깃을 세팅하지 않는다는 것은 컬러값을 사용하지 않는다는 것이다. 첫 번째 인자 0은 렌더 타깃에 적지 않는다는 뜻이다. 오직 뎁스 버퍼만 그린다.
mCommandList->ResourceBarrier 호출은 리소스 상태를 변경하는 역할을 한다.
(5.3) 그리는 공간이 다르기(카메라 관점, 빛 관점) 때문에 설정들을 모두 갱신해 줘야 한다.
- ViewPorts
- ScissorRects
- ClearDepthStencilview
- OMSetRenderTargets
- 상수 버퍼
- ResourceBarrier
- pso
다시 카메라 관점에서 그림을 그릴 때 빠진 게 있다면 수정해 줘야 한다.
그림자 그리기 쉽지 않았다. 프로젝트 복붙이 아니라 기존 프로젝트 기능을 유지하면서 기능을 넣으니 이해해야 넣을 수 있었다.
무튼 .. 재미있잖아 ~~ 그럼 다음에 또 오겠습니다.

챌린지에서 받은건 이 귀여운 임티~ㅎㅎ 너무 게을러서 챌린지 참석을 거의 못했다.. 다음에 또 기회가 있다면 제대로 해보고 싶다.
'🎯 game engine > ◽ directX12' 카테고리의 다른 글
[DirectX12] 큐브맵 만들기 / StructuredBuffer (0) | 2024.11.26 |
---|---|
[DirectX12] 스텐실(stencil) 거울 만들기 (0) | 2024.11.07 |
[DirectX12] 조명의 종류 (0) | 2023.12.19 |
[DirectX12] 조명 연산을 위한 법선 벡터, 법선 벡터의 변환 (0) | 2023.12.14 |
[DirectX12] 원기둥의 정점 찍기/렌더링 과정 이해하기 (1) | 2023.12.12 |
안 하는 것 보다 낫겠지
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!