43
42

More than 1 year has passed since last update.

[DirectX12] ハローワールドからFBXモデルを表示させるまで

Last updated at Posted at 2021-12-17

はじめに

この記事はAkatsuki Advent Calendar17日目の記事です
ノリでDirectX12に入門したらかなり大変だったので、やったことを全て書くことにしました

スクリーンショット 2021-12-16 01.01.34.png

目的

本記事の目的は、3DグラフィックスやDirectX12の詳細な説明というより、
とにかくDirectX12で3Dモデルを描画するところまでを手元で実践できることです

最近では DirectX12に関する書籍がいくつか出ていて、だいぶ入門しやすい時代になったかとは思いますが、やはりまだハードルが高いなあと思った次第です
その理由として、主観ではありますが、

  1. 3Dグラフィックスの知識が必要
  2. コード量が膨大で複雑

が主なところかなーと思いました
1は3Dグラフィックスに関する書籍や記事などはすでに大量に存在しているので、知識を得ることはそう難しくはないのですが、
得た知識をいざ試してみようと思った時に、2の問題でちょっと試すだけでも相当苦労するという状態かと思いました

なので、まず動かすものができてしまうところまでをある程度構造化した状態で実現できれば、
そのあとはそれをベースに試したいことを試せる状態を作れて自分なりに理解を深められるようになると思うので、そのための手順紹介となります

(動作するプロジェクトをgithubで公開したかったのですが、書き切るだけで精一杯だったので、需要があれば作ります)

参考資料

この辺りの書籍と合わせて記事を読んでいただくことを推奨します

実践

執筆にあたって作業を行った環境

OS Windows10
AMD Ryzen 9 5900 12-Core
RAM 16GB
GPU RTX3080
Visual Studio 2019

当然ですが、DirectX12に対応した環境である必要があります
「Win + R」を押して、「ファイル名を指定して実行」を押します
「dxdiag」と入れて出てきた「DirectX診断ツール」の「DirectXバージョン」が「12」となっていれば大丈夫です

環境設定

まずは環境設定とC++でウィンドウを作るところまでを行います
今回は、Windowsアプリとしてウィンドウを表示する部分DirectX12として描画の基盤となる部分描画の基盤を使って描画されるオブジェクトの部分を分けて作りたいので、ざっくりとこんな感じの設計にしたいと思います

image.png

Visual Studioのインストール

検索すると、Visual Studio Installerなるものが出てくると思うので、それをインストールしてそこからVisual Studio 2019をインストールしましょう

プロジェクトの作成

Visual Studioを起動して、「新しいプロジェクトの作成(N)」を選択
真ん中上辺りにあるプルダウンから言語をC++に設定して、「空のプロジェクト」を選択
次に表示されるウィンドウにプロジェクト名を入力してプロジェクトの作成は完了です

スクリーンショット 2021-12-13 12.21.45.png

ハローワールド

まずはみんな大好きハローワールドから始めましょう
サイドバーの「ソースファイル」を右クリックして、「追加」>「新しい項目」
出てきたウィンドウで「C++」ファイルを選択し、名前を「main.cpp」でcppファイルを追加しましょう

main.cpp
#include <stdio.h>

int wmain(int argc, wchar_t** argv, wchar_t** envp)
{
	printf("ハローワールド\n");
	return 0;
}

こんな表示がOK、なんてことはないですね

スクリーンショット 2021-12-13 12.30.48.png

ウィンドウの表示

次はWindowsのウィンドウを表示しましょう
今回はウィンドウを表示する部分のコードは単体で分けて作成しようと思います
「main.cpp」を作成したときと同じ要領で、「App.h」「App.cpp」を作っちゃいましょう

そしたら、それぞれこんなふうにコーディングします
いきなりたくさん書いてあって面食らうかもしれませんが、
やっていることは表示したいウィンドウの設定をして、ウィンドウを表示して、無限ループしながらウィンドウからのメッセージを待っているだけです
ゲームを作ったことがある方やUnityなどを触っている方は馴染みがあるかと思いますが、リアルタイムで描画を行うゲームやアプリは大体1秒間に60回程度、画面の表示を更新するという処理を行っています
これをフレームレートやリフレッシュレートと呼んだりしますね

App.h
#pragma once
#include <Windows.h>

const UINT WINDOW_WIDTH = 1920;
const UINT WINDOW_HEIGHT = 1080;
void StartApp(const TCHAR* appName); // これを呼んだらアプリが実行するようにする
App.cpp
#include "App.h"

HINSTANCE g_hInst;
HWND g_hWnd = NULL;

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch (msg)
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		break;
	}

	return DefWindowProc(hWnd, msg, wp, lp);
}

void InitWindow(const TCHAR* appName)
{
	g_hInst = GetModuleHandle(nullptr);
	if (g_hInst == nullptr)
	{
		return;
	}

	// ウィンドウの設定
	WNDCLASSEX wc = {};
	wc.cbSize = sizeof(WNDCLASSEX);
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = WndProc;
	wc.hIcon = LoadIcon(g_hInst, IDI_APPLICATION);
	wc.hCursor = LoadCursor(g_hInst, IDC_ARROW);
	wc.hbrBackground = GetSysColorBrush(COLOR_BACKGROUND);
	wc.lpszMenuName = nullptr;
	wc.lpszClassName = appName;
	wc.hIconSm = LoadIcon(g_hInst, IDI_APPLICATION);

	// ウィンドウクラスの登録。
	RegisterClassEx(&wc);

	// ウィンドウサイズの設定
	RECT rect = {};
	rect.right = static_cast<LONG>(WINDOW_WIDTH);
	rect.bottom = static_cast<LONG>(WINDOW_HEIGHT);

	// ウィンドウサイズを調整
	auto style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU;
	AdjustWindowRect(&rect, style, FALSE);

	// ウィンドウの生成
	g_hWnd = CreateWindowEx(
		0,
		appName,
		appName,
		style,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		rect.right - rect.left,
		rect.bottom - rect.top,
		nullptr,
		nullptr,
		g_hInst,
		nullptr
	);

	// ウィンドウを表示
	ShowWindow(g_hWnd, SW_SHOWNORMAL);

	// ウィンドウにフォーカスする
	SetFocus(g_hWnd);
}

void MainLoop()
{
	MSG msg = {};
	while (WM_QUIT != msg.message)
	{
		if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE == TRUE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			// 後で描画処理を実行するところ
		}
	}
}

void StartApp(const TCHAR* appName)
{
	// ウィンドウ生成
	InitWindow(appName);

	// 後でここで描画基盤や3Dモデルの初期化を行う

	// メイン処理ループ
	MainLoop();
}

ここまで書けたら、「main.cpp」にもどって、「StartApp」を呼んでみましょう
最初に書いた「ハローワールド」の表示はもういらないので消してしまって大丈夫ですが、思い出として残しておくのも良いかもしれません

main.cpp
#include "App.h"

int wmain(int argc, wchar_t** argv, wchar_t** envp)
{
	StartApp(TEXT("DirectX12入門"));
	return 0;
}

このようなウィンドウが表示されたらOKです
スクリーンショット 2021-12-13 13.47.23.png

DirectX12を使って描画を行う

この章では、今作ったウィンドウにDirectX12の基礎的なオブジェクトを紐付けて、DirectX12を使ってウィンドウ内の表示を初期化するところまでを行います。
絵面的にかなり地味で、しかもやることがたくさんあって初めてじゃよくわからん!となりそうなところではありますが、
DirectX12を使うには必要になる最初の手順なので、がんばっていきましょう!

スマートポインタの準備

と、DirectX12を使ったプログラミングに入る前に、
DirectX12関連のオブジェクトは終了時にちゃんと開放処理を行いたいのですが、毎回をそれを書くのは面倒です
なので、ComPtrというスマートポインタを導入することにします
ComPtrを使えば、いちいちRelease関数を呼ばなくても、必要なときに自動的に呼んでくれます

ヘッダーファイルに「ComPtr.h」を追加して、こんな感じに記述します
あとは、ComPtrを使いたくなったときに「ComPtr.h」をインクルードして、「ComPtr」のように呼んであげれば大丈夫です
今後ComPtrを使っている箇所では、「ComPtr.h」をインクルードしているものだと思ってください

ComPtr.h
#pragma once
#include <wrl/client.h>
template<typename T> using ComPtr = Microsoft::WRL::ComPtr<T>;

描画に使うオブジェクトを準備

それではさっそくDirectX12を使う準備をしていきましょう

といっても、DirectX12で描画を行う上で必要なものはたくさんあります

  • デバイス
  • コマンドキュー
  • スワップチェイン
  • コマンドアロケーター
  • コマンドリスト
  • フェンス
  • ビューポート
  • シザー矩形

それぞれがどんな役割なのかの解説は処理を書いていくときにするとして、
これらをひとまとめにして描画基盤を担うクラスとして定義したいと思います
新たに「Engine.h」を追加して、記述しましょう
このあとの章で中身を作っていきます

Engine.h
#pragma once
#include <d3d12.h>
#include <dxgi.h>
#include <dxgi1_4.h>
#include "ComPtr.h"

#pragma comment(lib, "d3d12.lib") // d3d12ライブラリをリンクする
#pragma comment(lib, "dxgi.lib") // dxgiライブラリをリンクする

class Engine
{
public:
	enum { FRAME_BUFFER_COUNT = 2 }; // ダブルバッファリングするので2

public:
	bool Init(HWND hwnd, UINT windowWidth, UINT windowHeight); // エンジン初期化

	void BeginRender(); // 描画の開始処理
	void EndRender(); // 描画の終了処理

public: // 外からアクセスしたいのでGetterとして公開するもの
	ID3D12Device6* Device();
	ID3D12GraphicsCommandList* CommandList();
	UINT CurrentBackBufferIndex();

private: // DirectX12初期化に使う関数たち
	bool CreateDevice(); // デバイスを生成
	bool CreateCommandQueue(); // コマンドキューを生成
	bool CreateSwapChain(); // スワップチェインを生成
	bool CreateCommandList(); // コマンドリストとコマンドアロケーターを生成
	bool CreateFence(); // フェンスを生成
	void CreateViewPort(); // ビューポートを生成
	void CreateScissorRect(); // シザー矩形を生成

private: // 描画に使うDirectX12のオブジェクトたち
	HWND m_hWnd;
	UINT m_FrameBufferWidth = 0;
	UINT m_FrameBufferHeight = 0;
	UINT m_CurrentBackBufferIndex = 0;

	ComPtr<ID3D12Device6> m_pDevice = nullptr; // デバイス
	ComPtr<ID3D12CommandQueue> m_pQueue = nullptr; // コマンドキュー
	ComPtr<IDXGISwapChain3> m_pSwapChain = nullptr; // スワップチェイン
	ComPtr<ID3D12CommandAllocator> m_pAllocator[FRAME_BUFFER_COUNT] = {nullptr}; // コマンドアロケーたー
	ComPtr<ID3D12GraphicsCommandList> m_pCommandList = nullptr; // コマンドリスト
	HANDLE m_fenceEvent = nullptr; // フェンスで使うイベント
	ComPtr<ID3D12Fence> m_pFence = nullptr; // フェンス
	UINT64 m_fenceValue[FRAME_BUFFER_COUNT]; // フェンスの値(ダブルバッファリング用に2個)
	D3D12_VIEWPORT m_Viewport; // ビューポート
	D3D12_RECT m_Scissor; // シザー矩形

private: // 描画に使うオブジェクトとその生成関数たち
	bool CreateRenderTarget(); // レンダーターゲットを生成
	bool CreateDepthStencil(); // 深度ステンシルバッファを生成

	UINT m_RtvDescriptorSize = 0; // レンダーターゲットビューのディスクリプタサイズ
	ComPtr<ID3D12DescriptorHeap> m_pRtvHeap = nullptr; // レンダーターゲットのディスクリプタヒープ
	ComPtr<ID3D12Resource> m_pRenderTargets[FRAME_BUFFER_COUNT] = {nullptr}; // レンダーターゲット(ダブルバッファリングするので2個)

	UINT m_DsvDescriptorSize = 0; // 深度ステンシルのディスクリプターサイズ
	ComPtr<ID3D12DescriptorHeap> m_pDsvHeap = nullptr; // 深度ステンシルのディスクリプタヒープ
	ComPtr<ID3D12Resource> m_pDepthStencilBuffer = nullptr; // 深度ステンシルバッファ(こっちは1つでいい)

private: // 描画ループで使用するもの
	ID3D12Resource* m_currentRenderTarget = nullptr; // 現在のフレームのレンダーターゲットを一時的に保存しておく関数
	void WaitRender(); // 描画完了を待つ処理
};

extern Engine* g_Engine; // どこからでも参照したいのでグローバルにする

D3D12Deviceの生成

D3D12Deviceはその名の通り、GPUのデバイスのインターフェースです
DirectX12で扱う様々なオブジェクトはデバイスを通して行われるので、まず最初に生成します

さっそく処理を書いていきましょう
ソースファイルに「Engine.cpp」を追加して、大本になる初期化関数「Init」を定義します

定義するときは、Visual Studioの右クリックから定義を作成する機能が便利です
スクリーンショット 2021-12-13 19.51.39.png

この後の「Create○○」系の処理は全てここで呼び出すようにしたいと思います
DirectX12関連の関数や構造体の定義は、「d3d12.h」でされています

Engine.cpp
#include "Engine.h"
#include <d3d12.h>
#include <stdio.h>
#include <Windows.h>

Engine* g_Engine;

bool Engine::Init(HWND hwnd, UINT windowWidth, UINT windowHeight)
{
	m_FrameBufferWidth = windowWidth;
	m_FrameBufferHeight = windowHeight;
	m_hWnd = hwnd;

	printf("描画エンジンの初期化に成功\n");
	return true;
}

