DX12 카메라와 깊이 버퍼
World·View·Projection과 Depth Test로 3D 장면 만들기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
정점 좌표에 z값을 추가하는 것만으로 3D 장면이 되지는 않는다. Object를 World에 배치하고, Camera 기준으로 바꾼 뒤, Perspective Projection으로 Clip Space를 만들고, 가까운 표면만 남기는 Depth Test까지 연결해야 한다.
좌표 공간의 흐름
Model Space
× World
World Space
× View
View Space
× Projection
Clip Space
÷ w
NDC
→ Viewport
Screen / Depth Buffer
World Matrix는 Object의 지역 정점을 장면에 배치한다. View Matrix는 World를 Camera의 위치와 축이 기준이 되도록 바꾼다. Projection Matrix는 Frustum을 Clip Space로 변환하고 Perspective Divide에 필요한 w를 만든다.
Matrix 곱 순서는 사용하는 행/열 Vector 규약과 HLSL mul 순서에 따라 달라 보일 수 있다. 중요한 것은 임의로 Transpose를 추가하는 것이 아니라 CPU 생성, Constant Buffer 저장, HLSL 선언, mul 순서를 하나의 규칙으로 고정하는 것이다.
Camera 상태와 View Matrix
using namespace DirectX;
const XMVECTOR eye = XMVectorSet(0.0f, 1.5f, -4.0f, 1.0f);
const XMVECTOR target = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
const XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
const XMMATRIX view = XMMatrixLookAtLH(eye, target, up);
Left-handed 규약에서 Camera의 앞은 보통 +z로 정한다. LH와 RH 함수, Mesh Winding, Cull Mode, Projection을 섞으면 Object가 뒤집히거나 보이지 않는다.
Camera를 Position, Forward, Up으로 저장할지 Position과 Quaternion으로 저장할지 정책을 정한다. Forward와 Up을 독립적으로 누적 갱신하면 직교 기저가 깨질 수 있다.
Projection은 화각과 깊이 범위를 정한다
const float aspect = static_cast<float>(width) /
static_cast<float>(height);
const XMMATRIX projection = XMMatrixPerspectiveFovLH(
XMConvertToRadians(60.0f),
aspect,
0.1f,
1000.0f);
Near Plane을 0으로 둘 수는 없다. Near을 필요 이상으로 0에 가깝게 두면 깊이 정밀도가 악화되고 멀리 있는 표면이 떨리는 z-fighting이 늘어난다. Far를 무조건 크게 만드는 것도 같은 문제를 키운다.
Resize 시 Aspect Ratio와 Projection을 다시 계산하지 않으면 원이 타원으로 보인다. Width/Height가 0인 최소화 상태에서 나눗셈하지 않도록 프레임을 미룬다.
Depth Resource와 DSV를 만든다
const auto depthDesc = CD3DX12_RESOURCE_DESC::Tex2D(
DXGI_FORMAT_D32_FLOAT,
width,
height,
1,
1,
1,
0,
D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL);
D3D12_CLEAR_VALUE clearValue{};
clearValue.Format = DXGI_FORMAT_D32_FLOAT;
clearValue.DepthStencil.Depth = 1.0f;
clearValue.DepthStencil.Stencil = 0;
auto heapProperties =
CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
ThrowIfFailed(device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&depthDesc,
D3D12_RESOURCE_STATE_DEPTH_WRITE,
&clearValue,
IID_PPV_ARGS(&depthBuffer)));
device->CreateDepthStencilView(
depthBuffer.Get(), nullptr,
dsvHeap->GetCPUDescriptorHandleForHeapStart());
Depth Buffer에는 다음 다섯 조건이 함께 맞아야 한다.
- Resource Format
ALLOW_DEPTH_STENCILFlag- Initial/Current Resource State
- DSV Descriptor
- PSO의 Depth/Stencil State와 DSV Format
Optimized Clear Value는 Resource 생성 시 Driver가 자주 사용할 Clear Pattern에 맞게 최적화할 수 있는 정보다. 실제 ClearDepthStencilView의 값과 같게 유지한다.
PSO와 Output Merger에 Depth를 연결한다
pso.DepthStencilState.DepthEnable = TRUE;
pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
pso.DSVFormat = DXGI_FORMAT_D32_FLOAT;
commandList->OMSetRenderTargets(
1, &rtv, FALSE, &dsv);
commandList->ClearDepthStencilView(
dsv,
D3D12_CLEAR_FLAG_DEPTH,
1.0f,
0,
0,
nullptr);
LESS와 Clear Depth 1.0을 쓰면 더 가까운 표면의 작은 Depth가 통과한다. Reverse-Z를 도입하면 Projection, Clear Value, Comparison Function을 모두 함께 바꾸어야 한다. 하나만 바꾸면 장면 대부분이 사라질 수 있다.
Constant Buffer에서 Matrix 계약을 고정한다
cbuffer ObjectConstants : register(b0)
{
float4x4 WorldViewProjection;
};
VSOutput VSMain(VSInput input)
{
VSOutput output;
output.Position = mul(float4(input.Position, 1.0f),
WorldViewProjection);
return output;
}
CPU와 HLSL이 행렬 배치를 다르게 해석한다면 Upload 전에 XMMatrixTranspose를 적용하는 정책을 쓸 수 있다. 그러나 코드 곳곳에 “안 나오면 Transpose”를 추가하지 말고 Matrix 경계 하나에서만 변환한다.
깊이 문제를 분리하는 Debug View
- Depth Test를 끌 때만 보인다면 Depth State, Clear, DSV Binding을 본다.
- 앞뒤 면이 반대라면 Handedness, Winding, Cull Mode를 본다.
- 거리가 멀수록 심하게 떨리면 Near/Far 비율을 본다.
- Resize 뒤에만 깨지면 Depth Resource, Viewport, Projection의 크기를 본다.
- Object가 Camera 이동과 같은 방향으로 움직이면 View Matrix의 역변환 의미를 본다.
Depth를 Grayscale로 표시할 때 Raw Depth가 선형 거리가 아니라는 점에 주의한다. 눈으로 거리를 비교하려면 Projection 규약에 맞게 Linearize한 Debug View를 추가한다.
3D Camera와 Depth는 두 기능이 아니라 좌표 계약과 가시성 계약이다. Matrix 규약, Projection 범위, Depth Format과 Comparison을 한 표로 고정해야 추측으로 고치는 버그를 피할 수 있다.