기타/DirectX

[DirectX11] 1일차 (윈도우 창 띄우기)

푸쿠이 2021. 2. 15. 14:46

지금 기준으로는 DirectX 12 버전까지 나와있다.

Direct3D 11과 Direct3D 12는 제공해주는 건 같은데, 12가 더 자유도가 높다.

11 버전을 마스터했을 때, 세부적인 것들을 건드리고 싶다면 12 버전을 건드리자.

 

최신 버전 중에 입문하기는 11 버전이 제일 좋은 것 같다.

9 버전은 좀 오래됨.

 

DirectX 12 버전과 짝이 맞는 OpenGL 버전은 VulKan이다.

 

'DirectX 11을 이용한 3D 게임 프로그래밍 입문' 책을 기반으로 공부함.

여기 정리가 잘 되어있음. 공부할 때 참고.

http://soen.kr/

 

 

지금은 일단 창부터 띄워야 한다.

DirectX는 그래픽 라이브러리고, 윈도우에서 창을 띄우는 것은 WinApi를 사용해야 한다.

WinApi를 사용해서 창을 띄워보자.

 

visual studio 2019에서 빈 프로젝트 생성하고, main 파일 추가.

구글에 msdn winmain 검색하면, winmain 함수에 대해서 나온다.

 

WinMain The Application Entry Point - Win32 apps

.

docs.microsoft.com

 

그대로 복사하면, 이렇게 되는데

hInstance 부분이 윈도우에서 뜨는 창 하나의 객체이다.

#include <Windows.h>

int WINAPI wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	PWSTR pCmdLine,
	int nCmdShow)
{

}

PWSTR 자료형을 찾아들어가보면 wchar_t 포인터 자료형이다.  wchar_t는 2Byte의 Char 자료형이다.

다국어를 처리하다보니 2Byte를 필요로 한 것이다. wide character이다.

굳이 2Byte char자료형을 쓸 필요는 없으니까, LPSTR로 바꾸고 함수명에서 w를 빼면 똑같이 작동한다.

#include <Windows.h>

int WINAPI WinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR pCmdLine, // 찾아들어가면 char 포인터 자료형
	int nCmdShow)
{

}

 

그러면 컴파일은 성공이 되는데, 솔루션 빌드는 실패한다.

솔루션 빌드는 컴파일 + 링커이다.

링커를 설정하면 솔루션 빌드를 할 수 있다.

32bit 64bit 등처럼 플랫폼에 따라 각각 설정할 수 있는데,

지금은 구분 안 할거니까 모든 플랫폼으로 해서 창으로 바꿔준다.

구글에 msdn wndclass를 검색해보면, 이 구조체가 나온다.

우리는 이 구조체를 쓸 때, WNDCLASS 자료형으로 쓰면 된다.

 

WNDCLASSA (winuser.h) - Win32 apps

Contains the window class attributes that are registered by the RegisterClass function.

docs.microsoft.com

창을 띄울 때 설정 값을 지정할 수 있는데, 이런 거는 구글링으로 정리된 문서에 가서 봐야 한다.

이번에는 msdn window procedure 검색해보자.

OS에서 발생하는 이벤트(마우스, 키보드 입력 등)들에 콜백을 걸 수 있도록 지원한다.

 

Writing the Window Procedure - Win32 apps

.

docs.microsoft.com

말이 길어졌는데, 모두 모은 코드이다.

한 줄 한 줄 이해한다고 주석을 엄청 열심히 달았다. 보기엔 안 좋지만 이해는 확실히 되는 것 같다.

#include <Windows.h>

// 창의 이벤트를 처리하는 함수.
// 콜백(Callback).
LRESULT CALLBACK WindowProc(
	HWND hwnd,
	UINT uMsg,
	WPARAM wParam,
	LPARAM lParam)
{
	// 기본 처리만. 윈도우 디폴드 설정에 따라 처리해달라는 것.
	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR pCmdLine,
	int nCmdShow)
{
	// 윈도우 클래스 선언.
	WNDCLASS wc;

	// 모두 0으로 초기화.
	ZeroMemory(&wc, sizeof(wc));

	// 필요한 값만 설정 (밑에서 다시 설명.)
	wc.hInstance = hInstance;
	wc.lpszClassName = TEXT("GraphicsEngine");
	wc.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
	wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = WindowProc;.

	// 윈도우 클래스 등록.
	if (RegisterClass(&wc) == false) // 설정 값을 지정할 때, 실수했을 수도 있으니까.
	{
		exit(-1);
	}

	// 창 생성 (만들기).
	// 창이 문제 없이 생성되면 핸들 값 반환.
	HWND hwnd = CreateWindow( // 함수 파라미터를 볼 때는 Ctrl+Shift+Space 누르면 보임.
		wc.lpszClassName, // 등록한 클래스 이름으로 지정을 해줘야한다.
		TEXT("그래픽스 엔진"),
		WS_OVERLAPPEDWINDOW,
		0, 0,
		1280, 800,
		NULL, NULL,
		hInstance,
		NULL
	);

	// 오류 검사.
	if (hwnd == NULL)
	{
		exit(-1);
	}

	// 창 보이기.
	ShowWindow(hwnd, SW_SHOW); // 창 띄우고
	UpdateWindow(hwnd); // 업데이트해야 보임. 한 쌍으로 쓴다고 보면 됨.

	// 테스트.
	while(true); // 바로 창이 안꺼지게 일단 무한루프 걸어 놈. 끌 때는 툴에서 디버깅 중지시키면 꺼짐.
}