このあと、初期化が成功したかどうかを確認しながらコーディングを勧めていきたいので、
「App.cpp」に戻って呼び出しも行いましょう

App.cpp
#include "App.h"
#include "Engine.h" // さっき作ったEngine.hを追加
	// : 省略
void StartApp(const TCHAR* appName)
{
	// ウィンドウ生成
	InitWindow(appName);

	// 描画エンジンの初期化を行う
	g_Engine = new Engine();
	if (!g_Engine->Init(g_hWnd, WINDOW_WIDTH, WINDOW_HEIGHT))
	{
		return;
	}

	// 後でここで3Dモデルの初期化を行う

	// メイン処理ループ
	MainLoop();
}

ここまできたら実行してみましょう
CUIの方に「描画エンジンの初期化に成功」と表示されたらOKです

前置きが長くなってしまいましたが、デバイスの初期化にいきましょう
「D3D12CreateDevice」関数を使って行います
ComPtrの「ReleaseAndGetAddressOf」を使うことで、一旦「m_pDevice」の中身をクリアしてから値を割り当てるようにしています

Engine.cpp
	// : 省略
	if (!CreateDevice())
	{
		printf("デバイスの生成に失敗");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}

bool Engine::CreateDevice()
{
	auto hr = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(m_pDevice.ReleaseAndGetAddressOf()));
	return SUCCEEDED(hr);
}

こんな感じで処理を書いて実行してみて、成功と表示されていればOKです

コマンドキューの生成

コマンドキューとは、デバイスに送るための描画コマンドの投稿や、描画コマンド実行の同期処理を行うものです
コマンドキューはデバイスが持つ「CreateCommandQueue」関数から生成することができます

これも「CreateCommandQueue」関数(こっちは自分で定義したやつ)に処理を書いて、「Init」から呼び出しましょう

Engine.cpp
bool Engine::Init(HWND hwnd, UINT windowWidth, UINT windowHeight)
{
	// : 省略

	if (!CreateDevice())
	{
		// : 省略
	}

	if (!CreateCommandQueue())
	{
		printf("コマンドキューの生成に失敗");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}

bool Engine::CreateCommandQueue()
{
	D3D12_COMMAND_QUEUE_DESC desc = {};
	desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
	desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	desc.NodeMask = 0;

	auto hr = m_pDevice->CreateCommandQueue(&desc, IID_PPV_ARGS(m_pQueue.ReleaseAndGetAddressOf()));

	return SUCCEEDED(hr);
}

SwapChainの生成

スワップチェインとは、ダブルバッファリングやトリプルバッファリングを実現するためのものです
GPUはフレームバッファという、モニタに表示される画像のバッファを持っているのですが、
モニターのリフレッシュレートはCPUに比べてかなり遅いので、フレームバッファを表示中に中身を更新してしまうと、
更新中のものがモニターに表示されてしまいチラつくという現象が発生してしまいます

ダブルバッファリングとは、このフレームバッファを2つ持っておき、
1つは表示用、1つは書き込み用としておいて、片方を表示している間にもう片方を書き込み、それをフレームごとに切り替えることで、
チラつきを無くそうという仕組みのことです
トリプルバッファリングはこのフレームバッファを3つにすることです

スワップチェインを生成する処理は、DXGIというものを使うのでちょっと長めです
DIXGIとは、カーネルモードドライバーとシステムハードウェアと通信するためのAPIで、アプリケーションとハードウェアの間に挟まる概念です

Engine.cpp
bool Engine::Init(HWND hwnd, UINT windowWidth, UINT windowHeight)
{
	// : 省略、さっき書いた初期化処理

	if (!CreateSwapChain())
	{
		printf("スワップチェインの生成に失敗");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}


bool Engine::CreateSwapChain()
{
	// DXGIファクトリーの生成
	IDXGIFactory4* pFactory = nullptr;
	HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&pFactory));
	if (FAILED(hr))
	{
		return false;
	}

	// スワップチェインの生成
	DXGI_SWAP_CHAIN_DESC desc = {};
	desc.BufferDesc.Width = m_FrameBufferWidth;
	desc.BufferDesc.Height = m_FrameBufferHeight;
	desc.BufferDesc.RefreshRate.Numerator = 60;
	desc.BufferDesc.RefreshRate.Denominator = 1;
	desc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	desc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
	desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	desc.SampleDesc.Count = 1;
	desc.SampleDesc.Quality = 0;
	desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	desc.BufferCount = FRAME_BUFFER_COUNT;
	desc.OutputWindow = m_hWnd;
	desc.Windowed = TRUE;
	desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

	// スワップチェインの生成
	IDXGISwapChain* pSwapChain = nullptr;
	hr = pFactory->CreateSwapChain(m_pQueue.Get(), &desc, &pSwapChain);
	if (FAILED(hr))
	{
		pFactory->Release();
		return false;
	}

	// IDXGISwapChain3を取得
	hr = pSwapChain->QueryInterface(IID_PPV_ARGS(m_pSwapChain.ReleaseAndGetAddressOf()));
	if (FAILED(hr))
	{
		pFactory->Release();
		pSwapChain->Release();
		return false;
	}

	// バックバッファ番号を取得
	m_CurrentBackBufferIndex = m_pSwapChain->GetCurrentBackBufferIndex();

	pFactory->Release();
	pSwapChain->Release();
	return true;
}

コマンドリストとコマンドアロケーターの生成

描画命令を溜めておくのがコマンドリストで、コマンドリストを生成するにはコマンドアロケーターが必要です
これも同様に初期化処理を書いていきましょう

Engine.cpp

bool Engine::Init(HWND hwnd, UINT windowWidth, UINT windowHeight)
{
	// : 省略、さっき書いた初期化処理

	if (!CreateCommandList())
	{
		printf("コマンドリストの生成に失敗");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}

bool Engine::CreateCommandList()
{
	// コマンドアロケーターの作成
	HRESULT hr;
	for (size_t i = 0; i < FRAME_BUFFER_COUNT; i++)
	{
		hr = m_pDevice->CreateCommandAllocator(
			D3D12_COMMAND_LIST_TYPE_DIRECT,
			IID_PPV_ARGS(m_pAllocator[i].ReleaseAndGetAddressOf()));
	}

	if (FAILED(hr))
	{
		return false;
	}

	// コマンドリストの生成
	hr = m_pDevice->CreateCommandList(
		0,
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		m_pAllocator[m_CurrentBackBufferIndex].Get(),
		nullptr,
		IID_PPV_ARGS(&m_pCommandList)
	);

	if (FAILED(hr))
	{
		return false;
	}

	//コマンドリストは開かれている状態で作成されるので、いったん閉じる。
	m_pCommandList->Close();

	return true;
}

フェンスの生成

フェンスとは、CPUとGPUの同期を行うためのものです
描画が完了したかどうかを、フェンスの値がインクリメントされたかどうかで判断します

Engine.cpp
bool Engine::Init(HWND hwnd, UINT windowWidth, UINT windowHeight)
{
	// : 省略、さっき書いた初期化処理

	if (!CreateFence())
	{
		printf("フェンスの生成に失敗");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}

bool Engine::CreateFence()
{
	for (auto i = 0u; i < FRAME_BUFFER_COUNT; i++)
	{
		m_fenceValue[i] = 0;
	}

	auto hr = m_pDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(m_pFence.ReleaseAndGetAddressOf()));
	if (FAILED(hr))
	{
		return false;
	}

	m_fenceValue[m_CurrentBackBufferIndex]++;

	//同期を行うときのイベントハンドラを作成する。
	m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
	return m_fenceEvent != nullptr;
}

ビューポートとシザー矩形の生成

ビューポートとは、ウィンドウに対してレンダリング結果をどう表示するかという設定です
シザー矩形とは、ビューポートに表示された画像のどこからどこまでを画面に映し出すかという設定です

Engine.cpp
{
	// : 省略、さっき書いた初期化処理

	// ビューポートとシザー矩形を生成
	CreateViewPort();
	CreateScissorRect();

	printf("描画エンジンの初期化に成功\n");
	return true;
}

void Engine::CreateViewPort()
{
	m_Viewport.TopLeftX = 0;
	m_Viewport.TopLeftY = 0;
	m_Viewport.Width = static_cast<float>(m_FrameBufferWidth);
	m_Viewport.Height = static_cast<float>(m_FrameBufferHeight);
	m_Viewport.MinDepth = 0.0f;
	m_Viewport.MaxDepth = 1.0f;
}

void Engine::CreateScissorRect()
{
	m_Scissor.left = 0;
	m_Scissor.right = m_FrameBufferWidth;
	m_Scissor.top = 0;
	m_Scissor.bottom = m_FrameBufferHeight;
}

レンダーターゲットの生成

レンダーターゲットとは、キャンバスのようなもので、要は描画先のことです
レンダーターゲットの実態はバックバッファやテクスチャなどのリソースです

Engine.cpp
{
	// : 省略、さっき書いた初期化処理

	if (!CreateRenderTarget())
	{
		printf("レンダーターゲットの生成に失敗");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}

bool Engine::CreateRenderTarget()
{
	// RTV用のディスクリプタヒープを作成する
	D3D12_DESCRIPTOR_HEAP_DESC desc = {};
	desc.NumDescriptors = FRAME_BUFFER_COUNT;
	desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	auto hr = m_pDevice->CreateDescriptorHeap(&desc, IID_PPV_ARGS(m_pRtvHeap.ReleaseAndGetAddressOf()));
	if (FAILED(hr))
	{
		return false;
	}

	// ディスクリプタのサイズを取得。
	m_RtvDescriptorSize = m_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = m_pRtvHeap->GetCPUDescriptorHandleForHeapStart();

	for (UINT i = 0; i < FRAME_BUFFER_COUNT; i++)
	{
		m_pSwapChain->GetBuffer(i, IID_PPV_ARGS(m_pRenderTargets[i].ReleaseAndGetAddressOf()));
		m_pDevice->CreateRenderTargetView(m_pRenderTargets[i].Get(), nullptr, rtvHandle);
		rtvHandle.ptr += m_RtvDescriptorSize;
	}

	return true;
}

DirectXTexの導入

ここで、一旦DirectXTexを導入したいと思います
DirectXTexとは、テクスチャを読み込むためのライブラリです
なぜこの段階でテクスチャ?と思った方もいるかもしれませんが、テクスチャは後々使うことになるというのと、
DirectXTexに含まれている「d3dx12.h」というヘッダーを使いたいので、この段階で導入することにします

「d3dx12.h」は、DirectX12で構造体を定義するときのヘルパーが多数含まれており、
普通何行も書かないと設定できないDescriptorの設定を1行にまとめてしまうなんかのことができます

1行にまとめてしまうことが一概に良いのかというと、中にどんな設定があるのかという話を全部すっ飛ばしてしまうことになるので、
そういう意味では一旦中身についての理解をした後に使った方が良いかもしれないのですが、
こと最初にDirectX12を触る時点では、何行もよくわからない設定を記述しないとうまく動作しないという方がストレスだと思ったので、
本記事では「d3dx12.h」のヘルパー構造体やマクロをガンガン使っていきたいと思います

この辺りの話は、最初に紹介した書籍なんかと合わせてどう違うのかを見ながら進めていくことをお勧めします

では、DiretXTexを導入していきましょう

https://github.com/microsoft/DirectXTex
からDirectXTexのファイルをダウンロードしましょう

解答したら、中にいくつかのソリューション「.sln」ファイルが入っているかと思います
今回はVisual Studio2019を使っているので、「DirectXTex_Desktop_2019_Win10.sln」を開きます

スクリーンショット 2021-12-16 23.19.02.png

そして、プラットフォームを「x64」、ソリューション構成を「Debug」にして、ビルドしましょう
ビルドしたら、「{展開先のフォルダ}/DirectXTex/Bin/Desktop_2019/x64/Debug」というフォルダが生成されていることを確認します
「x64」を選択したのは、後で3Dモデルを読み込む時にしようする「Assimp」を使う際に、「x86」だとエラーが発生したためです
(ここについては詳しく調査できていませんが、先ほどまで作業していたプロジェクトでも「x64」の設定に変更しておくと良いかもしれません
今回は「Debug」でビルドしましたが、「Release」でも使用するなら「Relase」でもビルドしておきましょう

2022/06/22追記
Visual Studio 2019では32bitモードが削除されたためのようです
https://stackoverflow.com/questions/63257921/how-to-build-assimp-library-in-32-bit-for-c

スクリーンショット 2021-12-16 23.21.22.png

ビルドに成功して、フォルダができたのを確認できたら、パスを通します
「Win」キーを押して、「環境変数」と入力すると、「システム環境変数の編集」という項目が出てくるので、それをクリックします

スクリーンショット 2021-12-16 23.22.26.png

すると、「システムのプロパティ」が開くので、「詳細設定」タブの「環境変数」をクリックします
下の「システム環境変数」で「新規」を押して、「変数名」に「DXTEX_DIR」、「変数値」に「{展開先のフォルダ}¥DirectXTex¥」を入力します
環境変数が適用されるように一度PCを再起動しておきましょう

スクリーンショット 2021-12-16 23.23.51.png

再起動が完了したら、プロジェクトに戻って、ソリューションエクスプローラーからプロジェクト名を右クリックして「プロパティ」を開きます
「構成」と「プラットフォーム」を先ほどビルドしたものに合わせて、

  • 「C/C++」 > 「全般」 > 「追加のインクルードディレクトリ」に「${DXTEX_DIR}」
  • リンカー > 「全般」 > 「追加のライブラリディレクトリ」に「${DXTEX_DIR}/Bin/Desctop_2019/{プラットフォーム名}/{構成名}」
    と設定します
    リンカーの方は、例えば「x64」の「Debug」なら、「${DXTEX_DIR}/Bin/Desctop_2019/{x64}/{Debug}」となります
    プロパティの「構成」と「プラットフォーム」が合っていることをしっかり確認しましょう

スクリーンショット 2021-12-16 23.25.04.png

スクリーンショット 2021-12-16 23.25.58.png

どこかのcppファイルで

#include <DirectXTex.h>

とインクルードしてみて、コンパイルが通れば成功です
ついでに、

#include <d3dx12.h>

も試してみましょう

深度ステンシルバッファの生成

レンダーターゲットにただ描画するだけだと、後から書かれたものが手前に表示されてしまい、表示がおかしくなってしまいます
これがポリゴン同士で起きてしまうと、どっちが手前でどっちが奥なのかを判別できない状況になってしまいます
これを解消するためによく使われるのがZバッファ法と呼ばれるもので、
カメラから見たZの値を持っておくためのバッファを作り、ピクセルごとにそのバッファを見ればどれを手前に描画すれば良いかわかるというものです
そして、このZ(深度)値を持っておくためのバッファを深度ステンシルバッファと呼びます

入門書などでよく見る形式としては、二つの三角形同士が重なってうまく表示できていないものを修正する例があるかと思いますが、
どうせ使うことになるものなので、その例のは一旦省いて今回は先に実装してしまいます

Engine.cpp
{
	// : 省略、さっき書いた初期化処理

	if (!CreateDepthStencil())
	{
		printf("デプスステンシルバッファの生成に失敗\n");
		return false;
	}

	printf("描画エンジンの初期化に成功\n");
	return true;
}

bool Engine::CreateDepthStencil()
{
	//DSV用のディスクリプタヒープを作成する
	D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
	heapDesc.NumDescriptors = 1;
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	auto hr = m_pDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&m_pDsvHeap));
	if (FAILED(hr))
	{
		return false;
	}

	//ディスクリプタのサイズを取得
	m_DsvDescriptorSize = m_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);

	D3D12_CLEAR_VALUE dsvClearValue;
	dsvClearValue.Format = DXGI_FORMAT_D32_FLOAT;
	dsvClearValue.DepthStencil.Depth = 1.0f;
	dsvClearValue.DepthStencil.Stencil = 0;

	auto heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
	CD3DX12_RESOURCE_DESC resourceDesc(
		D3D12_RESOURCE_DIMENSION_TEXTURE2D,
		0,
		m_FrameBufferWidth,
		m_FrameBufferHeight,
		1,
		1,
		DXGI_FORMAT_D32_FLOAT,
		1,
		0,
		D3D12_TEXTURE_LAYOUT_UNKNOWN,
		D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL | D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE);
	hr = m_pDevice->CreateCommittedResource(
		&heapProp,
		D3D12_HEAP_FLAG_NONE,
		&resourceDesc,
		D3D12_RESOURCE_STATE_DEPTH_WRITE,
		&dsvClearValue,
		IID_PPV_ARGS(m_pDepthStencilBuffer.ReleaseAndGetAddressOf())
	);

	if (FAILED(hr))
	{
		return false;
	}

	//ディスクリプタを作成
	D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = m_pDsvHeap->GetCPUDescriptorHandleForHeapStart();

	m_pDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, dsvHandle);

	return true;
}

