1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DirectX12でミクさんを躍らせてみよう17-ブルームエフェクト

Last updated at Posted at 2024-10-01

前回

ブルームエフェクト

こんにちは。

今回はブルームエフェクトを実装してみます。

個人的には、ポストプロセス効果の中で最も分かりやすい効果だと思います。

image.png

ブルームは、非常に明るい光が光を発する物体の形を超えて広がっているように見えるように見せる効果です。

最近のゲームではよく見られますね。

では、どのように作ればいいでしょうか?

image 1.png
画面の非常に明るい部分だけを別のテクスチャとして描画し、そのテクスチャをぼかし(ブラー)処理して、元の画面と合成すれば完成です。

原理自体は難しくありません。

高輝度のみを描画する

以前にマルチターゲットレンダリングを実装した際に、既に高輝度のみを描画できるようにレンダーターゲットを追加していました。

PixelOutPut BasicPS(Output input) : SV_TARGET
{
  ...
	
	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;
}

これはPMXモデルを描画するピクセルシェーダーで、highLumが高輝度テクスチャとして使用されるレンダーターゲットです。

では、画面の明るさを判断して、高輝度の部分のみを描画するようにしましょう。

float luminance = dot(float3(0.299f, 0.587f, 0.114f), output.color.xyz);
output.highLum = luminance > 0.9 ? output.color : 0.0f;

(0.299f, 0.587f, 0.114f)を色と内積した結果を輝度として使用できます。

この値は、Rec. 601というデジタルビデオ信号のエンコーディングで定義されている値だそうです。

輝度が0.9以上の色のみをhighLumに描画するようにします。

PIXで描画結果を確認してみましょう。
image 2.png
元のシーンはこのようになりますが
image 3.png
高輝度の色だけを描画したテクスチャはこのようになります。

ミクさんの白い服の部分が明るい箇所が多いため、このような結果になっているようです。

次に、このテクスチャをぼかす処理をする必要があります。

ガウシアンブラー

image 4.png

ブラーは画像をぼかす効果です。

画像をどうすればぼかすことができるでしょうか。

ブラーを作成する方法はいくつかありますが、その中でコンボリューションを使用します。

コンボリューションは、現在のピクセルを中心に、その周囲にあるピクセルそれぞれに重みを掛け、その結果をすべて足し合わせた値を現在のピクセルの値に変更する演算です。

ここで言う重みをカーネルまたはフィルターと呼びます。

このフィルターにはガウシアンフィルターを使用します。

ガウシアンフィルターはガウス関数から導出されます。

ガウス関数は数学的に釣鐘型を描く関数です。

そのため、中心から離れるほど値が急激に減少します。

この特性を利用して画像をぼかすことができます。

5x5ガウシアンフィルターのウエート値は次のとおりです。

image.png

シェーダーコードでガウシアンブラーメソッドを作成します。

float4 Get5x5GaussianBlur(Texture2D<float4> tex, SamplerState smp, float2 uv, float dx, float dy, float4 rect)
{
    float4 ret = tex.Sample(smp, uv);
    float4 blurColor = float4(0, 0, 0, 0);

    float weights[5][5] = {
        {1 / 273.0,  4 / 273.0,  7 / 273.0,  4 / 273.0, 1 / 273.0},
        {4 / 273.0, 16 / 273.0, 26 / 273.0, 16 / 273.0, 4 / 273.0},
        {7 / 273.0, 26 / 273.0, 41 / 273.0, 26 / 273.0, 7 / 273.0},
        {4 / 273.0, 16 / 273.0, 26 / 273.0, 16 / 273.0, 4 / 273.0},
        {1 / 273.0,  4 / 273.0,  7 / 273.0,  4 / 273.0, 1 / 273.0}
    };

    float offsets[5] = { -2.0f, -1.0f, 0.0f, 1.0f, 2.0f };

    for (int i = 0; i < 5; ++i) 
    {
        for (int j = 0; j < 5; ++j) 
        {
            float2 offset = float2(offsets[i] * dx, offsets[j] * dy);
            float2 sampleUV = uv + offset;

            sampleUV.x = clamp(sampleUV.x, rect.x + dx * 0.5, rect.z - dx * 0.5);
            sampleUV.y = clamp(sampleUV.y, rect.y + dy * 0.5, rect.w - dy * 0.5);

            blurColor += tex.Sample(smp, sampleUV) * weights[i][j];
        }
    }

    return float4(blurColor.rgb, ret.a);
}
float4 Get5x5GaussianBlur(Texture2D<float4> tex, SamplerState smp, float2 uv, float dx, float dy, float4 rect)

