본문 바로가기
dev log/directX 11

[번역] Tutorial 2: Creating a Framework and Window

by 망고먹는 개발자 2021. 8. 10.

https://www.rastertek.com/dx11s2tut02.html

 

Tutorial 2: Creating a Framework and Window

Tutorial 2: Creating a Framework and Window Before starting to code with DirectX 11 I recommend building a simple code framework. This framework will handle the basic windows functionality and provide an easy way to expand the code in an organized and read

www.rastertek.com

위 튜토리얼을 한국어로 번역한 글입니다.

주요 사항을 코드 블럭의 주석으로 포함시켰으니, 코드를 반드시 함께 보는 것이 좋습니다.

한국어로 옮기는 것이 오히려 이해를 방해하는 간단한 문장은 번역하지 않았습니다.


directx 11을 활용해 코딩을 시작하기 전에 간단한 프레임워크를 만들어 보자.

이 프레임워크는 기본 윈도우를 잘 다루고 코드를 조직적으로 확장하며 directX 11을 배우는 목적에 맞게 코딩할 수 있게 도와준다. 이 튜토리얼의 목적은 directX 11의 다양한 기능을 테스트해보는 것이므로, 우리는 의도적으로 프레임워크를 가능한한 가볍게 유지할 것이며 완벽한 렌더링 엔진을 만들지는 않을 것이다. 일단 DirectX 11을 잘 이해하고 나면 현대적인 그래픽스 렌더링 엔진을 만드는 법을 찾아볼 수 있을 것이다.

 

The Framework

이 프레임워크는 먼저 4가지 구성 요소로 시작된다. 먼저 애플리케이션의 진입점을 다루기 위해 WinMain 함수를 구현할 것이다. SystemClass는 WinMain 함수에서 호출할 전체 애플리케이션을 캡슐화 한 클래스이다. SystemClass 안에는 사용자 입력을 처리할 InputClass와 DirectX 코드를 처리할 GraphicsClass가 포함될 것이다. 다이어그램은 다음과 같다.

프레임워크가 어떤 구조를 이루고 있는지 확인하였으므로, main.cpp 내의 WinMain 함수가 어떻게 생겼는지부터 확인해 보자.

WinMain 함수 (main.cpp 내의)

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


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
{
	SystemClass* System;
	bool result;
	
	
	// Create the system object.
	System = new SystemClass;
	if(!System)
	{
		return 0;
	}

	// Initialize and run the system object.
	result = System->Initialize();
	if(result)
	{
		System->Run();
	}

	// Shutdown and release the system object.
	System->Shutdown();
	delete System;
	System = 0;

	return 0;
}

보다시피 우리의 WinMain 함수는 꽤 심플하다. SystemClass 객체를 만들고 초기화한다. 만약 문제없이 초기화가 완료된다면 SystemClass의 Run 함수를 호출한다. Run 함수는 모든 애플리케이션 코드가 완료될 때까지 루프를 실행한다. Run 함수가 종료된 뒤에는 System 객체를 종료하고 객체를 제거한다. 그래서 이 함수는 굉장히 간단할 수 있으며 System Class 내에 모든 어플리케이션 코드가 캡슐화되어 있다. 이제 그러면 SystemClass의 헤더 파일을 확인해보자.

 

SystemClass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_

// 여기서 WIN32_LEAN_AND_MEAN을 정의한다.
// 이는 빌드 속도를 높일 수 있는데, 덜 쓰이는 API들을 제거함으로써 win32 헤더 파일의 크기를 감소시킨다.
///////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///////////////////////////////
#define WIN32_LEAN_AND_MEAN

// SystemClass를 사용할 수 있도록 여기서 프레임워크의 다른 두 클래스를 인클루드한다.
//////////////
// INCLUDES //
//////////////
#include <windows.h>
//Windows.h가 인클루드됨으로써 우리는 window를 생성/파괴할 수 있고 다른 유용한 win32 함수들을 사용할 수 있다.

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "InputClass.h"
#include "GraphicsClass.h"
// class의 정의가 굉장히 단순하다.
// WinMain에서 호출되는 Initialize, ShutDown, Run 함수를 확인할 수 있다.
// 그리고 앞서 소개했던 m_Input과 m_Graphics 객체를 가리키는 포인터가 private 멤버로 선언된 것을 확인할 수 있다.

////////////////////////////////////////////////////////////////////////////////
// Class name: SystemClass
////////////////////////////////////////////////////////////////////////////////
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 m_applicationName;
	HINSTANCE m_hinstance;
	HWND m_hwnd;

	InputClass* m_Input;
	GraphicsClass* m_Graphics;
};


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


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