必要なものを作る処理は一旦ここで終了です
お疲れさまでした!

描画開始の処理を追加

ではいよいよ描画に入っていきましょう

描画時にどんな事をやっているのかというと、

  • 使用するレンダーターゲットが使用可能になるまで待つ
  • ビューポートとシザー矩形から描画先の領域を決める
  • レンダーターゲットと深度ステンシルバッファをクリアする ↑ここまで描画の前処理
  • 3Dオブジェクトの描画
  • レンダーターゲットに書き込みが終わるまで待つ ↓ここから描画の終了処理
  • ここまでためてきた描画命令を一括して実行
  • スワップチェーンで画面を切り替える
  • 画面に反映されるのを待つ

といったことを毎フレーム行っています

ここまでためてきた描画命令を一括して実行

と書いていますが、CommandListに対して行う命令は、その瞬間に実行されるわけでは無く、
一度CommandListに命令を溜めておいて、最後に一括して実行されるという仕組みになっています

それではさっそく、描画開始の処理から記述していきましょう
Engineクラスの「BeginRender」関数を以下のように実装します

Engine.cpp
void Engine::BeginRender()
{
	// 現在のレンダーターゲットを更新
	m_currentRenderTarget = m_pRenderTargets[m_CurrentBackBufferIndex].Get();

	// コマンドを初期化してためる準備をする
	m_pAllocator[m_CurrentBackBufferIndex]->Reset();
	m_pCommandList->Reset(m_pAllocator[m_CurrentBackBufferIndex].Get(), nullptr);

	// ビューポートとシザー矩形を設定
	m_pCommandList->RSSetViewports(1, &m_Viewport);
	m_pCommandList->RSSetScissorRects(1, &m_Scissor);

	// 現在のフレームのレンダーターゲットビューのディスクリプタヒープの開始アドレスを取得
	auto currentRtvHandle = m_pRtvHeap->GetCPUDescriptorHandleForHeapStart();
	currentRtvHandle.ptr += m_CurrentBackBufferIndex * m_RtvDescriptorSize;

	// 深度ステンシルのディスクリプタヒープの開始アドレス取得
	auto currentDsvHandle = m_pDsvHeap->GetCPUDescriptorHandleForHeapStart();

	// レンダーターゲットが使用可能になるまで待つ
	auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_currentRenderTarget, D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
	m_pCommandList->ResourceBarrier(1, &barrier);

	// レンダーターゲットを設定
	m_pCommandList->OMSetRenderTargets(1, &currentRtvHandle, FALSE, &currentDsvHandle);

	// レンダーターゲットをクリア
	const float clearColor[] = { 0.25f, 0.25f, 0.25f, 1.0f };
	m_pCommandList->ClearRenderTargetView(currentRtvHandle, clearColor, 0, nullptr);

	// 深度ステンシルビューをクリア
	m_pCommandList->ClearDepthStencilView(currentDsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
}

次は、「EndRender」と行きたいところですが、画面に反映されるのを待つ「WaitRender」を中で使いたいので、先に定義します

Engine.cpp
void Engine::WaitRender()
{
	//描画終了待ち
	const UINT64 fenceValue = m_fenceValue[m_CurrentBackBufferIndex];
	m_pQueue->Signal(m_pFence.Get(), fenceValue);
	m_fenceValue[m_CurrentBackBufferIndex]++;

	// 次のフレームの描画準備がまだであれば待機する.
	if (m_pFence->GetCompletedValue() < fenceValue)
	{
		// 完了時にイベントを設定.
		auto hr = m_pFence->SetEventOnCompletion(fenceValue, m_fenceEvent);
		if (FAILED(hr))
		{
			return;
		}

		// 待機処理.
		if (WAIT_OBJECT_0 != WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE))
		{
			return;
		}
	}
}

最後に、「EndRender」を実装します

Engine.cpp
void Engine::EndRender()
{
	// レンダーターゲットに書き込み終わるまで待つ
	auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_currentRenderTarget, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
	m_pCommandList->ResourceBarrier(1, &barrier);

	// コマンドの記録を終了
	m_pCommandList->Close();

	// コマンドを実行
	ID3D12CommandList* ppCmdLists[] = { m_pCommandList.Get() };
	m_pQueue->ExecuteCommandLists(1, ppCmdLists);

	// スワップチェーンを切り替え
	m_pSwapChain->Present(1, 0);

	// 描画完了を待つ
	WaitRender();

	// バックバッファ番号更新
	m_CurrentBackBufferIndex = m_pSwapChain->GetCurrentBackBufferIndex();
}

ここまで書けたら、「App.cpp」に戻って、今実装した「BeginRender」「EndRender」を実行しましょう

App.h
void MainLoop()
{
	MSG msg = {};
	while (WM_QUIT != msg.message)
	{
		if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE == TRUE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			// 後でこの行で更新処理を行う
			g_Engine->BeginRender();
			// 後でこの行で3Dオブジェクトのの描画処理を行う
			g_Engine->EndRender();
		}
	}
}

実行してみて、こんなグレーのウィンドウが表示されれば成功です
スクリーンショット 2021-12-14 20.47.08.png

このグレー何かというと、描画開始時にクリアしたときに指定した色で、「BeginRender」のここで指定されています

Engine.cpp
	// レンダーターゲットをクリア
	const float clearColor[] = { 0.25f, 0.25f, 0.25f, 1.0f };
	m_pCommandList->ClearRenderTargetView(currentRtvHandle, clearColor, 0, nullptr);

例えばこの色を

Engine.cpp
	const float clearColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; // R=255 G=255 B=0 A=255

こんな感じにしてやると、黄色で初期化することになって画面が黄色くなります
Unityなんかではカメラから指定することができるようになっていますが、そういうことをやろうとするとかなり大掛かりなことをやらなければならなくなっちゃうので、一旦グレーでおいておきます

スクリーンショット 2021-12-14 20.50.26.png

外部からも使いたい値をGetter関数として公開する

デバイスはGPUに対してリソースを確保したり、ディスクリプタヒープを確保したり、いろいろな場所から使いたい場面があるので、Getterとして取得できるようにします

Engine.cpp
ID3D12Device6* Engine::Device()
{
	return m_pDevice.Get();
}

コマンドリストも、3Dモデルを描画するときにコマンドリストを経由して描画命令を貯めるので、公開します

Engine.cpp
ID3D12GraphicsCommandList* Engine::CommandList()
{
	return m_pCommandList.Get();
}

そして、ダブルバッファリングのためにバッファを2つ確保しておきたい場面などのために、現在のフレーム番号も公開しておきます

Engine.cpp
UINT Engine::CurrentBackBufferIndex()
{
	return m_CurrentBackBufferIndex;
}

ポリゴンを表示してみる

ここまでで、DirectX12を使って描画を行う準備は整いました

3Dモデルを描画するには、「BeginRender」「EndRender」の間に3Dモデルの描画処理を書いてあげればいいのですが、
前の章で実装した「Engine.h」と「Engine.cpp」は描画の基盤としての役割だけにしておきたいので、新たに「Scene」というクラスを作ることにします
といっても、3Dモデルを描画するための処理に集中するための場所を作りたいという理由なので、Unityなどのゲームエンジンのようなシーンを作るというわけではないのであしからず

Scene.h
#pragma once

class Scene
{
public:
	bool Init(); // 初期化

	void Update(); // 更新処理
	void Draw(); // 描画処理
};

extern Scene* g_Scene;
Scene.cpp
#include "Scene.h"
#include "Engine.h"
#include "App.h"
#include <d3dx12.h>

Scene* g_Scene;

bool Scene::Init()
{
	printf("シーンの初期化に成功\n");
	return true;
}

void Scene::Update()
{
}

void Scene::Draw()
{
}

「App.cpp」に戻ってそれぞれの関数が呼ばれるようにしましょう

App.h
#include "App.h"
#include "Engine.h"
#include "Scene.h" // インクルード追加

// : 省略

void MainLoop()
{
	MSG msg = {};
	while (WM_QUIT != msg.message)
	{
		if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE == TRUE))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
		{
			g_Scene->Update();
			g_Engine->BeginRender();
			g_Scene->Draw();
			g_Engine->EndRender();
		}
	}
}

void StartApp(const TCHAR* appName)
{
	// ウィンドウ生成
	InitWindow(appName);

	g_Engine = new Engine();
	if (!g_Engine->Init(g_hWnd, WINDOW_WIDTH, WINDOW_HEIGHT))
	{
		return;
	}

	// シーン初期化
	g_Scene = new Scene();
	if (!g_Scene->Init())
	{
		return;
	}

	// メイン処理ループ
	MainLoop();
}

実行してみて、「シーンの初期化に成功」と出れば問題ありません

頂点データの定義

まずは、三角形を描画したいです
シェーダーに渡す頂点データを定義しましょう
「SharedStruct.h」を作って、それぞれこんな感じに書きましょう

頂点データとは、その名の通り頂点に関するいろいろな情報をまとめたデータです
文字通り頂点の位置座標を示すPositionもありますし、面がどちらを向いているかを示す法線なんかの情報も入っています

SharedStruct.h
#pragma once
#include <d3dx12.h>
#include <DirectXMath.h>
#include "ComPtr.h"

struct Vertex
{
    DirectX::XMFLOAT3 Position; // 位置座標
    DirectX::XMFLOAT3 Normal; // 法線
    DirectX::XMFLOAT2 UV; // uv座標
    DirectX::XMFLOAT3 Tangent; // 接空間
    DirectX::XMFLOAT4 Color; // 頂点色
};

最初の例で使うのは位置座標と頂点色だけですが、他にもよく使うことになる法線、uv、接空間を定義してしまっています
なぜこのタイミングで定義するのかというと、あとあと出てくるInputLayoutも一緒にいじらないといけなくなるからです
InputLayoutとは、このデータをリソースとしてデバイスに送った後に、そのリソースをどういうデータの並びとして解釈すれば良いのかを定義したデータです
InputLayoutは、パイプラインステートを生成する際に必要なので、定義はその時にしておきましょう

定義ができたら、「Scene.cpp」に戻って早速ポリゴンの頂点を定義します
三角形なので、3つの頂点を用意するわけですね
ある程度ざっくり色も表示してあげたいので、頂点からーも適当に入れておきます

Scene.cpp
#include "Scene.h"
#include "Engine.h"
#include "App.h"
#include <d3dx12.h>
#include "SharedStruct.h"

Scene* g_Scene;

using namespace DirectX;

bool Scene::Init()
{
	Vertex vertices[3] = {};
	vertices[0].Position = XMFLOAT3(-1.0f, -1.0f, 0.0f);
	vertices[0].Color = XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f);

	vertices[1].Position = XMFLOAT3(1.0f, -1.0f, 0.0f);
	vertices[1].Color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].Position = XMFLOAT3(0.0f, 1.0f, 0.0f);
	vertices[2].Color = XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f);

