3. Direct3D의 초기화
D3D 초기화 과정은 다음과 같은 단계들로 구성된다.
1. D3D12CreateDevice()를 이용해서 ID3D12Device를 생성한다.
2. ID3D12Fence 객체를 생성하고 서술자들의 크기를 얻는다.
3. 4X MSAA 품질 수준 지원 여부를 점검한다.
4. 명령 큐와 명령 할당자, 주 명령 리스트를 생성한다.
5. 교환 사슬을 서술하고 생성한다.
6. 응용 프로그램에 필요한 서술자 힙들을 생성한다.
7. 후면 버퍼 크기를 설정하고, 후면 버퍼에 대한 렌더 대상 뷰를 생성한다.
8. 깊이 · 스텐실 버퍼를 생성하고, 그와 연관된 깊이 · 스텐실 뷰를 생성한다.
9. 뷰 포트와 가위 판정용 사각형들을 설정한다.
1. D3D12CreateDevice()를 이용해서 ID3D12Device를 생성한다.
D3D 초기화는 D3D12 장치(ID3D12Device)를 생성하는 것으로 시작한다. Device란 디스플레이 어댑터를 나타내는 객체로 일반적으로는 그래픽 하드웨어 장치(ex 그래픽카드)이지만, 하드웨어 그래픽 기능을 흉내내는 소프트웨어 디스플레이 어댑터(ex WARP 어댑터)도 존재한다. 디바이스는 기능 지원 점검에 쓰이며, 자원이나 뷰, 명령 리스트 등 다른 모든 D3D 인터페이스 객체들의 생성에도 쓰인다. 디바이스는 다음과 같이 생성한다.
#if defined(DEBUG) || defined(_DEBUG)
// 디버그 계층을 활성화 한다.
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(debugController.GetAddressOf())));
debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(mFactory.GetAddressOf())));
// 하드웨어 장치 생성에 실패하면 WARP 어댑터 장치를 생성한다.
HRESULT hardware = mDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(mFence.GetAddressOf()));
if (FAILED(hardware))
{
ComPtr<IDXGIAdapter> pWarp;
ThrowIfFailed(mFactory->EnumWarpAdapter(IID_PPV_ARGS(pWarp.GetAddressOf())));
ThrowIfFailed(D3D12CreateDevice(
pWarp.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(mDevice.GetAddressOf())));
}
코드의 첫 줄의 디버그층(debug layer)이 활성화되어 있으면 D3D는 추가적인 디버깅을 활성화해서 Direct3D 객체에서 에러가 날 시 VC++의 출력창에 에러 내용을 출력해준다.
ID3D12Debug (d3d12sdklayers.h) - Win32 apps
An interface used to turn on the debug layer.
docs.microsoft.com
mFactory는 ID3D2Device를 생성하기 위해 필요한 오브젝트이고. 주석에도 달아놨듯 기본 하드웨어 장치 생성에 실패하면 WARP 장치 생성을 하는데 WARP 장치를 생성하려면 IDXGIFactory4 로 해주어야 한다. (1234 다있더라궁 ㅎㅎ) mFactory는 나중에 SwapChain 생성에도 쓰인다.
2. ID3D12Fence 객체를 생성하고 서술자들의 크기를 얻는다.
디바이스를 생성했으면 CPU와 GPU의 동기화를 위한 울타리 객체를 생성한다. 또한, 이후에 쓸 서술자들의 크기도 미리미리 설정해둔다. 서술자 크기는 GPU마다 다를 수 있으므로 실행 시점에서 적절한 메서드로 알아내야 한다.
ThrowIfFailed(mDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(mFence.GetAddressOf())));
mRtvDescriptorSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
3. 4X MSAA 품질 수준 지원 여부를 점검한다.
현재 장치의 기능 수준이 Direct3D 11 이상이면 4X MSAA 지원 여부를 따로 확인할 필요는 없지만, 그래도 이 책은 다음 코드를 통해 4X MSAA 지원 여부를 명시적으로 점검한다.
// 4X MSAA 품질 수준 지원 점검
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(mDevice->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels)));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
4. 명령 큐와 명령 할당자, 주 명령 리스트를 생성한다.
https://dandydot.tistory.com/3?category=988470
5. 교환 사슬을 서술하고 생성한다.
다른 객체들도 그렇겠지만 그 객체에 대해 형식이나 메모리 크기 등을 먼저 설명(descript)해주고 생성(create)한다.
void D3DApp::CreateSwapChain()
{
// 새 스왑체인을 생성하기 전에 기존 스왑체인을 해제한다.
mSwapChain.Reset();
DXGI_SWAP_CHAIN_DESC sd = {};
sd.BufferDesc.Width = mWidth;
sd.BufferDesc.Height = mHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = mBackBufferFormat;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SwapChainBufferCount;
sd.OutputWindow = mWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
// 교환 사슬은 명령 큐를 통해 방출(flush)한다.
ThrowIfFailed(mFactory->CreateSwapChain(
mDevice.Get(),
&sd,
mSwapChain.GetAddressOf()));
}
6. 응용 프로그램에 필요한 서술자 힙들을 생성한다.
응용 프로그램에 필요한 서술자/뷰 들을 담을 서술자 힙을 만들어야 한다. 지금 예제에서는 렌더 대상 뷰(render target view)와 깊이 · 스텐실 뷰(depth/stencil view)가 필요하다.
서술자 힙은 서술자 종류마다 따로 만들어야 한다. 그 종류는 아래 열거형으로 정의되어 있다.
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_descriptor_heap_type
D3D12_DESCRIPTOR_HEAP_TYPE (d3d12.h) - Win32 apps
Specifies a type of descriptor heap.
docs.microsoft.com
void D3DApp::CreateDescriptorHeap()
{
// 렌더 타겟 뷰(rtv) 힙 생성
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
ThrowIfFailed(mDevice->CreateDescriptorHeap(
&rtvHeapDesc,
IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
// 깊이 스텐실 뷰(dsv) 힙 생성
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(mDevice->CreateDescriptorHeap(
&dsvHeapDesc,
IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
}
힙을 성공적으로 생성하고 나면 힙에 저장된 서술자들에게 접근할 수 있다. 예제는 핸들(handle)을 통해 서술자들을 참조한다. 힙의 첫 서술자에 대한 핸들은 ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart()로 얻는다. 아래는 RTV와 DSV 핸들을 얻는 함수들이다.
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::GetRtvHandle() const
{
// 현재 후면 버퍼 rtv의 오프셋을 얻으려면 rtv의 크기(byte)를 알아야 한다.
return CD3DX12_CPU_DESCRIPTOR_HANDLE(
mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
mCurrentBuffer,
mRtvDescriptorSize);
}
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::GetDsvHandle() const
{
return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}
7. 후면 버퍼 크기를 설정하고, 후면 버퍼에 대한 렌더 대상 뷰를 생성한다.
앞서 말했듯 자원을 파이프라인에 직접 묶는 것이 아니라 그에 대한 뷰(=서술자)를 생성하고 그 뷰를 묶는다. 후면 버퍼를 파이프라인에 묶으려면 그에 대한 rtv를 생성해야 한다. 전면버퍼와 후면버퍼에 대한 rtv를 두 개 생성해야 한다.
void D3DApp::CreateRtv()
{
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
// 스왑체인의 i번째 버퍼를 가져옴
ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(mBackBuffer[i].GetAddressOf())));
// 그 버퍼에 대한 rtv 생성
mDevice->CreateRenderTargetView(
mBackBuffer[i].Get(),
nullptr,
rtvHeapHandle);
// 힙의 다음 항목으로 넘어감
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}
}
8. 깊이 · 스텐실 버퍼를 생성하고, 그와 연관된 깊이 · 스텐실 뷰를 생성한다.
깊이 버퍼는 그냥 가장 가까운 물체들의 깊이 정보를(스텐실을 사용하면 스텐실 정보도) 저장하는 2차원 텍스처이다. 깊이 · 스텐실 뷰는 GPU 자원 중 하나인 텍스처 자원이므로 텍스처 자원을 서술하는 D3D12_RESOURCE_DESC 구조체를 채운 후 생성해준다.
void D3DApp::CreateDsv()
{
D3D12_RESOURCE_DESC depthStencilDesc = {};
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mWidth;
depthStencilDesc.Height = mHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
// CreateCommittedResource 5번째 요소에 들어갈 구조체로
// 어떤 값으로 리소스를 초기화할 것인지에 대한 설정값
D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(mDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
mDevice->CreateDepthStencilView(
mDepthStencilBuffer.Get(),
nullptr,
GetDsvHandle());
// 자원을 초기 상태에서 깊이 버퍼로 사용할 수 있는 상태로 전이
mCommandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_DEPTH_WRITE));
}
요약하면
1. 깊이 스텐실 버퍼를 만든다.
2. 깊이 스텐실 뷰를 만든다.
3. 깊이 스텐실 자원을 '쓰기' 상태로 전이한다.
8. 뷰 포트와 가위 판정용 사각형들을 설정한다.
뷰포트란 3차원의 장면을 2D로 렌더링하여 사용자 눈에 보여질 직사각형이다. 가위 판정용 사각형(가위 직사각형)은 특정 픽셀을 *컬링하기 위한 것이다.
*컬링: 실제로 카메라의 시야 범위에 포함되는 것들만 렌더링하고, 나머지 것들은 렌더링 하지 않는 기법
void D3DApp::ViewPort()
{
mViewport.TopLeftX = 0;
mViewport.TopLeftY = 0;
mViewport.Width = static_cast<float>(mWidth);
mViewport.Height = static_cast<float>(mHeight);
mViewport.MinDepth = 0.0f;
mViewport.MaxDepth = 1.0f;
mScissorRect = { 0, 0, mWidth, mHeight };
}