20.オブジェクト側デスクプリタヒープの作成

前回に引き続き「Dx12」の検証を行っていきたいと思います。今回は実際の描画です。
前回は「Dx12の初期化」を行いました。今回からのテーマは、前回初期化されたDx12のデバイス(レンダリングターゲットなど)に、「どうやって個別に描画するのか」です。

Dx12に関する情報は、マイクロソフト社の「DirectX-Graphics-Samples」が主な情報源となります。

マイクロソフト社の「DirectX-Graphics-Samples」のGitHubサイトは以下になります。

https://github.com/Microsoft/DirectX-Graphics-Samples

ぜひ、興味ある方はダウンロードしてみてDx12研究を始めるといいと思います。

この記事は、
コミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」
から、
コミット「weak_ptrからshared_ptr取得方法の修正」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。
その間にいくつかのコミットがありますが、後ろのコミットを開いていただいてそれと合わせ読んでいただくとわかりやすいと思います。

まずBaseCrossに課せられた条件として「いろいろな描画方式を順不動に描画しなくてはならない」というものがあります。
DxBase2015やDxBase2016を実行した経験がある人なら、「AddGameObject関数」によって、どんなタイミングでも「新しいオブジェクトの追加」が可能なことを経験してると思います。
しかし、その柔軟さは当然、アクセラレータには大きな負担になります。常にシェーダが入れ替わり、ある時はテクスチャがある3Dオブジェクト、そしてある時はスプライトなど、1ターンの間に目まぐるしくパイプラインの設定が変わります。
「DirectX-Graphics-Samples」のソースを読むとわかるのですが、当然ながら個別のタイプ個別のシェーダに対して描画方法が、個別のサンプルとして紹介されています。
また「DirectX-Graphics-Samples」には「ミニエンジン」というフレームワークが紹介されているのですが、これは決して「ミニ」ではなく、印象的には「Dx12を使用したGPU操作」などを徹底的に追及したかなり高度な内容になっています。
BaseCrossがまず行いたいことは、単純に「シェーダーやパイプライン設定が違うオブジェクトを同時に(1ターン時に)描画するにはどうしたらよいか」ということです。CSやGPUを駆使してリアルなモデルを描画する方法ではありません。

BaseCrossの実行の流れは以下のようになります。フローチャートとして作成したわけではないので、流れだけわかってもらえればいいと思います。

2016081401

オレンジの「デバイスの初期化」は前項で説明しました。今回からは「各オブジェクトの初期化」を説明します。
現在のサンプルは、「18.アプリケーションクラス実装」で説明しましたように、回転する立方体が描画されるものです。今回はこのオブジェクトの初期化を説明しつつ、「Dx12」の各オブジェクトの描画のために必要な設定などを説明したいと思います。
このオブジェクトは「BaseCrossDx12」プロジェクトの「Character.h/cpp」に記述があります。「NormalTextureBoxクラス」です。
このクラスは「 ObjectInterface及び ShapeInterface」を多重継承しています。つまり、「OnCreate関数(ObjectInterface継承)」と、「OnUpdate、OnDraw両関数(ShapeInterface継承)」、を実装する必要があります。

以下は、「NormalTextureBox::OnCreate関数」の実体です。この中でDx12に直接関係するものと、直接は関係しない(Update系)の初期化があります。

void NormalTextureBox::OnCreate() {
	m_CubeMesh = MeshResource::CreateCube(1.0f);
	wstring DataDir;
	App::GetApp()->GetDataDirectory(DataDir);
	wstring strTexture = DataDir + L"wall.jpg";
	m_WallTex = TextureResource::CreateTextureResource(strTexture);
	m_DrawContext = ObjectFactory::Create<VSPSDrawContext>(VSPSDrawContext::CreateParam::CreateSrvSmpCbv);
	m_DrawContext->CreateConstantBuffer(sizeof(m_ConstantBufferData));
	m_DrawContext->CreateDefault3DPipelineCmdList<VertexPositionNormalTexture, VSPNTStatic, PSPNTStatic>();
	ZeroMemory(&m_ConstantBufferData, sizeof(m_ConstantBufferData));
	//各行列をIdentityに初期化
	m_ConstantBufferData.World = Matrix4X4EX::Identity();
	m_ConstantBufferData.View = Matrix4X4EX::Identity();
	m_ConstantBufferData.Projection = Matrix4X4EX::Identity();
	//初期値更新
	m_DrawContext->UpdateConstantBuffer(reinterpret_cast<void**>(&m_ConstantBufferData), sizeof(m_ConstantBufferData));
	//テクスチャ設定
	m_DrawContext->SetTextureResource(m_WallTex);
}