// : 省略
}

頂点バッファを作る

頂点の情報を定義したら、次は頂点バッファをデバイスに作り、そこに頂点のデータを送る必要があります
頂点バッファを生成するための処理をまとめたクラスを作り、それを行いたいと思います

頂点バッファとは、デバイスが頂点のデータを受け取るための領域のことです
その領域を確保して、先ほど定義した三角形の頂点をデバイスに渡すのですが、
描画時にそのどの頂点バッファを見て描画を行えば良いかを指定するために、頂点バッファビューというものも使います
なので、頂点バッファビューも一緒に取得できる形にしておきましょう

VertexBuffer
#pragma once
#include <d3d12.h>
#include "ComPtr.h"

class VertexBuffer
{
public:
	VertexBuffer(size_t size, size_t stride, const void* pInitData); // コンストラクタでバッファを生成
	D3D12_VERTEX_BUFFER_VIEW View() const; // 頂点バッファビューを取得
	bool IsValid(); // バッファの生成に成功したかを取得

private:
	bool m_IsValid = false; // バッファの生成に成功したかを取得
	ComPtr<ID3D12Resource> m_pBuffer = nullptr; // バッファ
	D3D12_VERTEX_BUFFER_VIEW m_View = {}; // 頂点バッファビュー

	VertexBuffer(const VertexBuffer&) = delete;
	void operator = (const VertexBuffer&) = delete;
};
VertexBuffer.cpp
#include "VertexBuffer.h"
#include "Engine.h"
#include <d3dx12.h>

VertexBuffer::VertexBuffer(size_t size, size_t stride, const void* pInitData)
{

	auto prop = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); 	// ヒーププロパティ
	auto desc = CD3DX12_RESOURCE_DESC::Buffer(size); 	// リソースの設定

	// リソースを生成
	auto hr = g_Engine->Device()->CreateCommittedResource(
		&prop,
		D3D12_HEAP_FLAG_NONE,
		&desc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(m_pBuffer.GetAddressOf()));

	if (FAILED(hr))
	{
		printf("頂点バッファリソースの生成に失敗");
		return;
	}

	// 頂点バッファビューの設定
	m_View.BufferLocation = m_pBuffer->GetGPUVirtualAddress();
	m_View.SizeInBytes = static_cast<UINT>(size);
	m_View.StrideInBytes = static_cast<UINT>(stride);

	// マッピングする
	if (pInitData != nullptr)
	{
		void* ptr = nullptr;
		hr = m_pBuffer->Map(0, nullptr, &ptr);
		if (FAILED(hr))
		{
			printf("頂点バッファマッピングに失敗");
			return;
		}

		// 頂点データをマッピング先に設定
		memcpy(ptr, pInitData, size);

		// マッピング解除
		m_pBuffer->Unmap(0, nullptr);
	}

	m_IsValid = true;
}

D3D12_VERTEX_BUFFER_VIEW VertexBuffer::View() const
{
	return m_View;
}

bool VertexBuffer::IsValid()
{
	return m_IsValid;
}

ここまでできたら、頂点バッファを生成する処理を呼んでみましょう
「頂点バッファの生成に失敗」とでなければ成功です

Scene.cpp
#include "Scene.h"
#include "Engine.h"
#include "App.h"
#include <d3dx12.h>
#include "SharedStruct.h"
#include "VertexBuffer.h"

Scene* g_Scene;

using namespace DirectX;

VertexBuffer* vertexBuffer;

bool Scene::Init()
{
	Vertex vertex[3] = {};
	// : 省略

  auto vertexSize = sizeof(Vertex) * std::size(vertices);
	auto vertexStride = sizeof(Vertex);
	vertexBuffer = new VertexBuffer(vertexSize, vertexStride, vertices);
	if (!vertexBuffer->IsValid())
	{
		printf("頂点バッファの生成に失敗\n");
		return false;
	}
}

これで頂点をデバイスに送るところまではできました

変換行列を作る

次は、変換行列というものを定義してデバイスに送ります
変換行列とは、座標を変換するための行列のことです
ざっくりと説明すると、オブジェクトを画面に表示するにあたって、
ローカル座標系 > ワールド座標系 > ビュー座標系と座標変換を行い、
それをさらに投影変換を書けることでカメラから見た座標に変換することができます
これにさらに、ビューポート変換を行うことで、スクリーンで見た時の座標とすることができます
この時、ローカル座標系からワールド座標系に変換する時にワールド行列を、
ワールド座標系からビュー座標系に変換する時にビュー行列を、
ビュー座標系から投影変換を行う時に投影行列を使用します
これら行列は4×4の表列で表されます

まずは変換行列の構造体を定義しましょう
「SharedStruct.h」に定義を追加します
行列には「DirectX::XMMATRIX」を使用します

SharedStruct.h
struct alignas(256) Transform
{
    DirectX::XMMATRIX World; // ワールド行列
    DirectX::XMMATRIX View; // ビュー行列
    DirectX::XMMATRIX Proj; // 投影行列
};

頂点をデバイスに送るときは頂点バッファを使いましたが、
変換行列をデバイスに送る際は、定数バッファというものを使います
上で作ったTransform構造体を入れるリソースを確保するというわけですね
早速定義していきましょう

ConstantBuffer.h
#pragma once
#include <d3dx12.h>
#include "ComPtr.h"

class ConstantBuffer
{
public:
    ConstantBuffer(size_t size); // コンストラクタで定数バッファを生成
    bool IsValid(); // バッファ生成に成功したかを返す
    D3D12_GPU_VIRTUAL_ADDRESS GetAddress() const; // バッファのGPU上のアドレスを返す
    D3D12_CONSTANT_BUFFER_VIEW_DESC ViewDesc(); // 定数バッファビューを返す

    void* GetPtr() const; // 定数バッファにマッピングされたポインタを返す

    template<typename T>
    T* GetPtr()
    {
        return reinterpret_cast<T*>(GetPtr());
    }

private:
    bool m_IsValid = false; // 定数バッファ生成に成功したか
    ComPtr<ID3D12Resource> m_pBuffer; // 定数バッファ
    D3D12_CONSTANT_BUFFER_VIEW_DESC m_Desc; // 定数バッファビューの設定
    void* m_pMappedPtr = nullptr;

    ConstantBuffer(const ConstantBuffer&) = delete;
    void operator = (const ConstantBuffer&) = delete;
};

ConstantBuffer.cpp
#include "ConstantBuffer.h"
#include "Engine.h"

ConstantBuffer::ConstantBuffer(size_t size)
{
    size_t align = D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT;
    UINT64 sizeAligned = (size + (align - 1)) & ~(align - 1); // alignに切り上げる.

    auto prop = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); // ヒーププロパティ
    auto desc = CD3DX12_RESOURCE_DESC::Buffer(sizeAligned); // リソースの設定

    // リソースを生成
    auto hr = g_Engine->Device()->CreateCommittedResource(
        &prop,
        D3D12_HEAP_FLAG_NONE,
        &desc,
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(m_pBuffer.GetAddressOf()));
    if (FAILED(hr))
    {
        printf("定数バッファリソースの生成に失敗\n");
        return;
    }

    hr = m_pBuffer->Map(0, nullptr, &m_pMappedPtr);
    if (FAILED(hr))
    {
        printf("定数バッファのマッピングに失敗\n");
        return;
    }

    m_Desc = {};
    m_Desc.BufferLocation = m_pBuffer->GetGPUVirtualAddress();
    m_Desc.SizeInBytes = UINT(sizeAligned);

    m_IsValid = true;
}

bool ConstantBuffer::IsValid()
{
    return m_IsValid;
}

D3D12_GPU_VIRTUAL_ADDRESS ConstantBuffer::GetAddress() const
{
    return m_Desc.BufferLocation;
}

D3D12_CONSTANT_BUFFER_VIEW_DESC ConstantBuffer::ViewDesc()
{
    return m_Desc;
}

void* ConstantBuffer::GetPtr() const
{
    return m_pMappedPtr;
}

定数バッファクラスが定義できたら、早速呼び出していきましょう
今回は変換行列の生成処理も一緒に行います

Scene.cpp
// : 省略
#include "ConstantBuffer.h"

Scene* g_Scene;

using namespace DirectX;

VertexBuffer* vertexBuffer;
ConstantBuffer* constantBuffer[Engine::FRAME_BUFFER_COUNT];

bool Scene::Init()
{
	// : 省略 さっきかいた頂点関係の処理

	auto eyePos = XMVectorSet(0.0f, 0.0f, 5.0f, 0.0f); // 視点の位置
	auto targetPos = XMVectorZero(); // 視点を向ける座標
	auto upward = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // 上方向を表すベクトル
	auto fov = XMConvertToRadians(37.5); // 視野角
	auto aspect = static_cast<float>(WINDOW_WIDTH) / static_cast<float>(WINDOW_HEIGHT); // アスペクト比

	for (size_t i = 0; i < Engine::FRAME_BUFFER_COUNT; i++)
	{
		constantBuffer[i] = new ConstantBuffer(sizeof(Transform));
		if (!constantBuffer[i]->IsValid())
		{
			printf("変換行列用定数バッファの生成に失敗\n");
			return false;
		}

		// 変換行列の登録
		auto ptr = constantBuffer[i]->GetPtr<Transform>();
		ptr->World = XMMatrixIdentity();
		ptr->View = XMMatrixLookAtRH(eyePos, targetPos, upward);
		ptr->Proj = XMMatrixPerspectiveFovRH(fov, aspect, 0.3f, 1000.0f);
	}

	printf("シーンの初期化に成功\n");
	return true;
}

ここまで書けたら実行してみて、失敗と表示されなければOKです

ルートシグネチャの生成

次はルートシグネチャの設定です
ルートシグネチャとは、シェーダー内で使用する定数バッファやテクスチャ、サンプラーなどのリソースのレイアウト(メモリ上にどう並んでいるか)を定義するオブジェクトです
ルートシグネチャを使って、シェーダー内のどのレジスターとGPUのどのメモリの内容を紐付けるかを決定するわけです

例えば、ルートシグネチャで、「バッファの0(b0)に定数バッファを使う」と指定して、描画時に「b0でこの定数バッファを使ってね」と指定することで、
シェーダー内部のレジスターb0で、描画時に指定した定数バッファを使えるようになるわけです
つまり、先ほどの変換行列はシェーダーの中から使いたいものなので、ルートシグネチャを通してそういう設定をしていきます

ルートシグネチャの概念がDirectX12の中でもなかなかややこしい概念だと思っていて、
シェーダーにいろいろなデータを渡したい場合、ルートシグネチャをちゃんと使いこなす必要があります

ルートシグネチャも定義するのに結構いろいろな手順が必要になるので、クラス分けをするとなるとなかなか設計が難しい部分だと思います
今回は、コードが散らからないようにクラス化することはしますが、中身はある程度決めうちのものを使っていこうと思います
決め打ちというのは、b0で変換行列を使うようにする、サンプラー(テクスチャのところで後述)も決め打ちものを一つ用意するという形です

早速実装していきましょう

RootSignature.h
#pragma once
#include "ComPtr.h"

struct ID3D12RootSignature;

class RootSignature
{
public:
	RootSignature(); // コンストラクタでルートシグネチャを生成
	bool IsValid(); // ルートシグネチャの生成に成功したかどうかを返す
	ID3D12RootSignature* Get(); // ルートシグネチャを返す

private:
	bool m_IsValid = false; // ルートシグネチャの生成に成功したかどうか
	ComPtr<ID3D12RootSignature> m_pRootSignature = nullptr; // ルートシグネチャ
};


RootSignature.cpp
#include "RootSignature.h"
#include "Engine.h"
#include <d3dx12.h>

RootSignature::RootSignature()
{
	auto flag = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; // アプリケーションの入力アセンブラを使用する
	flag |= D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS; // ドメインシェーダーのルートシグネチャへんアクセスを拒否する
	flag |= D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS; // ハルシェーダーのルートシグネチャへんアクセスを拒否する
	flag |= D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS; // ジオメトリシェーダーのルートシグネチャへんアクセスを拒否する

	CD3DX12_ROOT_PARAMETER rootParam[1] = {};
	rootParam[0].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL); // b0の定数バッファを設定、全てのシェーダーから見えるようにする

	// スタティックサンプラーの設定
	auto sampler = CD3DX12_STATIC_SAMPLER_DESC(0, D3D12_FILTER_MIN_MAG_MIP_LINEAR);

	// ルートシグニチャの設定(設定したいルートパラメーターとスタティックサンプラーを入れる)
	D3D12_ROOT_SIGNATURE_DESC desc = {};
	desc.NumParameters = std::size(rootParam); // ルートパラメーターの個数をいれる
	desc.NumStaticSamplers = 1; // サンプラーの個数をいれる
	desc.pParameters = rootParam; // ルートパラメーターのポインタをいれる
	desc.pStaticSamplers = &sampler; // サンプラーのポインタを入れる
	desc.Flags = flag; // フラグを設定

	ComPtr<ID3DBlob> pBlob;
	ComPtr<ID3DBlob> pErrorBlob;

	// シリアライズ
	auto hr = D3D12SerializeRootSignature(
		&desc,
		D3D_ROOT_SIGNATURE_VERSION_1_0,
		pBlob.GetAddressOf(),
		pErrorBlob.GetAddressOf());
	if (FAILED(hr))
	{
		printf("ルートシグネチャシリアライズに失敗");
		return;
	}

	// ルートシグネチャ生成
	hr = g_Engine->Device()->CreateRootSignature(
		0, // GPUが複数ある場合のノードマスク(今回は1個しか無い想定なので0)
		pBlob->GetBufferPointer(), // シリアライズしたデータのポインタ
		pBlob->GetBufferSize(), // シリアライズしたデータのサイズ
		IID_PPV_ARGS(m_pRootSignature.GetAddressOf())); // ルートシグニチャ格納先のポインタ
	if (FAILED(hr))
	{
		printf("ルートシグネチャの生成に失敗");
		return;
	}

	m_IsValid = true;
}

