36.Dx12版サンプルをDx11版に合わせて追加する(5)

今回は1つのDx12版サンプルをGitHubに修正しました。SimpleSample012です。
ドキュメントは申し訳ないですが、まだ記述してません。少しサンプルがたまった段階でアップします。

この記事は、
コミットSimplaSample011,012(Dx12版)追加。
から、
コミットSimplaSample012(Dx12版)の修正。ライブラリ修正あり
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

を参照して下さい。

前回の記事から少し時間がたってしまいました。
今回は、かねてよりDx12版の描画について、自由度が少なかったり、カスタマイズしにくかった点を修正し、ゲーム側で描画方法を指定しやすいようにしました。
ポイントは、Dx12を理解しよう!です。
と言いますのは、ここまで、例えばVSPSDrawContextクラスの調整を行ってきたのですが、結局のところ、ルートシグネチャやデスクプリタヒープを直接ゲーム側から調整できないと、Dx12の本来の意味はなく、まだ、フルバージョンであれば、ライブラリ側で描画処理をするのが通常であっても、シンプルバージョンの場合は、ゲーム制作者に完全に預けたほうが本来の姿と考えました。
ですから、Dx11版(シンプルバージョン)もDirectXを直接扱って描画しますが、Dx12版もそのように変更しました。
また、今回修正したのはSimpleSample012だけですが、今後、これまで記述したサンプルも修正していきます。
最終的にはVSPSDrawContextクラスはなくそうと考えています。

 

SimplaSample012の変更点について

SimplaSample012の実行イメージは、前項と変わりません。
ただ描画方法は、まったく違うものとなりました。どのように違うかというと、ほとんどのDx12処理を、ゲーム側で記述しなければいけないということです。

これまでもDx12の描画についての検証等は行ってきましたが、今回も、少し詳しく説明したいと思います。
まず、見ていただきたいのはBaseCrossDx12プロジェクトのCharacter.h/cppです。
そこに記述があるDx12DrawContext3D構造体が今回の主役です。この構造体には、Dx12描画に必要なリソースなどが一通りそろってます。
そこで、描画リソース(オブジェクト)の初期化実際の描画処理に分けて、説明したいと思います。

描画リソース(オブジェクト)の初期化

Dx12DrawContext3D構造体のインスタンスは、SphereObjectクラスのメンバとして定義されています。m_Dx12DrawContext3Dというインスタンスがそうです。
この構造体の初期化処理はDx12DrawContext3D::Init関数にまとめてあります。この関数は、描画オブジェクトのOnCreate関数から呼ばれます。
このDx12DrawContext3D::Init関数を呼ぶ前に、m_Dx12DrawContext3Dのメンバのメッシュ、テクスチャ、透明処理するかどうかを設定します。メッシュ、テクスチャについては、m_Dx12DrawContext3Dインスタンス内のメンバ変数に直接構築しています。
このあたりオブジェクト指向的にはどうかとも思いましたが、まずは、単純な(というかわかりやすい方法で)、描画処理をサンプル化したほうがいいだろと思いこうしてます。将来のサンプルではこの辺りは修正される可能性があります。

メッシュ作成、テクスチャ作成、透明処理するかどうかの設定が終わったら、Dx12DrawContext3D::Init関数を呼びます。
以下が、その序盤部分です


void Dx12DrawContext3D::Init() {
	auto Dev = App::GetApp()->GetDeviceResources();
	m_CbvSrvDescriptorHandleIncrementSize =
		Dev->GetDevice()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	//CbvSrvデスクプリタヒープ
	m_CbvSrvUavDescriptorHeap = DescriptorHeap::CreateCbvSrvUavHeap(1 + 1);
	//サンプラーデスクプリタヒープ
	m_SamplerDescriptorHeap = DescriptorHeap::CreateSamplerHeap(1);
	//ルートシグネチャ
	m_RootSignature = RootSignature::CreateSrvSmpCbv();

	//中略

}