この中で

	m_DrawContext = ObjectFactory::Create<VSPSDrawContext>(VSPSDrawContext::CreateParam::CreateSrvSmpCbv);

というのは、BaseCross(Dx12版)特有で、一番描画に重要な記述です。今回はここの説明です。

「m_DrawContext」というのは、「VSPSDrawContextクラス」のshared_ptrです。このクラスはVS(つまり頂点シェーダ)と「PS(つまりピクセルシェーダ)」を使う描画のためのクラス、という意味で命名しました。

描画には何が必要なのか
Dx12の描画は「コマンドリスト」と呼ばれる「パイプライン設定」を作成し、それを「実行」することで行います。
つまり、立方体描画のための「コマンドリスト」をどう構築するかが、ポイントになります。
上記コード内にある「VSPSDrawContext::CreateParam::CreateSrvSmpCbv」というパラメータはいったい何でしょうか?
「VSPSDrawContext」を構築するには、「コマンドリスト」の前に、その描画に必要とする「デスクプリタヒープ」を考えなければなりません。そのためには、描画に使用するシェーダ(頂点シェーダとピクセルシェーダ)を考えなければなりません。

今回の描画に使う頂点の定義は「VertexPositionNormalTexture」です。「頂点」には「位置」「法線」「テクスチャ」が含まれます。そして、描画する立方体の「ワールド行列」「ビュー行列」「射影行列」が必要です。これらの行列は「コンスタントバッファ」を介してシェーダに送られます。
そうすると、シェーダに渡す「頂点情報」以外では、「シェーダリソースビュー(テクスチャ)」「サンプラー」「コンスタントバッファ」の3つの追加情報が必要なのがわかります。ここはわかりますか?

言い方を変えると、ここで考えてるのは、「描画に必要な情報は何か」ということです。
まず頂点がありますね。これは入力スロットに直接入れます。
次に描画に使う「テクスチャ」が必要ですね。これは頂点とは別に入れなければなりません。テクスチャを使うには「シェーダリソースビュー(つまりテクスチャそのもの)」と「サンプラー(テクスチャの貼り付け方法を定義したもの)」が必要です。
そして「ワールド、ビュー、射影」各行列。これは「コンスタントバッファ」が必要です。
「VSPSDrawContext」を構築するにはそれらの情報を使うのに準備が必要で、その準備を行うというわけです。

「VSPSDrawContext」を「VSPSDrawContext::CreateParam::CreateSrvSmpCbv」というパラメータで初期化すると以下のようなコードを実行します。

void VSPSDrawContext::CreateSrvSmpCbv() {
	auto Dev = App::GetApp()->GetDeviceResources();
	pImpl->m_CbvSrvDescriptorHandleIncrementSize
		= Dev->GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	//CbvSrvデスクプリタヒープ
	pImpl->m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1 + 1);
	//サンプラーデスクプリタヒープ
	pImpl->m_SamplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(1);
	//ルートシグネチャ
	pImpl->m_RootSignature = RootSignature::CreateSrvSmpCbv();
	//GPU側デスクプリタヒープのハンドルの配列の作成
	pImpl->m_GPUDescriptorHandleVec.clear();
	CD3DX12_GPU_DESCRIPTOR_HANDLE SrvHandle(
		pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		0
	);
	pImpl->m_GPUDescriptorHandleVec.push_back(SrvHandle);
	CD3DX12_GPU_DESCRIPTOR_HANDLE SamplerHandle(
		pImpl->m_SamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		0
	);
	pImpl->m_GPUDescriptorHandleVec.push_back(SamplerHandle);
	CD3DX12_GPU_DESCRIPTOR_HANDLE CbvHandle(
		pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		pImpl->m_CbvSrvDescriptorHandleIncrementSize
	);
	pImpl->m_GPUDescriptorHandleVec.push_back(CbvHandle);
	//シェーダリソースの数を設定
	pImpl->m_SrvDescriptorHeapCount = 1;
	//コンスタントバッファの数を設定
	pImpl->m_CbxDescriptorHeapCount = 1;
	//サンプラーの数を設定
	pImpl->m_SamplerDescriptorHeapCount = 1;
	//サンプラーのデフォルト設定
	SetSamplerState(SamplerState::LinearClamp);
}