bool RootSignature::IsValid()
{
	return m_IsValid;
}

ID3D12RootSignature* RootSignature::Get()
{
	return m_pRootSignature.Get();
}

これも、Sceneから呼び出しを行ってエラーが出ないか確認してみましょう

Scene.cpp
#include "Scene.h"
#include "Engine.h"
#include "App.h"
#include <d3dx12.h>
#include "SharedStruct.h"
#include "VertexBuffer.h"
#include "ConstantBuffer.h"
#include "RootSignature.h"

Scene* g_Scene;

using namespace DirectX;

VertexBuffer* vertexBuffer;
ConstantBuffer* constantBuffer[Engine::FRAME_BUFFER_COUNT];
RootSignature* rootSignature;

bool Scene::Init()
{
	// : 省略 さっき書いた定数バッファまでの初期化処理

	rootSignature = new RootSignature();
	if (!rootSignature->IsValid())
	{
		printf("ルートシグネチャの生成に失敗\n");
		return false;
	}

	printf("シーンの初期化に成功\n");
	return true;
}

グラフィックスパイプラインステートを作る

最後に、グラフィックスパイプラインステートの設定を行います
パイプラインステートは、頂点シェーダーへの入力データのレイアウト、使用したいシェーダー、ラスタライザー、ブレンドの仕方など様々なものを設定します

パイプラインステートを生成するにはシェーダーが必要なので、まずはシェーダーを準備しましょう
今回は基礎的なシェーダーしか書かないので、頂点シェーダーとピクセルシェーダーをそれぞれ用意します

Visual Studioの「ソリューションエクスプローラー」から、「リソースファイル」を右クリックして、
「追加」 > 「新しい項目」 > 「HLSL」 > 「頂点シェーダー」 > 名前を「SimpleVS」として「SimpleVS.hlsl」を作成します
作成できたら、「ソリューションエクスプローダー」から今作成した「SimpleVS.hsls」を右クリックして、「プロパティ」を開きます
構成を「すべての構成」、プラットフォームを「すべてのプラットフォーム」に変更し、
「HLSLコンパイラ」 > 「全般」を選択します
「エントリポイント名」を「vert」、シェーダーモデルを「Shader Model 5.0 (/5_0)」に変更します
これで頂点シェーダーは作成できました

頂点シェーダーとは、入力された頂点を座標変換するシェーダーです
バーテックスシェーダーと呼ぶこともあります

スクリーンショット 2021-12-17 10.34.41.png

同様の手順で、「ピクセルシェーダー」も作成します
こちらは「SimplePS.hlsl」という名前にしましょう
プロパティから設定する「エントリポイント名」だけ「pixel」と設定し、シェーダーモデルは頂点シェーダーと同じ「Shader Model 5.0 (/5_0)」としておきます

ピクセルシェーダーとは、ポリゴンの面に対して色をつけるためのシェーダーです
フラグメントシェーダーと言ったりもします

スクリーンショット 2021-12-17 10.34.51.png

ここまでできたら、それぞれのシェーダーにこんな感じでコーディングしましょう
「Transform」が定数バッファのところで定義した「Transform」構造体と一致、
「VSInput」が頂点データのところで定義した「Vertex」構造体と一致していることがお分かりでしょうか
先ほどC++側で定義したシェーダーに渡したいデータの構造体が入ってくる受け皿がこれです
「Transform」は「register(b0)」となっていて、これはルートシグネチャに追加した「定数バッファ0を使う」という設定と対応しています
頂点データの流れについては、描画処理を書く際にまた解説したいと思います

SimpleVS.hsls
cbuffer Transform : register(b0)
{
	float4x4 World; // ワールド行列
	float4x4 View; // ビュー行列
	float4x4 Proj; // 投影行列
}

struct VSInput
{
	float3 pos : POSITION; // 頂点座標
	float3 normal : NORMAL; // 法線
	float2 uv : TEXCOORD; // UV
	float3 tangent : TANGENT; // 接空間
	float4 color : COLOR; // 頂点色
};

struct VSOutput
{
	float4 svpos : SV_POSITION; // 変換された座標
	float4 color : COLOR; // 変換された色
};

VSOutput vert(VSInput input)
{
	VSOutput output = (VSOutput)0; // アウトプット構造体を定義する

	float4 localPos = float4(input.pos, 1.0f); // 頂点座標
	float4 worldPos = mul(World, localPos); // ワールド座標に変換
	float4 viewPos = mul(View, worldPos); // ビュー座標に変換
	float4 projPos = mul(Proj, viewPos); // 投影変換

	output.svpos = projPos; // 投影変換された座標をピクセルシェーダーに渡す
	output.color = input.color; // 頂点色をそのままピクセルシェーダーに渡す
	return output;
}
SimplePS.hlsl
struct VSOutput
{
	float4 svpos : SV_POSITION; // 頂点シェーダーから来た座標
	float4 color : COLOR; // 頂点シェーダーから来た色
};

float4 pixel(VSOutput input) : SV_TARGET
{
	return input.color; // 色をそのまま表示する
}

一旦シェーダーは出来上がりました
そのまま実行してみて、コンパイルエラーが出ないか確認しましょう

次は入力レイアウト(InputLayout)を定義していきます
入力レイアウトは、頂点シェーダーの入力がどんなデータ構成であるかを設定するものです
つまり、VSInputに対応しています

今回は、C++側では「Vertex」が頂点シェーダーに入力される値なので、「Vertex」の中に入力レイアウトの定義を書いていきたいと思います
「POSITION」や「NORMAL」という部分がセマンティクスというもので、頂点シェーダーのインプットは全てセマンティクスをつける必要があります
DirectX12ではセマンティクスを自分で定義することができ、入力レイアウトも自分で設定することができます
Unityなどを使っている方は、「TEXCOORD0」や「TEXCOORD1」など、セマンティクスの文字列の末尾に数字がついているのを目にすることがあるかと思いますが、
入力レイアウトでは「”TEXCORRD0”」のように文字列部分に数字を入れるのではなく、「D3D12_INPUT_ELEMENT_DESC」の2個目の数字がセマンティクスのインデックスに対応しているので、そこに数字を入れる必要があります
つまり、「TEXCOORD1」としたかったら、{ "TEXCOORD", 1, … のように記述する必要があります
今回は同じセマンティクスは1つしか使わないので、インデックスは全て0でいきたいと思います

SharedStruct.h
struct Vertex
{
    // : 省略

    static const D3D12_INPUT_LAYOUT_DESC InputLayout;

private:
    static const int InputElementCount = 5;
    static const D3D12_INPUT_ELEMENT_DESC InputElements[InputElementCount];
};
SharedStruct.cpp
#include "SharedStruct.h"

const D3D12_INPUT_ELEMENT_DESC Vertex::InputElements[] =
{
	{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, // float3のPOSITION
	{ "NORMAL",   0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, // float3のNORMAL
	{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, // float2のTEXCOORD
	{ "TANGENT",  0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, // float3のTANGENT
	{ "COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, // float4のCOLOR
};
const D3D12_INPUT_LAYOUT_DESC Vertex::InputLayout =
{
	Vertex::InputElements,
	Vertex::InputElementCount
};

では、今回もパイプランステート用のクラスを定義して、ひとまとめにしておきたいと思います
ルートシグネチャの時はある程度決め打ちで中身を書いてしまいましたが、今回は外側からいくつかの値を設定できるようにし、
最後に「Create」を呼ぶことでパイプラインステートオブジェクトを生成するような作りにしたいと思います

PipelineState.h
#pragma once
#include "ComPtr.h"
#include <d3dx12.h>
#include <string>

class PipelineState
{
public:
	PipelineState(); // コンストラクタである程度の設定をする
	bool IsValid(); // 生成に成功したかどうかを返す

	void SetInputLayout(D3D12_INPUT_LAYOUT_DESC layout); // 入力レイアウトを設定
	void SetRootSignature(ID3D12RootSignature* rootSignature); // ルートシグネチャを設定
	void SetVS(std::wstring filePath); // 頂点シェーダーを設定
	void SetPS(std::wstring filePath); // ピクセルシェーダーを設定
	void Create(); // パイプラインステートを生成

	ID3D12PipelineState* Get();

private:
	bool m_IsValid = false; // 生成に成功したかどうか
	D3D12_GRAPHICS_PIPELINE_STATE_DESC desc = {}; // パイプラインステートの設定
	ComPtr<ID3D12PipelineState> m_pPipelineState = nullptr; // パイプラインステート
	ComPtr<ID3DBlob> m_pVsBlob; // 頂点シェーダー
	ComPtr<ID3DBlob> m_pPSBlob; // ピクセルシェーダー
};

PipelineState.cpp
#include "PipelineState.h"
#include "Engine.h"
#include <d3dx12.h>
#include <d3dcompiler.h>

#pragma comment(lib, "d3dcompiler.lib")

PipelineState::PipelineState()
{
	// パイプラインステートの設定
	desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // ラスタライザーはデフォルト
	desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; // カリングはなし
	desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // ブレンドステートもデフォルト
	desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); // 深度ステンシルはデフォルトを使う
	desc.SampleMask = UINT_MAX;
	desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; // 三角形を描画
	desc.NumRenderTargets = 1; // 描画対象は1
	desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
	desc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
	desc.SampleDesc.Count = 1; // サンプラーは1
	desc.SampleDesc.Quality = 0;
}

bool PipelineState::IsValid()
{
	return m_IsValid;
}

void PipelineState::SetInputLayout(D3D12_INPUT_LAYOUT_DESC layout)
{
	desc.InputLayout = layout;
}

void PipelineState::SetRootSignature(ID3D12RootSignature* rootSignature)
{
	desc.pRootSignature = rootSignature;
}

void PipelineState::SetVS(std::wstring filePath)
{
// 頂点シェーダー読み込み
	auto hr = D3DReadFileToBlob(filePath.c_str(), m_pVsBlob.GetAddressOf());
	if (FAILED(hr))
	{
		printf("頂点シェーダーの読み込みに失敗");
		return;
	}

	desc.VS = CD3DX12_SHADER_BYTECODE(m_pVsBlob.Get());
}

void PipelineState::SetPS(std::wstring filePath)
{
	// ピクセルシェーダー読み込み
	auto hr = D3DReadFileToBlob(filePath.c_str(), m_pPSBlob.GetAddressOf());
	if (FAILED(hr))
	{
		printf("ピクセルシェーダーの読み込みに失敗");
		return;
	}

	desc.PS = CD3DX12_SHADER_BYTECODE(m_pPSBlob.Get());
}

void PipelineState::Create()
{
	// パイプラインステートを生成
	auto hr = g_Engine->Device()->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(m_pPipelineState.ReleaseAndGetAddressOf()));
	if (FAILED(hr))
	{
		printf("パイプラインステートの生成に失敗");
		return;
	}

	m_IsValid = true;
}

ID3D12PipelineState* PipelineState::Get()
{
	return m_pPipelineState.Get();
}

こちらも呼び出してみましょう

Scene.cpp
#include "Scene.h"
#include "Engine.h"
#include "App.h"
#include <d3dx12.h>
#include "SharedStruct.h"
#include "VertexBuffer.h"
#include "ConstantBuffer.h"
#include "RootSignature.h"
#include "PipelineState.h"


Scene* g_Scene;

using namespace DirectX;

VertexBuffer* vertexBuffer;
ConstantBuffer* constantBuffer[Engine::FRAME_BUFFER_COUNT];
RootSignature* rootSignature;
PipelineState* pipelineState;

bool Scene::Init()
{
	// : 省略 さっき書いたルートシグネチャまでの初期化処理

	pipelineState = new PipelineState();
	pipelineState->SetInputLayout(Vertex::InputLayout);
	pipelineState->SetRootSignature(rootSignature->Get());
	pipelineState->SetVS(L"../x64/Debug/SimpleVS.cso");
	pipelineState->SetPS(L"../x64/Debug/SimplePS.cso");
	pipelineState->Create();
	if (!pipelineState->IsValid())
	{
		printf("パイプラインステートの生成に失敗\n");
		return false;
	}

	printf("シーンの初期化に成功\n");
	return true;
}

実行してみて、成功と表示されるか確認してみましょう

シェーダーのパスの指定を、

pipelineState->SetVS(L"../x64/Debug/SimpleVS.cso");

のようにしているのは、コンパイルされて吐き出されるシェーダーファイルは「{プロジェクト名}/{プラットフォーム名}/{構成}」フォルダに吐き出されるためです
例えば、「x64」「Debug」で作業しているなら、「{プロジェクト名}/x64/Debug」フォルダに入っています
今回はベタ書きしましたが、DebugとReleaseを切り替えてもうまく動くように変えておくと良いかもしれません

描画処理を書く

さて、いよいよ描画に必要な要素がすべて揃いました
描画処理を書いて三角形のポリゴンを表示してみましょう!

ここで、定数バッファと頂点バッファを設定していることに注目してください
やることがかなり多くて、三角形を描画するのに必要で定義したデータがどう使われているのかかなりおいづらいですが、
「SetGraphicsRootConstantBufferView」で先ほど定義した定数バッファを渡していて、
「IASetVertexBuffers」で頂点バッファビューを渡しています

定数バッファはルートシグネチャで「b0」を使うと設定しました
「SetGraphicsRootConstantBufferView(0, constantBuffer[currentIndex]->GetAddress());」の0がb0のことで、その中身が「constantBuffer[currentIndex]->GetAddress()」だよと指定しているわけです
定数バッファはこのようにシェーダーに渡されます

頂点バッファもみてみましょう
まず、「Vertex」構造体を3つ用意して三角形の座標を定義しました
これを頂点バッファクラスを使用してデバイス内のリソースとして配置し、頂点バッファビューを通してコマンドリストに「この頂点を使うぞ」と指示しています
これで、三角形を描画するのに必要なデータをシェーダーに伝えることができました

Scene.cpp
void Scene::Draw()
{
	auto currentIndex = g_Engine->CurrentBackBufferIndex(); // 現在のフレーム番号を取得する
	auto commandList = g_Engine->CommandList(); // コマンドリスト
	auto vbView = vertexBuffer->View(); // 頂点バッファビュー

	commandList->SetGraphicsRootSignature(rootSignature->Get()); // ルートシグネチャをセット
	commandList->SetPipelineState(pipelineState->Get()); // パイプラインステートをセット
	commandList->SetGraphicsRootConstantBufferView(0, constantBuffer[currentIndex]->GetAddress()); // 定数バッファをセット

	commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // 三角形を描画する設定にする
	commandList->IASetVertexBuffers(0, 1, &vbView); // 頂点バッファをスロット0番を使って1個だけ設定する

	commandList->DrawInstanced(3, 1, 0, 0); // 3個の頂点を描画する
}

こんな感じで表示されればOKです

スクリーンショット 2021-12-14 23.47.57.png

Updateにこんな感じで記述すると時間経過でゆっくりと回転するようになります
せっかくなのでアニメーションさせてみましょう

Scene.cpp
void Scene::Update()
{
	rotateY += 0.02f;
	auto currentIndex = g_Engine->CurrentBackBufferIndex(); // 現在のフレーム番号を取得
	auto currentTransform = constantBuffer[currentIndex]->GetPtr<Transform>(); // 現在のフレーム番号に対応する定数バッファを取得
	currentTransform->World = DirectX::XMMatrixRotationY(rotateY); // Y軸で回転させる
}

四角形の描画

次は四角形を描画してみましょう

四角形はその名の通り頂点は4つです
なので、Vertexをそのまま4つにすればよいのかというと、そういうわけには行きません
今回は D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST で三角形を描画することにしているので、
四角形を2つの三角形で描画する必要があります
とはいえ、四角形を2つの三角形で描画するときに、「じゃあ三角形2つで6個の頂点があればいいの?」と思われるかもしれませんが、頂点は4つで大丈夫です
4つの頂点があって、これを、012、023という順番でたどってあげれば、4つの頂点でも三角形が2つ書けますね!
というわけで、頂点をどういう順番でたどればいいのかを示すデータを渡してあげることにします

このデータのことをインデックスバッファといいます
インデックスバッファを、描画時にコマンドリストに渡してあげればいいわけです
さっそく、インデックスバッファを管理するクラスを定義していきましょう

IndexBuffer.h
#pragma once
#include <cstdint>
#include <d3d12.h>
#include "ComPtr.h"

class IndexBuffer
{
public:
	IndexBuffer(size_t size, const uint32_t* pInitData = nullptr);
	bool IsValid();
	D3D12_INDEX_BUFFER_VIEW View() const;

private:
	bool m_IsValid = false;
	ComPtr<ID3D12Resource> m_pBuffer; // インデックスバッファ
	D3D12_INDEX_BUFFER_VIEW m_View; // インデックスバッファビュー

	IndexBuffer(const IndexBuffer&) = delete;
	void operator = (const IndexBuffer&) = delete;
};

IndexBuffer.cpp
#include "IndexBuffer.h"
#include <d3dx12.h>
#include "Engine.h"

IndexBuffer::IndexBuffer(size_t size, const uint32_t* pInitData)
{
	auto prop = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); // ヒーププロパティ
	D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(size);	// リソースの設定

	// リソースを生成
	auto hr = g_Engine->Device()->CreateCommittedResource(
		&prop,
		D3D12_HEAP_FLAG_NONE,
		&desc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(m_pBuffer.GetAddressOf()));
	if (FAILED(hr))
	{
		printf("[OnInit] インデックスバッファリソースの生成に失敗");
		return;
	}

	// インデックスバッファビューの設定
	m_View = {};
	m_View.BufferLocation = m_pBuffer->GetGPUVirtualAddress();
	m_View.Format = DXGI_FORMAT_R32_UINT;
	m_View.SizeInBytes = static_cast<UINT>(size);

	// マッピングする
	if (pInitData != nullptr)
	{
		void* ptr = nullptr;
		hr = m_pBuffer->Map(0, nullptr, &ptr);
		if (FAILED(hr))
		{
			printf("[OnInit] インデックスバッファマッピングに失敗");
			return;
		}

		// インデックスデータをマッピング先に設定
		memcpy(ptr, pInitData, size);

		// マッピング解除
		m_pBuffer->Unmap(0, nullptr);
	}
	m_IsValid = true;
}

bool IndexBuffer::IsValid()
{
	return m_IsValid;
}

D3D12_INDEX_BUFFER_VIEW IndexBuffer::View() const
{
	return m_View;
}

定義が終わったら、早速インデックスバッファを作って、四角形を描画してみましょう

Scene.cpp
// : 省略 これまでのインクルード
#include "IndexBuffer.h"

// : 省略 これまでの宣言
IndexBuffer* indexBuffer;

bool Scene::Init()
{
	// 頂点を4つにして四角形を定義する
	Vertex vertices[4] = {};
	vertices[0].Position = XMFLOAT3(-1.0f, 1.0f, 0.0f);
	vertices[0].Color = XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f);

	vertices[1].Position = XMFLOAT3(1.0f, 1.0f, 0.0f);
	vertices[1].Color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].Position = XMFLOAT3(1.0f, -1.0f, 0.0f);
	vertices[2].Color = XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f);

	vertices[3].Position = XMFLOAT3(-1.0f, -1.0f, 0.0f);
	vertices[3].Color = XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f);

	auto vertexSize = sizeof(Vertex) * std::size(vertices);
	auto vertexStride = sizeof(Vertex);
	vertexBuffer = new VertexBuffer(vertexSize, vertexStride, vertices);
	if (!vertexBuffer->IsValid())
	{
		printf("頂点バッファの生成に失敗\n");
		return false;
	}

	uint32_t indices[] = { 0, 1, 2, 0, 2, 3 }; // これに書かれている順序で描画する

	// インデックスバッファの生成
	auto size = sizeof(uint32_t) * std::size(indices);
	indexBuffer = new IndexBuffer(size, indices);
	if (!indexBuffer->IsValid())
	{
		printf("インデックスバッファの生成に失敗\n");
		return false;
	}
	// : 省略 定数バッファの初期化以下の初期化処理

  return true;
}

