[01] DirectX 11 초기화 (1)
DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.
들어가며
이전에 만들었던 그래픽을 처리하는 클래스인 ApplicationClass에 또 다른 클래스 'D3DClass'를 추가한다.
ApplicationClass.h
업데이트 된건 windows.h가 아닌 새로운 클래스인 D3DClass를 include하고, D3DClass의 포인터 추가다.
private:
// D3DClass의 포인터 추가, 이 튜토리얼에서 모든 멤버 변수는 m_접두사를 사용.
D3DClass* m_Direct3D;
ApplicationClass.cpp
1. 생성자
안전상의 이유로 방금 추가한 멤버 변수를 초기화 한다.
ApplicationClass::ApplicationClass()
{
m_Direct3D = nullptr;
}
2. Initialize
bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
bool result;
// Direct3D 개체를 생성 및 초기화 합니다.
m_Direct3D = new D3DClass;
// 이 클래스의 전역 변수 4개와 창의 가로 세로 길이를 매개변수로 D3DClass의 초기화 함수를 호출한다.
// 그러면 D3DClass는 이 매개변수를 이용해 Direct3D 시스템을 설정한다.
result = m_Direct3D->Initialize( screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
if (!result)
{
MessageBox(hwnd, L"Could not initialize Direct3D", L"Error", MB_OK);
return false;
}
return true;
}
3. Shutdown
이전에 언급했듯이 특정 Windows 함수는 클래스 소멸자를 호출하지 않아 메모리 누수가 발생할 수 있기 때문에 소멸시 해야하는 작업은 이 Shutdown함수에서 담당한다.
void ApplicationClass::Shutdown()
{
// Direct3D 개체가 초기화 되었는지 확인 후 해제합니다.
if (m_Direct3D != nullptr)
{
m_Direct3D->Shutdown();
delete m_Direct3D;
m_Direct3D = 0;
}
return;
}
모든 그래픽 개체의 메모리 해제, 소멸이 여기서 발생해 마찬가지로 D3DClass 개체도 여기에서 소멸된다.
4. Frame
이제 Frame함수는 매 프레임마다 Render함수를 호출한다.
bool ApplicationClass::Frame()
{
bool result;
// 그래픽 Scene을 렌더링합니다.
result = Render();
if (result == false)
{
return false;
}
return true;
}
5. Render
화면에 그림을 그리는 함수. 아직은 별 게 없다.
bool ApplicationClass::Render()
{
// Scene을 회색으로 초기화 한다.
m_Direct3D->BeginScene(0.5f, 0.5f, 0.5f, 1.0f);
// 렌더링된 장면을 화면에 표시한다.
m_Direct3D->EndScene();
return true;
}
D3DClass.h
이 헤더파일에서는 닉값처럼 DirectX의 기능을 사용하기 위한 코드가 포함된다. 첫 부분은 라이브러리를 연결한다.
// DirectX의 라이브러리를 사용한다.
// #pragma comment : 해당 lib파일을 링크하라는 명령
#pragma comment(lib, "d3d11.lib") // 3D그래픽을 그리기 위한 모든 기능 포함
#pragma comment(lib, "dxgi.lib") // 하드웨어에 대한 정보를 얻는 기능 포함
#pragma comment(lib, "d3dcompiler.lib") // 셰이더 컴파일 기능 포함
그리고 가져온 라이브러리에서 사용하는 헤더파일을 include한다.
#include <d3d11.h>
#include <directxmath.h>
using namespace DirectX;
클래스는 지금 시간엔 단순하게 유지한다. 중요한 건 '초기화 및 종료' 이다. 이 튜토리얼의 중점이다.
class D3DClass
{
public:
D3DClass();
D3DClass(const D3DClass&) = default;
~D3DClass() = default;
bool Initialize(int, int, bool, HWND, bool, float, float);
void Shutdown();
void BeginScene(float, float, float, float);
void EndScene();
ID3D11Device* GetDevice();
ID3D11DeviceContext* GetDeviceContext();
void GetProjectionMatrix(XMMATRIX&);
void GetWorldMatrix(XMMATRIX&);
void GetOrthoMatrix(XMMATRIX&);
void GetVideoCardInfo(char*, int&);
void SetBackBufferRenderTarget();
void ResetViewport();
private:
bool m_vsync_enabled;
int m_videoCardMemory;
char m_videoCardDescription[128];
IDXGISwapChain* m_swapChain;
ID3D11Device* m_device;
ID3D11DeviceContext* m_deviceContext;
ID3D11RenderTargetView* m_renderTargetView;
ID3D11Texture2D* m_depthStencilBuffer;
ID3D11DepthStencilState* m_depthStencilState;
ID3D11DepthStencilView* m_depthStencilView;
ID3D11RasterizerState* m_rasterState;
XMMATRIX m_projectionMatrix;
XMMATRIX m_worldMatrix;
XMMATRIX m_orthoMatrix;
D3D11_VIEWPORT m_viewport;
};
D3DClass.cpp
1. 생성자
여태 그래왔던 것 처럼 모든 멤버 포인터를 초기화한다.
D3DClass::D3DClass()
{
m_swapChain = nullptr;
m_device = nullptr;
m_deviceContext = nullptr;
m_renderTargetView = nullptr;
m_depthStencilBuffer = nullptr;
m_depthStencilState = nullptr;
m_depthStencilView = nullptr;
m_rasterState = nullptr;
}
2. Intialize
이 초기화 함수는 Direct3D의 전체 설정을 수행하는 기능이다. 너무 길어 중요하다 싶은 것만 짚고 넘어간다. 먼저 매개변수를 확인한다.
bool D3DClass::Initialize(
int screenWidth, // 창의 가로 길이
int screenHeight, // 창의 세로 길이
bool vsync, // 수직 동기화 여부
HWND hwnd, // 프로그램 창의 핸들
bool fullscreen, // 전체화면의 여부
float screenDepth, // 렌더링 될 3D 환경에 대한 깊이 설정 1
float screenNear // 렌더링 될 3D 환경에 대한 깊이 설정 2
)
첫번째로 하는 것은 그래픽카드, 모니터의 정보를 통해 주사율 값을 가져와 주사율을 설정해야 한다.
그렇지 않으면 사용하는 장치보다 높은 주사율으로 설정될 수 있는데 이는 성능 저하, 디버그 출력에 오류를 발생시킨다.
※ 원문에서 나오는 디스플레이 모드란 나는 처음에 해상도만을 뜻하는 줄 알았지만 사실 해상도 뿐만아니라 주사율, 픽셀 형식 등 정보가 포함된 구조체였다. 먼저 보고가면 이해하기 수월하다.
// 디스플레이 모드
typedef struct DXGI_MODE_DESC {
UINT Width; // 화면의 가로 해상도
UINT Height; // 화면의 세로 해상도
DXGI_RATIONAL RefreshRate; // 주사율 (초당 화면 재생 빈도)
DXGI_FORMAT Format; // 픽셀 형식
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // 프레임을 그리는 방법
DXGI_MODE_SCALING Scaling; // 스케일링 방식
}
주사율을 가져오는 방법은 꼬리를 물듯이 이어진다. 먼저 IDXGIFactory 개체를 생성한다.
// DirectX graphics Infrastructure(DXGI) 개체를 생성하고 관리하는 기능을 한다.
// DXGI는 일단 프로그램과 그래픽 기능 장치(모니터, 그래픽카드 ..) 사이에서 일해주는 기능이라고 생각하자.
IDXGIFactory* factory;
// IDXGIFactory를 생성
result = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)&factory);
그리고 만든 IDXGIFactory 개체를 이용해 그래픽카드의 정보를 가져올 수 있는 인터페이스인 IDXGIAdapter을 생성한다.
// IDXGIAdapter : 그래픽 카드의 특정 기능과 정보를 관리하는 인터페이스
// 그래픽 카드의 정보가 나열 되고 해당 그래픽 카드와 관련된 작업을 수행 가능
IDXGIAdapter* adapter;
// 방금 만든 DXGI factory를 사용해 0번째 사용하는 그래픽카드의 IDXGIAdapter를 생성한다.
result = factory->EnumAdapters(0, &adapter);
그리고 만든 IDXGIAdapter를 이용해 그래픽 카드와 연결 된 모니터의 정보를 가져올 수 있는 IDXGIOuput을 생성한다.
// IDXGIOutput : 그래픽 카드의 출력장치(모니터, ...)의 정보를 관리하는 인터페이스
IDXGIOutput* adapterOutput;
// IDXGIAdapter를 이용해 그래픽 카드의 사용 가능한 0번째 모니터의 정보를 저장한다.
result = adapter->EnumOutputs(0, &adapterOutput);
그리고 만든 IDXGIOuput을 이용해 Display mode 종류 개수를 저장한다.
여기서 중요한건 이 함수는 마지막에 NULL이냐 아니냐에 따라 동작이 달라지는데, NULL인 경우 지원 하는 Display mode의 총 개수를 3번째 매개변수에 저장한다.
// 해상도의 개수를 저장할 uint
unsigned int numModes
// 픽셀 형식이 DXGI_FORMAT_R8G8B8A8_UNORM인 해상도의 종류 개수를 numModes에 저장한다.
// DXGI_FORMAT_R8G8B8A8_UNORM는 32비트 색상 형식으로 각 채널(R,G,B,A)가 8비트로 표현 된다는 뜻.
result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, NULL);
아까 저장한 Display mode 개수만큼 동적 배열을 생성한다.
// 위에서 말했던 구조체!
DXGI_MODE_DESC* displayModeList;
// 방금 저장한 해상도의 종류 개수 만큼 배열을 생성한다.
displayModeList = new DXGI_MODE_DESC[numModes];
다시 한 번 GetDisplayModeList를 사용하는데 이번에 마지막에 NULL이 아닌 값을 넣는다. 이렇게 하면 마지막 매개변수 배열에 세번째 매개변수 만큼 Display mode의 정보를 채워 넣는다.
※ GetDisplayModeList는 원래 두 번 사용하게끔 설계되었다
// numModes의 수 만큼 디스플레이 모드 정보를 displayModeList으로 가져온다.
result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, displayModeList);
이제 모든 Display mode를 보면서 모니터의 크기와 맞는 모드를 찾는다. 일치하는 모드면 그 모드에 주사율 분자 분모를 저장한다.
// 디스플레이 모드 목록에서 모니터의 크기에 맞는 디스플레이 모드를 찾는다.
// 일치하는 디스플레이 모드를 찾으면 주사율의 분자 분모를 저장한다.
for (i = 0; i < numModes; i++)
{
if (displayModeList[i].Width == (unsigned int)screenWidth)
{
if (displayModeList[i].Height == (unsigned int)screenHeight)
{
numerator = displayModeList[i].RefreshRate.Numerator;
denominator = displayModeList[i].RefreshRate.Denominator;
}
}
}
이렇게 주사율 설정 값을 알아보았고 다음으로 할 것은 그래픽 카드의 이름과 vram 용량을 알아낸다.
// 그래픽 카드의 정보 구조체
DXGI_ADAPTER_DESC adapterDesc;
// DXGI_ADAPTER_DESC를 IDXGIAdapter를 통해 가져온다.
result = adapter->GetDesc(&adapterDesc);
// dedicated video card memory(VRAM)를 메가바이트 단위로 저장합니다.
m_videoCardMemory = (int)(adapterDesc.DedicatedVideoMemory / 1024 / 1024);
// 그래픽 카드의 이름을 Char형 배열로 변환하여 저장합니다.
wcstombs_s(&stringLength, m_videoCardDescription, 128, adapterDesc.Description, 128);
원하는 정보였던 주사율에 대한 분자 분모, 그래픽 카드의 정보를 저장했으니 사용했던 인터페이스들을 해제한다.
// Release the display mode list.
delete[] displayModeList;
displayModeList = 0;
// Release the adapter output.
adapterOutput->Release();
adapterOutput = 0;
// Release the adapter.
adapter->Release();
adapter = 0;
// Release the factory.
factory->Release();
factory = 0;
중간 마감
Initialize 내용이 너무 길어 다음 포스팅에서 마무리하고, 이번에 중점이였던 DXGI를 잠깐 짚고 간다.
DXGI(DirectX graphics intrastructure)
DXGI : 애플리케이션과 그래픽 하드웨어 사이에서 그래픽 자원을 관리하고 제어하는 프레임워크
(유저모드단에서 커널 모드 드라이버 및 시스템 하드웨어와 통신하는 것)
우리가 아까 값을 가져왔던 주사율, 그래픽카드의 이름, VRAM의 크기 등... 모두 이 DXGI를 이용한것이다.
DXGI와 IDXGIFactory,, IDXGIAdapter 같은 IDXGI와의 관계라면, 이 IDXGI가 붙은건 모두 DXGI가 제공하는 많고 많은 인터페이스중 하나라고 할 수 있다.
개념 정리
모듈 : 특정 기능을 하는 단위, 그 크기는 함수부터 개체 등 기능에 따라 다양할 수있다.
pragma : 뒤에 오는 내용을 컴파일에게 수행하라는 전처리 명령.
디스플레이 모드 : DXGI_MODE_DESC 구조체를 뜻함
DXGI : 애플리케이션과 그래픽 하드웨어 사이에서 그래픽 자원을 관리하고 제어하는 프레임워크
픽셀 형식 : 픽셀의 색이 어떻게 표현되는지, 각 색상 채널이 몇 비트를 사용하는지, 알파 채널(투명)이 포함되는지 등을 지정하는 형식
IDXGIAdapter : 그래픽 카드와 관련된 정보 제공, 관련 작업을 수행하는 인터페이스
IDXGIOutput : 출력장치(대표적으로 모니터)에 대한 정보를 제공하고 제어하는 인터페이스