パラメータとして、
サンプリングするテクスチャ、
サンプラー、
現在のUV、
1ピクセルの幅、
1ピクセルの高さ、
サンプリングする領域
を渡します。

1ピクセルのサイズの計算については、後でこのメソッドを呼び出すコードを作成する際に説明します。

 float4 ret = tex.Sample(smp, uv);
 float4 blurColor = float4(0, 0, 0, 0);

まず、現在のピクセルの色をサンプリングし、ブラー値を初期化します。

 float weights[5][5] = {
        {1 / 273.0,  4 / 273.0,  7 / 273.0,  4 / 273.0, 1 / 273.0},
        {4 / 273.0, 16 / 273.0, 26 / 273.0, 16 / 273.0, 4 / 273.0},
        {7 / 273.0, 26 / 273.0, 41 / 273.0, 26 / 273.0, 7 / 273.0},
        {4 / 273.0, 16 / 273.0, 26 / 273.0, 16 / 273.0, 4 / 273.0},
        {1 / 273.0,  4 / 273.0,  7 / 273.0,  4 / 273.0, 1 / 273.0}
    };

先ほど見た5x5ガウシアンフィルターのウエート値を定義します。

float offsets[5] = { -2.0f, -1.0f, 0.0f, 1.0f, 2.0f };

5x5フィルターの各ピクセル位置に対するオフセットを定義します。

for (int i = 0; i < 5; ++i) 
{
     for (int j = 0; j < 5; ++j) 
     {
         float2 offset = float2(offsets[i] * dx, offsets[j] * dy);
         float2 sampleUV = uv + offset;

         sampleUV.x = clamp(sampleUV.x, rect.x + dx * 0.5, rect.z - dx * 0.5);
         sampleUV.y = clamp(sampleUV.y, rect.y + dy * 0.5, rect.w - dy * 0.5);

         blurColor += tex.Sample(smp, sampleUV) * weights[i][j];
     }
}

2つのネストされたループを通して5x5ブラーフィルターを適用します。

オフセットに応じてUVを計算し、サンプリングした後にウエート値を掛けて加算します。

UV座標を制限する理由は、UV座標がテクスチャの境界を超えないようにするためです。

0.5を掛けて半ピクセル分離れた座標に制限する理由は、境界の端をサンプリングすると画面の端に表示されるべきではない色が表示される場合があるからです。

マルチパスブルーム

ブラーメソッドを作成したので、次はブルーム効果を作成しましょう。

先ほど説明したように、高輝度テクスチャをブラーリングし、元の画面と合成すれば完成です。

では、高輝度テクスチャに先ほど作成した5x5ガウシアンブラーを使えば完成でしょうか?

残念ながら、そうではありません。

ブルーム効果は、元の光の大きさよりも大きく光が広がって見えるはずです。

しかし、5x5サイズのフィルターでは光が広がるという感覚を得るのは難しいです。

そのため、テクスチャを何度も縮小しながらブラーを適用し、縮小されたテクスチャを再び合算する方法を使用します。

もう少し詳しく説明すると、1600x800の解像度をまず800x400に、次に400x200というように段階的に縮小してテクスチャを描画し、これらのテクスチャを再び元の解像度を基準にサンプリングすると、低解像度テクスチャのピクセル値が補間されて自然なブルーム効果を得ることができます。

ここで一つの疑問が生じるかもしれません。

なぜわざわざそのような方法を使うのか?単に大きな範囲のフィルターを使えばいいのではないか?と考えるかもしれません。

まず考えなければならないのは、テクスチャ1枚をブラーリングするのはコストが軽くないということです。

なぜなら、5x5ブラーフィルターを使用すると仮定すると、1ピクセルあたり25回のサンプリングが必要になります。

そうすると、1600x800の解像度のテクスチャ1枚をブラーリングすると

$$
1600\times 800 \times 25=32,000,000
$$

では8回縮小して合算すると

$$
(1600\times 800 \times 25)+(800 \times400 \times25)+.... = 42,665,550
$$

このサンプリング回数が多いと思いますか?

それでは、縮小せずに1回のフィルターだけでブラーリングする場合、上記のように複数回縮小して混合して得るブルーム効果と同様の効果を得るには、どの程度の大きさのフィルターを使用する必要があるでしょうか?

上記のように縮小すると、同じサイズのフィルターを使用しても実際の画面上でのブラーリングの範囲は2倍ずつ大きくなります。