// : 省略 Update

void Scene::Draw()
{
	auto currentIndex = g_Engine->CurrentBackBufferIndex();
	auto commandList = g_Engine->CommandList();
	auto vbView = vertexBuffer->View();
	auto ibView = indexBuffer->View(); // インデックスバッファビュー

	commandList->SetGraphicsRootSignature(rootSignature->Get());
	commandList->SetPipelineState(pipelineState->Get());
	commandList->SetGraphicsRootConstantBufferView(0, constantBuffer[currentIndex]->GetAddress());

	commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	commandList->IASetVertexBuffers(0, 1, &vbView);
	commandList->IASetIndexBuffer(&ibView); // インデックスバッファをセットする

	commandList->DrawIndexedInstanced(6, 1, 0, 0, 0); // 6個のインデックスで描画する(三角形の時と関数名が違うので注意)
}

これで、インデックス順に頂点を描画することができるようになりました

実行してみて、このように表示されればOKです
スクリーンショット 2021-12-15 23.30.52.png

3Dモデルを表示する

今回は、FBXファイルを表示してみたいと思います
なぜFBXなのかというと、「Unity使っててよく見かけるなー」的なノリです、特に意味はありません

assimpの導入

Visual StudioのNuGetパッケージマネージャーから、assimpというツールをダウンロードしましょう
assimpとは、様々な3Dモデルファイルに対応した3Dモデルインポートライブラリです

Visual Studioの「ツール」> 「NuGetパッケージマネージャー」 > 「ソリューションのNuGetパッケージの管理」を押すと、 「NuGetソリューション」というタブが開きます
ここで、「参照」タブが表示されていて、右側の「パッケージソース」に「nuget.org」が選択されている事を確認して、「検索」に「assimp_native」と入力します
すると、「assimp_native_4.1_v152」という項目がどこかにあるはずなのでそれを探して選択します
「NuGetソリューション」タブの右側に情報が表示され、現在のプロジェクト名が表示されているチェックボックスが表示されているのでそこをチェックし、「インストール」を押します
これでassimpの導入は完了です

スクリーンショット 2021-12-17 10.00.03.png

assimpからモデルをロードして必要な情報をパースする

さっそくassimpを使用して3Dモデルをロードするコードを書いていきましょう

まず、「SharedStruct.h」に以下のような定義を追加します
今回読み込みたいFBXモデルは複数のメッシュから構成されているので、その単体のメッシュを表現している構造体です
先程の四角形を表示する例では頂点とインデックスを使いましたが、それが一まとまりになっていくつも存在しているというイメージです

ここの処理を記述するにあたって、 Direct3D 12 ゲームグラフィックス実践ガイド を参考にさせていただきました

SharedStruct.h
// : 省略 Transformまでの定義

struct Mesh
{
    std::vector<Vertex> Vertices; // 頂点データの配列
    std::vector<uint32_t> Indices; // インデックスの配列
    std::wstring DiffuseMap; // テクスチャのファイルパス
};

ここにモデルファイルを読み込んでいきます

AssimpLoader.h
#pragma once
#define NOMINMAX
#include <d3d12.h>
#include <DirectXMath.h>
#include <string>
#include <vector>

struct Mesh;
struct Vertex;

struct aiMesh;
struct aiMaterial;

struct ImportSettings // インポートするときのパラメータ
{
    const wchar_t* filename = nullptr; // ファイルパス
    std::vector<Mesh>& meshes; // 出力先のメッシュ配列
    bool inverseU = false; // U座標を反転させるか
    bool inverseV = false; // V座標を反転させるか
};

class AssimpLoader
{
public:
    bool Load(ImportSettings setting); // モデルをロードする

private:
    void LoadMesh(Mesh& dst, const aiMesh* src, bool inverseU, bool inverseV);
    void LoadTexture(const wchar_t* filename, Mesh& dst, const aiMaterial* src);
};

AssimpLoader.cpp
#include "AssimpLoader.h"
#include "SharedStruct.h"
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <d3dx12.h>
#include <filesystem>

namespace fs = std::filesystem;

std::wstring GetDirectoryPath(const std::wstring& origin)
{
    fs::path p = origin.c_str();
    return p.remove_filename().c_str();
}

std::string ToUTF8(const std::wstring& value)
{
    auto length = WideCharToMultiByte(CP_UTF8, 0U, value.data(), -1, nullptr, 0, nullptr, nullptr);
    auto buffer = new char[length];

    WideCharToMultiByte(CP_UTF8, 0U, value.data(), -1, buffer, length, nullptr, nullptr);

    std::string result(buffer);
    delete[] buffer;
    buffer = nullptr;

    return result;
}

// std::string(マルチバイト文字列)からstd::wstring(ワイド文字列)を得る
std::wstring ToWideString(const std::string& str)
{
    auto num1 = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED | MB_ERR_INVALID_CHARS, str.c_str(), -1, nullptr, 0);

    std::wstring wstr;
    wstr.resize(num1);

    auto num2 = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED | MB_ERR_INVALID_CHARS, str.c_str(), -1, &wstr[0], num1);

    assert(num1 == num2);
    return wstr;
}

bool AssimpLoader::Load(ImportSettings settings)
{
    if (settings.filename == nullptr)
    {
        return false;
    }

    auto& meshes = settings.meshes;
    auto inverseU = settings.inverseU;
    auto inverseV = settings.inverseV;

    auto path = ToUTF8(settings.filename);

    Assimp::Importer importer;
    int flag = 0;
    flag |= aiProcess_Triangulate;
    flag |= aiProcess_PreTransformVertices;
    flag |= aiProcess_CalcTangentSpace;
    flag |= aiProcess_GenSmoothNormals;
    flag |= aiProcess_GenUVCoords;
    flag |= aiProcess_RemoveRedundantMaterials;
    flag |= aiProcess_OptimizeMeshes;

    auto scene = importer.ReadFile(path, flag);

    if (scene == nullptr)
    {
        // もし読み込みエラーがでたら表示する
        printf(importer.GetErrorString());
        printf("\n");
        return false;
    }

    // 読み込んだデータを自分で定義したMesh構造体に変換する
    meshes.clear();
    meshes.resize(scene->mNumMeshes);
    for (size_t i = 0; i < meshes.size(); ++i)
    {
        const auto pMesh = scene->mMeshes[i];
        LoadMesh(meshes[i], pMesh, inverseU, inverseV);
        const auto pMaterial = scene->mMaterials[i];
        LoadTexture(settings.filename, meshes[i], pMaterial);
    }

    scene = nullptr;

    return true;
}