필요한 값만 설정. 부분의 주석이 많아서 따로 분리했다.

하나하나 이해하고 가는 게 내가 뭘 배우는지 알 수 있는 것 같다.

wc.hInstance = hInstance;

// wide character이기 때문에 L"GraphEngine"처럼 L 붙여야 함. 
// TEXT함수를 쓰면 wide character인지 아닌지 신경안쓰고, 자동으로 됨.
wc.lpszClassName = TEXT("GraphicsEngine"); 

// 창이 하나라서 hInstance 넣어도 됨. null 넣으면 알아서 찾아서 넣음.
// 아이콘 모양 지정한 것.
wc.hIcon = LoadIcon(nullptr, IDI_APPLICATION); 

// IDC는 Cursor의 ID, IDI는 Icon의 ID이다. 위랑 마찬가지로 null 넣으면 알아서 찾는다.
// 화살표 모양으로 커서를 지정한 것.
wc.hCursor = LoadCursor(nullptr, IDC_ARROW); 

// H는 Horizontal(가로), V는 Vertical(세로).
// 창 크기가 변경되면 다시 그리라고 지정한 것.
wc.style = CS_HREDRAW | CS_VREDRAW; 

// 함수 포인터. OS에서 발생하는 이벤트(키보드, 마우스 등)들이 발생할 때 알려달라는 것.
wc.lpfnWndProc = WindowProc; 

이렇게 하면 요렇게 창이 뜨는 것까지 완성이다.

디버깅(F5)을 해보면 무한루프를 걸어놔서 클릭이 안 먹는다. 디버깅 중지를 눌러서 끄면 된다.

테스트로 작성한 무한루프 말고, 이벤트를 받는 루프로 수정해보았다.

// 메시지 처리 루프.
// 엔진 루프.
MSG msg;
ZeroMemory(&msg, sizeof(msg));

while (msg.message != WM_QUIT)
{
	// 메시지 처리.
	// GetMessage(&msg, hwnd, 0, 0);
	if (PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE)) // PM_REMOVE의 자리는 이 메세지를 쓰고 어떡할거냐.의 의미인데 지운다는 것임.
	{
		// 메시지 해석해줘.
		TranslateMessage(&msg);
		// 메시지를 처리해야할 곳에 전달해줘.
		DispatchMessage(&msg);
	}
	else
	{
		// 우리의 일.
		// 그리기 (DirectX 11).
	}
}

이제 WinAPI를 사용해서 기본은 만들었다.

그리는 부분에서 삼각형을 그리든 뭘 하든 하면 된다.

 

그 전에 하나만 더 수정하자.

창 크기를 수정할 때 1280, 800으로 설정했는데,

이게 창 내용 중에서 본 내용의 크기가 아니라 창의 테두리, 타이틀 바와 같은 것도 포함된다.

크기 조정을 해줘야 한다.

// 사각형 조정
RECT rect = { 0, 0, 1280, 800 };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;

// 창 생성 (만들기).
// 창이 문제 없이 생성되면 핸들 값 반환.
HWND hwnd = CreateWindow(
	wc.lpszClassName,
	TEXT("그래픽스 엔진"),
	WS_OVERLAPPEDWINDOW,
	0, 0,
	width, height, // 조정한 크기로 지정.
	NULL, NULL,
	hInstance,
	NULL
 );

 

단축키

Ctrl+Home/End 문서에서 가장 앞/뒤.

Home/End 그 줄에서 가장 앞/뒤.

alt+shift+키보드화살표 여러 줄 변경.

Ctrl+K+O 헤더파일 cpp 파일 왔다갔다.

 

 

이제 만들었으니까 이걸 클래스화 시켜서 directX와 연동하면 된다.

클래스를 만들어서 옮겨보자.

 

Window.h

#pragma once

#include <Windows.h>
#include <string> // STL - Standard Template Library.