そのため、縮小せずに1つのフィルターで同じブラーリングの範囲を得るには、16倍のサイズのフィルターが必要になります。

5x5なので、80x80を使用すれば1回のブラーリングで近い結果を得ることができるでしょう。

では計算してみましょう。

80x80フィルターを使用すると、1ピクセルあたりのサンプリング回数は6400回です。

では1600x800のテクスチャをブラーリングするには?

$$
1600\times 800 \times 6400 =8,192,000,000
$$

先ほどマルチパスブルーム効果を作成するために必要なサンプリング回数と比較してみてください。

1倍、2倍程度ではなく、無視できないほど膨大な回数のサンプリングを行わなければなりません。

ここまでの内容で、マルチパスブルームを使用する理由が十分に伝わったと思います。

マルチステージブルームを使用すると、十分に広範囲の自然なブラー効果を適度な計算コストで得ることができます。(決して軽くはありませんが。)

必要なものを準備しましょう。

ブルームの実装

前述した内容に基づいて、追加すべき項目をまとめてみましょう。

  • オリジナルの高輝度テクスチャを縮小して描画するためのテクスチャバッファ:高輝度テクスチャを希望する回数だけ縮小して描画するバッファです。レンダーターゲットとしても使用できる必要があります。ここで覚えておくべきことは、複数回縮小しますが、すべて1枚のテクスチャに描画するということです。
  • 高輝度テクスチャを縮小して描画する際に使用するパイプラインステート:高輝度テクスチャを繰り返し縮小して描画する際に使用するパイプラインステートです。
  • ブルーム結果を描画するテクスチャバッファ:縮小しながらブラーがかけられたテクスチャを再びサンプリングして合算し、結果を出力するバッファです。
  • ブルームパイプラインステート:最終的なブルーム結果を描画するパイプラインステートです。

それでは新しいテクスチャバッファを追加します。

ComPtr<ID3D12Resource> mHighLumShrinkResource;
ComPtr<ID3D12Resource> mBloomResultResource;

Dx12Wrapperクラスにメンバーを追加します。

以前にマルチターゲットレンダリングを実装した際にバッファを生成したメソッドで初期化するように追加します。

HRESULT Dx12Wrapper::CreatePostProcessResource()
{
  ...
  
  auto result = mDevice->CreateCommittedResource(
			&heapProp,
			D3D12_HEAP_FLAG_NONE,
			&resDesc,
			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
			&clearValue,
			IID_PPV_ARGS(mBloomResultResource.ReleaseAndGetAddressOf())
		);

		if (FAILED(result))
		{
			return result;
		}
  
  
	auto result = mDevice->CreateCommittedResource(
			&heapProp,
			D3D12_HEAP_FLAG_NONE,
			&resDesc,
			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
			&clearValue,
			IID_PPV_ARGS(mHighLumShrinkResource.ReleaseAndGetAddressOf())
		);
		resDesc.Width >>= 1;

		if (FAILED(result))
		{
			return result;
		}
		
  auto heapDesc = mRtvHeaps->GetDesc();
	heapDesc.NumDescriptors = 5;
	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);
  
  handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
  mDevice->CreateRenderTargetView(mHighLumShrinkResource.Get(), &rtvDesc, handle);
  
  handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
  mDevice->CreateRenderTargetView(mBloomResultResource.Get(), &rtvDesc, handle);
  
  heapDesc.NumDescriptors = 5;
	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);
	
	handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	mDevice->CreateShaderResourceView(mHighLumShrinkResource.Get(), &srvDesc, handle);
	
	handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	mDevice->CreateShaderResourceView(mBloomResultResource.Get(), &srvDesc, handle);
	
	return result;
}

縮小して描画するためのバッファを生成する際、バッファのWidthを右に1ビットシフトして初期化します。これは2で割ることと同じです。

元の解像度が1600x800なので、800x800になるわけです。

バッファの生成が終わったら、レンダーターゲットビューとシェーダーリソースビューを作成します。