void AssimpLoader::LoadMesh(Mesh& dst, const aiMesh* src, bool inverseU, bool inverseV)
{
    aiVector3D zero3D(0.0f, 0.0f, 0.0f);
    aiColor4D zeroColor(0.0f, 0.0f, 0.0f, 0.0f);

    dst.Vertices.resize(src->mNumVertices);

    for (auto i = 0u; i < src->mNumVertices; ++i)
    {
        auto position = &(src->mVertices[i]);
        auto normal = &(src->mNormals[i]);
        auto uv = (src->HasTextureCoords(0)) ? &(src->mTextureCoords[0][i]) : &zero3D;
        auto tangent = (src->HasTangentsAndBitangents()) ? &(src->mTangents[i]) : &zero3D;
        auto color = (src->HasVertexColors(0)) ? &(src->mColors[0][i]) : &zeroColor;

        // 反転オプションがあったらUVを反転させる
        if (inverseU)
        {
            uv->x = 1 - uv->x;
        }
        if (inverseV)
        {
            uv->y = 1 - uv->y;
        }

        Vertex vertex = {};
        vertex.Position = DirectX::XMFLOAT3(position->x, position->y, position->z);
        vertex.Normal = DirectX::XMFLOAT3(normal->x, normal->y, normal->z);
        vertex.UV = DirectX::XMFLOAT2(uv->x, uv->y);
        vertex.Tangent = DirectX::XMFLOAT3(tangent->x, tangent->y, tangent->z);
        vertex.Color = DirectX::XMFLOAT4(color->r, color->g, color->b, color->a);

        dst.Vertices[i] = vertex;
    }

    dst.Indices.resize(src->mNumFaces * 3);

    for (auto i = 0u; i < src->mNumFaces; ++i)
    {
        const auto& face = src->mFaces[i];

        dst.Indices[i * 3 + 0] = face.mIndices[0];
        dst.Indices[i * 3 + 1] = face.mIndices[1];
        dst.Indices[i * 3 + 2] = face.mIndices[2];
    }
}

void AssimpLoader::LoadTexture(const wchar_t* filename, Mesh& dst, const aiMaterial* src)
{
    aiString path;
    if (src->Get(AI_MATKEY_TEXTURE_DIFFUSE(0), path) == AI_SUCCESS)
    {
        // テクスチャパスは相対パスで入っているので、ファイルの場所とくっつける
        auto dir = GetDirectoryPath(filename);
        auto file = std::string(path.C_Str());
        dst.DiffuseMap = dir + ToWideString(file);
    }
    else
    {
        dst.DiffuseMap.clear();
    }
}

これで3Dモデルをロードする処理はできました

今回は読み込むモデルには、アリシア・ソリッドちゃんを使ってみたいと思います
各自モデルファイルを用意して、「プロジェクト名/プロジェクト名/Assets/Alicia/」となるようにフォルダを配置しましょう
https://3d.nicovideo.jp/alicia/
image.png

早速読み込んでみます

Scene.cpp
// : 省略 これまでのインクルード
#include "AssimpLoader.h"

// : 省略 これまでの宣言
const wchar_t* modelFile = L"Assets/Alicia/FBX/Alicia_solid_Unity.FBX";
std::vector<Mesh> meshes;

bool Scene::Init()
{
	ImportSettings importSetting = // これ自体は自作の読み込み設定構造体
	{
		modelFile,
		meshes,
		false,
		true // アリシアのモデルは、テクスチャのUVのVだけ反転してるっぽい?ので読み込み時にUV座標を逆転させる
	};

	AssimpLoader loader;
	if (!loader.Load(importSetting))
	{
		return false;
	}
// : 省略 これまでの初期化処理
	return true;
}

ちょっと起動に時間がかかったあと、これまでの四角形が表示されればOKです

複数メッシュを読み込む準備

薄々感づいている方もいらっしゃるかと思いますが、このモデルにはメッシュが複数存在しています
つまり、Meshの配列が必要になるということです
そして、Meshの配列が必要になるということは、その分だけ頂点バッファとインデックスバッファも用意しなければなりませんし、描画処理もメッシュの数だけ呼ばなければいけません
まずは今の四角形の描画で、その準備をしていきましょう

Scene.cpp
// : 省略 これまでのインクルードと宣言
std::vector<Mesh> meshes; // メッシュの配列
std::vector<VertexBuffer*> vertexBuffers; // メッシュの数分の頂点バッファ
std::vector<IndexBuffer*> indexBuffers; // メッシュの数分のインデックスバッファ

bool Scene::Init()
{
	Vertex vertices[4] = {};
	vertices[0].Position = XMFLOAT3(-1.0f, 1.0f, 0.0f);
	vertices[0].Color = XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f);

	vertices[1].Position = XMFLOAT3(1.0f, 1.0f, 0.0f);
	vertices[1].Color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].Position = XMFLOAT3(1.0f, -1.0f, 0.0f);
	vertices[2].Color = XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f);

	vertices[3].Position = XMFLOAT3(-1.0f, -1.0f, 0.0f);
	vertices[3].Color = XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f);

	uint32_t indices[] = { 0, 1, 2, 0, 2, 3 };

	Mesh mesh = {}; // これまでの四角形を入れるためのメッシュ構造体
	mesh.Vertices = std::vector<Vertex>(std::begin(vertices), std::end(vertices));
	mesh.Indices = std::vector<uint32_t>(std::begin(indices), std::end(indices));
  meshes.clear(); // ちょっと無駄感あるが、一旦四角形で試したいのでAssimpLoaderで読み込んだモデルのメッシュを一旦クリア
  meshes.shrink_to_fit(); // 中身をゼロにする
	meshes.push_back(mesh);

	// メッシュの数だけ頂点バッファを用意する
	vertexBuffers.reserve(meshes.size());
	for (size_t i = 0; i < meshes.size(); i++)
	{
		auto size = sizeof(Vertex) * meshes[i].Vertices.size();
		auto stride = sizeof(Vertex);
		auto vertices = meshes[i].Vertices.data();
		auto pVB = new VertexBuffer(size, stride, vertices);
		if (!pVB->IsValid())
		{
			printf("頂点バッファの生成に失敗\n");
			return false;
		}

		vertexBuffers.push_back(pVB);
	}

	// メッシュの数だけインデックスバッファを用意する
	indexBuffers.reserve(meshes.size());
	for (size_t i = 0; i < meshes.size(); i++)
	{
		auto size = sizeof(uint32_t) * meshes[i].Indices.size();
		auto indices = meshes[i].Indices.data();
		auto pIB = new IndexBuffer(size, indices);
		if (!pIB->IsValid())
		{
			printf("インデックスバッファの生成に失敗\n");
			return false;
		}

		indexBuffers.push_back(pIB);
	}

  // : 省略 : 定数バッファ初期化以下の初期化処理
  return true;
}

void Scene::Draw()
{
	auto currentIndex = g_Engine->CurrentBackBufferIndex();
	auto commandList = g_Engine->CommandList();

	// メッシュの数だけインデックス分の描画を行う処理を回す
	for (size_t i = 0; i < meshes.size(); i++)
	{
		auto vbView = vertexBuffers[i]->View(); // そのメッシュに対応する頂点バッファ
		auto ibView = indexBuffers[i]->View(); // そのメッシュに対応する頂点バッファ

		commandList->SetGraphicsRootSignature(rootSignature->Get());
		commandList->SetPipelineState(pipelineState->Get());
		commandList->SetGraphicsRootConstantBufferView(0, constantBuffer[currentIndex]->GetAddress());

		commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
		commandList->IASetVertexBuffers(0, 1, &vbView);
		commandList->IASetIndexBuffer(&ibView);

		commandList->DrawIndexedInstanced(meshes[i].Indices.size(), 1, 0, 0, 0); // インデックスの数分描画する
	}
}

これで、複数メッシュを描画するための処理で、一つだけのメッシュを描画するという処理に置き換わりました
実行してみて、これまでの四角形が同じように表示されればOKです

スクリーンショット 2021-12-15 23.30.52.png

モデルを描画

いよいよさっき読み込んだモデルを描画していきましょう
名残惜しいですが、四角形の頂点とインデックスを定義している行を消して、
AssimpLoaderで読み込んだmeshesを使って描画を行いましょう

Scene.cpp
bool Scene::Init()
{
	ImportSettings importSetting = // これ自体は自作の読み込み設定構造体
	{
		modelFile,
		meshes,
		false,
		true
	};

	AssimpLoader loader;
	if (!loader.Load(importSetting))
	{
		return false;
	}
  ////////////////////////ここから//////////////////////////////////////////////////////////////////
	Vertex vertices[4] = {};
	vertices[0].Position = XMFLOAT3(-1.0f, 1.0f, 0.0f);
	vertices[0].Color = XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f);

	vertices[1].Position = XMFLOAT3(1.0f, 1.0f, 0.0f);
	vertices[1].Color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].Position = XMFLOAT3(1.0f, -1.0f, 0.0f);
	vertices[2].Color = XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f);

	vertices[3].Position = XMFLOAT3(-1.0f, -1.0f, 0.0f);
	vertices[3].Color = XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f);

	uint32_t indices[] = { 0, 1, 2, 0, 2, 3 };

	Mesh mesh = {};
	mesh.Vertices = std::vector<Vertex>(std::begin(vertices), std::end(vertices));
	mesh.Indices = std::vector<uint32_t>(std::begin(indices), std::end(indices));
	meshes.clear(); // ちょっと無駄感あるが、一旦四角形で試したいので上で読み込んだモデルのメッシュを一旦クリア
	meshes.shrink_to_fit();
	meshes.push_back(mesh);
  ////////////////////////ここまでを消す//////////////////////////////////////////////////////////////
	vertexBuffers.reserve(meshes.size());
	for (size_t i = 0; i < meshes.size(); i++)

  // : 中略

  // モデルのサイズが違うので、ちゃんと映るようにこの辺の値を変えておく
  auto eyePos = XMVectorSet(0.0f, 120.0, 75.0, 0.0f);
  auto targetPos = XMVectorSet(0.0f, 120.0, 0.0, 0.0f);
  auto upward = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
  auto fov = XMConvertToRadians(60);

  // : 省略 初期化処理と描画処理は同じ

実行してみましょう
スクリーンショット 2021-12-16 00.14.55.png

モデルは一応表示されたんですが、真っ黒ですね
これは、現在ピクセルシェーダーに頂点カラーを表示するような処理を書いているからです
読み込んだモデルには頂点カラーが全て0になっているので、表示領域が真っ黒になってしまっているわけですね

ちゃんと表示したいので、今度はテクスチャを貼る処理を書いていきましょう

テクスチャを貼る準備をする

、せっかくモデルを表示できたのに今の表示は寂しいので、後で使うことになるuvの値でモデルの色を決めてみます

頂点シェーダーとピクセルシェーダーをそれぞれ、次のようにしてみましょう

SimpleVS.hlsl
cbuffer Transform : register(b0)
{
	float4x4 World;
	float4x4 View;
	float4x4 Proj;
}

struct VSInput
{
	float3 pos : POSITION;
	float3 normal : NORMAL;
	float2 uv : TEXCOORD;
	float3 tangent : TANGENT;
	float4 color : COLOR;
};

struct VSOutput
{
	float4 svpos : SV_POSITION;
	float4 color : COLOR;
	float2 uv : TEXCOORD; // ピクセルシェーダーにuvを渡す
};

VSOutput vert(VSInput input)
{
	VSOutput output = (VSOutput)0;

	float4 localPos = float4(input.pos, 1.0f);
	float4 worldPos = mul(World, localPos);
	float4 viewPos = mul(View, worldPos);
	float4 projPos = mul(Proj, viewPos);

	output.svpos = projPos;
	output.color = input.color;
	output.uv = input.uv; // ここが変更点。入力からuvを渡す
	return output;
}
SimplePS.hsls
struct VSOutput
{
	float4 svpos : SV_POSITION;
	float4 color : COLOR;
	float2 uv : TEXCOORD; // 頂点シェーダーから来たuv
};

float4 pixel(VSOutput input) : SV_TARGET
{
	return float4(input.uv.xy, 1, 1);
}

変な色になっちゃってますが、このように表示されればOK

スクリーンショット 2021-12-16 00.30.52.png

これは、uvと呼ばれるテクスチャ画像のどこの位置の色を表示すれば良いかを示している値です
この値は二次元の値なので、それをrとgの色として使うと、青っぽいような紫っぽいようなこんな色になるわけですね

DirectXでは、左上が(0, 0)です

image.png

それでは、この値を使ってテクスチャを貼る処理を作っていきましょう

テクスチャを読み込む

定数バッファ同様、テクスチャも読み込みを行ってデバイスに送り、それをシェーダー側から参照する必要があります

まずはテクスチャを管理するクラスから作っていきましょう

ディスクリプタヒープの準備とテクスチャの読み込み

テクスチャは、シェーダーリソースというリソースでデバイスに確保することになります
そのシェーダーリソースをシェーダーから参照して、テクスチャの色を持ってきたいのですが、そのためには

  • テクスチャ画像を読み込む
  • デバイスにリソースを確保して、シェーダーリソースとしてテクスチャを入れる
  • シェーダーからテクスチャを参照できるようにルートパラメーターを設定する
  • 描画命令でシェーダーのレジスターにテクスチャが入っているリソースを指定する
    という手順が必要になります

テクスチャの読み込みは、先程使えるようにしたDirectXTexを使います
そして、シェーダーからテクスチャを使えるようにするためにルートパラメーターを設定する必要があるのですが、
これを設定するにあたってディスクリプタヒープというものが必要になるので、一旦これを準備したいと思います

DescriptorHeap.h
#pragma once
#include "ComPtr.h"
#include <d3dx12.h>
#include <vector>

class ConstantBuffer;
class Texture2D;

class DescriptorHandle
{
public:
	D3D12_CPU_DESCRIPTOR_HANDLE HandleCPU;
	D3D12_GPU_DESCRIPTOR_HANDLE HandleGPU;
};

class DescriptorHeap
{
public:
	DescriptorHeap(); // コンストラクタで生成する
	ID3D12DescriptorHeap* GetHeap(); // ディスクリプタヒープを返す
	DescriptorHandle* Register(Texture2D* texture); // テクスチャーをディスクリプタヒープに登録し、ハンドルを返す

private:
	bool m_IsValid = false; // 生成に成功したかどうか
	UINT m_IncrementSize = 0;
	ComPtr<ID3D12DescriptorHeap> m_pHeap = nullptr; // ディスクリプタヒープ本体
	std::vector<DescriptorHandle*> m_pHandles; // 登録されているハンドル
};
DescriptorHeap.cpp
#include "DescriptorHeap.h"
#include "Texture2D.h"
#include <d3dx12.h>
#include "Engine.h"

