이 책의 3장에서 배운 파이프라인 초기화 과정을 나만의 방식으로 구현해보고 그에 대한 내용을 기록해보려고 한다.

좀 더 구체적으로 내 스스로 DirectX12를 초기화하고 하나의 창을 띄워보는 것이다.

책에서 알려준 개념을 나름대로 재해석해서 연습해보는 것이기 때문에 정확하진 않겠지만,

머리속에서 개념이 잘 정립되길 기대하고있다.

 

WinMain, WndProc 구조 만들기 

첫번째로 해야할 것은 우선 WinAPI를 이용해서 윈도우창을 만들자.

나는 WinMain을 사용자가 직접 처리하게 하는 게 아닌 미리 초기화해서 자동화를 할 생각이다.

예를 들어, 사용자는 관련 라이브러리 헤더파일만 불러오고 update()함수만 작성해서 

게임 로직에만 신경쓸 수 있도록 하고 싶다. 

그러므로 WndProc, WinMain은 Framework 헤더파일 내부에서 전부 처리해낼 계획이다. 

물론 이 방법이 비효율적이라고 판단이 들게되면 구조를 다시 바꿀 여지는 있다.

 

일단 DirectX12와 WinAPI를 사용하려면 관련 헤더 및 라이브러리를 가져와야 한다.

그 방법은 Microsoft에서는 다음처럼 안내하고 있다. 

Direct3D 12 헤더 및 라이브러리는 Windows 10 SDK의 일부입니다. Direct3D 12를 사용하기 위해 별도의 다운로드 또는 설치가 필요하지 않습니다.
Windows 10 SDK 소프트웨어를 설치하고 Visual Studio 후에는 Direct3D 12 프로그래밍 환경의 설정이 완료됩니다. Visual Studio 2019는 D3D12 그래픽 디버깅 도구를 포함하지만 이전 버전의 Visual Studio 프로그램 개발에 적합하므로 권장됩니다.
Direct3D 12 API를 사용하려면 d3d12.h를 포함하고 d3d12.lib에 연결하거나, d3d12.dll에 직접 진입점을 쿼리합니다.

 

책의 예제 프레임워크에서는 #pragma comment를 통해 lib를 직접 불러오고 있다.

알아보니, #pragma comment를 사용하는 이유는 명시적으로 lib을 불러왔다는 표시 겸 사용하는 거라고 한다.

난 아직 배우고 있는 초보단계이고 예제처럼 판을 크게 벌리면 오히려 구조를 짜는데 방해가 될 듯해서 우선 간소하게 윈도우창만 만들어보고 추후에 필요한 부분을 점진적으로 추가해나가는게 좋다고 생각이 든다.

아무튼 우선 d3d12.h 헤더 파일을 불러와서 사용해보기로 했다. ( d3d12.h에는 WinAPI가 포함되어 있다. )

그리고 대략 어떤게 필요할까 고민하면서 클래스를 작성해본다.

내가 고민해서 작성해본 클래스의 형태는 다음과 같다.  

 

Framework.h
#pragma once

#include <d3d12.h>

class Framework
{
public:
	Framework( HINSTANCE hInst );

	// 윈도우와 Direct Device를 초기화한다.
	bool Initialize( );
	void InitMainWindow( );


private:
	HINSTANCE mhInstance;

};

 

Framework.cpp
#include "Framework.h"

// 윈도우 응용프로그램의 시작점이다.
int WINAPI WinMain(
	HINSTANCE hInstance,      // 응용 프로그램의 인스턴스 핸들이다.
	HINSTANCE hPrevInstance,  // 16비트와의 호환성을 위해서 존재하는 인수 ( 쓸 일이 없다 )
	LPSTR lpszCmdParam,       // 도스의 argv인수에 해당된다. 
	int nCmdShow )            // 응용 프로그램이 "기본창 or 최소화"일 때 상태 메세지를 전달받는다. http://www.soen.kr/lecture/win32api/lec9/lec9-4-2.htm
{
	Framework framework( hInstance );
	framework.Initialize( );

}

