본문 바로가기
게임 공부/DirectX

[DirectX] Direct3D 11 programming (1)

by woohyeon 2021. 3. 5.
반응형

Direct3D

MS 문서를 학습하며 작성하는 내용입니다. (틀린 부분이 있을 수 있음)

Direct3D3차원 그래픽을 다루기 위한 DirectX의 API이다. Direct3D엔 디바이스(Device)라는 개념이 존재한다. 디바이스는 그래픽 카드를 추상화한 객체이다. 쉽게 말하면 그래픽 카드를 다룰 수 있도록 해주는 기본적이고 핵심적인 객체이다. 디바이스는 오브젝트의 할당 및 해제, 렌더링, 그래픽 드라이버 및 하드웨어와의 통신을 담당한다.

Direct3D 11 이전엔 디바이스를 의미하는 오브젝트(클래스)가 하나였던 걸로 기억한다. Direct3D 11은 디바이스라는 개념이 2가지로 분리된다. 하나는 디바이스 오브젝트(Device Object)이고, 다른 하나는 디바이스 컨텍스트 오브젝트(Device Context Object)이다. 디바이스 오브젝트는 주로 초기화 및 릴리즈 단계에서 자원과 같이 무언가를 만들고 지우는 역할이며, 디바이스 컨텍스트 오브젝트는 렌더링과 같은 실질적인 어떤 행위들을 수행한다.

이러한 디바이스의 분리는 멀티 스레딩 활용에 용이하도록 변경된 디자인이라고 한다. 디바이스 컨텍스트는 1개 또는 그 이상 생성될 수 있다. 아마 디바이스 자체는 대부분 하나만 가지고, (필요하다면) 컨텍스트를 여러개 생성하여 흐름 별로 렌더링 등을 제어하는 것 같다.

Swap Chain

스왑 체인(Swap chain)이란 백 버퍼를 관리하고 렌더링 관련하여 컨트롤할 수 있도록 도와주는 도구이다. 스왑 체인은 내부적으로 2개 이상의 버퍼를 캡슐화한다. 이 버퍼들은 장면을 그리기 위한 공간이기도 하고, 화면에 보여질 버퍼이기도 하다. 기본적으로 프론트 버퍼(Front buffer)백 버퍼(Back buffer) 2개의 버퍼로 구성되며, 기본적으로 더블 버퍼링의 개념이 베이스다. 프론트 버퍼는 이름 그대로 전면에 있는 버퍼로, 화면에 보여질 버퍼이다. 백 버퍼는 뒤에 있는 버퍼로, 보이지 않는 곳에서 미리 장면을 그려 넣을 공간을 의미한다. 다음에 보여줄 장면이 백 버퍼에 모두 그려지면, 두 버퍼를 스왑(present)하여 다음 장면을 화면에 표시하게 된다.

* 참고 사항 
백버퍼의 데이터를 프론트 버퍼에 전송하는 방법은 `플립(Flip)`과 `블릿(blit)`이 있는데, 플립은 두 버퍼에 대해 포인터를 교환하는 것이고 빠르다. 블릿은 memcpy이다. 하지만 포인터는 포인터가 가리키는 주소 전체가 대상이기에 제한적이다. 예를 들어 윈도우 모드는 윈도우 밖의 화면도 고려해야 하기 때문에 플립을 사용할 수 없다. 풀스크린 모드에선 플립이 가능했지만 현재는 다양한 환경(?)을 고려해야 해서 사용되지 않는다고 한다. 결국 내부적으론 다 블릿이라는 의미이다.

스왑 체인에서 front buffer는 video memory에 위치하고, 윈도우 핸들을 출력할 화면의 대상으로서 등록하면 present 시 front buffer에 존재하는 데이터가 해당 윈도우로 출력된다. 백 버퍼에 그려진 이미지를 스크린에 보이도록 하는 작업을 D3D에선 present라 한다.

Device 및 Swap Chain 생성

디바이스 및 스왑 체인 생성은 Direct3D 프로그래밍의 고정적인 초기의 셋업 단계이다.

디바이스 객체는 다음과 같이 두 가지 타입으로 나뉜다.

  • ID3D11Device
  • ID3D11DeviceContext

스왑 체인은 다음과 같은 타입을 가진다.

  • IDXGISwapChain

디바이스와 스왑 체인을 따로 생성할 수도 있지만, 한번에 생성할 수 있는 헬퍼 함수가 존재한다. 한번에 생성하는 것이 간편하므로 여기선 한번에 생성하는 헬퍼 함수를 사용한다.

다음 함수는 디바이스와 스왑 체인을 생성해주는 함수이다.

HRESULT D3D11CreateDeviceAndSwapChain(
  IDXGIAdapter               *pAdapter,
  D3D_DRIVER_TYPE            DriverType,
  HMODULE                    Software,
  UINT                       Flags,
  const D3D_FEATURE_LEVEL    *pFeatureLevels,
  UINT                       FeatureLevels,
  UINT                       SDKVersion,
  const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc,
  IDXGISwapChain             **ppSwapChain,
  ID3D11Device               **ppDevice,
  D3D_FEATURE_LEVEL          *pFeatureLevel,
  ID3D11DeviceContext        **ppImmediateContext
);

파라미터가 꽤 많다. 간단하게 살펴보면, 우선 디바이스 타입인 ppDeviceppImmediateContext, 스왑 체인 타입인 pSwapChainDesc을 인자로 받는 것을 확인할 수 있다. 나머지 파라미터는 디바이스와 스왑 체인 생성을 위한 데이터이다. 이러한 입력 데이터를 받아 디바이스와 스왑 체인에 Output을 저장해준다.

DriverType은 해당 디바이스가 어떠한 드라이버 타입을 사용할 지 나타낸다. 하드웨어(GPU)의 지원을 100% 받을 것인지(최고의 퍼포먼스), 하드웨어와 소프트 웨어를 섞을 것인지(정확성과 성능의 밸런스), 소프트 웨어만을 사용할 것인지(느림) 를 결정한다. 주로 GPU를 100%로 사용하는 것이 좋으며 D3D_DRIVER_TYPE_HARDWARE 라는 enum 값을 가진다. 이는 GPU에 따라 지원되는 값들이 다를 수 있다.

pSwapChainDesc은 스왑 체인에 대한 디스크립터로 스왑 체인을 초기화하기 위한 용도로 사용된다.

나머지 궁금한 파라미터는 MS 문서에서..

스왑 체인은 다음과 같은 렌더 특성들을 가진다.

  • 렌더 영역에 대한 사이즈
  • 어떤 비율(기준)으로 화면을 갱신할 것인지
  • 디스플레이 모드
  • 표면(백 버퍼)에 대한 정보

따라서 스왑 체인에 대한 디스크립터 타입인 DXGI_SWAP_CHAIN_DESC 타입의 변수에 위와 같은 데이터들을 셋팅해주고 Create 함수의 인자로 전달해주면 된다. 해당 타입은 다음과 같은 데이터 멤버들을 가진다.

typedef struct DXGI_SWAP_CHAIN_DESC {
  DXGI_MODE_DESC   BufferDesc;
  DXGI_SAMPLE_DESC SampleDesc;
  DXGI_USAGE       BufferUsage;
  UINT             BufferCount;
  HWND             OutputWindow;
  BOOL             Windowed;
  DXGI_SWAP_EFFECT SwapEffect;
  UINT             Flags;
} DXGI_SWAP_CHAIN_DESC;

일부분만 살펴보면 BufferDesc는 백 버퍼에 대한 디스크립터이며, BufferCount는 사용할 백 버퍼의 개수이다. 보여줄 화면(front buffer)은 1개로 고정일테니.. 백 버퍼의 개수만 받는 것 같다.

BufferUsage는 백 버퍼에 대한 용도를 지정한다. 다양한 옵션이 있으며 장면을 그려넣을 용도일 경우 DXGI_USAGE_RENDER_TARGET_OUTPUT를 넣어준다.

OutputWindow는 output 결과를 출력할 윈도우에 대한 핸들을 의미한다.

Render target view 생성

렌더 타겟 뷰는 리소스 뷰(resource view) 중 하나로, 출력할 대상이라는 일종의 자원을 의미한다. 출력할 대상인 렌더 타겟 뷰는 보통 2D 텍스쳐이다. 이러한 자원들은 파이프라인에서 Input으로 필요한데, 2D Raw 텍스쳐와 같은 자원을 직접적으로 넣지 않고 렌더 타겟 뷰와 같은 리소스 뷰를 통해서 파이프라인에 바인딩하도록 설계되어 있다.

이것이 렌더 타겟 뷰가 필요한 이유이다. 또한 렌더 타겟 뷰를 텍스쳐 자체라고 생각하지말고, 텍스쳐를 wrapping 한 타입이라고 생각하는 게 이해하기 편하다. (실제로 그런 것 같고..) 따라서 렌더 타겟 뷰에 underlying 버퍼인 백 버퍼를 바인딩해주어야 한다. 쉽게 말하면 STL vector의 내부에 실제로 데이터가 저장되는 (underyling) array가 존재하듯이, 실제 데이터를 write할 대상인 백 버퍼를 렌더 타겟 뷰에 바인딩해주어야 한다. 2D 텍스쳐가 곧 2차원 배열이고 이 용도로 사용되는 것이 백 버퍼이므로.. 즉 파이프 라인에서 렌더 타겟 뷰를 통해 텍스쳐가 write된 백 버퍼에 접근할 것이다.

렌더 타겟 뷰는 디바이스의 CreateRenderTargetView라는 함수를 통해 생성하며, 렌더 타겟 뷰의 타입은 ID3D11RenderTargetView이다. 렌더 타겟 뷰를 생성하기 전에 백 버퍼를 얻어야 하는데, 백 버퍼는 미리 생성한 스왑 체인에서 GetBuffer를 통해 얻을 수 있다. 렌더 타겟 뷰를 생성했으면 Output merger에 바인딩해야한다. 이는 디바이스 컨텍스트의 OMSetRenderTargets 함수를 통해 수행한다. 아마 이 부분이 파이프라인에 바인딩해주는 부분이 아닌가 싶다.(나중에 정확히 알게 되면 수정 필요..)

다음은 위 과정들을 나타낸 코드이다.

ID3D11Texture2D* pBackBuffer;
// Get a pointer to the back buffer
hr = g_pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&pBackBuffer );


// Create a render-target view
g_pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &g_pRenderTargetView );


// Bind the view
g_pImmediateContext->OMSetRenderTargets( 1, &g_pRenderTargetView, NULL );

OMSetRenderTargets 함수의 첫 번째 인자는 사용할 렌더 타겟의 개수이다. 최대 8개 사용 가능하다. 세 번째 인자는 깊이 버퍼인데, 겹치는 이미지에서 어떤 이미지를 더 우선적으로 나타낼지와 같은 결정한다. 사용하지 않는다면 NULL을 준다.

그 다음은 뷰포트를 설정해주어야 하는데 여기부턴 다음 포스팅에..




댓글