24.Direct3D 기본지식
1.Direct3D의 개요
Direct3D는 3차원 그래픽 가속 기능을 이용하여 3차원 세계를 렌더링할 수 있게 하는 저수준 API(application programming interface)이다. 응용 프로그램과 그래픽 하드웨어 사이에 Direct3D라는 간접층이 존재하기 때문에 응용 프로그램 개발자는 3차원 그래픽 하드웨어가 Direct3D 11 대응 장치(그래픽 카드가 Direct3D 11를 지원하는가)이면 하드웨어의 구체적인 세부사항을 몰라도 된다.
Direct3D 11은 그래픽 장치가 반드시 Direct3D 11의 모든 능력 집합(capability set)을 지원해야 한다. 하지만 Direct3D 9은 그래픽 장치가 일부만 지원해도 사용이 가능했다. Direct3D 9의 기능을 사용하는 프로그램은 반드시 현재 하드웨어가 그 기능을 지원하는지 점검해야 했다. 그래서 그래픽 장치가 Direct3D 9의 일부만 지원해도 되었다. 하지만 Direct3D 11은 모두 지원해야 하므로 하드웨어 기능 점검을 하지 않아도 된다.
2.COM(Component Object Model)
COM(Component Object Model)은 DirectX의 프로그래밍 언어 독립성과 하위 호환성을 가지능하게 하는 기술이다. C++로 DirectX 응용 프로그램을 개발할 때 프로그래머가 알아야 할 것은 필요한 COM 인터페이스로의 포인터를 특별한 함수들 또는 다른 COM 인터페이스의 메서드를 이용해서 얻는 방법뿐이다. COM 인터페이스는 C++의 new 키워드 및delete 키워드를 사용하지 않는다. 삭제할 때 delete 키워드 대신 Release 메서드를 호출해야 한다. COM 객체들이 자신만의 고유한 방식으로 메모리를 관리하기 때문이다. 모든 COM 인터페이스는 IUnknown이라는 COM 인터페이스의 기능을 상속받고, 그 인터페이스가 Release 메서드를 제공한다.
COM 인터페이스들은 이들이 대문자 I로 시작한다. 예를들어 2차원 텍스처를 나타내는 COM 인터페이스는 ID3D11Texture2D이다.
3.텍스처 및 자료 자원 형식
텍스처(Texture)라고 하면 흔히 이미지 자료를 떠올리지만, 실제로는 여러가지 용도로 쓰인다. 예를 들어 법선 매핑이라는 기법에서는 3차원 벡터를 담는다. 텍스처에 밉맵 수준들이 존재할 수 있으며, GPU는 필터링, 다중표본화 등의 특별한 연산을 텍스처에 적용할 수 있다. 더 나아가서, 텍스처에 아무 자료나 담을 수 없다는 점이 중요하다. 텍스처에는 특정 형식(format)을 따르는 자료만 담을 수 있는데, 구체적인 형식은 DXGI_FORMAT이라는 열거형으로 지정한다.
1.DXGI_FORMAT_R32G32B32_FLOAT : 각 원소는 32bit 부동소수점 성분 세 개로 이루어 진다.
2.DXGI_FORMAT_R16G16B16A16_UNORM : 각 원소는 [0, 1] 구간으로 사상되는 16bit 성분 네 개로 이루어 진다.
3.DXGI_FORMAT_R32G32_UINT : 각 원소는 32bit 부호 없는 정수 성분 두 개로 이루어진다.
4.DXGI_FORMAT_R8G8B8A8_UNORM : 각 원소는 [0, 1] 구간으로 사상되는 8bit 부호 없는 성분 네 개로 이루어진다.
5.DXGI_FORMAT_R8G8B8A8_SNORM : 각 원소는 [-1, 1] 구간으로 사상되는 8bit 부호 있는 성분 네 개로 이루어진다.
6.DXGI_FORMAT_R8G8B8A8_SINT : 각 원소는 [-128, 127] 구간으로 사상되는 8bit 부호 있는 성분 네 개로 이루어진다.
7.DXGI_FORMAT_R8G8B8A8_UINT : 각 원소는 [0, 255] 구간으로 사상되는 8bit 부호 없는 성분 네 개로 이루어진다.
여기서 R은 red, G는 green, B는 blue, A는 alpha 성분을 뜻한다.
위에서 텍스처는 이미지(색상)만 담아야 하는 것은 아니다. 예를들어 DXGI_FORMAT_R32G32B32_FLOAT의 경우 부동소수점 좌표 성분들로 이루어진 하나의 3차원 벡터를 담을 수 있다.
또한 무형식(typeless)의 텍스처 형식도 존재하는데, 이 경우에는 메모리만 확보해 두고 자료의 구체적인 해석은 나중에 파이프라인에 묶을 때 지정한다(C++의 reinterpret_cast와 비슷). 무형식을 사용할 때 뒤에 자료형에 TYPELESS를 쓰면 된다. 예를들어 원소마다 8bit 성분 네 개를 할당하되 무형식일 때 DXGI_FORMAT_R8B8G8A8_TYPELESS를 사용한다.
4.교환 사슬(swap chain)과 페이지 전환
게임을 하다보면 그래픽 옵션에 수직 동기화가 있다. 수직 동기화는 그래픽 카드의 렌더링 갱신 속도를 모니터의 출력 갱신 속도에 맞추는 것이다. 그래픽 카드의 성능이 좋아서 그림을 다 그려서 모니터에 출력하라고 보냈는데 모니터의 주사율(흔히 60hz, 144hz라고 부르는 것)가 못 따라 간다면 그림은 계속 밀리게 될 것이다. 이럴때 수직 동기화 옵션을 사용하면 모니터의 주사율에 맞게 갱신한다.
렌더링 출력과 갱신을 동시에 한다고 생각해보자. 쉽게 말해서 그림을 그림과 동시에 전시를 한다고 생각해보자. 미쳐 다 그리지 못했는데 출력하는 일이 발생할 것이고 결과적으로 모니터가 깜빡거리는 일이 발생한다.
이를 방지하기 위해서 후면 버퍼(back buffer)와 전면 버퍼(front buffer)(*주 1)를 사용한다. 두 개의 버퍼를 사용하는 것을 이중 버퍼링(double buffering)이라고 부른다. 버퍼 세 개를 사용하면 삼중 버퍼링(triple buffering), 네 개는 (quad buffering)이라고 한다. 후면 버퍼에서는 그림을 그리고, 전면 버퍼에서는 그림을 출력한다.
작동 순서는
1.전면 버퍼가 화면에 표시되어 있는 동안 다음 프레임을 후면 버퍼가 그린다.
2.후면 버퍼가 다 그렸으면 전면 버퍼와 후면 버퍼의 역할을 바꾼다.
3.전면 버퍼의 포인터와 후면 버퍼의 포인터를 바꾼다(전면 버퍼 -> 후면 버퍼, 후면 버퍼 -> 전면 버퍼).
후면 버퍼를 전면 버퍼와 교환해서 화면에 표시되게 하는 것을 제시(presenting)라고 부른다. 버퍼들의 내용(그림)을 바꾸는 것이 아니라 포인터만 바꾼다.
전면 버퍼와 후면 버퍼는 하나의 교환 사슬(swap chain)을 형성한다. Direct3D에서 하나의 교환 사슬을 IDXGISwapChain이라는 인터페이스로 다룬다. 이 인터페이스는 전면 버퍼 텍스처와 후면 버퍼 텍스처를 담으며, 버퍼 크기 변경을 위한 메서드(IDXGISwapChain::ResizeBuffers)와 버퍼의 제시를 위한 메서드(IDXGISwapChain::Present)를 제공한다.
5.깊이 버퍼링
깊이 버퍼(depth buffer)는 각 필셀의 깊이 정보를 담는, 이미지 자료를 담지 않는 텍스처이다. 픽셀의 깊이는 0.0에서 1.0까지의 값으로, 0.0은 카메라와 가까운 픽셀이고 1.0은 최대한 먼 픽셀이다. 깊이 버퍼의 원소들과 후면 버퍼의 픽셀들은 일대일로 대응된다. 쉽게 말해서 후면 버퍼의 5번째 원소는 깊이 버퍼의 5번째 원소에 대응된다. 따라서 후면 버퍼의 해상도가 1280 × 1024라면 깊이 버퍼는 1280 × 1024개의 원소들로 구성된다.
한 오브젝트의 픽셀들이 다른 오브젝트보다 앞에 있는지 판정하기 위해 Direct3D는 깊이 버퍼링 또는 z-버퍼링 기법을 사용한다. 깊이 버퍼링에서 물체들을 그리는 순서는 중요하지 않다.
시야 창은 모니터라고 생각하고, 시점은 카메라, 사각뿔의 밑변은 최대 시야 거리이다. 시야 창의 한 픽셀 P는 구, 원뿔, 원기둥 세개가 겹치는 픽셀이다. 이때 무엇이 렌더링 되냐고 물어보면 당연히 제일 가까운 구의 픽셀 P1이 출력된다고 할 것이다. 즉, 시야 창의 한 픽셀 P에 대응되는 P1, P2, P3이 경쟁한다. 응용 프로그램은 렌더링이 수행하기 전에 먼저 후면 버퍼를 기본 색상(흰or검)으로 지운다. 이때 깊이 버퍼도 기본값(일반적으로 1.0)으로 지운다. 이 상황에서 원기둥, 구 , 원뿔의 순서로 렌더링한다고 해보자.
위 표의 요점은 픽셀과 해당 깊이 값은 그 깊이 값이 깊이 버퍼에 이미 들어 있는 값보다 작은 경우에만 후면 버퍼와 깊이 버퍼에 기록된다.
깊이 버퍼도 하나의 텍스처이므로 특정한 자료 형식을 지정해서 생성해야 한다.
1.DXGI_FORMAT_D32_FLOAT_S8X24_UINT : 각 텍셀(*주 2)은 32bit 부동소수점 깊이 값과 [0, 255] 구간으로 사상되는 8bit 부호 없는 정수 스텐실 값, 그리고 다른 용도 없이 채움(padding) 용으로 쓰이는 24bit로 구성된다.
2.DXGI_FORMAT_D32_FLOAT : 각 텍셀은 하나의 32bit 부동소수점 깊이 값이다.
3.DXGI_FORMAT_D24_UNORM_S8_UINT : 각 텍셀은 [0, 1] 구간으로 사상되는 부호 없는 24bit 깊이 값 하나와 [0, 255] 구간으로 사상되는 8bit 부호 없는 정수 스텐실 값으로 구성된다.
4.DXGI_FORMAT_D16_UNORM : 각 텍셀은 [0, 1] 구간으로 사상되는 부호 없는 16bit 깊이 값이다.
응용 프로그램이 스텐실 버퍼를 반드시 사용해야 하는것은 아니다. 만일 사용한다면 반드시 스텐실 버퍼를 깊이 버퍼에 부착해야 한다. 예를 들어 32bit중 24bit를 깊이 버퍼에, 나머지 8bit를 스텐실 버퍼에 사용한다면 DXGI_FORMAT_D24_UNORM_S8_UINT를 사용한다.
6.텍스처 자원 뷰
렌더링 파이프라인에는 텍스처를 묶을(bind) 수 있는 단계(stage)들이 있다. 주로 텍스처를 렌더 대상으로 묶는 것(Direct3D가 텍스처에 렌더링하는 경우)과 셰이더 자원으로서 묶을 때(셰이더 안에서 텍스처를 추출하는 경우) 사용한다. 이 두가지 용도의 텍스처를 생성할 때는 텍스처를 묶을 두 파이프라인 단계를 지정한 결속 플래그(bind flag)로D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE를 사용한다.
어떤 용도이든 Direct3D에서 텍스처를 사용하려면 텍스처의 초기화 시점에서 그 텍스처의 자원 뷰(resource view)를 생성해야 한다. 텍스처를 렌더 대상과 셰이더 자원으로 사용하는 경우에는 두 개의 뷰를 생성할 필요가 있다.
하나는 렌더 대상 뷰(ID3D11RenderTargetView)이고, 다른 하나는 셰이더 자원 뷰(ID3D11ShaderResourceView)이다. 자원 뷰는 기본적으로 두 가지 일을 한다. 하나는 Direct3D에게 자원의 사용 방식(파이프라인의 어떤 단계에서 묶을 것인지)을 알려주고, 또 하나는 생성 시점에서 무형식을 지정한 자원 형식의 구체적인 형식을 결정하는 것이다. 무형식 자원의 경우 텍스처 원소를 한 파이프라인 단계에서는 부동소수점 값으로서 사용하고 다른 단계에서는 정수로 사용하는 것이 가능함을 뜻한다.
어떤 자원에 대해 특정 뷰를 생성할 수 있으려면 그 자원을 생성할 때 해당 결속 플래그를 지정해야 한다. 예를 들어 D3D11_BIND_DEPTH_STENCIL 플래그를 지정하지 않고 생성한 자원에 대해서는 ID3D11DepthStencilView를 생성할 수 없다. 무작정 생성을 시도하면 디버그 오류 메시지가 나온다.
7.다중표본화의 이론
모니터의 픽셀들은 사각형으로 되어있기에 선을 완벽하게 나타내는 것은 불가능하다. 계단 현상이라고도 하는 앨리어싱(aliasing)이 예이다.
앨리어싱을 제거하는 기법으로 초과표본화(supersampling)이 있다. 초과표본화에서는 후면 버퍼와 깊이 버퍼를 해상도보다 4배(가로, 세로 두 배씩) 크게 잡고, 3차원 장면을 4배 크기의 해상도에서 후면 버퍼에 렌더링하고, 이미지를 화면에 제시할 때(전면 버퍼와 후면 버퍼 스왑) 후면 버퍼를 원래 크기의 버퍼로 환원(resolving)한다. 하향표준화(downsampling)이라고도 부르는 이 환원 공정은 4픽셀 블록의 네 생상의 평균을 최종 색상으로 사용한다. 효과 넣을 때 블러효과 생각하면 된다.
초과표본화는 픽셀 처리량과 메모리 소비량이 네 배라서 비용이 크다. Direct3D는 다중표본화(multisampling)라는 절충적인 앨리어싱 제거 기법을 지원한다. 다중표본화는 일부 계산 결과를 부분픽셀들 사이에서 공유하기 때문에 초과표본화보다 비용이 적다. 4배 다중표본화(픽셀당 부분픽셀 4개)의 경우 다중표본화도 화면 해상도의 4배인 후면 버퍼와 깊이 버퍼를 사용한다. 하지만, 다중표본화는 이미지 색상을 각 부분픽셀마다 계산하는 것이 아니라 픽셀당 한 번만 계산하고(픽셀의 중심에서), 그 색상과 부분픽셀들의 가시성과 포괄도를 이용해서 최종 색상을 결정한다.
위 그림에서 a는 픽셀 중심에서 평가된
8.Direct3D의 다중표본화
다중표본화를 위해서는 DXGI_SAMPLE_DESC라는 구조체를 채워야 한다.
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC, *LPDXGI_SAMPLE_DESC;
Count 멤버는 픽셀당 추출할 표본의 개수, Qaulity 멤버는 원하는 품질 수준이다. 품질의 수준들의 범위는 텍스처 형식과 픽셀당 표본 개수에 의존한다. 주어진 텍스처 형식과 표본 개수의 조합에 대한 품질 수준들의 개수를 알고 싶을 때 다음과 같은 메서드를 사용한다.
주어진 텍스처 형식과 표본 개수의 조합을 장치가 지원하지 않으면 0을 반환한다. 그 외의 경우에는 주어진 조합에 대한 품질 수준 개수가 pNumQualityLevels 매개변수가 가리키는 곳에 저장된다. 하나의 텍스처 형식 표본 개수 조합에 대한 유효한 품질 수준의 범위는 [0, *pNumQualityLevels - 1]이다.
9.기능 수준
Direct3D 11에는 기능 수준(feature level)이라는 개념이 있다. d3dcommon.h에 있다.
typedef enum D3D_FEATURE_LEVEL
{
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_1 = 0xa000,
D3D_FEATURE_LEVEL_10_2 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000,
} D3D_FREATURE_LEVEL;
D3D_FEATURE_LEVEL이라는 열겨형으로 DirectX9에서 11까지 여러 Direct3D 버전들에 대응된다. 여기서 핵심은 사용자의 하드웨어가 특정 기능 수준을 지원하지 않는다면 그보다 더 낮은 기능 수준으로 내려가면서 지원하는지 하나씩 점검한다. 예를 들어 먼저 Direct3D 11 지원 여부를 점검하고, 지원하지 않는다면 Direct3D 10.1 지원 여부를 점검하고 Direct3D 10 지원 여부를 점검하는 식이다.
배열을 Direct3D 초기화 함수에서 사용해서 점건 순서를 지정할 수 있다.
D3D_FEATURE_LEVEL featureLevels[4] =
{
D3D_FEATURE_LEVEL_11_0, //D3D 11 지원 여부 점검
D3D_FEATURE_LEVEL_10_1, //D3D 10.1 지원 여부 점검
D3D_FEATURE_LEVEL_10_0, //D3D 10 지원 여부 점검
D3D_FEATURE_LEVEL_9_3, //D3D 9.3 지원 여부 점검
};
(*주 1) 전면 버퍼(front buffer) vs 후면 버퍼(back buffer)
전면 버퍼 : 색상 버퍼만을 사용한다. 프로그래밍이 불가능하다.
후면 버퍼 : 색상 버퍼, 깊이 버퍼, 스텐실 버퍼 세 가지로 분류된다. 프로그래밍이 가능하다.
(*주 2) 텍셀(texel)
텍스처의 화소이다. 프레임버퍼에 있는 1화소는 픽셀이며 이미지에 있는 1화소가 텍셀이다.