[DirectX12] DirectX 시작하기

DirectX 12의 개요

Direct 3D는 응용 프로그램에서 GPU를 제어하고 프로그래밍하는 데 쓰이는 Low Level의 Graphics API이다.

우리는 이를 통해 응용 프로그램과 그래픽 하드웨어 사이에서 Direct 3D의 명령들을 이용해서 다양한 물체들을 렌더링할 수 있게 된다.

DirectX 12 초기화

DirectX 12의 초기화 과정은 길지만, 응용 프로그램 실행 시 한 번만 해 주면 된다.
과정은 다음과 같다.

1. DXGI Factory 생성
2. Device 생성
3. Fence 생성 및 서술자 크기 설정
4. 명령 대기열과 명령 목록 생성
5. Swap chain 생성
6. RTV & DSV Descriptor Heap 생성 7. RTV & DSV 생성

1. DXGI Factory 생성

DXGI(DirectX Graphics Infrastructure)는 Direct3D와 함께 쓰이는 API이다.
DXGI API에서 지원하는 기능으로는 SwapChain, Display Adapter 등이 있다.
DirectX의 초기화를 위해서 먼저 DXGIFactory를 생성해주어야 한다.

다음은 DXGI Factory를 생성하는 코드이다.

// 1. DXGI Factory 생성
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));


2. Device 생성

Direct3D 12 Device는 디스플레이 어댑터를 나타내는 객체이다. 일반적으로 디스플레이 어댑터는 물리적인 3차원 그래픽 하드웨어 장치(이를테면 그래픽카드)이지만, 소프트웨어 디스플레이 어댑터(이를테면 WARP 어댑터)도 존재한다.
Direct3D 12 Device는 자원이나 뷰(서술자), 명령 목록 등의 다른 모든 DirectX 인터페이스 객체들의 생성에 쓰인다.

다음은 Device를 생성하는 코드이다.

// 2. Device 생성
HRESULT hardwareResult = D3D12CreateDevice(
	nullptr,                        // 디스플레이 어댑터
	D3D_FEATURE_LEVEL_11_0,         // 최소 기능 수준
	IID_PPV_ARGS(&md3dDevice));     // 생성할 인터페이스 객채

// 실패했다면 WARP 어댑터 장치를 생성한다.
if (FAILED(hardwareResult))
{
	ComPtr<IDXGIAdapter> pWarpAdapter;
	ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

	ThrowIfFailed(D3D12CreateDevice(
		pWarpAdapter.Get(),
		D3D_FEATURE_LEVEL_11_0,
		IID_PPV_ARGS(&md3dDevice)));
}


3. Fence 생성 및 서술자 크기 설정

Fence는 CPU와 GPU 간 작업의 동기화를 위한 객체이다.
이를 통해 두 장치 간 Signal을 통해 이벤트를 다룬다.

또한 이후 렌더링에 필요한 서술자의 크기도 미리 설정해둔다.

다음은 Fence를 생성하는 코드이다.

// 3, Fence 생성
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
	IID_PPV_ARGS(&mFence)));

다음은 서술자들의 크기를 설정하는 코드이다.
이는 나중에 렌더링할 때 설정해도 괜찮다.

// 서술자 크기 설정 
// Render Target View
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
// Depth-Stencil View
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
// Constant Buffer View, Shader Resource View, Unordered Access View
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);


4. Command Queue, Command Allocator, CommandList 생성

GPU에는 Command Queue가 존재한다. CPU는 Command List를 작성해서 DirectX API를 이용하여 그 대기열을 GPU의 Command Queue에 제출한다.

다음은 명령 대기열과 명령 목록을 생성하는 코드이다.

// 4. Command Queue 생성
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));

ThrowIfFailed(md3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));

ThrowIfFailed(md3dDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mDirectCmdListAlloc.Get(), nullptr, IID_PPV_ARGS(mCommandList.GetAddressOf())));

// 이후 CommandList를 작성할 때, Reset을 호출하기 위해 미리 Close 해둔다.
mCommandList->Close();


5. Swap chain 생성

