서의 공간

2. Creating a Framework and Window 본문

Graphics API/DirectX 11 - Rastertek

2. Creating a Framework and Window

홍서의 2021. 1. 7. 06:58

DirectX 11 프로그래밍을 시작하기 전에 간단한 코드 프레임워크를 구축할 것이다. 이 프레임워크는 기본 Windows 기능을 관리하고 DirectX 11 학습을 위해 체계적이고 가독성 좋은 코드로 쉽게 확장할 수 있는 방법을 제공한다. 이 튜토리얼의 목적은 DirectX 11의 다양한 기능을 시도하는 것이므로 프레임 워크를 가능한 가볍게 유지할 것이다. 결코 종합적인 풀 렌더링 엔진을 만드는 것이 아님을 알린다. DirectX 11을 확실히 이해했다면 어떻게 최신 그래픽 렌더링 엔진을 만들지 연구할 수 있게 될 것이다.

 

프레임워크

프레임 작업은 4개의 항목으로 시작한다.

1. 응용프로그램의 진입점(entry point)을 처리하는 WinMain 함수.

2. WinMain 함수 내에서 호출될 전체 응용프로그램을 캡슐화하는 SystemClass.

3. SystemClass 내부에 사용자 입력을 처리하기 위한 InputClass와 DirectX 그래픽스 코드를 처리하기 위한 GraphicsClass. 

다음은 프레임워크의 다이어그램이다.

위 다이어그램을 참고하여 main.cpp 파일 내의 WinMain 함수에서부터 시작해보자.


WinMain

// Filename: main.cpp
#include "Systemclass.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
{
	SystemClass* System;
	bool result;

	// system object를 생성한다.
	System = new SystemClass;
	if (!System) return 0;
	
	// system object를 초기화하고 run 한다.
	result = System->Initialize();
	if (result) System->Run();

	// system object를 shutdown하고 release 한다.
	System->Shutdown();
	delete System;
	System = 0;

	return 0;
}

보다시피 WinMain 함수는 매우 간단하다. SystemClass 객체를 생성하고 초기화한 후 문제가 없다면 Run() 함수를 호출한다. Run() 함수는 내부적으로 루프를 실행하고 루프가 끝날 때까지 모든 응용프로그램 코드를 수행한다. Run() 함수가 끝나면(내부적으로 루프가 종료되어) System 객체를 정리하고 응용프로그램을 종료한다. SystemClass 내부에 전체 응용프로그램을 캡슐화한 것으로 단순함을 유지했다. 다음은 SystemClass의 헤더이다.


SystemClass.h

#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_

// PRE-PROCESSING DIRECTIVES
#define WIN32_LEAN_AND_MEAN

// INCLUDES
#include <Windows.h>

// MY CLASS INCLUDES
#include "InputClass.h"
#include "GraphicsClass.h"

class SystemClass
{
public:
	SystemClass();
	SystemClass(const SystemClass&);
	~SystemClass();

	bool Initialize();
	void Shutdown();
	void Run();

	LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);

private:
	bool Frame();
	void InitializeWindows(int&, int&);
	void ShutdownWindows();

private:
	LPCWSTR mApplicationName;
	HINSTANCE mHinstance;
	HWND mHwnd;

	InputClass* mInput;
	GraphicsClass* mGraphics;
};

// FUNCTION PROTOTYPES
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

// GLOBALS
static SystemClass* ApplicationHandle = 0;
#endif

WIN32_LEAN_AND_MEAN는 빌드 과정의 속도를 높이기 위해 Windows.h의 자주 사용되지 않은 일부 API를 제외한다. Windows.h 헤더는 윈도우를 생성/파괴하는 그리고 다른 유용한 Win32 함수를 제공한다.

SystemClass의 정의는 매우 간단하다. 이 클래스에는 WinMain에서 호출한 Initialize(), Shutdown() 및 Run() 함수가 정의되어 있다. 또한 MessageHandler() 함수를 클래스에 넣어 프로그램이 실행하는 동안 해당 프로그램으로 보내질 Windows 시스템 메시지를 처리하였다. 마지막으로 그래픽과 입력을 처리할 두 객체에 대한 포인터 멤버 변수 mInput과 mGraphics가 정의된다.