bool Dx12Wrapper::CreateScreenPipeline()
{
	D3D12_DESCRIPTOR_RANGE range[1] = {};

	range[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	range[0].BaseShaderRegister = 0;
	range[0].NumDescriptors = 5;

	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;

	...

ルートシグネチャを生成する部分で、ディスクリプタレンジのNumDescriptorsを5に設定します。

テクスチャごとに設定するのは面倒なので、ポストプロセッシングではこのルートシグネチャを使用して、1つのディスクリプタヒープですべてのスクリーンテクスチャを使用できるようにします。

mPostProcessSRVHeapとmPostProcessRTVHeapに生成したビューは

シーン、ノーマル、高輝度、マルチステージブルーム用のテクスチャ、ブルーム結果という5つなので、NumDescriptorsを5に設定します。

ComPtr<ID3D12PipelineState> mBlurShrinkPipeline;

Dx12Wrapperにテクスチャを縮小して描画するためのパイプラインステートをメンバーとして追加します。

bool Dx12Wrapper::CreateScreenPipeline()
{
	...
	
	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 = mScreenRootSignature.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;
	}
	
	ComPtr<ID3DBlob> blurPixelPs;
	
	result = D3DCompileFromFile(L"BlurPixel.hlsl",
		nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"ps", "ps_5_0", 0, 0, blurPixelPs.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());

	if (FAILED(result))
	{
		assert(0);
		return false;
	}
	
	gpsDesc.PS = CD3DX12_SHADER_BYTECODE(blurPixelPs.Get());
	
	result = mDevice->CreateGraphicsPipelineState(&gpsDesc, IID_PPV_ARGS(mBlurShrinkPipeline.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(0);
		return false;
	}
	
	return true;
}

mBlurShrinkPipelineに新しいパイプラインステートを生成します。

フルスクリーンクワッドを実装する際に作成したHLSLヘッダーを修正しましょう。

Texture2D<float4> tex : register(t0);
Texture2D<float4> texNormal : register(t1);
Texture2D<float4> texHighLum : register(t2);
Texture2D<float4> texShrinkHighLum : register(t3);
Texture2D<float4> texBloomResult : register(t4);

SamplerState smp : register(s0);

struct Output
{
	float4 svpos: SV_POSITION;
	float2 uv:TEXCOORD;
};

ディスクリプタヒープのすべてのテクスチャを使用できるように修正します。

これで、ポストプロセシングに関連するシェーダーではこのヘッダーをインクルードします。

テクスチャを複数回縮小する際に使用するピクセルシェーダーを作成します。

#include "HeaderPostprocess.hlsli"
#include "HeaderScreenForward.hlsli"

BlurOutput ps(Output input) : SV_Target
{
	float w, h, levels;
	tex.GetDimensions(0, w, h, levels);
	float dx = 1.0 / w;
	float dy = 1.0 / h;

	return Get5x5GaussianBlur(texHighLum, smp, input.uv, dx, dy, float4(0, 0, 1, 1));;
};

HeaderPostprocess.hlsliには先ほど作成した5x5ガウシアンブラーメソッドがあります。

GetDimensionsメソッドでテクスチャの幅、高さ、ミップマップレベルを取得できます。

取得した幅と高さで1を割ると、1ピクセルあたりのUV座標の間隔を計算できます。

この値をGet5x5GaussianBlurのパラメータとして渡します。

パイプラインステートを生成したので、これから複数回縮小してテクスチャを描画します。

void Dx12Wrapper::DrawShrinkTextureForBlur()
{
	mCmdList->SetPipelineState(mBlurShrinkPipeline.Get());
	mCmdList->SetGraphicsRootSignature(mPeraRootSignature.Get());

	mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
	mCmdList->IASetVertexBuffers(0, 1, &mPeraVertexBufferView);

	auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

	barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumShrinkResource.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);
	mCmdList->ResourceBarrier(1, &barrier);

	auto rtvBaseHandle = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
	auto rtvIncSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandles[1] = {};

	rtvHandles[0].InitOffsetted(rtvBaseHandle, rtvIncSize * 3);

	mCmdList->OMSetRenderTargets(1, rtvHandles, false, nullptr);

	auto srvHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();

	mCmdList->SetDescriptorHeaps(1, mPeraSRVHeap.GetAddressOf());
	mCmdList->SetGraphicsRootDescriptorTable(0, srvHandle);

	auto desc = mHighLumResource->GetDesc();
	D3D12_VIEWPORT vp = {};
	D3D12_RECT sr = {};

	vp.MaxDepth = 1.0f;
	vp.MinDepth = 0.0f;
	vp.Height = desc.Height / 2;
	vp.Width = desc.Width / 2;
	sr.top = 0;
	sr.left = 0;
	sr.right = vp.Width;
	sr.bottom = vp.Height;

	for (int i = 0; i < mBloomIteration; ++i)
	{
		mCmdList->RSSetViewports(1, &vp);
		mCmdList->RSSetScissorRects(1, &sr);
		mCmdList->DrawInstanced(4, 1, 0, 0);

		sr.top += vp.Height;
		vp.TopLeftX = 0;
		vp.TopLeftY = sr.top;

		vp.Width /= 2;
		vp.Height /= 2;
		sr.bottom = sr.top + vp.Height;
	}

	barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

	barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumShrinkResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

	barrier = CD3DX12_RESOURCE_BARRIER::Transition(mBloomResultTexture.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);
	mCmdList->ResourceBarrier(1, &barrier);
}
mCmdList->SetPipelineState(mBlurShrinkPipeline.Get());
mCmdList->SetGraphicsRootSignature(mPeraRootSignature.Get());

mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
mCmdList->IASetVertexBuffers(0, 1, &mPeraVertexBufferView);

auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumShrinkResource.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);
	mCmdList->ResourceBarrier(1, &barrier);