ここで行っているのは、主に「デスクプリタヒープ」の作成です。
「デスクプリタヒープ」を作成するにはそのヒープが「何に使うのか」をパラメータで渡さなければならないのですが、どうしたわけかそのタイプが「コンスタントバッファ」「シェーダリソース(テクスチャ)」か、「サンプラー」かを分けなければいけません。
それで

	//CbvSrvデスクプリタヒープ
	pImpl->m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1 + 1);
	//サンプラーデスクプリタヒープ
	pImpl->m_SamplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(1);

のような記述が必要になります。

	//CbvSrvデスクプリタヒープ
	pImpl->m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1 + 1);

で呼び出している(DescriptorHeap::CreateCbvSrvUavHeap関数)は、以下のようになります。

static inline ComPtr<ID3D12DescriptorHeap> CreateCbvSrvUavHeap(UINT NumDescriptorHeap) {
	//CbvSrvデスクプリタヒープ
	D3D12_DESCRIPTOR_HEAP_DESC CbvSrvHeapDesc = {};
	CbvSrvHeapDesc.NumDescriptors = NumDescriptorHeap;
	CbvSrvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	CbvSrvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	return CreateDirect(CbvSrvHeapDesc);
}

ここで設定している

	CbvSrvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;

は「コンスタントバッファもしくはシェーダリソース」に使いますよ、という設定です。
「コンスタントバッファで一つ」「シェーダリソース(テクスチャ)で一つ」必要ですので、

	//CbvSrvデスクプリタヒープ
	pImpl->m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1 + 1);

と(1 + 1)と記述しています(2)でもいいんですが、「1つづつ」というのを強調しています。
こんな感じで「サンプラーデスクプリタヒープ」も作成します。こちらは1つでかまいません。

	//サンプラーデスクプリタヒープ
	pImpl->m_SamplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(1);

この「DescriptorHeap::CreateSamplerHeap関数」は以下です。

static inline ComPtr<ID3D12DescriptorHeap> CreateSamplerHeap(UINT NumDescriptorHeap) {
	//サンプラーデスクプリタヒープ
	D3D12_DESCRIPTOR_HEAP_DESC SamplerHeapDesc = {};
	SamplerHeapDesc.NumDescriptors = NumDescriptorHeap;
	SamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
	SamplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	return CreateDirect(SamplerHeapDesc);
}

ここでは

	SamplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;

で「サンプラーですよ」と設定してるのがわかります。

続いて「VSPSDrawContext::CreateSrvSmpCbv関数」では、「ルートシグネチャ」を作成します。ここでも「コンスタントバッファ」「シェーダリソース(テクスチャ)」「サンプラー」が入力するという設定を行います。
それを実装しているのが「RootSignature::CreateSrvSmpCbv関数」です。
以下はその内容です。

static inline ComPtr<ID3D12RootSignature> CreateSrvSmpCbv() {

	CD3DX12_DESCRIPTOR_RANGE ranges[3];
	ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
	ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);
	ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);

	CD3DX12_ROOT_PARAMETER rootParameters[3];
	rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL);
	rootParameters[1].InitAsDescriptorTable(1, &ranges[1], D3D12_SHADER_VISIBILITY_PIXEL);
	rootParameters[2].InitAsDescriptorTable(1, &ranges[2], D3D12_SHADER_VISIBILITY_ALL);

	CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
	rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

	return CreateDirect(rootSignatureDesc);
}

最初に設定する「レンジ」は以下の感じです。

	CD3DX12_DESCRIPTOR_RANGE ranges[3];
	ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
	ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);
	ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);

パイプラインに入力するデスクプリタレンジを設定します。
ここでは「シェーダーリソースビュー(テクスチャ)」を1つ、「サンプラー」を1つ、「コンスタントバッファ」を1つ、というう設定です。
続く

	CD3DX12_ROOT_PARAMETER rootParameters[3];
	rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL);
	rootParameters[1].InitAsDescriptorTable(1, &ranges[1], D3D12_SHADER_VISIBILITY_PIXEL);
	rootParameters[2].InitAsDescriptorTable(1, &ranges[2], D3D12_SHADER_VISIBILITY_ALL);

はそれぞれのレンジが頂点シェーダに渡すか、ピクセルシェーダに渡すか、両方に渡すかを設定します。最初のレンジはシェーダーリソースビューなので、ピクセルシェーダのみです。次もサンプラーなので同じです。コンスタントバッファのみ両方にわたすので「D3D12_SHADER_VISIBILITY_ALL」となります。