WndProc() 함수와 ApplicationHandle 포인터 변수도 이 클래스에 포함되어 있으므로 Windows 시스템 메시징을 SystemClass의 MessageHandler() 함수로 리다이렉트 할 수 있다.

이제 SystemClass의 소스파일을 살펴본다.

 

SystemClass.cpp

생성자에서 두 객체의 포인터를 모두 nullptr로 초기화한다. 객체의 초기화에 실패하면 Shutdown() 함수가 해당 객체를 해제하려 하기 때문에 중요한 부분이다. Shutdown() 함수는 객체가 nullptr이 아닌 경우에만 해당 객체가 유효하다고 판단하여 해제하기 때문이다. 즉 nullptr로 초기화하지 않은 상태에서 객체의 초기화에 실패하면 이후 호출될 Shutdown() 함수는 해당 객체를 유효하다고 판단하기 때문에 문제가 발생한다.

복사 생성자와 소멸자 부분은 이 튜토리얼에서는 사용하지 않으므로 비어두었다. 또한 클래스의 소멸자가 인스턴스들을 해제하는 것이 아니라 Shutdown() 함수에서 이 일을 처리한다(역주: 이유는 원글 작성자가 특정 Windows 함수를 사용하는 데 있어서 클래스 소멸자를 믿지 않기 때문이다, 또는 조심스러워하기 때문이다).

Initialize() 함수는 응용프로그램에 대한 모든 초기화 설정을 수행한다. 먼저 InitializeWindows() 함수를 호출하여 응용프로그램에서 사용할 윈도우를 만든다. 그리고 응용프로그램이 사용자 입력을 처리할 InputClass 객체와 화면에 그래픽을 렌더링 하는 데 사용할 GraphicsClass 객체를 초기화한다.

Shutdown() 함수는 '청소'를 수행한다. InputClass와 GraphicsClass의 인스턴스와 관련된 모든 것을 종료시키고 해제한다. 또한 윈도우 역시 종료하고 관련된 핸들을 해제한다.

Run() 함수는 종료하기 전까지 모든 응용프로그램의 프로세스를 수행한다. 응용프로그램(애플리케이션) 프로세스는 매 루프의 Frame() 함수에서 처리한다. 이후 이를 염두에 두고 튜토리얼을 진행하므로 이해해야 할 중요한 개념이다. 다음은 이 부분의 의사 코드이다.

while not done
    check for windows system messages	// 윈도우즈 시스템 메시지 확인
    process system messages				// 시스템 메시지 처리
    process application loop			// 애플리케이션 루프 처리
    check if user wanted to quit during the frame processing // 프레임 처리 진행동안 종료 메시지 확인

Frame() 함수는 애플리케이션에 대한 모든 프로세스를 수행하는 곳이다. Input 객체(InpusClass의 인스턴스)를 통해 사용자 'Esc'키를 눌러 종료를 원했는지 확인한다. 종료를 하지 않았다면 이어서 Graphics객체(GraphicsClass의 인스턴스)를 호출하여 해당 프레임의 그래픽 렌더링을 처리한다. 

MessageHandler() 함수는 Windows 시스템 메시지를 처리하는 함수이다. 우리가 원하는 특정 메시지들을 읽어 들인다. 현재는 키보드의 키가 눌렸거나 키를 뗄 때의 메시지만 읽고 해당 정보를 Input 객체에 전달한다. 다른 모든 메시지는 Windows 기본 메시지 처리 함수(DefWindowProc() 함수)로 전달한다.

