이제부터 Direct3D를 초기화 하는 구체적인 방법을 알아본다. 다음과 같은 순서를 따른다.
제일 먼저 장치(ID3D11Device)와 문맥 장치(ID3D11DeviceContext)를 생성하는 것이다. 응용 프로그램들이 이 인터페이스들을 통해서 하드웨어에게 할 일(GPU메모리에 자원 할당, 후면 버퍼 지우기, 자원을 여러 파이프라인에 묶기, 기하구조 그리기 등)을 지시한다. 좀 더 구체적으로 말하면
다음은 장치와 장치 문맥을 생성하는 함수이다.
HRESULT D3D11CreateDevice(
IDXGIAdapter *pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
CONST D3D_FEATURE_LEVEL* pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
ID3D11Device** ppDevice,
D3D_FEATURE_LEVEL* pFeatureLevel,
ID3D11DeviceContext** ppImmdiateContext
);
매개변수 하나하나씩 살펴보자.
1. pAdapter : 이 함수로 생성할 장치를 나타내는 디스플레이 어댑터를 지정한다. 이 매개변수에 널 값(or 0)을 지정하면 기본 디스플레이 어댑터가 사용된다.
2. DriverType : 일반적으로 렌더링에 3차원 그래픽 가속이 적용되게 하기 위해 이 매개변수에 D3D_DRIVER_TYPE_HARDWARE를 지정한다.
3. Software : 소프트웨어 구동기를 지정한다. 하드웨어를 사용해서 렌더링 하려면 널 값을 넣으면 된다. 만약 다른 값으로 하고 싶다면, 실제로 사용 가능한 소프트웨어 구동기가 있어야 한다.
4. Flags : 추가적인 장치 생성 플래그들을 지정한다.
5. pFeatureLevels : D3D_FEATURE_LEVEL 형식 원소들의 배열로, 원소들의 순서가 곧 기능 수준들을 점검하는 순서이다. 이 매개변수에 널 값을 지정하면 지원되는 최고 기능 수준이 선택된다.
6. FeatureLevels : 배열 pFeatureLevels의 D3D_FEATURE_LEVEL 원소 개수이다. pFeatureLevels에 널 값을 지정했다면, 이 매개변수는 0으로 지정하면 된다.
7. SDKVersion : 항상 D3D11_SDK_VERSION을 지정한다.
8. ppDevice : 함수가 생성한 장치를 돌려준다.
9. pFeatureLevel : pFeatureLevels 배열에서 처음으로 지원되는 기능을 돌려준다.
10. ppImmediateContext : 생성된 장치 문맥을 돌려준다.
다음은 이 함수 호출의 예이다.
UINT createDeviceFlags = 0;
D3D_FEATURE_LEVEL featureLevel;
ID3D11Device* md3dDevice;
ID3D11DeviceContext* md3dImmediateContext;
HRESULT hr = D3D11CreateDevice(
0, // 기본 어댑터.
D3D_DRIVER_TYPE_HARDWARE,
0, // 소프트웨어 장치를 사용하지 않음.
createDeviceFlags,
0, 0, // 기본 기능 수준 배열.
D3D11_SDK_VERSION,
&md3dDevice,
&featureLevel,
&md3dImmediateContext);
if (FAILED(hr))
{
MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
return false;
}
if (featureLevel != D3D_FEATURE_LEVEL_11_0)
{
MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);
return false;
}
다 알아야 하는가? 아니다. 특별한 일 없으면 고정해놓고 쓴다. 그리고 특이한 건 매개변수에 더블 포인터가 많다. 왜일까? 보통 함수는 하나만 반환할 수 있다. 하지만 여기서는 값 여러 개를 대입하기 위해, 주소값인 더블 포인터를 쓴다. 앞으로도 이런 함수들이 많이 나온다.
여기서 중요한 건 장치와 장치 문맥을 생성했다는 것이다.
장치를 생성했으니, 이제 하드웨어가 4X MSAA를 위한 품질 수준을 지원하는지 점검한다. 다음 코드는 그 예시다.
UINT m4xMsaaQuality;
HR(md3dDevice->CheckMultisampleQualityLevels(
DXGI_F0RMAT_R8G8B8A8_UN0RM, 4, &m4xMsaaQuality));
assert(m4xMsaaQuality > 0 );
다음 단계는 교환 사슬을 생성하는 것이다. 이를 통해서는 DXGI_SWAP_CHAIN_DESC 구조체의 인스턴스를 만들어서 설정해야 한다. 이 구조체의 정의는 다음과 같다.
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_MODE_DESC는 또 다른 구조체이다.
struct DXGI_MODE_DESC
{
UINT Width; // 원하는 후면 버퍼 너비.
UINT Height; // 원하는 후면 버퍼 높이
DXGI_RATIONAL RefreshRate; // 디스플레이 모드 갱신율.
DXGI_FORMAT Format; // 후면 버퍼 픽셀 형식.
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // 디스플레이 스캔라인 모드.
DXGI_MODE_SCALING Scaling; // 디스플레이 비례 모드.
};
다음은 예제 코드이다.
DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth; // 창의 클라이언트 영역 크기 사용.
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_NSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
// 4X MSAA를 사용한다면,
if (mEnabled4xMsaa)
{
sd.SampleDesc.Count = 4;
sd.SampleDesc.Quality = m4xMsaaQuality - 1;
}
else
{
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
지금까지 뭘 했는가? 교환 사슬 '설정'을 했다. 이제 설정을 완료했으니 '생성'을 하면 된다.
HRESULT IDXGIFactory::CreateSwapChain(
IUnknown* pDevice, // ID3D11Device를 가리킴.
DXGI_SWAP_CHAIN_DESC* pDesc, // 교환 사슬 서술 구조체를 가리킴.
IDXGISwapChain** ppSwapChain); // 생성된 교환 사슬 인터페이스 반환.
교환 사슬과 관련돼서'DXGI'가 많이 나온다. 이는 Direct3D와는 개별적인 API로, 교환 사슬 설정이나 그래픽 하드웨어 나열, 창 모드와 전체화면 모드 전환 등, 그래픽에 관련된 작업을 처리한다. 왜 분리했는가? 다른 그래픽 API(예를 들어 Direct2D)에도 위의 것들이 필요하기 때문이다.
파이프라인에 묶을 자원 뷰를 만들어보자. 다음은 렌더 대상 뷰를 생성하는 코드의 예시이다.
ID3D11RenderTargetView* mRenderTargetView;
ID3D11Texture2D* backBuffer;
mSwapChain->GetBuffer(0, \_uuidof(ID3D11Texture2D),
reinterpret_cast<void**>(&backBuffer));
md3dDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView);
ReleaseCOM(backBuffer);
깊이 버퍼는 깊이 정보를 담는(그리고 스텐실을 사용하는 경우 스텐실 정보도 담는) 2차원 텍스처이다. 2차원 텍스처를 생성할 때에는 생성할 텍스처를 서술하는 D3D11_TEXTURE2D_DESC 구조체를 채우고 ID3D11Device::CreateTexture2D 메서드를 호출해야 한다. 구조체는 다음과 같다.
struct D3D11_TEXTURE2D_DESC {
UINT Width;
UINT Height;
UINT MipLevels;
UINT ArraySize;
DXGI_FORMAT Format;
DXGI_SAMPLE_DESC SampleDesc;
D3D11_USAGE Usage;
UINT BindFlags;
UINT CPUAccessFlags;
UINT MiscFlags;
}
Width : 텍스처의 너비(텍셀 단위).
Height : 텍스처의 높이(텍셀 단위).
MipLevels : 밉맵 수준의 개수. 나~중에 나옴. 깊이, 스텐실 버퍼를 위한 텍스처에서는 밉맵 수준이 1개만 있으면 된다.
ArraySize : 텍스처 배열의 텍스처 개수. 깊이,스텐실 버퍼의 경우에는 텍스처 하나만 필요하다.
Format : 텍셀의 형식을 뜻하는 필드로, DXGI_FORMAT 열거형의 값들 중 하나를 지정한다.
SampleDesc : 다중 표본 개수와 품질 수준을 서술하는 구조체이다. 렌더 대상에 쓰인 설정과 일치해야 한다.
Usage : 텍스처의 용도를 뜻하는 필드로, D3D11_USAGE 열거형의 값들 중 하나를 지정한다. 다음 4가지 중 하나이다.
(a) D3D11_USAGE_DEFAULT : 자원을 GPU가 읽고 써야 한다면 이 용도를 설정한다. 이 용도를 설정하면 CPU는 자원을 읽거나 쓸 수 없다. 깊이 스텐실 버퍼에 대한 모든 읽기,쓰기는 GPU가 수행하므로 깊이, 스텐실 버퍼를 위한 텍스처를 생성할 때는 이걸 사용한다.
(b) D3D11_USAGE_IMMUTABLE : 자원을 일단 생성한 후에는 그 내용을 바꾸지 않는 경우에 이 용도를 지정한다. 이 용도를 지정하면 자원이 GPU 읽기 전용이 되어서 몇 가지 최적화가 가능해진다. CPU와 GPU는 이러한 불변 자원에 자료를 기록할 수 없다(생성 할 때 빼고.) CPU는 불변 자원의 자료를 읽을 수 없다.
(c) D3D11_USAGE_DYNAMIC : CPU가 자원의 내용을 빈번하게(예를 들면 매 프레임마다) 갱신해야 한다면 이걸 쓴다. 이 용도로 생성된 자원은 GPU가 읽을 수 있고, CPU가 기록할 수 있다.
(d) D3D11_USAGE_STAGING : CPU가 자원의 복사본을 읽어야 한다면 이걸 쓴다. GPU에서 CPU 메모리로의 자료 복사는 느린 연산이므로 꼭 필요한 경우가 아니면 사용하지 않는다.(자주 나옴)
BindFlags : 자원을 파이프라인에 어떤 식으로 묶을 것인지를 지정하는 하나 이상의 플래그들을 OR로 결합해서 지정한다. 깊이, 스텐실 버퍼의 경우 D3D11_BIND_DEPTH_STENCIL 플래그를 지정해야 한다. 그 외의 텍스처에 결합 가능한 플래그들은 다음과 같다.
(a) D3D11_BIND_RENDER_TARGET : 텍스처를 렌더 대상으로서 파이프라인에 묶는다.
(b) D3D11_BIND_SHADER_RESOURCE : 텍스처를 셰이더 자원으로서 파이프라인에 묶는다.
CPUAccessFlags : CPU가 자원에 접근하는 방식을 결정하는 플래그들을 지정한다. CPU가 자원에 자료를 기록해야 한다면 D3D11_CPU_ACCESS_WRITE를 지정해야 한다. CPU의 쓰기 접근이 가능하려면 자원의 용도(Usage)가 D3D11_USAGE_DYNAMIC이나D3D11_USAGE_STAGING이어야 함을 주의할 것. CPU가 자원을 읽어야 한다면 D3D11_CPU_ACCESS_READ를 지정한다. CPU의 읽기 접근이 가능하려면 자원의 용도가 D3D11_USAGE_STAGING이어야 한다.(자주 나옴) 깊이, 스텐실 버퍼의 경우 GPU만 깊이, 스텐실 버퍼를 읽고 쓸 뿐 CPU는 전혀 접근하지 않으므로 이 필드에는 그냥 0을 지정 하면 된다.
MiscFlags : 기타 플래그들로, 깊이, 스텐실 버퍼에는 적용되지 않으므로 그냥 0 지정한다.
후면 버퍼와 마찬가지로, 깊이, 스텐실 버퍼를 사용하려면 깊이,스텐실 뷰를 생성해서 파이프라인에 붙여야한다. 마치 렌더 대상 뷰와 비슷하다. 다음은 예시 코드다.
D3D11_TEXTURE2D_DESC depthStencilDesc;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
// 4X MSAA를 사용하는가? 반드시 교환 사슬의 MSAA 설정과 일치해야 함.
if (mEnable4xMsaa)
{
depthStencilDesc.SampleDesc.Count = 4;
depthStencilDesc.SampleDesc.Quality = m4xMsaaQuality - 1;
}
else
{
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
}
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;
ID3D11Texture2D* mDepthStencilBuffer;
ID3D11DepthStencilView* mDepthStencilView;
HR(md3dDevice->CreateTexture2D(
&depthStencilDesc, // 생성할 텍스처를 서술하는 구조체.
0,
&mDepthStencilBuffer)); // 깊이, 스텐실 버퍼를 가리키는 포인터를 돌려준다.
HR(md3dDevice->CreateDepthStencilView(
&mDepthStencilBuffer, // 뷰를 생성하고자 하는 자원.
0,
&mDepthStencilView)); // 깊이, 스텐실 뷰를 돌려준다.
CreateTexture2D 메서드의 제2 매개변수는 텍스터에 채울 초기 자료를 가리키는 포인터이다. 근데 지금 이 텍스처는 깊이, 스텐실 버퍼용이므로 따로 자료를 채울 필요가 없다. 깊이 버퍼링과 스텐실 연산을 수행할 때 Direct3D가 직접 깊이, 스텐실 버퍼를 기록한다. 따라서 0을 넣는다.
CreateDepthStencilView 메서드의 제2 매개변수는 D3D11_DEPTH_STENCIL_VIEW_DESC 구조체를 가리키는 포인터이다. ID3D11Device::CreateRenderTargetView의 제2 매개변수처럼 0을 넣는다.
뷰들도 다 생성했으니 이제 파이프라인의 출력 병합기 단계에 묶어야 한다. 출력 병합기는 무엇인가? 예를 들어 3D 프린터로 총을 만든다고 생각해보자. 총 모형 이론 제작은 따로하고, 그 자료를 3D 프린터에 입력을 하는 과정이 있어야 한다. 그 과정을 뷰를 묶는 과정이라 생각하면 된다. 만약 다른 자료를 묶으면 다른 모형이 나올 것이다. 코드는 다음과 같다.
md3dImmediateContext->OMSetRenderTargets(
1, &mRenderTargetView, mDepthStencilView);
지금까지 자원 생성에는 장치(device)를 사용했지만, 이제 묶는 과정, 그리는 과정들은 장치 문맥(deviceContext)를 사용한다. 제1 매개변수는 묶고자 하는 렌더 대상의 개수이다. 여러 장면을 하는 경우는 고급 기술이다. 제2 매개변수는 파이프라인에 묶을 대상 뷰들을 가리키는 포인터들을 담은 배열의 첫 원소를 가리키는 포인터, 제3 매개변수는 파이프라인에 묶을 깊이, 스텐실 뷰를 가리키는 포인터이다.
교환 사슬 단계에서 후면 버퍼의 크기를 정했다. 보통은 3차원 장면을 이 후면 버퍼 전체에 그리는데, 원한다면 3차원 장면을 후면 버퍼의 일부를 차지하는 직사각형 안에서만 그리게 할 수도 있다. 그 직사각형을 뷰포트(viewport)라 부른다. 구조체는 다음과 같다.
struct D3D11_VIEWPORT {
FLOAT TopLeftX; // 뷰포트 왼쪽 위 x좌표.
FLOAT TopLeftY; // 뷰포트 왼쪽 위 y좌표.
FLOAT Width; // 너비.
FLOAT Height; // 높이.
FLOAT MinDepth; // 최소 깊이 버퍼 값. 최소 0.
FLOAT MaxDepth; // 최대 깊이 버퍼 값. 최대 1.
구조체를 다 채운 다음에는 ID3D11DeviceContext::RSSetViewports 메서드를 사용해서 Direct3D에게 뷰포트를 알려준다. 다음은 그 예이다.
D3D11_VIEWPORT vp;
vp.TopLeftX = 0.0f;
vp.TopLeftY = 0.0f;
vp.Width = static_cast<float>(mClientWidth);
vp.Height = static_cast<float>(mClientHeight);
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
md3dImmediateContext->RSSetViewports(1, &vp);
제1 매개변수는 묶을 뷰포트 개수이고, 제2 매개변수는 뷰포트 배열을 가리키는 포인터이다. 뷰포트를 사용해서 예전에 크레이지 아케이드 2인 모드같은 화면 분할을 할 수 있다.