続いて「VSPSDrawContext::CreateSrvSmpCbv関数」では、「GPU側デスクプリタヒープのハンドルの配列の作成」を行います。
デスクプリタヒープはCPU側、GPU側という概念があり、これはGPU側の設定です。

	//GPU側デスクプリタヒープのハンドルの配列の作成
	pImpl->m_GPUDescriptorHandleVec.clear();
	CD3DX12_GPU_DESCRIPTOR_HANDLE SrvHandle(
		pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		0
	);
	pImpl->m_GPUDescriptorHandleVec.push_back(SrvHandle);
	CD3DX12_GPU_DESCRIPTOR_HANDLE SamplerHandle(
		pImpl->m_SamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		0
	);
	pImpl->m_GPUDescriptorHandleVec.push_back(SamplerHandle);
	CD3DX12_GPU_DESCRIPTOR_HANDLE CbvHandle(
		pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		pImpl->m_CbvSrvDescriptorHandleIncrementSize
	);
	pImpl->m_GPUDescriptorHandleVec.push_back(CbvHandle);

「デスクプリタヒープ」は「pImpl->m_CbvSrvUavDescriptorHeap」と「pImpl->m_SamplerDescriptorHeap」があります。
前者は2つ(シェーダーリソースビューとコンスタントバッファ用)、後者は1つ(サンプラー用)です。これらのデスクプリタヒープを「ハンドル」(つまりID化)して、それを配列にしておきます。
pImpl->m_GPUDescriptorHandleVecは、

	//GPU側デスクプリタヒープのハンドルの配列
	vector<CD3DX12_GPU_DESCRIPTOR_HANDLE> m_GPUDescriptorHandleVec;

となっています。CD3DX12_GPU_DESCRIPTOR_HANDLEの配列です。
ここに、ここまでで作成したデスクプリタヒープを「ハンドル化」してまとめます。
まずシェーダリソースビューは、pImpl->m_CbvSrvUavDescriptorHeapの先頭に位置してますので、

	CD3DX12_GPU_DESCRIPTOR_HANDLE SrvHandle(
		pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		0
	);

で「先頭から0番目」として作成します。
そして

	pImpl->m_GPUDescriptorHandleVec.push_back(SrvHandle);

でハンドルの配列に登録します。
続くサンプラーは、pImpl->m_SamplerDescriptorHeapの先頭に位置してますので、

	CD3DX12_GPU_DESCRIPTOR_HANDLE SamplerHandle(
		pImpl->m_SamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		0
	);

で「先頭から0番目」として作成します。
そして

	pImpl->m_GPUDescriptorHandleVec.push_back(SamplerHandle);

最後にコンスタントバッファは、pImpl->m_CbvSrvUavDescriptorHeapの2つ目に位置してますので、

	CD3DX12_GPU_DESCRIPTOR_HANDLE CbvHandle(
		pImpl->m_CbvSrvUavDescriptorHeap->GetGPUDescriptorHandleForHeapStart(),
		1,
		pImpl->m_CbvSrvDescriptorHandleIncrementSize
	);

で、「スタート位置から」「pImpl->m_CbvSrvDescriptorHandleIncrementSize」だけ離れた(オフセット位置)のデスクプリタヒープを「CbvHandle」として作成します。シェーダーリソースビューとコンスタントバッファは、デスクプリタヒープは共有するのでこのような設定になりま
す。
作成したハンドルは

	pImpl->m_GPUDescriptorHandleVec.push_back(CbvHandle);

で配列に追加します。

最後の「VSPSDrawContext::CreateSrvSmpCbv関数」の処理は、

	//シェーダリソースの数を設定
	pImpl->m_SrvDescriptorHeapCount = 1;
	//コンスタントバッファの数を設定
	pImpl->m_CbxDescriptorHeapCount = 1;
	//サンプラーの数を設定
	pImpl->m_SamplerDescriptorHeapCount = 1;
	//サンプラーのデフォルト設定
	SetSamplerState(SamplerState::LinearClamp);

です。これは各デスクプリタの数と、サンプラーに「LinearClamp」の設定をしています。この設定は、テクスチャを包み込みしない設定です。

ここまでで、VSPSDrawContextの構築時設定は終了です。おもにデスクリタヒープの作成と、ルートシグネチャの作成が主な内容でした。
ここまでで、「NormalTextureBox::OnCreate関数」の

	m_DrawContext = ObjectFactory::Create<VSPSDrawContext>(VSPSDrawContext::CreateParam::CreateSrvSmpCbv);

が終了しました。

このあと「コンスタントバッファの作成」「コマンドリストの作成」と続くわけですが、長くなってしまいましたので、ここでこの項は終了します。
次回に続きを説明します。

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP