DirectX12の描画処理 #1 [Devlog #001]

Table of Contents

DirectX12を使った描画処理


環境構築

VisualStudio2022でプロジェクトの作成

  • (新規作成->プロジェクト->)新しいプロジェクトの作成->空のプロジェクト
  • プロジェクト名(ソリューション名)と保存パスを指定
    • “ソリューションとプロジェクトを同じディレクトリに配置する"にチェック
  • 作成
  • ソリューションのプロパティを変更
    • 構成プロパティ->リンカー->システム->サブシステム
      • “コンソール"から"Windows"に変更
  • 以下のようにフォルダを構成
    • 新しいフォルダを作成
    • フォルダに新しい項目を追加(以降説明省略)
      • Applicationフィルターに新しい項目を追加
        • .cpp
          • 名前:Application.cpp
          • 場所:Source/Application(フォルダを作成)
        • .h
          • 名前:Application.h
          • 場所:Source/Application
dev-dx12-202308(プロジェクト名)
└─ Source
   ├─ Application
   │  ├─ Application.h
   │  └─ Application.cpp
   └─ Graphics

Application.h

Applicationクラス

ウィンドウ作成の実行を行うExecute()関数を宣言

Applicationクラスオブジェクトを生成するInstance()関数を定義する(シングルトン:クラスのインスタンスが1つしか存在しないことを保証する)

// Application.h

// Applicationクラス
class Application {
public:
	void Execute();

	static Application& Instance() {
		static Application instance;
		return instance;
	}

private:
	Window m_window;

	Application() {}
}

Application.cpp

Windows.hのインクルード

WindowsプログラムのためにWindows.hをインクルード(Windowsプログラムの型や構造体、定数、ファンクションコールが定義されている)
(※Windows.hとWindow.hの混同に注意)

Windowsプログラムにmain()関数は無く、WinMain()関数からプログラムが開始される

WinMain()関数

WINAPIWindows.hでWin32 API 関数を呼び出すときの規約として定義される。#define WINAPI __stdcall

ほとんどのWindows関数はWINAPIと定義されている

第一引数はプログラムを識別するためのインスタンスハンドル、第二引数はアプリケーションの別インスタンスの確認に使われるインスタンスハンドル(常にNULL)、第三引数はWindows標準の文字列型、第四引数はアプリケーションの初期表示方法を指定する

Application::Execute()関数

(このあと定義する)ウィンドウ作成を実行し、失敗なら終了する

assert()は式が真であることを表明する。https://cpprefjp.github.io/reference/cassert/assert.html

// Application.cpp

// WinMain()関数
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
	Application::Instance().Execute();

	return 0;
}

// Application::Execute()関数
class Application::Execute() {
	if (!m_window.Create(width, height, L"DX12-framework", L"Window")) {
		assert(0 && "ウィンドウ作成失敗。");
		return;
	}

	while (true) {
		if (!m_window.ProcessMessage()) {
			break;
		}
	}
}

ウィンドウの表示

System/Window

dev-dx12-202308(プロジェクト名)
└─ Source
   ├─ Application
   │  ├─ Application.h
   │  └─ Application.cpp
   ├─ Graphics
   └─ System
      └─ Window
         ├─ Window.h
         └─ Window.cpp

Window.h

Windowクラス

ウィンドウの作成をするCreate()関数を宣言
▶ 第一引数は横幅の長さ
▶ 第二引数は縦幅の長さ
▶ 第三引数はタイトル名
▶ 第四引数はクラス名

ウィンドウメッセージの処理をするProcessMessage()関数を宣言する

m_hWnd変数はウィンドウハンドル型のデータ型であるHWND型で宣言する

// Window.h

// Windowクラス
class Window {
public:
	bool Create(int clientWidth, int clientHeight, const std::wstring& titleName, const std::wstring& windowClassName);

	bool ProcessMessage();

private:
	HWND m_hWnd;
};

Window.cpp

WindowProcedure()関数

ウィンドウプロシージャはアプリケーションに送られてくるメッセージを処理する

LRESULT型は、コールバック関数やウィンドウプロシージャから返却される32ビットの値である

値を渡し、関数側でその値に応じた処理を行わせ、値を返してもらう関数、つまりCALLBACK関数として定義する

第一引数はメッセージが発生したウィンドウのハンドル、第二引数はメッセージ、第三引数と第四引数にはメッセージの付加情報が入る(Windows API(Win32 API)のMSG構造体の最初の4つのメンバと同じ)

第二引数のメッセージから判断して処理を行う(ウィンドウが破棄されたときに渡されるWM_DESTROYメッセージは最低限指定しないといけない)

