[DirectX11] 1일차 (윈도우 창 띄우기)
지금 기준으로는 DirectX 12 버전까지 나와있다.
Direct3D 11과 Direct3D 12는 제공해주는 건 같은데, 12가 더 자유도가 높다.
11 버전을 마스터했을 때, 세부적인 것들을 건드리고 싶다면 12 버전을 건드리자.
최신 버전 중에 입문하기는 11 버전이 제일 좋은 것 같다.
9 버전은 좀 오래됨.
DirectX 12 버전과 짝이 맞는 OpenGL 버전은 VulKan이다.
'DirectX 11을 이용한 3D 게임 프로그래밍 입문' 책을 기반으로 공부함.
여기 정리가 잘 되어있음. 공부할 때 참고.
지금은 일단 창부터 띄워야 한다.
DirectX는 그래픽 라이브러리고, 윈도우에서 창을 띄우는 것은 WinApi를 사용해야 한다.
WinApi를 사용해서 창을 띄워보자.
visual studio 2019에서 빈 프로젝트 생성하고, main 파일 추가.
구글에 msdn winmain 검색하면, winmain 함수에 대해서 나온다.
그대로 복사하면, 이렇게 되는데
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 자료형으로 쓰면 된다.
창을 띄울 때 설정 값을 지정할 수 있는데, 이런 거는 구글링으로 정리된 문서에 가서 봐야 한다.
이번에는 msdn window procedure 검색해보자.
OS에서 발생하는 이벤트(마우스, 키보드 입력 등)들에 콜백을 걸 수 있도록 지원한다.
말이 길어졌는데, 모두 모은 코드이다.
한 줄 한 줄 이해한다고 주석을 엄청 열심히 달았다. 보기엔 안 좋지만 이해는 확실히 되는 것 같다.
#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).
}
}
}
순서대로 만들 때는 한 줄 한 줄이 어렵지 않았는데, 마지막 코드를 보니까 어떻게 만들었나 싶다.
외워서 코딩하는 게 아니라, 방식을 이해하고 차근차근 코딩하는 게 훨씬 좋은 것 같다.