KoreanFoodie's Study

4-1. Direct3D 기초 : COM, 텍스쳐 형식, 교환사슬과 페이지 전환, 깊이 버퍼링, 다중 표본화, DXGI, 상주성 본문

Game Dev/DirectX

4-1. Direct3D 기초 : COM, 텍스쳐 형식, 교환사슬과 페이지 전환, 깊이 버퍼링, 다중 표본화, DXGI, 상주성

GoldGiver 2021. 11. 16. 11:40

DirectX 12 3D 게임 프로그래밍의 바이블

'DirectX 12를 이용한 3D 게임 프로그래밍 입문'을 읽으며 내용을 정리하고 중요한 부분을 기록하는 글입니다.


4-1. Direct3D 기초 : COM, 텍스쳐 형식, 교환사슬과 페이지 전환, 깊이 버퍼링, 다중 표본화, DXGI, 상주성 

 

알아 두어야 할 개념들 : 

 

COM(Component Object Model)

COM은  DirectX의 프로그래밍 언어 독립성과 하위 호환성을 가능하게 하는 기술이다. COM 객체를 C++ 클래스로 간주하고 사용해도 무방하다. 프로그래머는 COM 인터페이스의 함수들과 메서드를 이용하기만 한다. (new로 생성할 일이 없다는 뜻이다.)

또한 delete로 메모리를 해제하는 것이 아니라 release를 통해 더 이상 해당 객체를 사용하지 않음을 명시해 주어야 한다. 참조되지 않는 COM 객체는 자동으로 메모리에서 해제된다.

