기록공간

[DirectX 12] Direct3D 초기화 - 2 본문

DirectX/기초

[DirectX 12] Direct3D 초기화 - 2

입코딩 2020. 3. 12. 19:07
반응형

서술자 힙 생성

스왑 체인까지 만들었다면, 응용 프로그램에 필요한 서술자/뷰들을 담을 서술자 힙을 만들어야한다. 서술자 힙은 ID3D12DescriptorHeap 인터페이스로 대표된다. 힙을 생성하는 메서드는 ID3D12Device::CreateDescriptorHeap이다. 

 

SwapChainBufferCount에 설정된 개수만큼의 렌더 대상 뷰(RTV)들과 하나의 깊이 스텐실 뷰(DSV)가 필요하다. RTV는 스왑 체인에서 렌더링의 대상이 되는 버퍼 자원을 서술하고, DSV는 깊이 판정을 위한 버퍼 자원을 서술한다. 

 

서술자 힙은 서술자 종류마다 따로 만들어야 한다. 따라서, SwapChainBufferCount 개의 RTV들을 담을 힙 하나와 하나의 DSV를 담을 힙이 필요하다.

 

다음은 이 힙들을 생성하는 코드이다.

bool D3DApp::InitDirect3D()
{
	// 앞의 내용 생략...
    CreateRtvAndDsvDescriptorHeaps();

	return true;
}

ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;
void D3DApp::CreateRtvAndDsvDescriptorHeaps()
{
    // 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;
    
    // RTV 서술자를 담을 서술자 힙을 생성한다.
    ThrowIfFailed(md3dDevice->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;
    
    // DSV 서술자를 담을 서술자 힙을 생성한다.
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
        &dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
}

 힙을 성공적으로 생성하고 나면 힙에 저장된 서술자들에 접근할 수 있다. 응용 프로그램은 핸들을 통해서 서술자들을 참조한다. 힙의 첫 서술자에 대한 핸들은 ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart 메서드로 얻는다. 다음은 각각 현재의 후면 버퍼 RTV와 DSV에 대한 핸들을 얻는 함수들이다.

D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const
{
    // 편의를 위해 CD3DX12_CPU_DESCRIPTOR_HANDLE의 생성자를 사용한다.
    // 이 생성자는 주어진 오프셋에 해당하는 후면 버퍼 
    // RTV의 핸들(D3D12_CPU_DESCRIPTOR_HANDLE)을 돌려준다.
	return CD3DX12_CPU_DESCRIPTOR_HANDLE(
		mRtvHeap->GetCPUDescriptorHandleForHeapStart(),    // 첫 핸들
		mCurrBackBuffer,                                   // 오프셋 인덱스
		mRtvDescriptorSize);                               // 서술자의 바이트 크기
}

D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const
{
	return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

첫 함수는 서술자의 크기가 필요한 상황의 예이다. 현재 후면 버퍼 RTV 서술자의 오프셋을 얻으려면 RTV 서술자의 크기를 알아야 한다.

 

렌더 대상 뷰(RTV) 생성

앞서 말했듯, 자원 자체를 직접 파이프라인의 단계에 묶지 않는다. 대신 반드시 자원에 대한 뷰(서술자)를 생성해서 그 뷰를 파이프라인 단계에 묶어야 한다. 특히, 후면 버퍼를 파이프라인의 출력 병합기(Ouput Merger : OM) 단계에 묶으려면 후면 버퍼에 대한 RTV를 생성해야 한다. 우선 할 일은 스왑 체인에 저장되어 있는 버퍼 자원을 얻는 것인데, 이를 위해 다음과 같은 메서드를 사용한다. 

 

GetBuffer를 호출하면 해당 후면 버퍼의 COM 참조 횟수가 증가한다. 따라서 버퍼를 다 사용한 후에는 반드시 해제해야 한다. ComPtr을 사용하면 해제가 자동으로 처리된다.

 

RTV를 생성할때 ID3D12Device::CreateRenderTargetView 메서드를 사용한다.

 

pDesc는 무형식 자원이 아니라면 NULL을 지정해도 된다. 그런 경우 이 메서드는 그 자원을 생성할 때 지정한 자료 형식을 적용해서 그 자원의 첫 번째 밉맵 수준에 대한 뷰를 생성한다. (후면 버퍼는 밉맵 수준이 하나뿐이다)

 

다음은 이 메서드를 이용해서 스왑 체인의 두 버퍼에 대해 각각 RTV를 생성하는 코드이다.

ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
// RTV 서술자 힙에 저장된 RTV 서술자 핸들
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
// 스왑 체인의 버퍼 수만큼
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
    // 스왑 체인의 i번째 버퍼를 얻는다.
	ThrowIfFailed(mSwapChain->GetBuffer(
		i, IID_PPV_ARGS(&mSwapChainBuffer[i])));

    // 그 버퍼에 대한 RTV를 생성한다.
	md3dDevice->CreateRenderTargetView(
		mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
	
    // 힙의 다음 항목으로 넘어간다.
	rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

 

깊이 스텐실 버퍼와 뷰 생성

이제 깊이 스텐실 버퍼를 생성해야 한다. 깊이 버퍼는 그냥 가장 가까운 가시 물체들의 깊이 정보(필요한 경우 스텐실도)를 저장하는 2차원 텍스처이다. 텍스처는 GPU 자원의 하나이므로, 텍스처 자원을 서술하는 D3D12_RESOURCE_DESC 구조체를 채운 후 ID3D12Device::CreateCommittedResource를 호출하면 깊이 스텐실 버퍼를 생성할 수 있다. 구조체의 정의는 다음과 같다.

 

GPU 자원들은 GPU 힙에 존재한다. 본질적으로 GPU 힙은 GPU 메모리의 블록인데, 특정한 속성들을 가지고 있다. CreateCommittedResource 메서드는 자원을 생성하고, 지정된 속성들에 부합하는 힙에 그 자원을 맡긴다.

 

 

  1. pHeapProperties : 자원을 맡길 힙의 속성들을 담은 구조체를 가리키는 포인터. 일부 속성들은 고급 응용에나 필요하다. 지금 우리의 주된 관심사는 힙의 종류를 뜻하는 D3D12_HEAP_TYPE 형식의 멤버인데, 이 열거형에는 다음과 같은 멤버들이 있다.

    (a) D3D12_HEAP_TYPE_DEFAULT : 기본 힙. 전적으로 GPU가 접근할 자원들이 담긴다. CPU      가 깊이 스텐실 버퍼에 접근할 필요는 전혀 없으므로, 깊이 스텐실 버퍼는 기본 힙에 넣        는  것이 좋다.

    (b) D3D12_HEAP_TYPE_UPLOAD : 자료 올리기 힙. CPU에서 GPU로 자료를 올려서 갱신해        야 할 자원들을 이 힙에 맡긴다.

    (c) D3D12_HEAP_TYPE_READBACK : 다시 읽기 힙. CPU가 읽어야 할 자원들을 이 힙에 맡긴      다.

    (d) D3D12_HEAP_TYPE_CUSTOM : 고급 응용을 위한 것으로, MSDN 문서를 참조하자.

    최적의 성능을 위해서는 자원들을 기본 힙에 넣어야 한다. 나머지 속성들은 정말 필요할때만 사용해야 한다.

  2. HeapFlags : 자원을 맡길 힙이 가졌으면 하는 속성들을 나타내는 추가적인 플래그들, 흔히 D3D12_HEAP_MISC_NONE을 지정한다. 

  3. pResourceDesc : 생성하고자 하는 자원을 서술하는 D3D12_RESOURCE_DESC 인스턴스 포인터

  4. InitializeResourceState : 자원에는 어떤 용도인지 나타내는 상태가 있다. 이 매개변수에는 자원의 초기 상태를 지정한다. 깊이 스텐실 버퍼로 사용할 자원은 D3D12_RESOURCE_USAGE_INITIAL을 초기 상태로 지정하고, 생성 후 D3D12_RESOURCE_USAGE_DEPTH 상태로 전이해서 깊이 스텐실 버퍼로서 파이프라인에 묶으면 된다. 

  5. pOptimizedClearValue : 자원 지우기에 최적화된 값을 나타내는 D3D12_CLEAR_VALUE 구조체를 가리키는 포인터. 최적화된 지우기 값과 부합하는 지우기 호출은 부합하지 않는 호출보다 빠를 수 있다. 최적화된 지우기 값을 설정하지 않으려면 NULL을 지정하면 된다. 

  6. riidResource : 생성하려는 자원에 해당하는 ID3D12Resource 인터페이스의 COM ID.

  7. ppvResource : 새로 생성된 자원을 나타내는 ID3D12Resource의 포인터를 이 매개변수를 통해 돌려준다. (출력 매개변수)

깊이 스텐실 버퍼를 사용하기 전에 반드시 그와 관련된 깊이 스텐실 뷰를 생성해서 파이프라인에 묶어야 한다. 구체적인 방법은 RTV를 생성할 때와 비슷하다.

 

다음은 깊이 스텐실 텍스처와 해당 깊이 스텐실 뷰를 생성하는 방법을 보여주는 예제 코드이다. 

// 깊이 스텐실 버퍼와 뷰를 생성한다.
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())));