まず、このブロックではデスクプリタヒープを作成しています。ここで必要とするのはシェーダリソースビュー、コンスタントバッファ、サンプラーのためのデスクプリタヒープです。
シェーダリソースビュー、コンスタントバッファにつきましてはDescriptorHeap::CreateCbvSrvUavHeap関数を使います。この関数はDescriptorHeapネームスペースに属する関数で、D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAVというタイプで作成します。このタイプはコンスタントバッファもしくはシェーダーリソース(テクスチャ)といいう意味です。
サンプラーに関してはDescriptorHeap::CreateSamplerHeap関数を使います。これはD3D12_DESCRIPTOR_HEAP_TYPE_SAMPLERというタイプになります。
続く構文ではルートシグネチャを作成します。RootSignature::CreateSrvSmpCbv関数は、シェーダリソースビュー。コンスタントバッファ、サンプラーを利用するルートシグネチャです。
RootSignature::CreateSrvSmpCbv関数内では、もし、すでに作成されていれば(アプリケーション全体で)、そのルートシグネチャを使用します。サンプルの2つの球体は、同じ描画方法なので使いまわしが可能です。
このすでに作成されているかどうかDeviceResourcesクラスに実装されています。GetRootSignature関数、SetRootSignature関数を確認してください。このメカニズムを使って、ルートシグネチャを独自に作成した場合も、キーワードを添えて登録することができます。
続いて以下のブロックです。


void Dx12DrawContext3D::Init() {
	//中略

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

	//中略
}

ここでは、先ほど作成したデスクプリタヒープをもとに、GPU側デスクプリタヒープのハンドルというのを取得して、配列に登録しています。m_GPUDescriptorHandleVecは、後ほど描画時に使用します。
続いてサンプラーの作成です。


void Dx12DrawContext3D::Init() {
	//中略

	m_SamplerDescriptorHandle = m_SamplerDescriptorHeap->GetCPUDescriptorHandleForHeapStart();
	DynamicSampler::CreateSampler(SamplerState::LinearClamp, m_SamplerDescriptorHandle);

	//中略


}

ここは、サンプラーのデスクプリタヒープをもとにCPU側のハンドルを取得して、そのハンドルとサンプラーそのもの(サンプラーのビューともいえる)を作成します。
そして、デスクプリタヒープの数を設定し、続いて、コンスタントバッファです。
コンスタントバッファは、ちょっと厄介です。


void Dx12DrawContext3D::Init() {

	//中略

	//コンスタントバッファ
	ThrowIfFailed(Dev->GetDevice()->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(sizeof(StaticConstantBuffer)),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(&m_ConstantBufferUploadHeap)),
		L"コンスタントバッファ用のアップロードヒープ作成に失敗しました",
		L"Dev->GetDevice()->CreateCommittedResource()",
		L"Dx12DrawContext3D::Init()"
	);

	//コンスタントバッファのビューを作成
	D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
	cbvDesc.BufferLocation = m_ConstantBufferUploadHeap->GetGPUVirtualAddress();
	//コンスタントバッファは256バイトにアラインメント
	cbvDesc.SizeInBytes = (sizeof(StaticConstantBuffer) + 255) & ~255;
	//コンスタントバッファビューを作成すべきデスクプリタヒープ上のハンドルを取得
	//シェーダリソースがある場合コンスタントバッファはシェーダリソースビューのあとに設置する
	CD3DX12_CPU_DESCRIPTOR_HANDLE cbvSrvHandle(
		m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
		m_SrvDescriptorHeapCount,
		m_CbvSrvDescriptorHandleIncrementSize * m_SrvDescriptorHeapCount
	);
	Dev->GetDevice()->CreateConstantBufferView(&cbvDesc, cbvSrvHandle);
	//コンスタントバッファのアップロードヒープのマップ
	CD3DX12_RANGE readRange(0, 0);
	ThrowIfFailed(m_ConstantBufferUploadHeap->Map(0, &readRange, reinterpret_cast<void**>(&m_pConstantBuffer)),
		L"コンスタントバッファのマップに失敗しました",
		L"pImpl->m_ConstantBufferUploadHeap->Map()",
		L"Dx12DrawContext3D::Init()"
	);

	//中略


}

ここではまず、コンスタントバッファのアップロードヒープを作成します。
そのあとアップロードヒープをもとにGPU側の仮想アドレスを取得して、それとコンスタントバッファのデスクプリタヒープのハンドルを作成し。そのハンドルと、D3D12_CONSTANT_BUFFER_VIEW_DESCをもとにコンスタントバッファのビューを作成します。ここで注意したいのは、コンスタントバッファのデスクプリタヒープの中には、シェーダリソース用のも含まれることです。先頭はシェーダリソース用のものとし、その分インクリメントした場所のハンドルを作成し、それでコンスタントバッファのビューを作成します。
最後にコンスタントバッファのアップロードヒープを、C++側に用意したポインタにマップします。そうしておくとそのポインタがさしている個所に書き込むことでコンスタントバッファの内容を更新できます。
続いて、パイプラインステートとコマンドリスストを作成します。