const UINT HANDLE_MAX = 512;

DescriptorHeap::DescriptorHeap()
{
	m_pHandles.clear();
	m_pHandles.reserve(HANDLE_MAX);

	D3D12_DESCRIPTOR_HEAP_DESC desc{};
	desc.NodeMask = 1;
	desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	desc.NumDescriptors = HANDLE_MAX;
	desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;

	auto device = g_Engine->Device();

	// ディスクリプタヒープを生成
	auto hr = device->CreateDescriptorHeap(
		&desc,
		IID_PPV_ARGS(m_pHeap.ReleaseAndGetAddressOf()));

	if (FAILED(hr))
	{
		m_IsValid = false;
		return;
	}

	m_IncrementSize = device->GetDescriptorHandleIncrementSize(desc.Type); // ディスクリプタヒープ1個のメモリサイズを返す
	m_IsValid = true;
}

ID3D12DescriptorHeap* DescriptorHeap::GetHeap()
{
	return m_pHeap.Get();
}

DescriptorHandle* DescriptorHeap::Register(Texture2D* texture)
{
	auto count = m_pHandles.size();
	if (HANDLE_MAX <= count)
	{
		return nullptr;
	}

	DescriptorHandle* pHandle = new DescriptorHandle();

	auto handleCPU = m_pHeap->GetCPUDescriptorHandleForHeapStart(); // ディスクリプタヒープの最初のアドレス
	handleCPU.ptr += m_IncrementSize * count; // 最初のアドレスからcount番目が今回追加されたリソースのハンドル

	auto handleGPU = m_pHeap->GetGPUDescriptorHandleForHeapStart(); // ディスクリプタヒープの最初のアドレス
	handleGPU.ptr += m_IncrementSize * count; // 最初のアドレスからcount番目が今回追加されたリソースのハンドル

	pHandle->HandleCPU = handleCPU;
	pHandle->HandleGPU = handleGPU;

	auto device = g_Engine->Device();
	auto resource = texture->Resource();
	auto desc = texture->ViewDesc();
	device->CreateShaderResourceView(resource, &desc, pHandle->HandleCPU); // シェーダーリソースビュー作成

	m_pHandles.push_back(pHandle);
	return pHandle; // ハンドルを返す
}

テクスチャを管理するクラスも作ります
ちょっと急ぎで書いたのでかなり荒い状態ですが、ご容赦ください

Texture2D.h
#pragma once
#include "ComPtr.h"
#include <string>
#include <d3dx12.h>

class DescriptorHeap;
class DescriptorHandle;

class Texture2D
{
public:
	static Texture2D* Get(std::string path); // stringで受け取ったパスからテクスチャを読み込む
	static Texture2D* Get(std::wstring path); // wstringで受け取ったパスからテクスチャを読み込む
	static Texture2D* GetWhite(); // 白の単色テクスチャを生成する
	bool IsValid(); // 正常に読み込まれているかどうかを返す

	ID3D12Resource* Resource(); // リソースを返す
	D3D12_SHADER_RESOURCE_VIEW_DESC ViewDesc(); // シェーダーリソースビューの設定を返す

private:
	bool m_IsValid; // 正常に読み込まれているか
	Texture2D(std::string path);
	Texture2D(std::wstring path);
	Texture2D(ID3D12Resource* buffer);
	ComPtr<ID3D12Resource> m_pResource; // リソース
	bool Load(std::string& path);
	bool Load(std::wstring& path);

	static ID3D12Resource* GetDefaultResource(size_t width, size_t height);

	Texture2D(const Texture2D&) = delete;
	void operator = (const Texture2D&) = delete;
};
Texture2D.cpp
#include "Texture2D.h"
#include <DirectXTex.h>
#include "Engine.h"

#pragma comment(lib, "DirectXTex.lib")

using namespace DirectX;

// std::string(マルチバイト文字列)からstd::wstring(ワイド文字列)を得る。AssimpLoaderと同じものだけど、共用にするのがめんどくさかったので許してください
std::wstring GetWideString(const std::string& str)
{
	auto num1 = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED | MB_ERR_INVALID_CHARS, str.c_str(), -1, nullptr, 0);

	std::wstring wstr;
	wstr.resize(num1);

	auto num2 = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED | MB_ERR_INVALID_CHARS, str.c_str(), -1, &wstr[0], num1);

	assert(num1 == num2);
	return wstr;
}

// 拡張子を返す
std::wstring FileExtension(const std::wstring& path)
{
	auto idx = path.rfind(L'.');
	return path.substr(idx + 1, path.length() - idx - 1);
}

Texture2D::Texture2D(std::string path)
{
	m_IsValid = Load(path);
}

Texture2D::Texture2D(std::wstring path)
{
	m_IsValid = Load(path);
}

Texture2D::Texture2D(ID3D12Resource* buffer)
{
	m_pResource = buffer;
	m_IsValid = m_pResource != nullptr;
}

bool Texture2D::Load(std::string& path)
{
	auto wpath = GetWideString(path);
	return Load(wpath);
}

bool Texture2D::Load(std::wstring& path)
{
	//WICテクスチャのロード
	TexMetadata meta = {};
	ScratchImage scratch = {};
	auto ext = FileExtension(path);

	HRESULT hr = S_FALSE;
	if (ext == L"png") // pngの時はWICFileを使う
	{
		LoadFromWICFile(path.c_str(), WIC_FLAGS_NONE, &meta, scratch);
	}
	else if (ext == L"tga") // tgaの時はTGAFileを使う
	{
		hr = LoadFromTGAFile(path.c_str(), &meta, scratch);
	}

	if (FAILED(hr))
	{
		return false;
	}

	auto img = scratch.GetImage(0, 0, 0);
	auto prop = CD3DX12_HEAP_PROPERTIES(D3D12_CPU_PAGE_PROPERTY_WRITE_BACK, D3D12_MEMORY_POOL_L0);
	auto desc = CD3DX12_RESOURCE_DESC::Tex2D(meta.format,
		meta.width,
		meta.height,
		static_cast<UINT16>(meta.arraySize),
		static_cast<UINT16>(meta.mipLevels));

	// リソースを生成
	hr = g_Engine->Device()->CreateCommittedResource(
		&prop,
		D3D12_HEAP_FLAG_NONE,
		&desc,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		nullptr,
		IID_PPV_ARGS(m_pResource.ReleaseAndGetAddressOf())
	);

	if (FAILED(hr))
	{
		return false;
	}

	hr = m_pResource->WriteToSubresource(0,
		nullptr, //全領域へコピー
		img->pixels, //元データアドレス
		static_cast<UINT>(img->rowPitch), //1ラインサイズ
		static_cast<UINT>(img->slicePitch) //全サイズ
	);
	if (FAILED(hr))
	{
		return false;
	}

	return true;
}

Texture2D* Texture2D::Get(std::string path)
{
	auto wpath = GetWideString(path);
	return Get(wpath);
}

Texture2D* Texture2D::Get(std::wstring path)
{
	auto tex = new Texture2D(path);
	if (!tex->IsValid())
	{
		return GetWhite(); // 読み込みに失敗した時は白単色テクスチャを返す
	}
	return tex;
}

Texture2D* Texture2D::GetWhite()
{
	ID3D12Resource* buff = GetDefaultResource(4, 4);

	std::vector<unsigned char> data(4 * 4 * 4);
	std::fill(data.begin(), data.end(), 0xff);

	auto hr = buff->WriteToSubresource(0, nullptr, data.data(), 4 * 4, data.size());
	if (FAILED(hr))
	{
		return nullptr;
	}

	return new Texture2D(buff);;
}

ID3D12Resource* Texture2D::GetDefaultResource(size_t width, size_t height)
{
	auto resDesc = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R8G8B8A8_UNORM, width, height);
	auto texHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_CPU_PAGE_PROPERTY_WRITE_BACK, D3D12_MEMORY_POOL_L0);
	ID3D12Resource* buff = nullptr;
	auto result = g_Engine->Device()->CreateCommittedResource(
		&texHeapProp,
		D3D12_HEAP_FLAG_NONE, //特に指定なし
		&resDesc,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		nullptr,
		IID_PPV_ARGS(&buff)
	);
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return nullptr;
	}
	return buff;
}

bool Texture2D::IsValid()
{
	return m_IsValid;
}

ID3D12Resource* Texture2D::Resource()
{
	return m_pResource.Get();
}

D3D12_SHADER_RESOURCE_VIEW_DESC Texture2D::ViewDesc()
{
	D3D12_SHADER_RESOURCE_VIEW_DESC desc = {};
	desc.Format = m_pResource->GetDesc().Format;
	desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; //2Dテクスチャ
	desc.Texture2D.MipLevels = 1; //ミップマップは使用しないので1
	return desc;
}

これでテクスチャを読み込む機構ができました
早速読み込んでいきましょう

Scene.cpp
// : 省略 他のインクルード
#include "DescriptorHeap.h"
#include "Texture2D.h"

// 拡張子を置き換える処理
#include <filesystem>
namespace fs = std::filesystem;
std::wstring ReplaceExtension(const std::wstring& origin, const char* ext)
{
	fs::path p = origin.c_str();
	return p.replace_extension(ext).c_str();
}

DescriptorHeap* descriptorHeap;
std::vector< DescriptorHandle*> materialHandles; // テクスチャ用のハンドル一覧

bool Scene::Init()
{
	// : 省略 定数バッファの初期化処理まで
  // マテリアルの読み込み
	materialHandles.clear();
	for (size_t i = 0; i < meshes.size(); i++)
	{
		auto texPath = ReplaceExtension(meshes[i].DiffuseMap, "tga"); // もともとはpsdになっていてちょっとめんどかったので、同梱されているtgaを読み込む
		auto mainTex = Texture2D::Get(texPath);
		auto handle = descriptorHeap->Register(mainTex);
		materialHandles.push_back(handle);
	}
  // : 省略 ルートシグネチャの初期化処理以下
  return true;
}

これでテクスチャがデバイスに送られて、ディスクリプタヒープも確保できたことになりますが、これだけではダメで、
ルートパラメータの設定に戻って、シェーダーからどのレジスターにテクスチャが入っているのかを伝える必要があります

「Transform」を渡す時は、定数バッファを1個だけ用意しましたが、
今回はメッシュの数だけテクスチャがメモリ上に並んでいることになるので、ディスクリプタテーブルを使うことにします
ディスクリプタテーブルとは、いくつか並んでいるディスクリプタをひとまとめにして1つのルートパラメーターとして使うための仕組みです
(この辺、筆者もまだ理解が甘くうまく解説できません、ご容赦ください)

RootSignature.cpp
#include "RootSignature.h"
#include "Engine.h"
#include <d3dx12.h>

RootSignature::RootSignature()
{
	// : 省略 フラグの初期化処理

	CD3DX12_ROOT_PARAMETER rootParam[2] = {}; // 定数バッファとテクスチャの2
	rootParam[0].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL);

	CD3DX12_DESCRIPTOR_RANGE tableRange[1] = {}; // ディスクリプタテーブル
	tableRange[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); // シェーダーリソースビュー
	rootParam[1].InitAsDescriptorTable(std::size(tableRange), tableRange, D3D12_SHADER_VISIBILITY_ALL);
	// : 省略

これで、シェーダーのt1レジスターで、シェーダーリソースを使うぞという設定ができました
そうしたら、シェーダーのt1レジスターにさっき読み込んだテクスチャを指定する処理を書いていきます
(今回のディスクリプタテーブルの使い方はあまり良い使い方ではないと思います)

Scene.cpp
void Scene::Draw()
{
	auto currentIndex = g_Engine->CurrentBackBufferIndex();
	auto commandList = g_Engine->CommandList();
	auto materialHeap = descriptorHeap->GetHeap(); // ディスクリプタヒープ

	for (size_t i = 0; i < meshes.size(); i++)
	{
		auto vbView = vertexBuffers[i]->View();
		auto ibView = indexBuffers[i]->View();

		commandList->SetGraphicsRootSignature(rootSignature->Get());
		commandList->SetPipelineState(pipelineState->Get());
		commandList->SetGraphicsRootConstantBufferView(0, constantBuffer[currentIndex]->GetAddress());

		commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
		commandList->IASetVertexBuffers(0, 1, &vbView);
		commandList->IASetIndexBuffer(&ibView);

		commandList->SetDescriptorHeaps(1, &materialHeap); // 使用するディスクリプタヒープをセット
		commandList->SetGraphicsRootDescriptorTable(1, materialHandles[i]->HandleGPU); // そのメッシュに対応するディスクリプタテーブルをセット

		commandList->DrawIndexedInstanced(meshes[i].Indices.size(), 1, 0, 0, 0);
	}
}

もちろん、これだけではだめなので、シェーダーも対応させましょう
ルートシグネチャでしれっと設定していたサンプラーが「s0」レジスターに登録されています
そして、テクスチャが「t0」レジスターに登録されています
今描画するべきピクセルの色を、テクスチャ、サンプラー、uvを元に描画するように書き換えます

SimplePS.hlsl
struct VSOutput
{
	float4 svpos : SV_POSITION;
	float4 color : COLOR;
	float2 uv : TEXCOORD;
};

SamplerState smp : register(s0); // サンプラー
Texture2D _MainTex : register(t0); // テクスチャ

float4 pixel(VSOutput input) : SV_TARGET
{
	return _MainTex.Sample(smp, input.uv);
}

ついに!アリシア・ソリッドちゃんを表示させることができました!!
お疲れさまでした!!
(シェーダーとかもちゃんと書きたかったのですが、分量がかなり多くなってしまったので今回はやりません)

スクリーンショット 2021-12-16 01.01.34.png

おわりに

今回は自分の知識を確認する意味でも、ほんとに書いたコードほぼ全てを記事にしてみました
これを機に、みなさんもぜひDirectX12に入門していただければと思います
アデュー

43
42
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
42