COM 객체의 수명 관리를 돕게 위해, Windows 런타임 라이브러리(Windows Runtime Library, WRL)은 Microsoft::WRL::ComPtr 이라는 클래스 (#include <wrl.h>) 를 제공한다. ComPtr(COM 객체를 위한 smart pointer)의 메서드 중 세 메서드를 자주 사용할 것이다.

1. Get : 바탕 COM 인터페이스를 가리키는 포인터를 돌려준다.

2. GetAddressOf: 바탕 COM 인터페이스를 가리키는 포인터의 주소를 돌려준다.

3. Reset : ComPtr 인스턴스를 nullptr로 설정하고 참조 횟수를 1 감소시킨다. 이 메서드를 사용하는 대신 직접 nullptr 를 배정해도 된다.

 

 

텍스처 형식

2차원 텍스처는 자료 원소들의 행렬(2차원 배열)이다. 2차원 텍스처의 용도 중 하나는 2차원 이미지의 자료를 저장하는 것인데, 이때 텍스처의 각 원소는 픽셀 하나의 색상을 담는다.

하지만 각 원소가 항상 픽셀 정보만 담는 것은 아니다. 텍스처에 밉맵 수준이 있거나, 벡터를 담기도 한다. 텍스처에 담을 수 있는 자료 원소 형식의 예를 보자.

1. DXGI_FOPRMAT_R32G32B32_FLOAT : 각 원소는 32비트 부동소수점 성분 3 개로 이루어짐

2. DXGI_FORMAT_R8G8A8_UINT : 각 원소는 [0, 255] 구간으로 사상되는 부호 없는 8비트 정수 성분 네 개로 이루어진다.

A는 alpha로, 일반적으로 투명도를 제어하는 것에 쓰인다. 하지만 1번처럼 3차원 벡터를 담기도 한다.

무형식(typeless) 텍스처들도 있는데, 이런 텍스처는 일단 메모리만 확보해 두고 자료의 구체적인 해석 방식은 나중에 텍스처를 파이프라인에 묶을 때 지정하는 (C++의 reinterpret_cast와 비슷하게) 용도로 쓰인다. 예를 들어 다음의 형식은 원소마다 16비트 성분 네 개를 지정하되, 각 16비트 성분의 구체적인 자료 형식 (int, float, uint 등)은 지정하지 않는다.

DXGI_FORMAT_R16G16B16A16_TYPELESS

 

 

교환 사슬과 페이지 전환

애니메이션이 껌뻑이는 현상을 피하려면 애니메이션 한 프레임 전체를 화면 바깥의(off-screen) 텍스처를 그린다. 그런 텍스처를 후면 버퍼(back buffer)라고 부른다. 주어진 한 프레임을 위해 장면 전체를 후면 버퍼에 그린 다음에는, 그 후면 버퍼를 하나의 완전한 프레임으로서 화면에 표시한다. 이렇게 하면 화면이 그려지는 과정이 표시되지 않는다. 이러한 기법을 이중 버퍼링이라고 부른다. 전면 버퍼(front buffer)와 후면 버퍼를 이용한다. (버퍼 3개로 삼중 버퍼링을 쓰는 경우도 있다.)

먼저, 전면 버퍼에 화면을 그리고, 그 동안 후면 버퍼에 다음 그림을 그린다. 다음 프레임에서 후면 버퍼를 전면 버퍼가 되게 만들고, 전면 버퍼가 새 후면 버퍼가 된다. 이러한 페이지 전환 과정을 제시(presenting)이라고 하는데, 버퍼의 내용을 바꾸는 것이 아니라 포인터만 교환해주면 된다.

전면 버퍼와 후면 버퍼는 하나의 교환 사슬(swap chain)을 형성한다. Direct3D에서 교환 사슬을 대표하는 인터페이스는 IDXGISwapChain이다. 이 인터페이스는 전면 버퍼 텍스처와 후면 버퍼 텍스처를 담으며, 버퍼 크기 변경을 위한 메서드(IDXGISwapChain::ResizeBuffers)와 제시를 위한 메서드(IDXGISwapChain::ResizeBuffers)도 제공한다.

 

 

깊이 버퍼링

깊이 버퍼(depth bufffer)는 이미지 자료를 담지 않는 텍스처의 한 예이다. 깊이 버퍼는 각 픽셀의 깊이 정보를 담는다. 0.0에서 1.0까지의 값으로, 0.0은 시야 절두체(view frustum) 안에서 관찰자에 최대한 가까운 물체에 해당하고 1.0은 시야 절두체 안에서 관찰자와 최대한 먼 물체에 해당한다. 깊이 버퍼의 원소들과 후면 버퍼의 픽셀들은 일대일로 대응된다.

한 물체의 픽셀들이 다른 물체보다 앞에 있는지 판정하기 위해, Direct3D는 깊이 버퍼링 또는 z-버퍼링이라는 기법을 이용한다. 여기서 중요한 점은, 깊이 버퍼링을 이용하면 물체들을 그리는 순서와 무관하게 물체들이 제대로 가려진다는 것이다.

깊이 값이 작은 녀석이 실제로 후면 버퍼와 깊이 버퍼에 기록되고 후에 렌더링된다!

자료형 예시 : DXGI_FORMAT_D32_FLOAT_S8X24_UINT : 각 텍셀은 32비트 float 깊이값과 8비트 정수 스텐실 값(스텐실 버퍼에 쓰임) 그리고 다른 다른 용도 없이 padding 용으로만 쓰이는 24비트로 구성. 스텐실 버퍼는 후에 설명하도록 하겠다.

 

 

자원과 서술자

실제로 무엇인가를 화면에 출력하기 위해서는 GPU 자원을 파이프라인에 "연결(link)"하거나 "바인딩(binding)"하는 작업이 필요하다. GPU 자원은 범용적인 메모리 조각이며, 스스로 어떻게 사용될지 결정하지 않는다. 따라서 서술자(descriptor) 객체를 이용해서 이런 자원을 렌더링 파이프라인에서 어떻게 사용할지를 명시한다. 뷰(view)는 서술자(descriptor)와 같은 의미로 쓰이기도 한다. 서술자는 다음과 같은 종류들이 있다.

 

1. CBV/SRV/UAV 서술자들은 각각 상수 버퍼(constant buffer), 셰이더 자원(shader resource), 순서 없는 접근(unordered access view)를 서술한다.

2. 표본추출기 서술자는 텍스처 적용에 쓰이는 표본추출기 (sampler) 자원을 서술한다.

3. RTV (Render Target)

4. DSV (Depth/Stencil)

 

서술자 힙(descriptor heap)은 서술자들의 배열이며, 하나의 자원을 참조하는 서술자가 하나뿐이여야 하는 것은 아니다. 다만 서술자들은 유효성 검증 등을 위해 응용 프로그램의 초기화 시점에서 생성해야 한다.

 

 

다중표본화의 이론

모니터 화면에 선을 그린다고 하자. 픽셀이 무한히 작지 않기 때문에, 완전하게 매끄러운 선을 그린다는 것은 불가능하다. 이는 계단 현상이라고 하는 앨리어싱(aliasing) 효과를 보여준다. 이를 해결하기 위해 안티 앨리어싱(antialiasing) 기술을 이용하는데, 대표적으로 초과표본화(supersampling)과 다중표본화(multisampling)이 있다.

앨리어싱

 

초과표본화 : 초과표본화에서는 버퍼를 화면 해상도의 4배씩 잡고, 한 픽셀을 부분 픽셀로 나누어 계산한 후, 화면에 presenting 하는 과정에서 후면 버퍼를 원래 크기의 버퍼로 환원(resolving)한다. 이를 하향표본화(Downsampling)이라고 한다. 이는 소프트웨어 단에서의 처리량이 크다.

 

다중표본화 : Direct3D에서의 다중표본화는, 먼저 해상도를 4배씩 잡은 후, 그냥 픽셀당 이미지 색상을 주변 픽셀들의 평균으로 한 번만 계산한다. 그 색상과 부분픽셀들의 가시성, 포괄도를 반영한다. OpenGL에서는 supersampling과 동일하되, edge부분만 계산하는 방식으로 작동한다. OpenGL 위키 링크를 참고해 읽어보자.

 

다중표본화를 위해서는 DXGI_SAMPLE_DESC 라는 구조체를 적절히 채워야 한다.

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;
    UINT Quality;
} DXGI_SAMPLE_DESC;

