D3D12の概要
前回は、「なぜ低レイヤーを触るのか」ということに焦点を置いて話しました。
今回はなるべく初心者でもわかりやすいように解説しながら実装を進めていきたいと思います。
Direct3D12(DirectX12とも言う) とは、主にMicrosoft製品(WindowsやXboxなど)において高速な描画をするためのAPIです。以降は簡略化のためにD3D12と呼ぶことにしましょう。
UnityやUnreal Engineなどのエンジンを触ったことのある方は普段、単純な四角形を表示するために座標や大きさなどの情報を設定するかと思いますが、D3D12ではもっと深いことをやります。
どの画面のどこ に、どんな色で、どんな合成方式で、どういう形の板を表示するのか、といった泥臭くて面倒な自由度の高い手続きを行っていくのがDirectX12です。
では、実際にアプリケーションを作成していきましょう。
Visual Studioで画面を表示する
プロジェクトの作成
これがないとプログラムは始まらない。初期設定もついでに解説しながらやっていきましょう。
Visual Studioを立ち上げて、「新しいプロジェクトの作成」-> 「コンソールアプリ(C++)」 でプロジェクトを作成しましょう。
プロジェクト名や場所は何でもいいです。
コンソールアプリを選ぶ理由
ここは「Windows デスクトップアプリケーション」でも作成できますが、アイコンやらリソースヘッダやら必要ないものがついてくるので、まっさらな状態で始めるために「コンソールアプリ」で作成しています。
ここまで完了したら、いつもの"Hello World!"を出力するmain関数が出来上がると思います。
今回はWinMain関数を使うので、一旦全部削除してください。
プロジェクトの初期設定
プロジェクトができたので、初期設定をしていきます。
「プロジェクト」->「(プロジェクト名)のプロパティ」からプロパティウィンドウを開き、
「構成」を「すべての構成」にするのをお忘れなく。
ー忘れるとReleaseビルドが通らないことがあります。
1.「C++言語標準」 を新しいものにしましょう。
C++20以上であると好ましいです。

2.「リンカー」->「サブシステム」を 「Windows」 に変更します。

3.「C++」->「全般」から、「追加のインクルードディレクトリ」にソースコードを置く場所
を追加しておきましょう。実際にフォルダを作成しないとうまく使えないので注意
今回はフォルダ名を「src」としました。

エクスプローラー上ではこんな感じです。「main.cpp」もsrcフォルダの中に作成しておきましょう。
4.プリコンパイル済みヘッダを 「使用(/Yu)」 に切り替えておきましょう。
インクルードしているヘッダが変更されていない場合のビルドが高速化します。

また、作成したプリコンパイルヘッダを 「必ず使用されるインクルードファイル」 に追加しておきましょう。