auto rtvBaseHandle = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
auto rtvIncSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandles[1] = {};

rtvHandles[0].InitOffsetted(rtvBaseHandle, rtvIncSize * 3);
mCmdList->OMSetRenderTargets(1, rtvHandles, false, nullptr);

auto srvHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();

mCmdList->SetDescriptorHeaps(1, mPeraSRVHeap.GetAddressOf());
mCmdList->SetGraphicsRootDescriptorTable(0, srvHandle);

パイプラインステートを設定します。

高輝度テクスチャは、これからシェーダーでサンプリングする必要があるため、D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEに転換させます。

mHighLumShrinkResourceはレンダーターゲットとして使用できるように転換させます。

レンダーターゲットを設定し、シェーダーでテクスチャを使用できるようにディスクリプタヒープを設定します。

auto desc = mHighLumResource->GetDesc();
D3D12_VIEWPORT vp = {};
D3D12_RECT sr = {};

vp.MaxDepth = 1.0f;
vp.MinDepth = 0.0f;
vp.Height = desc.Height / 2;
vp.Width = desc.Width / 2;
sr.top = 0;
sr.left = 0;
sr.right = vp.Width;
sr.bottom = vp.Height;

for (int i = 0; i < mBloomIteration; ++i)
{
	mCmdList->RSSetViewports(1, &vp);
	mCmdList->RSSetScissorRects(1, &sr);
	mCmdList->DrawInstanced(4, 1, 0, 0);

	sr.top += vp.Height;
	vp.TopLeftX = 0;
	vp.TopLeftY = sr.top;

	vp.Width /= 2;
	vp.Height /= 2;
	sr.bottom = sr.top + vp.Height;
}

この部分が最も重要です。

高輝度テクスチャのD3D12_RESOURCE_DESCを通じて解像度を取得します。

ビューポートのサイズを解像度の半分に設定し、それに合わせてシザーレクトも設定します。

そして、決められた回数だけ繰り返しながらレンダリングを行います。

毎回ビューポートのサイズを前回の半分に設定し、それに合わせてシザーレクトの位置も移動させます。

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mHighLumShrinkResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mBloomResultTexture.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);
	mCmdList->ResourceBarrier(1, &barrier);

最後に、高輝度テクスチャと縮小テクスチャをシェーダーリソースに変換し、

ブルームの結果を描画するテクスチャをレンダーターゲットに変更します。

ビルドしてPIXで確認してみましょう。
image 5.png
上の図は8回縮小して描画したテクスチャです。

ミップマップに似ていますね。

これで、このように縮小されたテクスチャを再度サンプリングして合成すれば、ブルーム効果を作成することができます。

最終的にブルームエフェクトを描画するパイプラインステートを追加しましょう。

ComPtr<ID3D12PipelineState> mBlurResultPipeline;

Dx12Wrapperにメンバーを追加してください。

D3D12_GRAPHICS_PIPELINE_STATE_DESC blurResultPipelineDesc = {};
blurResultPipelineDesc.VS = CD3DX12_SHADER_BYTECODE(vs.Get());
blurResultPipelineDesc.DepthStencilState.DepthEnable = false;
blurResultPipelineDesc.DepthStencilState.StencilEnable = false;
blurResultPipelineDesc.InputLayout.NumElements = _countof(layout);
blurResultPipelineDesc.InputLayout.pInputElementDescs = layout;
blurResultPipelineDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
blurResultPipelineDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
blurResultPipelineDesc.NumRenderTargets = 1;
blurResultPipelineDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
blurResultPipelineDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
blurResultPipelineDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
blurResultPipelineDesc.SampleDesc.Count = 1;
blurResultPipelineDesc.SampleDesc.Quality = 0;
blurResultPipelineDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
blurResultPipelineDesc.pRootSignature = mScreenRootSignature.Get();