void Dx12DrawContext3D::Init() {

	//中略

	//パイプラインステートとコマンドリススト
	PipelineState::CreateDefault3D<VertexPositionNormalTexture, VSPNTStatic, PSPNTStatic>(m_RootSignature, m_PineLineDesc);
	//ブレンドステートとラスタライザ差し替え
	if (m_Trace) {
		D3D12_BLEND_DESC blend_desc;
		D3D12_RENDER_TARGET_BLEND_DESC Target;
		ZeroMemory(&blend_desc, sizeof(blend_desc));
		blend_desc.AlphaToCoverageEnable = false;
		blend_desc.IndependentBlendEnable = false;
		ZeroMemory(&Target, sizeof(Target));
		Target.BlendEnable = true;
		Target.SrcBlend = D3D12_BLEND_SRC_ALPHA;
		Target.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
		Target.BlendOp = D3D12_BLEND_OP_ADD;
		Target.SrcBlendAlpha = D3D12_BLEND_ONE;
		Target.DestBlendAlpha = D3D12_BLEND_ZERO;
		Target.BlendOpAlpha = D3D12_BLEND_OP_ADD;
		Target.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
		for (UINT i = 0; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; i++) {
			blend_desc.RenderTarget[i] = Target;
		}
		m_PineLineDesc.BlendState = blend_desc;
	}

	D3D12_FILL_MODE FillMode;
	D3D12_CULL_MODE CullMode;
	FillMode = D3D12_FILL_MODE::D3D12_FILL_MODE_SOLID;
	CullMode = D3D12_CULL_MODE::D3D12_CULL_MODE_FRONT;
	m_PineLineDesc.RasterizerState.FillMode = FillMode;
	m_PineLineDesc.RasterizerState.CullMode = CullMode;

	m_FrontPipelineState = PipelineState::CreateDirect(m_PineLineDesc);

	CullMode = D3D12_CULL_MODE::D3D12_CULL_MODE_BACK;
	m_PineLineDesc.RasterizerState.CullMode = CullMode;
	m_BackPipelineState = PipelineState::CreateDirect(m_PineLineDesc);

	//コマンドリストは裏面カリングに初期化
	m_CommandList = CommandList::CreateDefault(m_BackPipelineState);
	CommandList::Close(m_CommandList);

	//中略

}

ここでは、まずPipelineState::CreateDefault3Dテンプレート関数を使ってパイプラインステートを作成しますが、ここで必要としてるのはm_PineLineDescというパイプラインステートを作成するための定義です。
実際に使用するパイプラインステートは透明にするかどうかを振り分けたものを、背面カリング前面カリングの2種類のパイプラインステートを用意します。
透明でない場合は、背面カリング(つまり前面描画)のみ描画、とし、透明の場合は前面カリングで描画のあと背面カリングで描画します。そのため2種類用意します。
パイプラインステートを作成したら、背面カリングのパイプラインでコマンドリストを作成します。コマンドリストは作成後、すぐにクローズします。
最後に、シェーダリソースビュー(テクスチャ)の作成です。


void Dx12DrawContext3D::Init() {
	//中略

	//テクスチャ
	//ハンドルを作成
	CD3DX12_CPU_DESCRIPTOR_HANDLE Handle(
		m_CbvSrvUavDescriptorHeap->GetCPUDescriptorHandleForHeapStart(),
		1,
		0
	);
	//テクスチャのシェーダリソースビューを作成
	D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
	srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	//フォーマット
	srvDesc.Format = m_TextureResource->GetTextureResDesc().Format;
	srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
	srvDesc.Texture2D.MipLevels = m_TextureResource->GetTextureResDesc().MipLevels;
	//シェーダリソースビュー
	Dev->GetDevice()->CreateShaderResourceView(
		m_TextureResource->GetTexture().Get(),
		&srvDesc,
		Handle);


}

テクスチャリソースはすでに作成されているので、そこからテクスチャデータを取り出し、すでに作成されているテクスチャのデスクプリタヒープ(これはコンスタントバッファと兼用なのを思い出してください)をもとにハンドルを作成し、シェーダリソースビューを作成します。

ここまでがクリエイト時の準備です。

実際の描画処理

続いて描画処理です。描画処理はDx12DrawContext3D::DrawContext関数です。OnDraw関数では、この描画関数を呼ぶ前にコンスタントバッファの内容を更新しています。
以下はDx12DrawContext3D::DrawContext関数の前半です。


