DirectX 11/DX11 Tutorial

[01] DirectX 11 초기화 (1)

김띠띵 2024. 5. 19. 04:11

 

 

Tutorial 3: Initializing DirectX 11

Tutorial 3: Initializing DirectX 11 This tutorial will be the first introduction to working with DirectX 11. We will address how to initialize and shut down Direct3D as well as how to render to a window. Updated Framework We are going to add another class

www.rastertek.com

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 : 애플리케이션과 그래픽 하드웨어 사이에서 그래픽 자원을 관리하고 제어하는 프레임워크

(유저모드단에서 커널 모드 드라이버 및 시스템 하드웨어와 통신하는 것)

https://learn.microsoft.com/ko-kr/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi

 

우리가 아까 값을 가져왔던 주사율, 그래픽카드의 이름, VRAM의 크기 등... 모두 이 DXGI를 이용한것이다.

DXGI와 IDXGIFactory,, IDXGIAdapter 같은 IDXGI와의 관계라면, 이 IDXGI가 붙은건 모두 DXGI가 제공하는 많고 많은 인터페이스중 하나라고 할 수 있다.

 

DXGI 인터페이스 - Win32 apps

이 섹션에는 DXGI에서 제공하는 인터페이스에 대한 정보가 포함되어 있습니다.

learn.microsoft.com

 


개념 정리

 

모듈 : 특정 기능을 하는 단위, 그 크기는 함수부터 개체 등 기능에 따라 다양할 수있다.

 

pragma : 뒤에 오는 내용을 컴파일에게 수행하라는 전처리 명령.

 

디스플레이 모드 : DXGI_MODE_DESC 구조체를 뜻함

 

DXGI : 애플리케이션과 그래픽 하드웨어 사이에서 그래픽 자원을 관리하고 제어하는 프레임워크

 

픽셀 형식 : 픽셀의 색이 어떻게 표현되는지, 각 색상 채널이 몇 비트를 사용하는지, 알파 채널(투명)이 포함되는지 등을 지정하는 형식


IDXGIAdapter : 그래픽 카드와 관련된 정보 제공, 관련 작업을 수행하는 인터페이스


IDXGIOutput : 출력장치(대표적으로 모니터)에 대한 정보를 제공하고 제어하는 인터페이스