Compute ShaderでSummed Area Table
Summed Area Table
コンピュートシェーダーに興味が出てきたので,練習がてらSummed Area Table作成してブラーをかけてみます。
https://www.slideshare.net/egorodet/cpu-is-in-focus-again-implementing-dof-on-cpu
Summed Area Tableとは,テクスチャのテクセルと表したときに,以下のようにX方向,Y方向に順番に和を取っていったテーブルのことです
Summed Area Tableを作成しておくと,テーブルから4点フェッチして重みを工夫してサンプルするだけで, 元のテクスチャの任意の矩形範囲内部のピクセルの積分が計算できます。
テーブルさえ作ってしまえば,どんな積分範囲であっても高速に計算ができるので, 被写界深度処理のような ピクセルによってブラー範囲(=積分範囲)を変えるというケースで特に有効かもしれません。
ただし,Summed Area Table自体の作成するときに, 全ピクセルに対して依存するピクセルの総和と計算しておく必要があるので,この計算コストが問題となります。
Summed Area Tableの作成
Summed Area Tableの作成には,Compute Shaderを利用しました。
実際のシェーダーコードとCPU側のソースは次のような感じです。
//// HLSL側のCompute Shader Texture2D<float4> g_SrcTexture : register(t0); SamplerState g_LinearSampler : register(s0); RWTexture2D<float4> g_DstTexture : register(u0); #define PIXEL_SIZE g_aTemp[0].xy groupshared float3 s_aTempBuffer[LENGTH]; [numthreads(LENGTH, 1, 1)] void CS_CreateTable(uint3 position : SV_DispatchThreadID) { const uint bufferId = position.x; #ifdef PASS_1ST // 水平パス const uint2 texturePos = position.xy; #endif #ifdef PASS_2ND // 垂直パス const uint2 texturePos = position.yx; #endif // テクスチャの読み込み s_aTempBuffer[bufferId] = SampleLod0(g_SrcTexture, g_LinearSampler, float2(texturePos)*PIXEL_SIZE).rgb; // イテレーション float3 addBuf; for (uint i = 0; i < ITERATION; ++i) { GroupMemoryBarrierWithGroupSync(); const uint delta = (0x1 << i); const float3 buffer = (bufferId >= delta) ? (s_aTempBuffer[bufferId - delta]) : (0.0f); GroupMemoryBarrierWithGroupSync(); s_aTempBuffer[bufferId] += buffer; } float4 ret = float4(0.0f, 0.0f, 0.0f, 1.0f); ret.rgb = s_aTempBuffer[bufferId]; g_DstTexture[texturePos] = ret; }
//// CPP側の擬似コード // 水平方向加算 auto pPass = rPfxEffect.GetPass("SummedAreaTableCreate", "L512_1ST").lock(); auto pTempBuffer = rPfxEffect.GetTempTexturePool().Get(dstSize, dstSize, tableFormat, bindFlags).GetTextureView(); pPass->CSBindConstantBuffer(0, tempCB); pPass->CSBindTexture(0, pSrcTextureView); pPass->CSBindSampler(0, rPfxEffect.GetLinearSampler()); pPass->CSBindRWTexture(0, pTempBuffer); result = rPfxEffect.Dispatch(pDeviceContext, pPass, 1, dstSize, 1); // 垂直方向加算 pPass = rPfxEffect.GetPass("SummedAreaTableCreate", "L512_2ND").lock(); auto pResultTextureView = rPfxEffect.GetTempTexturePool().Get(dstSize, dstSize, tableFormat, bindFlags).GetTextureView(); pPass->CSBindConstantBuffer(0, tempCB); pPass->CSBindTexture(0, pTempBuffer); pPass->CSBindSampler(0, rPfxEffect.GetLinearSampler()); pPass->CSBindRWTexture(0, pResultTextureView); result = rPfxEffect.Dispatch(pDeviceContext, pPass, 1, dstSize, 1);
このコードの要点をまとめると
という感じです。
何も考えずに加算をすると,テクスチャ幅]として,一つのピクセルあたり回のループを回すことになります。
これを,以下の図のように2べき飛ばしで再帰的に加算をしていくことで, 回まで減らすことが可能です。
この再帰計算を行うときに便利なのがgroupsharedで宣言されたグループ共有メモリです。
個のスレッドを走らせるグループを個作成し,それぞれのグループで個のfloat3を計算バッファとして確保しておきます。
最初の一回だけこのバッファに対してテクスチャ読み出しをおこなって,あとの再帰的な加算を行うときはバッファを利用することで,テクスチャ読み出しを必要最低限に抑えます。
Summed Area Tableの作成自体はVertex ShaderとPixel Shaderでも工夫をすれば可能ですが,再帰的に加算をする度にテクスチャ読み出しをするので, Compute Shaderほど高速には行なえません。 DirectX11ではグループ共有メモリは32KBまでしか利用できないので,float4で考えるとテクスチャサイズは2048が限界ということに注意が必要です。 また,加算時の誤差が激しいので,32bit浮動小数テクスチャを利用しています。
ブラー結果
512x512ピクセルのSummed Area Tableを作成すると,次のような感じになりました。
入力テクスチャ |
Summed Area Table |
---|
Summed Area Tableの右端の値は9000ぐらいで,普通に表示すると飽和して見えないので,適当にスケールしてあります。
このテーブルを利用して,ただ矩形状に平均値を計算するボックスフィルタを適用してみます。
だいぶカクカクとした結果になりました。 今度は矩形を3つ重ねて少し滑らかになるようなフィルタを適用してみます。
まずまずではないでしょうか。
このパス全体のGPU実行時間は,GTX970の環境で
- 512x512テクスチャのTable作成 : 0.14 ms
- 1280x720のテクスチャへのブラー書き出し : 0.34 ms
程度のGPU時間でした。なかなか高速だと思います。
Log Summed Area Table
おまけです。
Summed Area Tableを作成するときに,対数平均を取るようにして,ブラー時にexpで元に戻すようにしたら,誤差が減るのではないかと思ってやってみました。
Summed Area Table |
Log Summed Area Table |
---|
Summed Area Table |
Log Summed Area Table |
---|
少しブラーの角が取れたような気もします。