precompile.cppを用意し、この一行を書いておきましょう。
#include "precompile.cpp" //これだけでいい
今度は このcppだけ を選択して、 「プロパティ」 からプリコンパイルヘッダを 「作成(/Yc)」
に切り替えておきましょう。
正直4はやらなくてもいいですが、何回もプロジェクトをビルドしていると、
プロジェクトが大きくなるにつれ、ビルド時間が長くなっていきます。
サイズにもよりますが 巨大なものだと3~5分もかかり、
非常に大きなストレス が生まれます。
精神衛生的にやっておくに越したことはないです。
長かったですね。やっと初期化が終わりました。
DirectXに限らず、プロジェクトを一から作成する経験は学生だとあまりないと思います。
専門学生とかだと、先生の作ったフレームワークを使ったり、Dxライブラリなどで
「おまじない」としてやることが多いでしょう。
今後困らないためにも、今のうちに慣れておきましょう。
ウィンドウの作成
ここはDirectXとか関係なく、Windowsにアプリケーションのウィンドウを登録する段階です。
先に長々と説明するのもですし、パパっとやっていきましょう。説明は後です。
一旦すべてmain.cppに書いていきます。
ウィンドウプロシージャの作成
ウィンドウに紐づけるウィンドウプロシージャを定義します。
#include <Windows.h>
#include <string>
//-----------------------------------------------------------------------
// @brief ウィンドウプロシージャ。ウィンドウに送られてくるメッセージを処理する関数。
//
// @param [in] hwnd メッセージが送られてきたウィンドウのハンドル
// @param [in] uMsg メッセージの種類を示す定数
// @param [in] wParam メッセージに関連する追加情報
// @param [in] lParam メッセージに関連する追加情報
//
//------------------------------------------------------------------------
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) { // メッセージを確認
case WM_DESTROY: // 内容が「ウィンドウが破棄されたよ」なら
PostQuitMessage(0); // メッセージループにWM_QUIT(「アプリ終わるでー」のメッセージ)を送る
return 0;
}
// それ以外のメッセージはデフォルトの処理を行う
return DefWindowProc(hwnd, uMsg, wParam, lParam); //Windowsのデフォルト処理
}
ウィンドウプロシージャとは
ウィンドウプロシージャは、OSがウィンドウに対して発生するイベントやメッセージを処理するための関数
要するに、
「君のウィンドウがクリックされたよー」とか
「君のウィンドウ閉じられちゃったよ...」とか
「今君のウィンドウがドラッグされてるよー」
みたいな情報をOSが通知するので、それに対してどういう処理をするかを決める
って認識でいいです。
ウィンドウクラスの作成と登録
定義したプロシージャを紐づけし、実際に作成したウィンドウをOSに登録(Register)します。
// 前略
//-----------------------------------------------------------------------
// @brief メインウィンドウを作成し、表示・初期化を行う関数。
//
// @param [out] win_handle 作成されたウィンドウのハンドルが格納される。失敗時はNULL
// @param [in] class_name ウィンドウクラス名
// @param [in] window_name ウィンドウタイトルバーに表示される名前
// @param [in] window_w ウィンドウの幅(ピクセル単位)
// @param [in] window_h ウィンドウの高さ(ピクセル単位)
//
// @return int 成功時は0、失敗時は - 1を返す
//
//-----------------------------------------------------------------------
int CreateMainWindow(HWND& win_handle, const std::wstring& class_name, const std::wstring& window_name, unsigned int window_w, unsigned int window_h) {
WNDCLASSEX window_class = {}; // 空っぽのウィンドウクラスの構造体を作成
window_class.cbSize = sizeof(WNDCLASSEX); // ウィンドウクラスの構造体のサイズを設定
window_class.lpfnWndProc = WindowProc; // ウィンドウプロシージャ
//このウィンドウがどのアプリ(.exe)に属するかを識別するための識別子
window_class.hInstance = GetModuleHandle(NULL); // このアプリケーションのハンドルを取得して設定
//OSがウィンドウを識別するための名前を設定(基本的になんでもいい)
window_class.lpszClassName = class_name.c_str();
//ウィンドウクラスをOSに登録
RegisterClassEx(&window_class);
//ウィンドウのクライアント領域(実際に描画できる領域)のサイズを指定するための構造体を作成
RECT rect = { 0,0,window_w,window_h };
//ウィンドウのスタイル(WS_OVERLAPPEDWINDOW)に合わせて、ウィンドウ全体のサイズを調整する。
//これにより、書き込み領域が指定されたサイズになるようにする
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
//ウィンドウそのものを作成
win_handle = CreateWindowEx(
0, //特にスタイル拡張はしないので0
class_name.c_str(), //ウィンドウクラス名
window_name.c_str(), //ウィンドウタイトル
WS_OVERLAPPEDWINDOW, //ウィンドウスタイル(タイトルバーとサイズ変更可能な枠がある一般的なウィンドウ)
//ウィンドウの位置とサイズを指定。CW_USEDEFAULTはOSに任せるという意味
//この場合、
// ウィンドウの位置はOSに任せる(CW_USEDEFAULT)ので、OSがいい感じに配置してくれる
// ウィンドウの幅と高さは引数で指定された値になる
CW_USEDEFAULT, CW_USEDEFAULT, rect.right-rect.left,rect.bottom-rect.top,
NULL, NULL, //親ウィンドウ、メニューハンドルは特に必要ないのでNULL
//このウィンドウが属するアプリケーションのハンドルを指定(GetModuleHandle(NULL)で取得したもの)
window_class.hInstance,
NULL //このウィンドウに関連付ける追加のデータがあればここで渡すことができるが、今回は特にないのでNULL
);
if (win_handle == NULL) { //ウィンドウの作成に失敗した場合はNULLが返るので、失敗を示す -1 を返す
return -1;
}
//ウィンドウを表示する。SW_SHOWは「ウィンドウを表示する」という意味の定数
ShowWindow(win_handle, SW_SHOW);
//ウィンドウの内容を更新する。これも特に必要ないが、念のため。
UpdateWindow(win_handle);
return 0;
}
これでウィンドウを作る関数は定義できました。
実際にWinMainを定義して呼び出してみましょう
//前略
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
std::wstring class_name = L"BasicD3D12Window"; // ウィンドウクラス名
std::wstring window_name = L"そんなウィンドウ名で大丈夫か?"; // ウィンドウタイトル
// メインウィンドウの作成と表示
if (CreateMainWindow(window_handle, class_name, window_name, 800, 600) != 0)
{
// ウィンドウの作成に失敗した場合は、アプリケーションを終了する
return -1;
}
MSG msg; // Windowsのメッセージを格納する構造体
while (true) {
// メッセージが来ているか確認。来ていればmsgに格納して、キューから削除する
// 英語でPeek = 「覗く」という意味。PeekMessageは「メッセージを覗いてみる」という意味で、メッセージが来ているか確認する関数
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
// 来ていた場合、TranslateMessageでキーボード入力などのメッセージを翻訳して、DispatchMessageでウィンドウプロシージャに送る
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// WM_QUIT(「アプリ終わるでー」のメッセージ)が来ていたら、ループを抜けてアプリを終了する
if (msg.message == WM_QUIT) {
break;
}
{
// ここにゲームの更新や描画のコードを入れることになる
}
}
// ウィンドウクラスの登録を解除する
UnregisterClass(L"BasicD3D12Window", GetModuleHandle(NULL));
return 0;
}
実行してみると、こんな感じのウィンドウが出てきたかと思います。
×ボタンを押してきちんと終了できることも確認しておきましょう。
おめでとうございます。ウィンドウは出せました。
これで完成です。
ーーーとはいかないんですよね。
冒頭でも言ったように、まだDirectXの関数は一つも出てきていません。
ちなみにこれでどれくらいの進捗率かというと、
シンプルな画像表示を100%とした場合、 5% です。
次回の更新はなるべく早く行う予定ですので、もう少しだけお待ちください。