// 사용자나 운영체제로부터 오는 메세지를 처리한다.
// CALLBACK이라는 매크로가 있는데, _stdcall로 정의되어있다.
// 이는 호환성과 이식성을 위한 매크로이다.
// Win32는 _stdcall 표준 호출 규약을 따르는데 이를 따르지 않는
// 다른 시스템과의 호환성을 대비해 중간 매크로를 설정해둔 것이다.
LRESULT CALLBACK WndProc(
	HWND hWnd,               
	UINT iMessage,
	WPARAM wParam,
	LPARAM lParam )
{

}


Framework::Framework( HINSTANCE hInst )
	: mhInstance( hInst )
{
	// TODO:: 생성자 작성하기
}

bool Framework::Initialize( )
{
	return false;
}

 

WinMain은 프로그램이 시작되는 곳이다. 여기서 Framework 초기화를 진행해준다.

우선 초기화 구문을 구현하기 위해 고려해야할 부분을 크게 2가지로 정했다. 

1. 메인 윈도우 생성하기

2. Device 생성하기.

WinMain 함수는 다음처럼 작성해준다. 

// 윈도우 응용프로그램의 시작점이다.
// http://www.soen.kr/lecture/win32api/lec9/lec9-4-2.htm
int WINAPI WinMain(
	HINSTANCE hInstance,      // 응용 프로그램의 인스턴스 핸들이다.
	HINSTANCE hPrevInstance,  // 16비트와의 호환성을 위해서 존재하는 인수 ( 쓸 일이 없다 )
	LPSTR lpszCmdParam,       // 도스의 argv인수에 해당된다. 
	int nCmdShow )            // 응용 프로그램이 "기본창 or 최소화"일 때 상태 메세지를 전달받는다. 
{
	FUJI::Framework framework( hInstance );
	framework.Initialize( );
}

 

 

FUJI:: namespace가 보일텐데 이건 나중에 외부에서 update() 함수만으로 게임을 만들 때 외부 라이브러리와 충돌을 방지하기 위해 namespace를 사용했다. 그 외에 헤더에서 바뀐 부분은 다음과 같다.

 

#pragma once

#include <d3d12.h>

namespace FUJI {

class Framework
{
public:
	Framework( HINSTANCE hInst );

	// 윈도우와 Direct Device를 초기화한다.
	bool Initialize( );
protected:
	bool InitMainWindow( );
	bool InitDX12Device( );
    
private:
	int mClientWidth = 800;
	int mClientHeight = 600;

	HINSTANCE mhInstance = nullptr;
	HWND      mhMainWindow = nullptr;
};

} // namespace FUJI

 

Initialize에서 메인 윈도우와 디바이스 장치를 초기화해줄 수 있도록 InitMainWindow, InitDX12Device 함수로 나누어주었다. 반환형은 bool인데 두 초기화 과정중 실패한 부분이 있다면 메세지를 통해 알리기 위함이다. getWndMessage는 윈도우에서 보내는 메세지를 처리하는 함수이다.  해당 함수들이 protected로 둘러쌓인 이유는 외부에서 사용할 수 있는 인터페이스가 아니므로 제한해두었다.

 

전체 코드

다음은 메인 윈도우 구조체를 작성하고, 등록하고 생성하는 역할을 하는

InitMainWindow함수 정의와 WndProc, WinMain 등 전체 코드이다.

 

Framework.h
#pragma once

#include <d3d12.h>

namespace FUJI {

class Framework
{
public:
	Framework( HINSTANCE hInst, HWND hWnd );

	// 윈도우와 Direct Device를 초기화한다.
	bool Initialize( );
protected:
	bool InitMainWindow( );
	bool InitDX12Device( );

private:
	int mClientWidth = 800;
	int mClientHeight = 600;