Count 멤버는 픽셀당 추출할 표본의 개수를 지정하고, Quality 멤버는 원하는 품질 수준(quality level)을 지정한다. 실 사용 예를 알아보자.

// CheckFeatureSupport의 두번째 인자에 들어갈 구조체 
// (Direct3D에서 이미 정의되어 있음) 
typedef struct D3D12_FEATURE_DATA_MULTISAMPLING_QUALITY_LEVELS 
{ DXGI_FORMAT Format; 
UINT SampleCount; 
D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG Flags; 
UINT NumQualityLevels; 
} D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS; 

// 사용법 
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS d3dQualityLevels; 

// 텍스처 형식을 지정한다. 
d3dQualityLevels.Format = DXGI_FORMAT_R8G8B8A8_UNORM; 

// 표본화 갯수를 지정한다. 
d3dQualityLevels.SampleCount = 4; 

// 추가 기능을 지정한다. (대부분은 NONE) 
d3dQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;

// 값을 0으로 초기화 한다. 
d3dQualityLevels.NumQualityLevels = 0; 

// CheckFeatureSupport 메서드를 사용하여 품질 수준들을 받아온다. 
pd3dDevice->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, &d3dQualityLevels, sizeof(D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS));

 

 

기능 수준 (feature level)

기능 수준(feature level)이라는 개념은 Direct3D 11에서 도입된 것으로, 코드에서는 D3D_FEATURE_LEVEL이라는 열거형으로 대표된다. 기능 수준들은 대략 버전 9에서 11까지의 여러 Direct3D 버전들에 대응된다.