#endif

또한 이 클래스에는 WndProc 함수와 ApplicationHandle 포인터가 포함되어 있으며 그래서 우리는 윈도우즈 시스템의 메세지 루프를 SystemClass의 MessageHandler 함수로 중계할 수 있다.

 

이제 SystemClass 소스 파일을 보자.

SystemClass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "SystemClass.h"
// 클래스 생성자는 포인터 객체를 null로 초기화한다.
// 이러한 객체는 초기화가 실패했을 때 지워야 하기 때문에 굉장히 중요한 과정이다.
// 만약 개체가 null이 아니면 지울 대상으로 가정한다.
// 그래서 애플리케이션에서 모든포인터와 변수를 null로 초기화하는 것이 좋다.
// 어떤 릴리즈 빌드는 그렇게 하지 않으면 실패하기도 한다.
SystemClass::SystemClass()
{
	m_Input = 0;
	m_Graphics = 0;
}

// 빈 복사 생성자와 소멸자를 만든다.
// 만약 만들지 않으면 일부 컴파일러는 이를 자동으로 생성하므로 비워서 만드는게 좋다.
// 또한 소멸자에서는 객체를 파괴하지 않는다.
// 소멸자가 불리지 않을 수도 있기 때문이다.
// ExitThread()와 같은 특정한 윈도 함수는 클래스 소멸자를 부르지 않아 memory leak을 일으키는 것으로 알려져 있다.
// 물론 더 안전한 버전의 함수를 호출할 수도 있지만, 윈도 운영체제에서는 더욱 조심해서 짜려고 한다.
SystemClass::SystemClass(const SystemClass& other)
{
}

SystemClass::~SystemClass()
{
}
// 다음 Initialize 함수는 어플리케이션의 모든 설정을 수행한다.
// 먼저 어플리케이션에서 사용할 창을 생성하는 InitializeWindows를 호출한다.
// 또한 Input 과 Graphics 객체를 초기화한다.
bool SystemClass::Initialize()
{
	int screenWidth, screenHeight;
	bool result;


	// Initialize the width and height of the screen to zero before sending the variables into the function.
	screenWidth = 0;
	screenHeight = 0;

	// Initialize the windows api.
	InitializeWindows(screenWidth, screenHeight);

	// Create the input object.  This object will be used to handle reading the keyboard input from the user.
	m_Input = new InputClass;
	if(!m_Input)
	{
		return false;
	}

	// Initialize the input object.
	m_Input->Initialize();

	// Create the graphics object.  This object will handle rendering all the graphics for this application.
	m_Graphics = new GraphicsClass;
	if(!m_Graphics)
	{
		return false;
	}

	// Initialize the graphics object.
	result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
	if(!result)
	{
		return false;
	}
	
	return true;
}

// Shutdown 함수는 Clean Up을 수행한다.
// Input과 Graphics에 관련된 모든것을 종료하고 release한다.
// 창과 그와 관련된 handle도 모두 닫는다.
void SystemClass::Shutdown()
{
	// Release the graphics object.
	if(m_Graphics)
	{
		m_Graphics->Shutdown();
		delete m_Graphics;
		m_Graphics = 0;
	}

	// Release the input object.
	if(m_Input)
	{
		delete m_Input;
		m_Input = 0;
	}

	// Shutdown the window.
	ShutdownWindows();
	
	return;
}