	HINSTANCE mhInstance = nullptr;
	HWND      mhMainWindow = nullptr;
};

} // namespace FUJI

 

Framework.cpp
#include "Framework.h"

// 윈도우 응용프로그램의 시작점이다.
// http://www.soen.kr/lecture/win32api/lec9/lec9-4-2.htm
int WINAPI WinMain(
	HINSTANCE hInstance,      // 응용 프로그램의 인스턴스 핸들이다.
	HINSTANCE hPrevInstance,  // 16비트와의 호환성을 위해서 존재하는 인수 ( 쓸 일이 없다 )
	LPSTR lpszCmdParam,       // 도스의 argv인수에 해당된다. 
	int nCmdShow )            // 응용 프로그램이 "기본창 or 최소화"일 때 상태 메세지를 전달받는다. 
{
	HWND hWnd = 0; // 윈도우의 핸들 
	MSG  msg  = { 0,};  // 발생된 메세지를 저장하기 위한 구조체
	FUJI::Framework framework( hInstance, hWnd );
	framework.Initialize( );

	while ( msg.message != WM_QUIT )
	{
		// 처리할 메시지가 없어도 계속 동작한다.
		// 자세한 내용은 : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=ljc8808&logNo=220344272173
		if ( PeekMessage( &msg, hWnd, 0, 0, PM_REMOVE ) ) {
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}

	}
	return static_cast<int>( msg.wParam );
}

// 사용자나 운영체제로부터 오는 메세지를 처리한다.
// CALLBACK이라는 매크로가 있는데, _stdcall로 정의되어있다.
// 이는 호환성과 이식성을 위한 매크로이다.
// Win32는 _stdcall 표준 호출 규약을 따르는데 이를 따르지 않는
// 다른 시스템과의 호환성을 대비해 중간 매크로를 설정해둔 것이다.
LRESULT CALLBACK WndProc(
	HWND hWnd,
	UINT iMessage,
	WPARAM wParam,
	LPARAM lParam )
{

	switch ( iMessage ) {
		case WM_DESTROY: {  // 사용자가 종료시킬 경우
			PostQuitMessage( 0 );  // WM_QUIT 메세지를 보낸다
			return 0;
		}break;
	}

	return DefWindowProc( hWnd, iMessage, wParam, lParam );
}