DirectX에서는 렌더링이 되는 과정을 보이지 않기 위해 일반적으로 이중 버퍼링 기법을 사용한다. 이를 위해 전면 버퍼(Front Buffer)와 후면 버퍼(Back Buffer)를 이용한다.
전면 버퍼가 화면에 표시된 동안 다음 프레임은 후면 버퍼에 그려지고, 후면 버퍼가 다 그려지면 Swap Chain을 통해 전면 버퍼와 후면 버퍼를 맞바꾼다.

다음은 Swap Chain을 생성하는 코드이다.

// Swap chain을 생성하기 전에 초기화
mSwapChain.Reset();

	swapChainDesc.BufferDesc.Width = mClientWidth; ;									// 바퍼의 해상도 가로
	swapChainDesc.BufferDesc.Height = mClientHeight;									// 버퍼의 해상도 세로
	swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;								// 버퍼의 프레임레이트
	swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;								
	swapChainDesc.BufferDesc.Format = mBackBufferFormat;								// 버퍼 디스플레이 형식
	swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;	// 화면 주사 방식
	swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;					// 확대 축소 방식
	swapChainDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;								// 다중표본화 설정
	swapChainDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
	swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;						// Usage
	swapChainDesc.BufferCount = SwapChainBufferCount;									// Swap Chain이 사용할 버퍼 개수
	swapChainDesc.OutputWindow = mhMainWnd;												// 렌더링 결과가 표시될 창의 핸들
	swapChainDesc.Windowed = true;														// 창모드 설정
	swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;							// Swap Effect
	swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;						// 디스플레이 모드 설정

ThrowIfFailed(mdxgiFactory->CreateSwapChain(mCommandQueue.Get(), &swapChainDesc, mSwapChain.GetAddressOf()));


6. RTV & DSV Descriptor Heap 생성

Render Target View는 Swap chain에서 렌더링 대상이 되는 버퍼 자원을 서술하고, Depth-Stencil View는 깊이 판정을 위한 버퍼 자원을 서술한다.

이를 이용해 Render Target View와 Depth-stencil View를 생성한다.

다음은 RTV와 DSV Descriptor Heap을 생성하는 코드이다.

// RenderTargetView(RTV) Descriptor Heap 생성
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(md3dDevice->CreateDescriptorHeap(
	&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));

// Depth-Stencil View(DSV) Descriptor Heap 생성
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(md3dDevice->CreateDescriptorHeap(
	&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));


7, RTV & DSV 생성

이제 앞서 생성한 객체들을 가지고 RTV를 생성한다.

다음은 Swap Chain의 두 버퍼에 대해 각각 RTV를 생성하는 코드이다.

// Flush before changing any resources.
FlushCommandQueue();

ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));

// Buffer 초기화
for (int i = 0; i < SwapChainBufferCount; ++i)
	mSwapChainBuffer[i].Reset();
mDepthStencilBuffer.Reset();

// Swap Chain Resize
ThrowIfFailed(mSwapChain->ResizeBuffers(
	SwapChainBufferCount,
	mClientWidth, mClientHeight,
	mBackBufferFormat,
	DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH));

mCurrBackBuffer = 0;

// Swap chain의 갯수만큼 Render Target View 생성
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
    // Swap Chain의 i번째 버퍼를 얻는다.
	ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
    // 버퍼에 대한 RTV를 생성한다.
	md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
    // Heap의 다음 항목으로 넘어간다.
	rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

다음은 DSV를 생성하는 코드이다.

// Depth-Stencil Buffer와 View 생성
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;	
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1; 
depthStencilDesc.Format = DXGI_FORMAT_R24G8_TYPELESS;
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;

D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
	&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
	D3D12_HEAP_FLAG_NONE,
	&depthStencilDesc,
	D3D12_RESOURCE_STATE_COMMON,
	&optClear,
	IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));

// DSV 생성
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = mDepthStencilFormat;
dsvDesc.Texture2D.MipSlice = 0;
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());

// 자원을 초기 상태에서 깊이 버퍼로 사용할 수 있는 상태로 바꾼다.
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
	D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE));

// CommandList 실행
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);

// 명령이 끝날 때까지 대기
FlushCommandQueue();


해당 코드들은 Introduction to 3D Game Programming with DirectX 12 책의 코드를 참고했습니다.

코드 샘플 : d3d12book