// Run 함수는 어플리케이션의 루프와 종료 전까지의 모든 프로세스가 실행되는 곳이다.
// 어플리케이션의 프로세스는 loop라고 불리는 프레임 함수에서 처리된다.
// 나머지 코드는 이것을 염두에 두고 작성될 것이므로 반드시 이해해야 하는 개념이다.
// 수도 코드는 다음과 같다
/*
while not done
    check for windows system messages
    process system messages
    process application loop
    check if user wanted to quit during the frame processing
*/
void SystemClass::Run()
{
	MSG msg;
	bool done, result;


	// Initialize the message structure.
	ZeroMemory(&msg, sizeof(MSG));
	
	// Loop until there is a quit message from the window or the user.
	done = false;
	while(!done)
	{
		// Handle the windows messages.
		if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		// If windows signals to end the application then exit out.
		if(msg.message == WM_QUIT)
		{
			done = true;
		}
		else
		{
			// Otherwise do the frame processing.
			result = Frame();
			if(!result)
			{
				done = true;
			}
		}

	}

	return;
}
// 다음의 Frame 함수는 어플리케이션의 모든 프로세스가 완료되는 곳이다.
// 체크하는건 꽤 단순한데, 사용자가 esc를 눌러 종료하길 원하는지 확인하면 된다.
// 만약 종료를 원하지 않을 땐 Graphics 객체를 호출해 해당 프레임을 처리하여 렌더링하도록 한다.
// 어플리케이션이 확장되면서 우리는 여기에 더 많은 코드를 삽입할 것이다.
bool SystemClass::Frame()
{
	bool result;


	// Check if the user pressed escape and wants to exit the application.
	if(m_Input->IsKeyDown(VK_ESCAPE))
	{
		return false;
	}

	// Do the frame processing for the graphics object.
	result = m_Graphics->Frame();
	if(!result)
	{
		return false;
	}

	return true;
}
// MessageHandler 함수는 창의 시스템 메세지가 바로 전달되는 곳이다.
// 이렇게 하면 우리가 관심있는 특정한 정보들에 대해 관찰할 수 있다.
// 현재 우리는 키가 눌렸는지 떼어졌는지를 읽어서 Input 객체에 전달할 것이다.
// 모든 다른 정보들은 윈도우 기본 메세지 핸들러로 다시 전달된다.
LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
	switch(umsg)
	{
		// Check if a key has been pressed on the keyboard.
		case WM_KEYDOWN:
		{
			// If a key is pressed send it to the input object so it can record that state.
			m_Input->KeyDown((unsigned int)wparam);
			return 0;
		}

		// Check if a key has been released on the keyboard.
		case WM_KEYUP:
		{
			// If a key is released then send it to the input object so it can unset the state for that key.
			m_Input->KeyUp((unsigned int)wparam);
			return 0;
		}

		// Any other messages send to the default message handler as our application won't make use of them.
		default:
		{
			return DefWindowProc(hwnd, umsg, wparam, lparam);
		}
	}
}
// InitializeWindows 함수는 우리가 렌더링할 창을 만드는 코드를 넣는 곳이다.
// screenWidth와 screenHeight을 반환하여 모든 애플리케이션 전반에 사용할 수 있도록 한다.
// 기본 설정으로 창을 생성하여 보더가 없는 단순한 검정 창을 초기화 할 것이다.
// 이 함수는 FULL_SCREEN이라는 전역 변수에 따라 창 모드 혹은 전체화면 모드를 만들어 낸다.
// 이 값이 true이면 전체 데스크탑 화면을 모두 가리게끔 만들어 질 것이다.
// 이 값이 false이면 화면 중앙에 800x600의 창을 만든다.
// GraphicsClass.h 최상단에 이 전역 변수를 수정할 수 있도록 작성해 두었다.
// 이 파일의 헤더가 아닌 그 파일의 전역 변수로 선언한 이유를 나중에 이해할 수 있다.
void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
{
	WNDCLASSEX wc;
	DEVMODE dmScreenSettings;
	int posX, posY;


	// Get an external pointer to this object.	
	ApplicationHandle = this;

	// Get the instance of this application.
	m_hinstance = GetModuleHandle(NULL);

	// Give the application a name.
	m_applicationName = L"Engine";

	// Setup the windows class with default settings.
	wc.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc   = WndProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = m_hinstance;
	wc.hIcon         = LoadIcon(NULL, IDI_WINLOGO);
	wc.hIconSm       = wc.hIcon;
	wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = m_applicationName;
	wc.cbSize        = sizeof(WNDCLASSEX);
	
	// Register the window class.
	RegisterClassEx(&wc);

	// Determine the resolution of the clients desktop screen.
	screenWidth  = GetSystemMetrics(SM_CXSCREEN);
	screenHeight = GetSystemMetrics(SM_CYSCREEN);

	// Setup the screen settings depending on whether it is running in full screen or in windowed mode.
	if(FULL_SCREEN)
	{
		// If full screen set the screen to maximum size of the users desktop and 32bit.
		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;

		// Change the display settings to full screen.
		ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);

		// Set the position of the window to the top left corner.
		posX = posY = 0;
	}
	else
	{
		// If windowed then set it to 800x600 resolution.
		screenWidth  = 800;
		screenHeight = 600;

		// Place the window in the middle of the screen.
		posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth)  / 2;
		posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
	}

	// Create the window with the screen settings and get the handle to it.
	m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName, 
				WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
				posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL);

	// Bring the window up on the screen and set it as main focus.
	ShowWindow(m_hwnd, SW_SHOW);
	SetForegroundWindow(m_hwnd);
	SetFocus(m_hwnd);

	// Hide the mouse cursor.
	ShowCursor(false);

	return;
}
// 그냥 말그대로 창을 끈다.
// 스크린 설정을 보통으로 돌리고 창과 핸들을 release 한다.
void SystemClass::ShutdownWindows()
{
	// Show the mouse cursor.
	ShowCursor(true);

	// Fix the display settings if leaving full screen mode.
	if(FULL_SCREEN)
	{
		ChangeDisplaySettings(NULL, 0);
	}

	// Remove the window.
	DestroyWindow(m_hwnd);
	m_hwnd = NULL;

	// Remove the application instance.
	UnregisterClass(m_applicationName, m_hinstance);
	m_hinstance = NULL;

	// Release the pointer to this class.
	ApplicationHandle = NULL;

	return;
}