InitializeWindows() 함수는 렌더링에 사용할 윈도우를 만든다. 파라미터인 screenWidth와 screenHeight는 출력 파라미터로서 함수가 반환된 이후 애플리케이션 전체에서 이 변수들을 사용할 수 있도록 한다. 기본 윈도우의 설정은 테두리가 없고, 검은색 배경으로 초기화한다. InitializeWindows() 함수는 FULL_SCREEN 전역 변수에 따라 작은 윈도우를 만들거나 전체 화면 윈도우를 만든다. FULL_SCREEN 변수가 true일 경우 모니터의 전체 화면을 덮는 윈도우가 만들어지고 false인 경우 모니터 화면 중앙에 800x600 크기의 윈도우를 만든다. 이 전역 변수는 GraphicsClass.h 파일에 정의되어 있다.

ShutdownWindows() 함수는 화면 설정을 기본 값으로 되돌리고 윈도우와 관련된 핸들을 해제한다.

WndProc() 함수는 Windows가 메시지를 보내는 곳이다. InitializeWindows() 함수의 wc.lpfnWndProc = WndProc 구문에서 이미 설정해 주었다. SystemClass 내에 정의된 MessageHandler 함수로 발생하는 모든 메시지를 보내도록 하여 시스템 클래스에 직접 연결하기 때문에 SystemClass.h 파일 내부에 포함했다. 이를 통해 메시징 기능을 클래스에 직접 연결하고 코드를 깔끔하게 유지할 수 있다.

#include "SystemClass.h"

SystemClass::SystemClass()
{
	mInput = nullptr;
	mGraphics = nullptr;
}

SystemClass::SystemClass(const SystemClass &)
{
}

SystemClass::~SystemClass()
{
}

bool SystemClass::Initialize()
{
	int screenWidth, screenHeight;
	bool result;

	// 화면의 너비와 높이를 0으로 초기화.
	screenWidth = 0;
	screenHeight = 0;
	// Windows api 초기화.
	InitializeWindows(screenWidth, screenHeight);

	// Input 객체 생성. 이 객체는 사용자로부터 키보드 입력을 읽어 처리한다.
	mInput = new InputClass;
	if (!mInput) return false;
	// Input 객체 초기화.
	mInput->Initialize();
	
	// Graphics 객체 생성. 이 객체는 응용프로그램을 위한 그래픽 렌더링을 처리한다.
	mGraphics = new GraphicsClass;
	if (!mGraphics) return false;
	// Graphics 객체 초기화
	result = mGraphics->Initialize(screenWidth, screenHeight, mHwnd);
	if (!result) return false;

	return true;
}

void SystemClass::Shutdown()
{
	// Graphics 객체 해제
	if (mGraphics)
	{
		mGraphics->Shutdown();
		delete mGraphics;
		mGraphics = nullptr;
	}
	// Input 객체 해제
	if (mInput)
	{
		delete mInput;
		mInput = nullptr;
	}
	// 윈도우(창) 종료
	ShutdownWindows();

	return;
}

void SystemClass::Run()
{
	MSG msg;
	bool done, result;
	// 메시지 구조체 초기화.
	ZeroMemory(&msg, sizeof(MSG));

	// windows 또는 사용자로부터 quit 메시지를 받을 때까지 while 루프문을 실행한다.
	done = false;
	while (!done)
	{
		// Windows 메시지 처리
		if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		// 만약 종료 Windows 메시지라면 애플리케션을 종료한다.
		if (msg.message == WM_QUIT)
		{
			done = true;
		}
		else
		{
			// 다른 메시지라면 프레임 처리를 수행한다.
			result = Frame();
			if (!result)
			{
				done = true;
			}
		}
	}

	return;
}

LRESULT SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
	switch (umsg)
	{
		// 키보드의 키를 눌렀는지 확인하여 처리
		case WM_KEYDOWN:
		{
			// 만약 키가 눌렸을 경우 해당 키 정보를 Input 객체에 보내 키 상태를 저장할 수 있도록 한다.
			mInput->KeyDown((unsigned int)wparam);
			return 0;
		}
		// 키보드의 키를 뗐을 경우 처리
		case WM_KEYUP:
		{
			// 만약 키가 떼졌을 경우 해당 키 정보를 Input 객체에 보내 키 상태를 저장할 수 있도록 한다.
			mInput->KeyUp((unsigned int)wparam);
			return 0;
		}
		// 다른 모든 메시지는 기본 메시지 처리 함수로 보낸다. 
		default:
		{
			return DefWindowProc(hwnd, umsg, wparam, lparam);
		}
	}
}