class Window
{
public:
	static bool InitializeWindow();

	static bool InitializeWindow(
		HINSTANCE hInstance,
		int width,
		int height,
		std::wstring title
	);

	// Getter / Setter.
	static HWND WindowHandle() { return hwnd; }

	static HINSTANCE Instance() { return hInstance; }
	static void SetInstance(HINSTANCE hInstance);

	static int Width() { return width; }
	static void SetWidth(int width);

	static int Height() { return height; }
	static void SetHeight(int height);

	static std::wstring Title() { return title; }
	static void SetTitle(std::wstring title);


private:
	static int width;			// 가로 길이.
	static int height;			// 세로 길이.
	static std::wstring title;	// 윈도우 제목(타이틀). wide string 한글 처리에도 문제없도록.
	static HWND hwnd;			// 윈도우 핸들.
	static HINSTANCE hInstance;	// 윈도우 인스턴스.
};

Window.cpp

#include "Window.h"

int Window::width;
int Window::height;
std::wstring Window::title;
HWND Window::hwnd;
HINSTANCE Window::hInstance;

LRESULT CALLBACK WindowProc(
	HWND hwnd,
	UINT uMsg,
	WPARAM wParam,
	LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_DESTROY:
	{
		PostQuitMessage(0);
	}
	return 0;

	case WM_KEYDOWN:
	{
		if (wParam == VK_ESCAPE)
		{
			if (MessageBox(
				NULL,
				TEXT("종료하시겠습니까?"),
				TEXT("종료"),
				MB_YESNO | MB_ICONQUESTION) == IDYES)
			{
				// 창 삭제.
				DestroyWindow(hwnd);
			}
		}
	}
	return 0;
	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

bool Window::InitializeWindow()
{
    return InitializeWindow(hInstance, width, height, title);
}

bool Window::InitializeWindow(
    HINSTANCE hInstance,
    int width,
    int height,
    std::wstring title)
{
    Window::hInstance = hInstance;
    Window::width = width;
    Window::height = height;
    Window::title = title;

	// 윈도우 클래스 선언.
	WNDCLASS wc;

	// 모두 0으로 초기화.
	ZeroMemory(&wc, sizeof(wc));

	// 필요한 값만 설정.
	wc.hInstance = hInstance;
	wc.lpszClassName = TEXT("GraphicsEngine");
	wc.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
	wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = WindowProc;

	// 윈도우 클래스 등록.
	if (RegisterClass(&wc) == false)
	{
		exit(-1);
	}

	// 사각형 조정
	RECT rect = { 0, 0, width, height };
	AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
	Window::width = rect.right - rect.left;
	Window::height = rect.bottom - rect.top;

	// 창 생성 (만들기).
	// 창이 문제 없이 생성되면 핸들 값 반환.
	hwnd = CreateWindow(
		wc.lpszClassName,
		title.c_str(),
		WS_OVERLAPPEDWINDOW,
		0, 0,
		Window::width, Window::height,
		NULL, NULL,
		hInstance,
		NULL
	);

	// 오류 검사.
	if (hwnd == NULL)
	{
		exit(-1);
	}

	// 창 보이기.
	ShowWindow(hwnd, SW_SHOW); // 창 띄우고
	UpdateWindow(hwnd); // 업데이트해야 보임. 한 쌍으로 쓴다고 보면 됨.

    return true;
}
 
void Window::SetInstance(HINSTANCE hInstance)
{
	Window::hInstance = hInstance;
}
 
void Window::SetWidth(int width)
{
	Window::width = width;
}
 
void Window::SetHeight(int height)
{
	Window::height = height;
}
 
void Window::SetTitle(std::wstring title)
{
	Window::title = title;
}
 

이제는 main.cpp에서 분리했던 Window 헤더파일을 Include해서 불러보자.

이렇게 구분할 수 있다.

 

main.cpp

#include "Window.h"

int WINAPI WinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR pCmdLine,
	int nCmdShow)
{
	// Window 분리한 것 부르기
	if (Window::InitializeWindow(hInstance, 1280, 800, L"그래픽스 엔진") == false)
	{
		exit(-1);
	}

	// 메시지 처리 루프
	MSG msg;
	ZeroMemory(&msg, sizeof(msg));

	while (msg.message != WM_QUIT)
	{
		// GetMessage(&msg, hwnd, 0, 0);
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			// 우리의 일.
			// 그리기 (DirectX 11).
		}
	}
}

 

순서대로 만들 때는 한 줄 한 줄이 어렵지 않았는데, 마지막 코드를 보니까 어떻게 만들었나 싶다.

외워서 코딩하는 게 아니라, 방식을 이해하고 차근차근 코딩하는 게 훨씬 좋은 것 같다.