Direct3D의 초기화 작업에 필요한 기본 형식 및 몇 가지 기본적인 그래픽 개념을 다뤄보겠다.
Direct3D의 개요
Direct3D는 그래픽 하드웨어를 제어할 수 있는 소프트웨어 인터페이스를 제공한다.
Graphic Hardware가 Direct3D capable device이기만 하다면 Hardware의 구체적인 세부사항을 걱정할 필요가 없다.
물론 다중 표본화 횟수 등 Hardware마다 다를 수 있는 항목들도 존재한다.
그래도 Direct3D capable device는 반드시 Direct3D capability set 전체를 지원해야 한다.
이는 Direct3D 9와 비교해서 가장 두드러지는 특징이다
COM(Component Object Model)
COM은 DirectX의 프로그래밍 언어 독립성과 하위 호환성을 가능하게 하는 기술이다.
C++로 DirectX Application을 개발 할 때에 세부 사항 대부분이 드러나지 않는다.
프로그래머가 알아야 할 것은 COM Interface로의 포인터를 얻는 방법 뿐이다.
COM Interface는 C++의 new 키워드로 생성하지 않는다.
또한 삭제 할 때도 delete가 아니라 Interface의 Release 메서드를 호출한다.
이는 COM Object들이 자신만의 고유한 방식으로 메모리를 관리하기 때문이다.
COM에 관한 이야기는 이보다 훨씬 많지만, DirectX를 사용하는데 이 이상은 불필요하다.
마지막으로 COM Interface들은 Class 이름이 I로 시작한다.
예를 들어 DirectX11의 2차원 Texture를 나타내는 COM Interface는 ID3D11Texture2D이다.
텍스처 및 자료 자원 형식
2차원 Texture는 2차원 배열로 원소 당 2차원 이미지 한 Pixel의 색상을 담는다.
하지만 이것이 2차원 Texture의 유일한 용도는 아니다.
예를 들어, 법선 매핑에서는 Texture의 각 원소가 색상이 아니라 3차원 Vector를 담는다.
Texture라고 하면 이미지 자료 저장을 떠올리지만, 실제로는 그보다 훨씬 범용적이다.
Texture에는 밉맵 수준들이 존재할 수도 있다.
GPU Filtering, 다중 표본화 등의 특별한 연산도 적용할 수 있따.
더 나아가 아무 자료나 아무 자료나 담을 수 있는 것은 아니라는 점도 중요하다.
Texture는 DXGI_FORMAT이라는 Enumeration으로 지정된 format을 따르는 자료만 담을 수 있다.
https://docs.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format
DXGI_FORMAT (dxgiformat.h) - Win32 apps
Resource data formats, including fully-typed and typeless formats. A list of modifiers at the bottom of the page more fully describes each format type.
docs.microsoft.com
몇가지만 예시를 들어보겠다.
DXGI_FORMAT_R32G32B32_FLOAT
각 원소는 32비트 float 3개로 이루어진다.
DXGI_FORMAT_R16G16B16_UNORM
각 원소는 [0, 1] 구가능로 사상되는 16비트 성분 4개로 이루어진다.
DXGI_FORMAT_R8G8B8A8_SINT
각 원소는 [-128, 127] 구간으로 사상되는 8비트 signed int 성분 4개로 이루어진다.
여기서 R, G, B, A는 빛의 삼원색과 알파(Alpha, 투명도) 성분을 뜻한다.
앞서 얘기 했듯이, RGBA라 하여 반드시 Texture에 색상을 담아야 하는 것은 아니다.
예를 들어 DXGI_FORMAT_R32G32B32_FLOAT에는 float로 이루어진 3차원 Vector를 담을 수 있다.
또한 typeless한 Texture도 존재한다.
이는 메모리만 확보해두고 자료의 구체적인 해석 방식은 나중에 Texture를 Pipeline에 묶을 때 지정한다.
C++의 reinterpret_cast와 비슷하다고 생각하면 된다.
예를 들어, 8비트 성분 4개를 할당하되, 구체적인 자료 형식을 지정하지 않으려면 DXGI_FORMAT_R8G8B8A8_TYPELESS를 지정하면 된다.
교환 사실과 페이지 전환
페이지 전환은 애니메이션이 깜빡대는 현상을 방지하기 위한 최선의 방법은 버퍼링이다.
애니메이션 한 프레임 전체를 후면 버퍼에 그린 뒤, 하나의 프레임으로 화면에 표시를 한다.
이는 사용자에게 프레임이 그려지는 과정을 나타내지 않는다는 장점이 있다.
버퍼링을 구현 할 때에는 Hardware로 두 개의 Texture Buffer를 관리한다.
화면 모니터에 표시되는 이미지 자료를 담는 것은 전면 버퍼(front buffer)
화면 밖에서 현재 화면에 표시되고 있는 프레임 다음 것을 담는 것은 후면 버퍼(back buffer)
Front Buffer가 화면에 표시되면 Back Buffer와 역할을 맞바꾼다.
Back Buffer를 Front Buffer와 교환하여 화면에 표시되게 하는 것을 제시(Presenting)이라 부른다.
Presenting은 Buffer의 내용이 아니라 각 Buffer의 Pointer를 맞바꾸는 매우 효율적인 연산이다.
Front Buffer와 Back Buffer는 하나의 교환 사슬(Swap Chain)을 형성한다.
이는 IDXGISwapChain이라는 Interface로 대표된다.
이 Interface는 Front Buffer와 Back Buffer를 담을 수 있다.
또한 Buffer 크기 변경을 위한 ResizeBuffers와 Presenting을 위한 Present 메소드를 제공한다.
물론 2개 이상의 Buffer를 사용할 수도 있다. 하지만 일반적으로 2개면 충분하다.
깊이 버퍼링(Depth Buffer)
Depth Buffer는 이미지 자료를 담지 않는 Texture의 한 예로 각 Pixel의 깊이 정보를 담는다.
Pixel의 깊이는 [0.0, 1.0]으로 값이 작을수록 관찰자와 가깝다.
Depth Buffer는 Back Buffer의 Pixel들과 일대일 대응 관계이다.
구체적으로 Buffer의 크기와 대응하는 Pixel들의 Index까지 동일하다.
Direct3D에서는 한 물체의 Pixel들이 다른 물체 앞에 있는지 판정 하기 위해 Depth Buffering 혹은 Z-Buffering을 사용한다.
Depth Buffering에서는 그리는 순서는 중요하지 않다.
실제로 먼 물체부터 그리는 방법으로 깊이 문제를 해결하기도 한다.
하지만 이는 크게 두가지 문제가 발생한다.
하나는 커다란 자료 집합을 정렬 해야 한다는 것.
다른 하나는 맞물린 형태의 물체들은 제대로 처리하지 못한다는 것.
이에 반해 Depth Buffering은 Graphic Hardware 안에서 공짜로 일어나며, 맞물린 물체도 제대로 처리해준다.
Depth Buffer의 작동 방식
먼저 Back Buffer를 흰색이나 검은색과 같은 기본 색으로 지운다.
이 때 Depth Buffer의 기본 값인 1.0으로 초기화 된다.
이후 Rendering 할 물체의 Depth Buffer 값을 비교한다.
물체의 Depth 값이 Depth Buffer 값보다 작을 경우, Depth Buffer와 함께 Back Buffer 값이 갱신된다.
Depth 값이 작을수록 관찰자와 가까운 곳에 위치한다는 점을 생각한다면 실로 합당한 알고리즘이다.
Depth Buffer도 하나의 Texture이므로 특정한 자료 형식을 지정해서 생성해야 한다.
이 또한 DXGI_FORMAT이란 Enumeration을 따른다.
몇가지 예시를 들어보자.
DXGI_FORMAT_D32_FLOAT_S8X24_UINT
각 Texel은 32bit의 float형의 depth 값을 가진다.
Stencil은 [0, 255] 구간으로 사상되는 8bit의 unsigned int형의 값을 가진다.
마지막으로 용도 없이 채우는 용으로만 쓰이는 24bit의 padding으로 구성된다.
DXGI_FORMAT_D32_FLOAT
각 Texel은 하나의 32bit float형의 depth 값을 가진다.
DXGI_FORMAT_D24_UNORM_S8_UINT
각 Texel은 [0, 1] 구간으로 사사오디는 unsigned 24bit의 depth 값을 가진다.
Stencil은 [0, 255]구간으로 사상되는 8bit unsigned int형의 값을 가진다.
사실 Depth Buffer는 Depth-Stencil Buffer이라 부르는 것이 더 정확하다.
Stencil Buffer를 사용하려면 반드시 Depth Buffer에 부착해야 하기 때문이다.
텍스처 자원 뷰
Rendering Pipeline에 Texture를 bind 할 수 있는 단계는 여럿 있따.
흔한 용도는 다음 두가지이다.
Texture를 Render 대상으로 묶는 것.
Direct3D가 Texture에 Rendering하는 경우.
Shader Resource로서 묶는 것
Shader 안에서 Texture를 추출하는 경우
이 두가지 용도의 Texture를 생성할 때에는 다음과 같이 Texture를 bind 할 단계를 지정하는 bind flag를 사용한다.
D3D11_BIND_RENDER_TARET | D3D11_BIND_SHADER_RESOURCE
https://docs.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_bind_flag
D3D11_BIND_FLAG (d3d11.h) - Win32 apps
Identifies how to bind a resource to the pipeline.
docs.microsoft.com
사실 Texture Resource가 Pipeline이 단계에 직접 묶이는 것은 아니다.
실제로는 Texture가 연고나된 Resource View를 Pipeline 단계에 묶는다.
어떤 용도이든 Direct3D에서 Texture를 사용하려면 초기화 시점에서 그 Texture의 Resource View를 생성해야 한다.
이러한 절차는 효율성을 위한 것으로, SDK에는 다음과 같이 나와 있다.
이렇게 하면 실행시점 Runtime과 Driver가 유효성 점검과 매핑을 뷰 생성 시점에서 수행할 수 있기 때문에 결속 시점에서의 형식 점검이 최소화 된다.
따라서 Texture를 Render 대상과 Shader Resource로 사용하기 위해서는 두 개의 View를 생성할 필요가 있다.
하나는 Render 대상 View인 ID3D11RenderTargetView
다른 하나는 Shader Resource View인 ID3D11ShaderResourceView
Resource View는 본질적으로 두가지 일을 한다.
하나는 DirectEd에게 Resource의 사용 방식(Pipeline의 어떤 단계에 묶을 것인지)을 알려주는 것.
다른 하나는 생성 시점에서 Typeless Resource의 Type을 지정하는 것.
이 경우, Texture 원소를 한 Pipeline 단계에서는 float로 사용하고, 다른 단계에서는 int로 사용할 수 있다.
어떤 Resource에 대한 View를 생성할 수 있으려면 애초에 해당 Resource을 생성 할 때 Bind Flag를 지정해야 한다.
예를 들어 D3D11_BIND_DEPTH_STENCIL flag를 지정하지 않고 생성한 자원은 ID3D11DepthStencilView를 생성할 수 없다.
무작정 생성하려 하면 다음 오류 메시지가 뜬다
D3D11: ERROR: ID3D11Device::CreateDepthStencilView: A DepthStencilView
cannot be created of a Resource that did not specify
D3D11_BIND_DEPTH_STENCIL.
다중표본화의 이론
모니터의 Pixel들은 무한히 작지 않기 위해, 임의의 선을 완벽하게 나타내는 것은 불가능하다.
때문에 Aliasing 효과가 나타나기도 한다.
모니터 해상도를 키워 Pixel 크기를 줄이면 문제를 완화할 수도 있다.
그러나 모니터 해상도를 키우는 것으로 해결 못할 때도 있는데, 이를 위해 Anti Aliasing 기법이 존재한다.
다중표본화(Super Sampling)도 그 중 하나이다.
Super Sampling에서는 Back Buffer와 Depth Buffer를 화면 해상도의 4배로 크게 잡는다.
3차원 장면을 4배 크기의 해상도에서 Back Buffer에 Rendering한다.
이를 화면에 출력 할 때는 원래 크기로 Resolving(혹은 down sampling)한다.
이 Resolving 공정은 4개의 Pixel 블록의 각 색상의 평균을 최종 색상으로 사용한다.
간단히 말해서, Super Sampling은 화면 해상도를 Software 상에서 증가하는 것이라 할 수 있다.
Super Sampling은 Pixle 처리량과 메모리 소비량이 4배라서 비용이 크다.
Direct3D는 이를 대신해 다중표본화(Multi Sampling)라는 절충안을 지원한다.
Multi Sampling은 일부 계산 결과를 부분 Pixel들 사이에서 공유하기 때문이 Super Sampling보다 비용이 적다.
4X Multi Sampling의 경우 Super Sampling처럼 해상도 4배의 Depth Buffer와 Back Buffer를 사용한다.
그러나 이미지 색상을 각 부분 Pixel마다 계산하는 것이 아니다.
Pixel마다 1번씩만 계산하고, 그 색상과 부분 Pixel들의 가시성과 포괄도를 이용해 최종 색을 결정한다.
가시성: 이를 위해 부분 Pixel당 Depth, Stencil 판정이 일어남
포괄도: 부분 Pixel을 다각형이 어느정도나 덮고 있는지를 뜻하는 값
Multi Sampling과 Super Sampling의 차이를 잘 파악하자.
Super Sampling은 Pixel별로 계산되므로 한 Pixel당 부분 Pixel의 값이 다를 수 있다.
반면 Multi Sampling은 Pixel당 한번 계산되어 덮인 모든 가시 부분 Pixel에 계산된다.
색을 계산하는 것은 매우 무거운 연산이다.
때문에 Multi Sampling이 Super Sampling에 비해 상당한 비용 절감을 이룬 방식이다.
하지만 더 정확하한 Texture와 Shader Aliasing 처리는 Super Sampling이 월등하다.
하나의 Pixel을 부분 Pixel로 분할하는 패턴은 Hardware 제조사마다 다를 수 있다.
Direct3D의 Multi Sampling
Multi Sampling을 위해서는 DXGI_SAMPLE_DESC라는 구조체를 채워야 한다.
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC;
Count
Quality
원하는 품질 수준을 지정한다.
"품질 수준"에 대한 구체적인 의미는 Hardware 제조사마다 다를 수 있다.
표본 개수가 많을수록, 품질 수준이 높을수록 렌더링 비용이 증가한다.
때문에 비용과 속도 사이에 절충선을 잘 잡아야 한다.
품질 수준들의 범위는 Texture type과 Pixel당 표본 개수에 의존한다.
주어진 Texture type과 표본 개수의 조합에 대한 품질 수준들의 개수를 알고 싶을때는 다음 메소드를 사용한다.
주어진 Texture type과 표본 개수의 조합을 장치가 지원하지 않으면 0을 반환한다.
그 외에는 주어진 조합에 대한 품질 수준 개수가 pNumQualityLevels에 저장된다.
하나의 Texture type과 표본 개수 조합에 대한 유효한 품질 수준은 [0, *pNumQualityLevels - 1]이다.
HRESULT ID3D11Device::CheckMultisampleQualityLevels(
DXGI_FORMAT Format,
UINT SampleCount,
UINT *pNumQualityLevels
);
D3D12에서는 해당 객체에 이 메서드를 찾을 수 없었다. 대신 다음 구조체를 발견할 수 있었다.
typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS {
DXGI_FORMAT Format;
UINT SampleCount;
D3D12_MULTISAMPLE_QUALITY_LEVEL_FLAGS Flags;
UINT NumQualityLevels;
} D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;
한 Pixel당 추출할 수 있는 최대 표본 개수는 다음과 같이 정의된다.
#define D3D12_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )
그러나 실제로는 Multi Sampling의 성능과 메모리 비용을 합리적인 수준으로 유지하기 위해 4개나 8개만 추출한다.
Multi Sampling을 하고 싶지 않으면 표본 개수를 1로, 품질 수준을 0으로 설정하면 된다.
기능 수준(Feature Level)
Direct3D 11에서 도입된 Feature Level은 엄밀한 기능성 집합을 정의한다.
typedef
enum D3D_FEATURE_LEVEL
{
D3D_FEATURE_LEVEL_1_0_CORE = 0x1000,
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_0 = 0xa000,
D3D_FEATURE_LEVEL_10_1 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000,
D3D_FEATURE_LEVEL_11_1 = 0xb100,
D3D_FEATURE_LEVEL_12_0 = 0xc000,
D3D_FEATURE_LEVEL_12_1 = 0xc100
} D3D_FEATURE_LEVEL;
이 기능의 핵심은, 사용자의 Hardware가 특정 Feature Level을 지원하지 않는다면 그보다 더 낮은(오래된) Feature Level로 내려가서 Application을 실행한다는 것이다.
이를 위해 Application은 Feature Level 지원 여부를 최신서부터 오래된 순서로 하나씩 점검해나간다.
------------------------------------------------
책에서 4-1챕터에 해당하는 내용들입니다.
이론적인 부분과 코드 부분이 많아서 분리해서 올리고자 합니다.
다음에는 5챕터에 해당하는 내용을 정리하고, 그 다음에는 4-2부터 남은 4챕터의 내용을 코드와 함께 정리하겠습니다.