bool SystemClass::Frame()
{
	bool result;

	// 만약 사용자가 esc 키를 눌렀다면 애플리케이션을 종료한다.
	if (mInput->IsKeyDown(VK_ESCAPE)) return false;

	// Graphics 객체를 위해 프레임 처리를 수행한다.
	result = mGraphics->Frame();
	if (!result) return false;

	return true;
}

void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
{
	WNDCLASSEX wc;
	DEVMODE dmScreenSettings;
	int posX, posY;

	// SystemClass 객체를 외부에서 사용하기 위한 포인터
	ApplicationHandle = this;

	// 애플리케이션의 인스턴스 핸들.
	mHinstance = GetModuleHandle(nullptr);

	// 애플리케이션의 이름
	mApplicationName = L"Engine";

	// 윈도우(창) 클래스의 설정.
	wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc = WndProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = mHinstance;
	wc.hIcon = LoadIcon(nullptr, IDI_WINLOGO);
	wc.hIconSm = wc.hIcon;
	wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	wc.lpszMenuName = nullptr;
	wc.lpszClassName = mApplicationName;
	wc.cbSize = sizeof(WNDCLASSEX);

	// 윈도우(창) 클래스를 등록한다.
	RegisterClassEx(&wc);

	// 사용자의 모니터 화면의 해상도.
	screenWidth = GetSystemMetrics(SM_CXSCREEN);
	screenHeight = GetSystemMetrics(SM_CYSCREEN);

	// FULL_SCREEN 전역 변수에 따라 전체화면 모드 또는 창 모드로 만든다.
	if (FULL_SCREEN)
	{
		// 만약 FULL_SCREEN == true이면 가장 큰 크기로 픽셀 하나당 32비트 크기를 가지는 윈도우를 만든다.
		memset(&dmScreenSettings, 0, sizeof(dmScreenSettings));
		dmScreenSettings.dmSize = sizeof(dmScreenSettings);
		dmScreenSettings.dmPelsWidth = (unsigned long)screenWidth;
		dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight;
		dmScreenSettings.dmBitsPerPel = 32;
		dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
		// 전체화면 모드로 설정.
		ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);
		// 윈도우의 왼쪽 위 끝의 위치.
		posX = posY = 0;
	}
	else
	{
		// 만약 창모드라면 윈도우의 크기.
		screenWidth = 800;
		screenHeight = 600;
		// 윈도우는 모니터 화면에 정중앙에 위치하도록 한다.
		posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth) / 2;
		posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
	}

	// 위 설정에 따라 윈도우를 만들고 윈도우의 핸들을 얻는다.
	mHwnd = CreateWindowEx(WS_EX_APPWINDOW, mApplicationName, mApplicationName, 
		WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
		posX, posY, screenWidth, screenHeight, nullptr, nullptr, mHinstance, nullptr);

	// 모니터 화면에 윈도우를 띄우고 메인 포커스로 설정한다.
	ShowWindow(mHwnd, SW_SHOW);
	SetForegroundWindow(mHwnd);
	SetFocus(mHwnd);

	// 마우스 커서를 보이게 설정.
	ShowCursor(true);

	return;
}