// WndProc 함수는 창이 메세지를 보내는 곳이다.
// 창을 초기화할때 상단의 InitializeWindows 함수에서 wc.IpfnWndProc = WndProc로 창의 이름을 전달한다.
// SystemClass내에 정의된 MessageHandler 함수로 모든 메세지를 바로 전달함으로써 코드를 꺠끗하게 유지시킬 수 있다.
LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
	switch(umessage)
	{
		// Check if the window is being destroyed.
		case WM_DESTROY:
		{
			PostQuitMessage(0);
			return 0;
		}

		// Check if the window is being closed.
		case WM_CLOSE:
		{
			PostQuitMessage(0);		
			return 0;
		}

		// All other messages pass to the message handler in the system class.
		default:
		{
			return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
		}
	}
}

InputClass.h

튜토리얼을 간단하게 만들기 위해서 당분간 윈도우 입력을 사용할 것이다.

Input Class는 키보드로부터 들어온 사용자 입력을 처리한다.

이 클래스는 SystemClass::MessageHandler 함수로부터 입력을 전달받는다.

Input 객체는 키보드 배열에 모든 키의 상태를 저장한다.

이것이 쿼리될 때 특정한 키가 눌렸는지 호출한 함수에게 전달될 것이다.

다음은 헤더파일이다:

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


////////////////////////////////////////////////////////////////////////////////
// Class name: InputClass
////////////////////////////////////////////////////////////////////////////////
class InputClass
{
public:
	InputClass();
	InputClass(const InputClass&);
	~InputClass();

	void Initialize();

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

	bool IsKeyDown(unsigned int);

private:
	bool m_keys[256];
};

#endif

InputClass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "InputClass.h"


InputClass::InputClass()
{
}


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


InputClass::~InputClass()
{
}


void InputClass::Initialize()
{
	int i;
	

	// Initialize all the keys to being released and not pressed.
	for(i=0; i<256; i++)
	{
		m_keys[i] = false;
	}

	return;
}


void InputClass::KeyDown(unsigned int input)
{
	// If a key is pressed then save that state in the key array.
	m_keys[input] = true;
	return;
}


void InputClass::KeyUp(unsigned int input)
{
	// If a key is released then clear that state in the key array.
	m_keys[input] = false;
	return;
}


bool InputClass::IsKeyDown(unsigned int key)
{
	// Return what state the key is in (pressed/not pressed).
	return m_keys[key];
}

GraphicsClass.h

어플리케이션의 모든 그래픽 기능이 이 클래스에 캡슐화된다.

전체화면/창모드와 같이 변경 가능한 모든 그래픽 관련 설정들도 이 파일의 헤더에 기록된다.

현재 이 클래스는 비어있지만 추후 모든 그래픽 개체를 포함한다.

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#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 name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
	GraphicsClass();
	GraphicsClass(const GraphicsClass&);
	~GraphicsClass();

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

private:
	bool Render();

private:

};

#endif

GraphicsClass.cpp

아직 구현부가 비어있지만 앞으로 채워넣을 것이다.

////////////////////////////////////////////////////////////////////////////////
// Filename: 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;
}

요약

이제 프레임워크가 있고 스크린에 창이 나타날 것이다.

이 프레임워크는 추후 튜토리얼의 베이스가 될 것이므로 이 프레임워크를 이해하는 것은 굉장히 중요하다.

To Do Excercise 를 통해 반드시 연습을 하고 코드를 컴파일해서 다음 튜토리얼로 진행할 수 있도록 하자.

이 프레임워크를 이해하지 못해도 다음으로 넘어갈 수 있다. 나중에 채워나가면서 이해도가 높아질 수 있다.

결과

중앙에 검은 창이 나타난다.

 
반응형

댓글