前回
ポストプロセス
こんにちは。
前のチャプターまでの内容で、ミクさんが踊る様子を描画することをすべて実装しました。
何か足りないと思われるかもしれませんが、基本的な要素はすべて揃っていると考えています。
今度はミクさん以外にも画面自体にエフェクトを加えたいと思います。
これは一般的にポストプロセスと呼ばれています。
ゲームをよくプレイする方なら、グラフィック設定でポストプロセスに関する設定を触ったことがあるでしょう。
ポストプロセスは文字通り後処理という意味です。
つまり、3D空間に何かを追加して効果を出すのではなく、描画が完了したシーンのみを使って効果を加えるのです。
ポストプロセスを行うためには、いくつかの処理が必要です。
これまではバックバッファに直接シーンを出力していました。
しかし、ポストプロセシングを適用するためには、このシーンを別のテクスチャに描画し、そのテクスチャを加工した後に画面に表示する必要があります。
そのため、フルスクリーンクワッドレンダリングを実装する必要があります。
2つ目は、どのような効果を実装するかによって異なりますが、エフェクトを作成する際に、シーンの色情報だけでなく、深度、法線、スペキュラーなど、様々な情報を必要とする場合があります。
しかし、これまでは結果として色情報のみを出力してレンダリングしていました。
そのため、これからは色情報だけでなく、法線やその他の情報も出力できるようにする必要があります。
つまり、レンダーターゲットが1つではなく、複数のレンダーターゲットにレンダリングするということです。
これをマルチレンダーターゲットと呼びます。
フルスクリーンクワッドレンダリング
フルスクリーンクワッドレンダリングの方法は簡単です。
画面全体を覆う四角形(クワッド)を作成し、それをレンダリングする方法です。
実際に描画したいシーンは別のテクスチャに描画し、そのテクスチャをこの四角形のテクスチャとして塗ります。
早速コードを書いてみましょう。
画面のテクスチャとして使用するリソースを追加します。
基本的なシーンの色情報を描画する対象となるリソースです。つまり、レンダーターゲットとしても使用可能でなければならず、クワッドにも塗る必要があるため、シェーダーリソースとしても使用できる必要があります。
Dx12Wrapperにメンバーを追加します。
ComPtr<ID3D12Resource> mScreenResource;
ComPtr<ID3D12DescriptorHeap> mPostProcessRTVHeap = nullptr;
ComPtr<ID3D12DescriptorHeap> mPostProcessSRVHeap = nullptr;
ポストプロセスに関連するリソースを生成するメソッドも追加しましょう。
このメソッドはDx12Wrapperのコンストラクタで呼び出すことになります。
HRESULT Dx12Wrapper::CreatePostProcessResource()
{
auto& bbuff = mBackBuffers[0];
auto resDesc = bbuff->GetDesc();
D3D12_HEAP_PROPERTIES heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
float clsClr[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
D3D12_CLEAR_VALUE clearValue = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_R8G8B8A8_UNORM, clsClr);
auto result = mDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(mScreenResource.ReleaseAndGetAddressOf())
);
if (FAILED(result))
{
return result;
}
auto heapDesc = mRtvHeaps->GetDesc();
heapDesc.NumDescriptors = 1;
result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessRTVHeap.ReleaseAndGetAddressOf()));
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
auto handle = mPostProcessRTVHeap->GetCPUDescriptorHandleForHeapStart();
mDevice->CreateRenderTargetView(mScreenResource.Get(), &rtvDesc, handle);
heapDesc.NumDescriptors = 1;
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
heapDesc.NodeMask = 0;
result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessSRVHeap.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
return result;
}
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Format = rtvDesc.Format;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
handle = mPostProcessSRVHeap->GetCPUDescriptorHandleForHeapStart();
mDevice->CreateShaderResourceView(mScreenResource.Get(), &srvDesc, handle);
}
auto& bbuff = mBackBuffers[0];
auto resDesc = bbuff->GetDesc();
D3D12_HEAP_PROPERTIES heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
float clsClr[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
D3D12_CLEAR_VALUE clearValue = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_R8G8B8A8_UNORM, clsClr);
auto result = mDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(mScreenResource.ReleaseAndGetAddressOf())
);
if (FAILED(result))
{
return result;
}
シーンを出力するテクスチャであるため、バックバッファと同じサイズです。
バックバッファのD3D12_RESOURCE_DESCを取得し、これを使用してリソースを生成します。
auto heapDesc = mRtvHeaps->GetDesc();
heapDesc.NumDescriptors = 1;
result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessRTVHeap.ReleaseAndGetAddressOf()));
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
auto handle = mPostProcessRTVHeap->GetCPUDescriptorHandleForHeapStart();
mDevice->CreateRenderTargetView(mScreenResource.Get(), &rtvDesc, handle);
mRtvHeapsは、最初のチャプターでスワップチェーンに使用するバッファのレンダーターゲットビューを生成するために生成したディスクリプタヒープです。
mScreenResourceもレンダーターゲットとして使用し、スワップチェーンバッファと同じ設定である必要があるため、
GetDescでD3D12_DESCRIPTOR_HEAP_DESCを取得し、そのまま使用します。
ポストプロセスに関連するリソースをレンダーターゲットとして使用するためのディスクリプタヒープを生成し、ハンドルを取得して最初の位置にレンダーターゲットビューを作成します。
今回追加したリソースだけでなく、今後ポストプロセス効果を作成するために追加されるリソースもこのディスクリプタヒープに生成する予定です。
heapDesc.NumDescriptors = 1;
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
heapDesc.NodeMask = 0;
result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessSRVHeap.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
return result;
}
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Format = rtvDesc.Format;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
handle = mPostProcessSRVHeap->GetCPUDescriptorHandleForHeapStart();
mDevice->CreateShaderResourceView(mScreenResource.Get(), &srvDesc, handle);
D3D12_DESCRIPTOR_HEAPのDESCをシェーダーリソース用の設定に変更し、ディスクリプタヒープを生成します。
今回も上と同じようにシェーダーリソースビューを作成します。
今度は画面全体を覆うクワッドを作成する必要があります。
つまり、クワッドの頂点バッファを作成します。
ComPtr<ID3D12Resource> mScreenVB;
D3D12_VERTEX_BUFFER_VIEW mScreenVertexBufferView;
Dx12Wrapperにメンバーを追加します。
頂点バッファとして使用するリソースと頂点バッファビューです。
bool Dx12Wrapper::CreateScreenVertex()
{
struct ScreenVertex
{
XMFLOAT3 pos;
XMFLOAT2 uv;
};
ScreenVertex sv[4] = { {{-1,-1,0.1},{0,1}},
{{-1,1,0.1},{0,0}},
{{1,-1,0.1},{1,1}},
{{1,1,0.1},{1,0}} };
auto heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto resDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(sv));
auto result = mDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(mScreenVB.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
assert(0);
return false;
}
ScreenVertex* mapped = nullptr;
mScreenVB->Map(0, nullptr, (void**)&mapped);
copy(begin(sv), end(sv), mapped);
mScreenVB->Unmap(0, nullptr);
mScreenVertexBufferView.BufferLocation = mScreenVB->GetGPUVirtualAddress();
mScreenVertexBufferView.SizeInBytes = sizeof(sv);
mScreenVertexBufferView.StrideInBytes = sizeof(ScreenVertex);
return true;
}
頂点を直接定義してバッファを生成し、マッピングします。
頂点バッファビューも設定します。
今度はこのクワッドを画面に描画するためのパイプラインステートを作成します。ルートパラメータとルートシグネチャも必要になります。
ComPtr<ID3D12RootSignature> mScreenRootSignature;
ComPtr<ID3D12PipelineState> mScreenPipelineDefault;
Dx12Wrapperにメンバーを追加します。
ルートシグネチャとパイプラインステートです。
bool Dx12Wrapper::CreateScreenPipeline()
{
D3D12_DESCRIPTOR_RANGE range[1] = {};
range[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
range[0].BaseShaderRegister = 0;
range[0].NumDescriptors = 1;
D3D12_ROOT_PARAMETER rp[1] = {};
rp[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rp[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
rp[0].DescriptorTable.pDescriptorRanges = &range[0];
rp[0].DescriptorTable.NumDescriptorRanges = 1;
D3D12_ROOT_SIGNATURE_DESC rsDesc = {};
rsDesc.NumParameters = 1;
rsDesc.pParameters = rp;
D3D12_STATIC_SAMPLER_DESC sampler = CD3DX12_STATIC_SAMPLER_DESC(0);
sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
rsDesc.pStaticSamplers = &sampler;
rsDesc.NumStaticSamplers = 1;
rsDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ComPtr<ID3DBlob> rsBlob;
ComPtr<ID3DBlob> errBlob;
auto result = D3D12SerializeRootSignature(&rsDesc, D3D_ROOT_SIGNATURE_VERSION_1, rsBlob.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());
if (FAILED(result))
{
assert(0);
return false;
}
result = mDevice->CreateRootSignature(0, rsBlob->GetBufferPointer(), rsBlob->GetBufferSize(), IID_PPV_ARGS(mPeraRootSignature.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
assert(0);
return false;
}
ComPtr<ID3DBlob> vs;
ComPtr<ID3DBlob> ps;
result = D3DCompileFromFile(L"ScreenVertex.hlsl",
nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
"vs", "vs_5_0", 0, 0, vs.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());
if (FAILED(result))
{
assert(0);
return false;
}
D3D12_GRAPHICS_PIPELINE_STATE_DESC gpsDesc = {};
gpsDesc.VS = CD3DX12_SHADER_BYTECODE(vs.Get());
gpsDesc.DepthStencilState.DepthEnable = false;
gpsDesc.DepthStencilState.StencilEnable = false;
D3D12_INPUT_ELEMENT_DESC layout[2] =
{
{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,D3D12_APPEND_ALIGNED_ELEMENT,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
{"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,D3D12_APPEND_ALIGNED_ELEMENT,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
};
gpsDesc.InputLayout.NumElements = _countof(layout);
gpsDesc.InputLayout.pInputElementDescs = layout;
gpsDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
gpsDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
gpsDesc.NumRenderTargets = 1;
gpsDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
gpsDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
gpsDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
gpsDesc.SampleDesc.Count = 1;
gpsDesc.SampleDesc.Quality = 0;
gpsDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
gpsDesc.pRootSignature = mPeraRootSignature.Get();
result = D3DCompileFromFile(L"ScreenPixelForward.hlsl",
nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
"ps", "ps_5_0", 0, 0, ps.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());
if (FAILED(result))
{
assert(0);
return false;
}
gpsDesc.PS = CD3DX12_SHADER_BYTECODE(ps.Get());
result = mDevice->CreateGraphicsPipelineState(&gpsDesc, IID_PPV_ARGS(mScreenPipelineDefault.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
assert(0);
return false;
}
return true;
}
シーンを描画したテクスチャをクワッドに適用する必要があるため、それに合わせてルートパラメータを設定し、ルートシグネチャを生成します。
D3D12_GRAPHICS_PIPELINE_STATE_DESCを設定し、パイプラインステートを生成します。
画面を埋めるクワッドのみを描画すればよいため、デプステストやステンシルテストは必要ありません。
今度はクワッドを描画するためのシェーダーを作成します。
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s0);
struct Output
{
float4 svpos: SV_POSITION;
float2 uv:TEXCOORD;
};
これはHLSLのヘッダーです。
現在はクワッドにシーンの色テクスチャのみを適用するため、そのテクスチャだけあれば十分です。
頂点シェーダーの戻り値として使用するOutput構造体のみを追加します。
#include "HeaderScreenForward.hlsli"
Output vs(float4 pos:POSITION, float2 uv : TEXCOORD)
{
Output output;
output.svpos = pos;
output.uv = uv;
return output;
}
頂点シェーダーです。
本当に何もないですね?
頂点の座標をそのままsvposに入れています。
空間変換を一切行っていません。
これだけで、なぜ画面全体を埋めるクワッドを描画できるのでしょうか?
クワッドの頂点バッファを作成したときの頂点座標を覚えていますか。
ScreenVertex sv[4] =
{ {{-1,-1,0.1},{0,1}},
{{-1,1,0.1},{0,0}},
{{1,-1,0.1},{1,1}},
{{1,1,0.1},{1,0}} };
ミクさんを描画する時の頂点シェーダーを考えてみると、空間変換を通じて最終的にはNDC座標に変換していました。
ですから、最初からNDC座標系の頂点であれば空間変換は必要ありません。
NDC座標系の範囲は-1から1です。そのため、上記のように座標を設定すれば画面全体を埋めるクワッドが生成されるのです。
#include "HeaderScreenForward.hlsli"
float4 ps(Output input) : SV_TARGET
{
float4 texColor = tex.Sample(smp, input.uv);
float4 result = texColor;
return result;
}
ピクセルシェーダーです。これも大したことはありません。
クワッドにシーンテクスチャを塗るだけです。
さあ、描画を始めましょう。
ミクさんはもはやバックバッファに描画されません。
先ほど新しく用意したテクスチャに描画されます。
そしてバックバッファにはクワッドが描画され、クワッドにテクスチャが塗られます。
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mScreenResource.Get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET);
mCmdList->ResourceBarrier(1, &barrier);
まず、シーンを出力するテクスチャバッファの状態をD3D12_RESOURCE_STATE_RENDER_TARGETに変更します。
auto rtvHeapPointer = mPostProcessRTVHeap->GetCPUDescriptorHandleForHeapStart();
auto dsvHeapPointer = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();
mCmdList->OMSetRenderTargets(1, &rtvHeapPointer, false, &dsvHeapPointer);
シーンテクスチャのレンダーターゲットビューをレンダーターゲットとして設定します。
当然、深度テストを使用するので、深度ステンシルビューも設定します。
float clearColorBlack[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
mCmdList->ClearRenderTargetView(&rtvHeapPointer, clearColorBlack, 0, nullptr);
描画する前にテクスチャをクリアしましょう。
この後、いつものようにミクさんをレンダリングしてください。
ミクさんのレンダリング部分に変更はないので、特に説明はしません。
レンダーターゲットがシーンテクスチャに設定されているため、バックバッファではなくシーンテクスチャに描画されるはずです。
ではクワッドをバックバッファにレンダリングする必要があります。
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mScreenResource.Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
mCmdList->ResourceBarrier(1, &barrier);
クワッドにシーンテクスチャを適用する必要があるため、シェーダーリソースとして状態を変更します。
void Dx12Wrapper::Clear()
{
auto backBufferIndex = mSwapChain->GetCurrentBackBufferIndex();
auto rtvHeapPointer = mRtvHeaps->GetCPUDescriptorHandleForHeapStart();
rtvHeapPointer.ptr += backBufferIndex * mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mCmdList->OMSetRenderTargets(1, &rtvHeapPointer, false, nullptr);
float clsClr[4] = { 0.0,0.0,0.0,1.0 };
mCmdList->ClearRenderTargetView(rtvHeapPointer, clsClr, 0, nullptr);
}
バックバッファをレンダーターゲットとして設定し、クリアします。
auto wsize = Application::Instance().GetWindowSize();
D3D12_VIEWPORT vp = CD3DX12_VIEWPORT(0.0f, 0.0f, wsize.cx, wsize.cy);
mCmdList->RSSetViewports(1, &vp);
CD3DX12_RECT rc(0, 0, wsize.cx, wsize.cy);
mCmdList->RSSetScissorRects(1, &rc);
mCmdList->SetPipelineState(mScreenPipelineDefault.Get());
mCmdList->SetGraphicsRootSignature(mScreenRootSignature.Get());
mCmdList->SetDescriptorHeaps(1, mPostProcessSRVHeap.GetAddressOf());
auto handle = mPostProcessSRVHeap->GetGPUDescriptorHandleForHeapStart();
mCmdList->SetGraphicsRootDescriptorTable(0, handle);
mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
mCmdList->IASetVertexBuffers(0, 1, &mScreenVertexBufferView);
mCmdList->DrawInstanced(4, 1, 0, 0);
クワッドを描画するパイプラインステートとルートシグネチャを設定し、
シーンテクスチャのシェーダーリソースビューを設定します。
そして頂点バッファビューを設定し、描画を命令します。
頂点4つを順番に描画するだけなので、インデックスバッファビューは必要ありません。
ここまで実装してビルドして確認してみましょう。
問題がなければ、以前と変わらない画面が表示されるはずです。
クワッドに正しく描画されているかどうか、よくわからないですか?
では、簡単なエフェクトを追加してみましょう。
クワッドを描画するピクセルシェーダーを開いて修正しましょう。
#include "HeaderScreenForward.hlsli"
float4 ps(Output input) : SV_TARGET
{
float4 texColor = tex.Sample(smp, input.uv);
float4 result = texColor;
float gray = dot(result.rgb, float3(0.299, 0.587, 0.114));
result.rgb = gray;
return result;
}
上記で追加したコードはRGBをグレースケールに変換するコードです。
これにより、画面全体がモノクロに変換されるでしょう。
ビルドして確認してみましょう。
画面がモノクロに変わりました。
シーンテクスチャが正しくクワッドに適用されて表示されていることを確認しました。
すでにグレースケール効果を追加することでポストプロセスを体験しました。
これからはさらに多様な効果を追加できるようにしていきましょう。
マルチターゲットレンダリング
これまでは1つのレンダーターゲットにオブジェクトをレンダリングしていました。
しかし、ポストプロセス効果の中には、オブジェクトの法線情報、位置情報、深度情報など、様々な情報を必要とするものがあります。
例えば、法線情報は現在ミクさんを描画する際にシェーダーで使用していますが、それ以降は分からなくなってしまいます。
そのため、クワッドにテクスチャを適用する際に、法線情報も分かるように、シェーダーの出力を複数出力できるように修正します。
シーンの色、法線、そして後で追加するブルーム効果のための高輝度、この3つのレンダーターゲットに出力するようにします。
PMXRendererのパイプラインステートを生成する部分に戻りましょう。
D3D12_GRAPHICS_PIPELINE_STATE_DESC gpipeline = {};
...
result = _dx12.Device()->CreateGraphicsPipelineState(&gpipeline, IID_PPV_ARGS(_pipeline.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
assert(SUCCEEDED(result));
}
return result;
D3D12_GRAPHICS_PIPELINE_STATE_DESCを設定し、パイプラインステートを生成しました。
この設定を少し変更します。
gpipeline.NumRenderTargets = 3;
gpipeline.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM;
gpipeline.RTVFormats[1] = DXGI_FORMAT_B8G8R8A8_UNORM;
gpipeline.RTVFormats[2] = DXGI_FORMAT_B8G8R8A8_UNORM;
レンダーターゲットの数を増やします。
ブレンド設定も増やします。
最初の色レンダーターゲットを除いて、すべてブレンドは不要です。
D3D12_RENDER_TARGET_BLEND_DESC defaultBlendDesc;
defaultBlendDesc.BlendEnable = false;
defaultBlendDesc.LogicOpEnable = false;
defaultBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
D3D12_RENDER_TARGET_BLEND_DESC transparencyBlendDesc;
transparencyBlendDesc.BlendEnable = true;
transparencyBlendDesc.LogicOpEnable = false;
transparencyBlendDesc.SrcBlend = D3D12_BLEND_SRC_ALPHA;
transparencyBlendDesc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
transparencyBlendDesc.BlendOp = D3D12_BLEND_OP_ADD;
transparencyBlendDesc.SrcBlendAlpha = D3D12_BLEND_ONE;
transparencyBlendDesc.DestBlendAlpha = D3D12_BLEND_ZERO;
transparencyBlendDesc.BlendOpAlpha = D3D12_BLEND_OP_ADD;
transparencyBlendDesc.LogicOp = D3D12_LOGIC_OP_NOOP;
transparencyBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
gpipeline.BlendState.RenderTarget[0] = transparencyBlendDesc;
gpipeline.BlendState.RenderTarget[1] = defaultBlendDesc;
gpipeline.BlendState.RenderTarget[2] = defaultBlendDesc;
今度は出力するテクスチャリソースを追加します。
シーンの色を出力するテクスチャはすでにあるので、法線と高輝度テクスチャの2つを追加します。
ComPtr<ID3D12Resource> mNormalResource;
ComPtr<ID3D12Resource> mHighLumResource;
Dx12Wrapperにバッファリソースを追加します。
HRESULT Dx12Wrapper::CreatePostProcessResource()
{
...
auto result = mDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValue,
IID_PPV_ARGS(mScreenResource.ReleaseAndGetAddressOf())
);
if (FAILED(result))
{
return result;
}
result = mDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValueBlack,
IID_PPV_ARGS(mNormalResource.ReleaseAndGetAddressOf())
);
if (FAILED(result))
{
return result;
}
result = mDevice->CreateCommittedResource(
&heapProp,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
&clearValueBlack,
IID_PPV_ARGS(mHighLumResource.ReleaseAndGetAddressOf())
);
if (FAILED(result))
{
return result;
}
auto heapDesc = mRtvHeaps->GetDesc();
heapDesc.NumDescriptors = 3;
result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessRTVHeap.ReleaseAndGetAddressOf()));
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
auto handle = mPostProcessRTVHeap->GetCPUDescriptorHandleForHeapStart();
mDevice->CreateRenderTargetView(mScreenResource.Get(), &rtvDesc, handle);
handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDevice->CreateRenderTargetView(mNormalResource.Get(), &rtvDesc, handle);
handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDevice->CreateRenderTargetView(mHighLumResource.Get(), &rtvDesc, handle);
heapDesc.NumDescriptors = 3;
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
heapDesc.NodeMask = 0;
result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessSRVHeap.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
return result;
}
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Format = rtvDesc.Format;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
handle = mPostProcessSRVHeap->GetCPUDescriptorHandleForHeapStart();
mDevice->CreateShaderResourceView(mScreenResource.Get(), &srvDesc, handle);
handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mDevice->CreateShaderResourceView(mNormalResource.Get(), &srvDesc, handle);
handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mDevice->CreateShaderResourceView(mHighLumResource.Get(), &srvDesc, handle);
return result;
}
色テクスチャリソースを生成し、レンダーターゲットビューとシェーダーリソースビューを作成していたメソッドに、法線および高輝度テクスチャに関連する内容を追加します。
これから描画命令を行う前に、レンダーターゲットを複数設定するように修正します。
int rtvNum = 3;
D3D12_CPU_DESCRIPTOR_HANDLE* rtvs = new D3D12_CPU_DESCRIPTOR_HANDLE[rtvNum];
auto rtvHeapPointer = mPostProcessRTVHeap->GetCPUDescriptorHandleForHeapStart();
for (int i = 0; i < rtvNum; i++)
{
rtvs[i] = rtvHeapPointer;
rtvHeapPointer.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
}
auto dsvHeapPointer = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();
mCmdList->OMSetRenderTargets(3, rtvs, false, &dsvHeapPointer);
ディスクリプタヒープに色、法線、高輝度の順にレンダーターゲットビューを作成したため、レンダーターゲットの数だけハンドルのポインタを配列に入れます。
ハンドルポインタの配列をOMSetRenderTargetsに設定し、レンダーターゲットの数を3に設定します。
これで3つの出力が可能になりました。
これに合わせてシェーダーも修正しましょう。
PMXモデルを描画するシェーダーのヘッダーを開いてください。
struct PixelOutput
{
float4 color : SV_TARGET0;
float4 normal : SV_TARGET1;
float4 highLum : SV_TARGET2;
};
ピクセルシェーダーの戻り値として使用する構造体を追加します。
SV_TARGETセマンティクスは、特定のレンダーターゲットを関連付けるためのセマンティクスです。
PixelOutPut BasicPS(Output input) : SV_TARGET
{
PixelOutPut output;
float3 light = normalize(lightVec);
float3 normal = normalize(input.normal);
float diffuseB = saturate(dot(-light, normal));
float4 toonDif = toon.Sample(smpToon, float2(0, 1.0 - diffuseB));
float3 refLight = normalize(reflect(light, normal));
float specularB = pow(saturate(dot(refLight, -input.ray)), specular.a);
float3 specularColor = specular.rgb * specularB;
float4 color = tex.Sample(smp, input.uv);
color.rgb = color.rgb * toonDif + specularColor;
float3 posFromLightVP = input.tpos.xyz / input.tpos.w;
float2 shadowUV = (posFromLightVP + float2(1, -1)) * float2(0.5, -0.5);
float depthFromLight = lightDepthTex.SampleCmp(shadowSmp, shadowUV, posFromLightVP.z - 0.005f);
float shadowWeight = lerp(0.5f, 1.0f, depthFromLight);
color.rgb *= shadowWeight;
output.color = color;
output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
output.normal.a = depthFromLight;
output.highLum = 1.0f;
return output;
}
ピクセルシェーダーを修正します。
PixelOutputを返すように変更します。
output.colorには既存の出力していた色を入れます。
output.normal.rgbには法線値を0から1に正規化して入れます。
output.normal.aにはシャドウデプス値を入れます。
このようにすると、後でシャドウデプスマップをサンプリングしなくても、法線テクスチャのa値を参照してシャドウデプス値として使用できますね。
output.highLumは後でブルームを実装する時に話します。今回は1.0fで出力しましょう。
マルチレンダーターゲットの準備が完了しました。
ビルドして確認しても、当然ながら画面に変化はありません。
PIXで実行してみると、複数のレンダーターゲットに出力されていることが確認できます。
法線値のみが出力されているのがわかります。
highLumには1.0fのみで出力したので、白いテクスチャとして見えるはずです。
まとめ
マルチレンダーターゲットの実装が完了しました。
これらを使って追加のエフェクトを作成できるようになりました。
マルチレンダーターゲットを利用すればディファードライティングも実装できます。
ディファードライティングについてもっと話したいところですが、残念ながら私の記事ではフォワードライティングをベースに進めていきます。
次の記事ではキーボードやマウスの入力とデバッグ用のIMGUIについて話す予定です。
その後の記事でブルームエフェクトを追加する予定です。
ありがとうございました。
次回