void SystemClass::ShutdownWindows()
{
	// 마우스 커서를 보이게 설정.
	ShowCursor(true);

	// 전체화면 모드라면 창모드로 재설정한다.
	if (FULL_SCREEN)
	{
		ChangeDisplaySettings(nullptr, 0);
	}

	// 윈도우를 파괴.
	DestroyWindow(mHwnd);
	mHwnd = nullptr;

	// 애플리케이션 인스턴스 삭제.
	UnregisterClass(mApplicationName, mHinstance);
	mHinstance = nullptr;

	// 이 클래스의 포인터 해제.
	ApplicationHandle = nullptr;
	
	return;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
	switch (umessage)
	{
		// 윈도우 파괴 메시지라면
		case WM_DESTROY:
		{
			PostQuitMessage(0);
			return 0;
		}
		// 윈도우 종료 메시지라면
		case WM_CLOSE:
		{
			PostQuitMessage(0);
			return 0;
		}
		// 다른 메시지라면 MessageHandler() 함수로 해당 메시지를 보낸다.
		default:
		{
			return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
		}
	}
}

InputClass.h

튜토리얼을 간단히 유지하기 위해 DirectInput의 튜토리얼을 진행하기 전까지 Windows의 입력을 사용한다. InputClass는 키보드의 사용자 입력을 처리한다. 이 객체는 SystemClass::MessageHandler() 함수에서 파라미터를 입력받아 호출된다. 이 객체는 키보드 배열에 각 키의 상태를 저장하여 특정 키가 눌려지면 쿼리를 요청한 호출 함수에 알려준다. 헤더는 다음과 같다.

// Filename: InputClass.h
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_

class InputClass
{
public:
	InputClass();
	InputClass(const InputClass&);
	~InputClass();

	void Initialize();

	void KeyDown(unsigned int);
	void KeyUp(unsigned int);

	bool IsKeyDown(unsigned int);

private:
	bool mKeys[256];
};
#endif

 

InputClass.cpp

#include "InputClass.h"

InputClass::InputClass()
{
}

InputClass::InputClass(const InputClass &)
{
}

InputClass::~InputClass()
{
}

void InputClass::Initialize()
{
	int i;
	
	// 키보드 배열을 모두 누르지 않는 상태인 false로 초기화한다.
	for (i = 0; i < 256; i++)
	{
		mKeys[i] = false;
	}

	return;
}

void InputClass::KeyDown(unsigned int input)
{
	// 키보드 키가 눌렸다면 true
	mKeys[input] = true;
	return;
}

void InputClass::KeyUp(unsigned int input)
{
	// 키보드 키가 떼졌다면 false
	mKeys[input] = false;
	return;
}

bool InputClass::IsKeyDown(unsigned int key)
{
	// 해당 키의 상태를 반환한다.(눌렀는지 뗐는지)
	return mKeys[key];
}

GraphicsClass.h

GraphicsClass는 SystemClass에 의해 생성되는 객체이다. 애플리케이션의 모든 그래픽 기능은 이 클래스에서 캡슐화한다. 또한 전체 화면 모드 또는 창 모드와 같이 변경할 수 있는 모든 그래픽 관련 전역 설정이, 이 파일의 헤더를 사용한다. 현재 이 클래스는 비어있지만 향후 추가할 것이다.

#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_

// INCLUDES
#include <Windows.h>

// GLOBALS
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;

class GraphicsClass
{
public:
	GraphicsClass();
	GraphicsClass(const GraphicsClass&);
	~GraphicsClass();

	bool Initialize(int, int, HWND);
	void Shutdown();
	bool Frame();

private:
	bool Render();
};
#endif

 

GraphicsClass.cpp

#include "GraphicsClass.h"

GraphicsClass::GraphicsClass()
{
}

GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}

GraphicsClass::~GraphicsClass()
{
}

bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	return true;
}

void GraphicsClass::Shutdown()
{
	return;
}

bool GraphicsClass::Frame()
{
	return true;
}

bool GraphicsClass::Render()
{
	return true;
}

실행결과

아무것도 그려지지 않은 검은색 배경의 창이 생성된다.

'Graphics API > DirectX 11 - Rastertek' 카테고리의 다른 글

5. Texturing  (0) 2021.01.07
4. Buffers, Shaders, and HLSL  (0) 2021.01.07
3. Initializing DirectX 11  (0) 2021.01.07
1. Setting up DirectX 11 with Visual Studio  (0) 2021.01.07
DirectX 11 Framework - Rastertek  (0) 2021.01.02
Comments