namespace FUJI {

Framework::Framework( HINSTANCE hInst, HWND hWnd )
	: mhInstance( hInst )
	, mhMainWindow( hWnd )
{
	// TODO:: 생성자 작성하기
}

bool Framework::Initialize( )
{
	if ( InitMainWindow( ) == false )
	{ return false; }



	return true;
}

bool Framework::InitMainWindow( )
{
	WNDCLASS wndDesc;
	wndDesc.style = CS_HREDRAW | CS_VREDRAW; // 수직, 수평 크기가 변경될 경우 윈도우를 다시 그린다.
	wndDesc.lpfnWndProc = WndProc;
	wndDesc.cbClsExtra = 0;
	wndDesc.cbWndExtra = 0;
	wndDesc.hInstance = mhInstance;
	wndDesc.hIcon = LoadIcon( 0, IDI_APPLICATION );
	wndDesc.hCursor = LoadCursor( 0, IDC_ARROW );
	wndDesc.hbrBackground = WHITE_BRUSH;
	wndDesc.lpszMenuName = 0;
	wndDesc.lpszClassName = L"MainWindow";

	// 윈도우 정보가 담긴 구조체를 설정한 후
	// RegisterClass 함수를 호출해서 윈도우 클래스를 등록해준다.
	// 운영체제가 이 윈도우 클래스를 읽고 기억해놓는다.
	if ( !RegisterClass( &wndDesc ) ) {
		MessageBox( 0, L"RegisterClass Failed", 0, 0 );
		return false;
	}

	// 이 함수는 사용자가 원하는 클라이언트 크기를 계산해준다.
	// 자세한 내용은 https://m.blog.naver.com/gjduddnr5923/220112741125
	RECT R = { 0, 0, mClientWidth, mClientHeight };
	AdjustWindowRect( &R, WS_OVERLAPPEDWINDOW, false );  // 3번째 인자는 메뉴의 유무이다. true / false
	int width = R.right - R.left;
	int height = R.bottom - R.top;

	// 등록된 윈도우 클래스를 가지고 
	// 윈도우 정보들을 메모리에 생성한다.
	// 메모리를 생성했을 뿐이며, 표시하지는 않는다.
	mhMainWindow = CreateWindow(
		L"MainWindow",        // lpClassName : 윈도우를 의미하는 문자열을 지정한다. 
		L"MyDX12",            // lpWindowName : 윈도우의 타이틀바에 나타낼 문자열을 지정한다.
		WS_OVERLAPPEDWINDOW,  // dwStyle : 윈도우의 형태를 결정하는 비트 필드값이다. (스크롤바 유무 등) 현재 기본값으로 설정했다.
		CW_USEDEFAULT, CW_USEDEFAULT, // x, y : 윈도우의 표시 위치이다. 기본 값은 CW_USEDEFAULT 
		width, height,        // nWidth, nHeight : 윈도우의 크기이다.
		NULL, NULL,           // 부모 윈도우의 핸들과 메뉴 핸들이다. 현재 메인 윈도우는 부모가 없는 최상위 윈도우이므로, NULL로 해준다. 
		mhInstance, NULL );   // 윈도우를 만드는 프로그램의 주체, 즉 WinMain의 핸들을 넘겨준다. lpvParam은 윈도우의 파라미터 값을 전달하는 용도이다.

	if ( !mhMainWindow ) {
		MessageBox( 0, L"CreateWindow Failed", 0, 0 );
		return false;
	}

	// 윈도우를 표시한다. 
	// nCmdShow는 윈도우를 출력할 방법을 지정한다.
	/* 매크로 상수    의미
	* ---------------------------------------------------------
	*  SW_HIDE        윈도우를 숨긴다.
	*  SW_MINIMIZE    윈도우를 최소화하고 활성화시키지 않는다.
	*  SW_RESTORE     윈도우를 활성화시킨다.
	*  SW_SHOW        윈도우를 활성화하여 보여준다.
	*  SW_SHOWNORMAL  윈도우를 활성화하여 보여준다.
	* ---------------------------------------------------------*/
	// 또는 WinMain에서 전달된 nCmdShow를 전달한다. 
	// 이는 프로그램을 실행시킨 쉘로부터 전달된 값이며,
	// 사용자가 프로그램 등록 정보 대화상자에서 설정한 값이다.
	// UpdateWindow 함수는 갱신할 영역이 있으면 
	// 즉각적으로 갱신시키라는 명령을 전달하는 함수다.
	// 이에 대한 자세한 내용은 https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=tipsware&logNo=221697472368
	ShowWindow( mhMainWindow, SW_SHOW );
	UpdateWindow( mhMainWindow );

	return true;
}

bool Framework::InitDX12Device( )
{
	return true;
}


}  // namespace FUJI

 

일일히 하나하나 메모하기엔 코드 덩어리가 너무 커서 자세한 내용은 주석으로 남겨두었다.

 

 

빌드 결과

 

이렇게 창을 하나 띄울 수 있게 되었다.

사실상 지금까지 만든건 창을 띄우기 위한 WinAPI 개념이 전부다.

내일은 해당 창에다가 본격적으로 DX Device장치를 생성하고 각종 초기화를 작성해볼 계획이다.

 

 


 

'과거 자료' 카테고리의 다른 글

[메모] 객체지향 디자인  (0) 2023.02.18
#1 C++ 메모  (0) 2023.02.12
[DX12] IDXGISwapChain (dxgi.h)  (0) 2022.12.12
[DX12] Texture format (텍스처 형식)  (0) 2022.12.12
[DX12] Microsoft::WRL::ComPtr  (0) 2022.12.12