void  Dx12DrawContext3D::DrawContext() {
	//コマンドリストのリセット
	if (m_Trace) {
		CommandList::Reset(m_FrontPipelineState, m_CommandList);
	}
	else {
		CommandList::Reset(m_BackPipelineState, m_CommandList);
	}

	m_SphereMesh->UpdateResources<VertexPositionNormalTexture>(m_CommandList);
	m_TextureResource->UpdateResources(m_CommandList);

	//描画
	m_CommandList->SetGraphicsRootSignature(m_RootSignature.Get());
	ID3D12DescriptorHeap* ppHeaps[] = { m_CbvSrvUavDescriptorHeap.Get(), m_SamplerDescriptorHeap.Get() };
	m_CommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);

	for (size_t i = 0; i < m_GPUDescriptorHandleVec.size(); i++) {
		m_CommandList->SetGraphicsRootDescriptorTable(i, m_GPUDescriptorHandleVec[i]);
	}
	auto Dev = App::GetApp()->GetDeviceResources();
	m_CommandList->RSSetViewports(1, &Dev->GetViewport());
	m_CommandList->RSSetScissorRects(1, &Dev->GetScissorRect());

	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
		Dev->GetRtvHeap()->GetCPUDescriptorHandleForHeapStart(),
		Dev->GetFrameIndex(),
		Dev->GetRtvDescriptorSize());
	CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(
		Dev->GetDsvHeap()->GetCPUDescriptorHandleForHeapStart()
	);
	m_CommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);

	//中略
}

ここでの処理は、まずコマンドリストを目的のパイプラインステートでリセットします。透明の場合は前面カリングのパイプラインステートで、透明でない場合は背面カリングのパイプラインステートです。
続いて、メッシュの内容が修正されたり、テクスチャの内容が修正した場合のために、それぞれ反映させます。
そしてコマンドリストで、ルートシグネチャを設定し、デスクプリタヒープを設定します。そのあと初期化時に作成しておいた、GPU側テスクプリタヒープのハンドルを配列順にルートデスクプリタテーブルに登録します
そして、レンダーターゲットビューとデプスステンシルビューをデバイスリソースから取り出し、セットします。
続いては実際の描画です。


void  Dx12DrawContext3D::DrawContext() {
	//中略

	m_CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	m_CommandList->IASetIndexBuffer(&m_SphereMesh->GetIndexBufferView());
	m_CommandList->IASetVertexBuffers(0, 1, &m_SphereMesh->GetVertexBufferView());
	m_CommandList->DrawIndexedInstanced(m_SphereMesh->GetNumIndicis(), 1, 0, 0, 0);
	if (m_Trace) {
		m_CommandList->SetPipelineState(m_BackPipelineState.Get());
		m_CommandList->DrawIndexedInstanced(m_SphereMesh->GetNumIndicis(), 1, 0, 0, 0);
	}

	//コマンドリストのクローズ
	CommandList::Close(m_CommandList);
	//デバイスにコマンドリストを送る
	Dev->InsertDrawCommandLists(m_CommandList.Get());
}

ここでは描画前は、メッシュの設定と登録です。また、透明の場合は、2回描画してるのがわかります。
描画が終わったら、コマンドリストをクローズさせ、このコマンドリストを登録します。
前にも説明しましたが、このコマンドリストは、最終的に、デバイスリソースによりまとめて実行されます。

まとめ

このように、Dx12の描画処理を、一つ一つ記述していくと、かなり厄介なことがわかります。
しかし、よく見ると、その厄介さは自由度に直結しているのもわかります。
たとえば、このサンプルでは、サンプラーやテクスチャーはひとつづつしかシェーダで使用してませんが、これを複数使用するようにも記述できます。
パイプラインステートも、現時点では頂点シェーダとピクセルシェーダのみ実装してますが、ゆくゆくは、同様の流れでジオメトリシェーダも使えるようになります。(ほかのシェーダも可能です)

このように、シンプルバージョンではDx12がむき出しですが、そういった自由度が増えたと思って、頑張って実装していっていただければと思います。
また、このサンプルの描画方法はいろんな描画方法のひとつにすぎません。GPUの設定も、ほとんどゲーム制作側で実装できます。そういう意味でも、いろいろ挑戦できる形になったかなあと思ってます。

では、次回は、これまでのサンプルをDx12むき出し版に書き直します。冒頭述べましたが、最終的にはVSPSDrawContextクラスは無くしたいと思っています。
そんなわけで、次回もよろしくお願いします。

カテゴリー

ピックアップ記事

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