typedef enum D3D_FEATURE_LEVEL {
  D3D_FEATURE_LEVEL_1_0_CORE,
  D3D_FEATURE_LEVEL_9_1,
  D3D_FEATURE_LEVEL_9_2,
  D3D_FEATURE_LEVEL_9_3,
  D3D_FEATURE_LEVEL_10_0,
  D3D_FEATURE_LEVEL_10_1,
  D3D_FEATURE_LEVEL_11_0,
  D3D_FEATURE_LEVEL_11_1,
  D3D_FEATURE_LEVEL_12_0,
  D3D_FEATURE_LEVEL_12_1,
  D3D_FEATURE_LEVEL_12_2
} ;

 

 

DXGI(DirectX Graphics Infrasructure)

DXGI는 Direct3D와 함께 쓰이는 API이다. 전체 화면 모드 전환, 디스플레이 어댑터나 모니터, 지원되는 디스플레이 모드(해상도, 갱신율 등) 같은 그래픽 시스템 정보의 열거 기능 등은 DXGI가 제공한다. 또한, 지원되는 표현 형식들도 DXGI에 정의되어 있다(DXGI_FORMAT).

 

DXGI의 핵심 인터페이스 중 하나로 IDXGIFactory 인터페이스가 있다. 이 인터페이스는 IDXGISwapChain 인터페이스 생성과 디스플레이 어댑터 열거에 쓰인다. 디스플레이 어탭터(display adapter)는 그래픽 기능성을 구현한다. 이는 일반적으로 물리적인 하드웨어 장치(e.g. 그래픽 카드)이지만, 하트웨어를 흉내내는 소프트웨어 디스플레이 어댑터도 존재한다.

typedef struct DXGI_MODE_DESC {
  UINT                     Width;
  UINT                     Height;
  DXGI_RATIONAL            RefreshRate;
  DXGI_FORMAT              Format;
  DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
  DXGI_MODE_SCALING        Scaling;
} DXGI_MODE_DESC;

DXGI_MODE_DESC는 디스플레이 모드를 서술한다.

추가적으로 "DXGI Overview"와 "DirectX Graphics Infrasturcture: Best Practices", "DXGI 1.4 Improvements"를 읽어보자.

 

 

기능 지원 점검

ID3D12Device::CheckFeatureSupport 메서드를 통해 현재 그래픽 드라이버의 다중 표준화 지원 여부 등 다양한 것들을 확인할 수 있다.

HRESULT ID3D12Device::CheckFeatureSupport(
D3D12_FEATURE Feature,
void *pFeatureSupportData,
UINT FeatureSupportDataSize);

Feature는 지원 여부를 점검할 기능들의 종류를, pFeatureSupportData는 기능 지원 정보가 설정될 구조체를, FeatureSupportDataSize는 매개 변수로 전달할 구조체의 크기를 나타낸다. 갇단한 예시를 보자.

	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(md3dDevice->CheckFeatureSupport(
		D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
		&msQualityLevels,
		sizeof(msQualityLevels)));

 

 

상주성

게임의 경우 매우 많은 리소스를 렌더링할 필요가 있다. 하지만 사용자가 볼 수 없는 물체를 미리 렌더링하는 것은 자원의 낭비일 수 있다. 이를 위해 사용하지 않는 자원들을 GPU 메모리에서 내리는 것을 관리하는데, 이를 상주성이라고 한다.

다음 메서드를 이용해 상주성을 제어할 수도 있다. MakeResident는 입주, Evict는 퇴거를 의미한다. 첫 변수는 자원들의 갯수, 두번째는 자원들의 배열이다.

HRESULT ID3D12Device::MakeResident(
UINT			NumObjects,
ID3D12Pageable *const *ppOjbects
);

HRESULT ID3D12Device::Evict(
UINT			NumObjects,
ID3D12Pageable *const *ppOjbects
);

추가적인 내용은 MSDN의 "Residency" 항목을 참고하자.

 

 
Comments