// 전체 자원이 밉맵 수준 0에 대한 서술자를,
// 해당 자원의 픽셀 형식을 적용해서 생성한다.
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), nullptr, DepthStencilView());

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

 

 

뷰포트 설정

보통은 3차원 장면을 화면 전체에 해당하는 후면 버퍼 또는 창의 클라이언트 영역 전체에 해당하는 후면 버퍼 전체에 그리지만, 필요하다면 3차원 장면을 후면 버퍼의 일부를 차지하는 직사각형 영역에만 그리는 것도 가능하다. 

 

장면을 그려 넣고자 하는 후면 버퍼의 부분직사작형(subrectangle) 영역을 뷰포트(Viewport)라고 부른다. 다음은 이 뷰포트를 서술하는 데 쓰이는 구조체이다.

typedef struct D3D12_VIEWPORT
    {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
    } 	D3D12_VIEWPORT;

이 구조체의 처음 네 멤버 변수는 뷰포트 직사각형의 위치와 크기를 절정한다. Direct3D에서 깊이 값들은 0 이상 1이하의 구간으로 정규화된다. MinDepth 멤버와 MaxDepth 멤버는 깊이 구간

[0, 1]을 깊이 구간 [MinDepth, MaxDepth]로 변환하는 데 쓰인다. 이러한 깊이 구간 변환을 활용하면 몇 가지 특별한 효과를 구현할 수 있다. 예를 들어 MinDepth = 0, MaxDepth = 0으로 설정하면 이 뷰포트에 그려면 모든 물체는 깊이 값이 0이 되어 장면의 다른 모든 객체보다 앞에 나타나게 된다. 그렇지만 보통은 MinDepth를 0으로, MaxDepth를 1로 설정해서 깊이 값들이 바뀌지 않게 한다.

 