ComPtr<ID3DBlob> bloomResultPs;

result = D3DCompileFromFile(L"BloomResult.hlsl",
		nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"ps", "ps_5_0", 0, 0, bloomResultPs.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());

if (FAILED(result))
{
	assert(0);
	return false;
}

blurResultPipelineDesc.PS = CD3DX12_SHADER_BYTECODE(bloomResultPs.Get());
result = mDevice->CreateGraphicsPipelineState(&blurResultPipelineDesc, IID_PPV_ARGS(mBlurResultPipeline.ReleaseAndGetAddressOf()));
if (FAILED(result))
{
	assert(0);
	return false;
}

パイプラインステートを生成します。

float4 ps(Output input) : SV_TARGET
{
	float w, h, levels;
	texHighLum.GetDimensions(0, w, h, levels);

	float dx = 1.0f / w;
	float dy = 1.0f / h;

	float4 bloomAccum = float4(0, 0, 0, 0);
	float2 uvSize = float2(1, 0.5);
	float2 uvOffset = float2(0, 0);

	for (int i = 0; i < iteration; ++i)
	{
		bloomAccum += Get5x5GaussianBlur(texShrinkBlur, smp, input.uv * uvSize + uvOffset, dx, dy, float4(uvOffset, uvOffset + uvSize));
		uvOffset.y += uvSize.y;
		uvSize *= 0.5f;
	}

	float4 bloomColor = Get5x5GaussianBlur(texHighLum, smp, input.uv, dx, dy, float4(0, 0, 1, 1)) + saturate(bloomAccum);
	return bloomColor;
}

縮小回数分繰り返しながら、縮小されたテクスチャの位置に合わせてサンプリングできるようにします。

各段階でブラーを適用し、bloomAccumに値を加えます。

最後に元の高輝度テクスチャにもう一度ブラーを適用し、bloomAccumを加えます。

void Dx12Wrapper::DrawShrinkTextureForBlur()
{
  ...

  auto wsize = Application::Instance().GetWindowSize();

  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(mBlurResultPipeline.Get());
  mCmdList->SetGraphicsRootSignature(mScreenRootSignature.Get());

  mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
  mCmdList->IASetVertexBuffers(0, 1, &mPeraVertexBufferView);

  rtvBaseHandle = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
  rtvBaseHandle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) * 4;

  mCmdList->OMSetRenderTargets(1, &rtvBaseHandle, false, nullptr);

  srvHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();

  mCmdList->SetDescriptorHeaps(1, mPeraSRVHeap.GetAddressOf());
  mCmdList->SetGraphicsRootDescriptorTable(0, srvHandle);

  mCmdList->DrawInstanced(4, 1, 0, 0);

  barrier = CD3DX12_RESOURCE_BARRIER::Transition(mBloomResultTexture.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	  mCmdList->ResourceBarrier(1, &barrier);
}

次に、最後にブルーム描画を行います。

描画が完了すると、描画されたテクスチャはシェーダーで使用できるように状態が変更されます。

これで最後に全体のシーンを描画する際にこのブルームテクスチャを適用すれば完成です。

最終画面を出力するピクセルシェーダーを修正します。

同じルートシグネチャを使用しているので、既にブルームテクスチャを使用できます。

#include "HeaderScreenForward.hlsli"

float4 ps(Output input) : SV_TARGET
{
	float4 texColor = tex.Sample(smp, input.uv);
	float4 result = texColor;
	
	float4 bloomColor = texBloomResult.Sample(smp, input.uv);
	result += bloomColor;

	return result;
}

難しいことは何もありません。

ブルームテクスチャをサンプリングし、その値を最終的な色に加えるだけです。

ビルドして、PIXでまずブルームテクスチャを確認してみましょう。
image 6.png
image 7.png
シーンに適用された様子はこのようになります。

目がくらむほどのブルーム効果ですね。

うまく適用されたようです。

ミクさんの服を光らせるつもりはないので、シェーダーを再度修正して、高輝度部分に色が出力されないように修正しましょう。

まとめ

ブルームエフェクトを実装しました。

今回は結果を見るためだけに、高輝度のピクセルを出力してブルームを適用しましたが、

次の記事ではGPUインスタンシングと共に、もっとかっこいい画面になれるようにします。

参考リンク

https://learnopengl.com/Advanced-Lighting/Bloom
https://en.wikipedia.org/wiki/Rec._601

次回

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?