PostQuitMessage(0)は終了(WM_QUITメッセージ)を伝え、DefWindowProc()はさまざまなメッセージ(ウィンドウの移動など)を自動で処理して終了コードを伝える

// Window.cpp

// WindowProcedure()関数
LRESULT CALLBACK WindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
	switch (message) {
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
		break;
	}

	return 0;
}
Window::Create()関数

ウィンドウの作成を行う

HINSTANCE型は、アプリケーション自体を表すインスタンスハンドル型のデータ型

GetModuleHandle(NULL)関数によりインスタンスハンドルを取得する

WNDCLASSEX構造体を用いてウィンドウクラスの定義を行う
構造体のサイズ:wc.cbSize
ウィンドウ関数:wc.lpfnWndProcWNDPROCはウィンドウプロシージャ(コールバック関数)へのポインターを定義する)
ウィンドウクラスの名前:wc.lpszClassName
インスタンスハンドル:wc.hInstance

ここで初期化した変数をRegisterClassEx()関数に渡すことで、ウィンドウクラスは登録され、成功すると登録されたクラスを一意的に識別するATOM型で返す(失敗すると0が返却される)

ウィンドウの作成にはHWND型を戻り値とするCreateWindow()関数を使用する。

ShowWindow()関数は指定されたウィンドウの表示状態を設定する
▶ 第一引数では表示状態を変更するウィンドウのハンドルを指定する
▶ 第二引数ではウィンドウの表示状態を指定する
指定する値と意味はこちらを参照:http://chokuto.ifdef.jp/urawaza/api/ShowWindow.html

UpdateWindow()関数はWM_PAINTメッセージがウィンドウプロシージャに直接送られ、ウィンドウのクライアントエリアを再描画させる

// Window.cpp

// Window::Create()関数
bool Window::Create(int clientWidth, int clientHeight, const std::wstring& titleName, const std::wstring& windowClassName) {
	HINSTANCE hInst = GetModuleHandle(0);

	WNDCLASSEX wc = {};
	wc.cbSize = sizeof(WNDCLASSEX);				// 構造体のサイズ
	wc.lpfnWndProc = (WNDPROC)WindowProcedure;	// ウィンドウ関数
	wc.lpszClassName = windowClassName.c_str();	// ウィンドウクラス名
	wc.hInstance = hInst;						// インスタンスハンドル

	if (!RegisterClassEx(&wc)) {
		return false;
	}

	m_hWnd = CreateWindow(
		windowClassName.c_str(),							// ウィンドウクラス名
		titleName.c_str(),									// ウィンドウのタイトル
		WS_OVERLAPPEDWINDOW - WS_THICKFRAME,				// ウィンドウタイプを標準タイプに	
		0,													// ウィンドウの位置(X座標)
		0,													// ウィンドウの位置(Y座標)						
		clientWidth,										// ウィンドウの幅
		clientHeight,										// ウィンドウの高さ			
		nullptr,											// 親ウィンドウのハンドル
		nullptr,											// メニューのハンドル
		hInst,												// インスタンスハンドル 
		this
	);

	if (m_hWnd == nullptr) {
		return false;
	}

	ShowWindow(m_hWnd, SW_SHOW);

	UpdateWindow(m_hWnd);

	return true;
}
Window::ProcessMessage()関数

常にウィンドウを表示させるため、以下の処理をApplication.cppのメインループで呼び出す

ウィンドウがメッセージを処理しておらず、ユーザからの入力を待っている状態(デッドタイム)を利用した処理を行う
参考ページ:http://wisdom.sakura.ne.jp/system/winapi/win32/win46.html

PeekMessage()関数は今の状態がデッドタイム、つまりメッセージを取得しなかったときは0を返し、そうでないときは1を返す

PeekMessage()関数の第一引数はメッセージを格納するMSG構造体へのポインタ、第二引数はメッセージを取得するウィンドウへのハンドル、第三引数と第四引数は受け取るメッセージの最小値と最大値を指定し、第五引数はメッセージの処理方法を指定する(第五引数がPM_NOREMOVEならメッセージを削除せず、PM_REMOVEならメッセージをキューから削除する)

終了メッセージが送られた場合はfalseを返す

MSG構造体のポインタを用いて、
TranslateMessage()関数は仮想キーメッセージを文字メッセージに変換し、DispatchMessage()関数はウィンドウプロシージャにメッセージをディスパッチする

// Window.cpp

// Window::ProcessMessage()関数
bool Window::ProcessMessage() {
	MSG msg;
	while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
		if (msg.message == WM_QUIT) {
			return false;
		}

		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return true;
}

結果

GitHubリポジトリ