D3D12_VIEWPORT 구조체를 모두 채운 후에는 ID3D12CommandList::RSSetViewports 메서드를 이용해서 뷰포트를 Direct3D에 설정한다. 다음은 후면 버퍼 전체에 장면을 그리는 뷰포트를 설정하는 예이다.

D3D12_VIEWPORT mScreenViewport;

mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width    = static_cast<float>(mClientWidth);
mScreenViewport.Height   = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;

mCommandList->RSSetViewports(1, &vp);

RSSetViewports 메서드의 첫 매개변수는 설정할 뷰포트들의 개수이고, 둘째 매개변수는 뷰포트 구조체들의 배열을 가리키는 포인터이다.

 

뷰포트의 한가지 용도는 2인용 모드를 위한 화면 분할이다. 그런 경우 화면 왼쪽 절반을 위한 뷰포트와 화면 오른쪽 절반을 위한 뷰포트를 만들고, 플레이어 1의 관점에서 본 3차원 장면을 왼쪽 뷰포트에, 플레이어 2의 관점에서 본 장면은 오른쪽 뷰포트에 그리면 된다.

 

가위 직사각형 설정

가위 직사각형(Scissor rectangle)은 특정 픽셀들을 선별(Culling)하는 용도로 쓰인다. 후면 버퍼를 기준으로 가위 직사각형을 정의, 설정하면, 렌더링 시 가위 직사각형의 바깥에 있는 픽셀들은 후면 버퍼에 래스터화되지 않는다. 이러한 픽셀 선별은 일종의 최적화 기법이다. 

 

예를 들어 다른 모든 것을 가리는 직사각형 UI 요소가 화면의 특정 영역에 있다면, 그 부분에 있는 3차원 세게의 픽셀들은 처리할 필요가 없다. 

 

가위 직사각형은 D3D12_RECT라는 구조체로 서술한다. 사실 이 구조체는 구조체 RECT에 typedef를 이용해서 다른 이름을 붙인 것이다. 구조체의 RECT의 정의는 다음과 같다.

typedef struct tagRECT
{
    LONG    left;
    LONG    top;
    LONG    right;
    LONG    bottom;
} RECT

가위 직사각형을 Direct3D에 설정할 때에는 ID3D12CommandList::RSSetScissorRects라는 메서드를 사용한다. 다음은 후면 버퍼의 왼쪽 위 사분면을 덮는 가위 사각형을 설정하는 예이다.

mScissorRect = { 0, 0, mClientWidth / 2, mClientHeight / 2 };
mCommandList->RSSetScissorRects(1, &mScissorRect);

RSSetViewports와 비슷하게, 이 메서드의 첫 매개변수는 설정한 가위 직사각형들의 개수이고(고급 효과에는 여러 개의 가위 직사각형이 필요할 수 있다.), 둘째 매개변수는 직사각형 구조체들의 배열을 가리키는 포인터이다.

 

명령 리스트를 Reset하는 경우 가위 직사각형들도 재설정해줘야 한다.

 

 

아직 타이머 관련 파트가 남아 있지만 대략적인 Direct3D 초기화가 끝이 났다. 초기화를 제대로 했다면 출력 값은 다음과 같이 나올것이다.

 

반응형
Comments