あるプロジェクトにおいて、広範囲で高頻度に適用される高斯ぼかし処理が、GPU使用率の急上昇とフレームレートの大幅な低下を引き起こし、深刻なパフォーマンス問題となりました。この問題を解決するため、2019年のSIGGRAPHで発表された高斯ぼかしの最適化アルゴリズムを導入しました。この最適化により、GPUの負荷を66%削減し、処理時間を1/3に短縮することができました。本稿では、まず従来の高斯ぼかしアルゴリズムについて説明します。
従来の高斯ぼかしアルゴリズム
対象となる画像の幅を W、高さを H、高斯ぼかしカーネルのサイズを S、半径を R(中心点を含まない、つまり S=2×R+1)とします。
高斯ぼかしの基本的な実装では、単一のピクセルをレンダリングするために、周囲の S × S 個のピクセルをサンプリングし、対応する高斯分布の重みを乗算する必要があります。その式は以下の通りです。
G(x,y)=12πσexp(−x2+y22σ2)
ここで x、y は、サンプリングされるピクセルが中心ピクセルからの横方向、縦方向の距離です。したがって、1フレームのレンダリングにかかる時間計算量は O(W×H×S²) となります。
しかし、厳密な数学的証明により、2次元の高斯ぼかしは、まず横方向に、次に縦方向に行う、あるいはその逆の2回の1次元ぼかしに分解できます。この2回のぼかしは、カーネルサイズと重みが同じで、以下の分布に従います。
G(x,y)=12πσexp(−x22σ2)
したがって、単一のピクセルをレンダリングするには、横方向の S 個と縦方向の S 個、合計 2S 個のピクセルをサンプリングするだけで済みます。これにより、1フレームのレンダリング時間計算量は O(W×H×S) に削減されます。
ただし、この方法では、最初の1次元ぼかしの結果を記録するための中間バッファ(テクスチャ)が必要となるため、O(W×H) の空間計算量の追加消費が発生します。
NonuniformBlurによる高速化
高斯ぼかしの結果は、カーネルサイズ S と σ の影響を受けます。どちらも大きいほど画像はぼやけますが、後者はパフォーマンスに影響を与えません。しかし、前者のサイズはプログラムのパフォーマンスに顕著な影響を与えます。そこで、高性能を目的とした高斯ぼかしの近似最適化手法を導入します。この手法は、『The Power of Box Filters: Real-time Approximation to Large Convolution Kernel by Box-filtered Image Pyramid』[1] で提案されています。以下に要約します:
高斯ぼかしのパフォーマンスの大部分は、周囲ピクセルのサンプリング頻度に起因します。頻度が高いほど消費が大きくなります。mipmapの各レベルのピクセルは、元のテクスチャの特定領域のピクセルの平均値です。したがって、この論文の著者は、このデータ構造を利用して高斯ぼかしを近似し、サンプリング頻度を低いレベルに抑えることを提案しています。
同時に、mipmapは現代のGPUで高速なハードウェア生成が可能です(コンピュートシェーダーによる生成も高速です)。そのため、大規模な高斯ぼかしをこの手法に置き換えることで、パフォーマンスの消費を大幅に削減できます。
あるピクセルの高斯ぼかし結果を p_Gaussian、サンプリングされるピクセル値を s(x,y)、mipmapのレベルを l(OpenGL ESのレベルとは逆の順序に注意)、mipmapの l レベルでの重みを w_G(l)、l レベルでのボックスフィルタリング結果を S_box(l) とすると、以下の式が成り立ちます。
pGaussian=∬−∞+∞G(x,y)s(x,y)dxdy=∬0+∞wG(l)Sbox(l)dl
ここで
wG(l)≈16lln44π2σ4exp(−4l2πσ2)
または
pGaussian=∑l=0lmaxwG(l)Sbox(l)∑l=0lmaxwG(l)
これらの式から、サンプリング頻度はもはや高斯カーネルのサイズに依存せず、画像の解像度のみに依存することがわかります。単一ピクセルのサンプリング頻度は ⌊log₂max(W,H)⌋+1 となり、各画像のぼかしにかかる時間計算量は O(W×H×(1+log₂max(W,H))) となります。
同時に、mipmapを生成するため、元の画像メモリの1/3の追加メモリが必要となります。
1920×720の画像に対してこの手法を適用し、σ を5とした場合、実行時に生成されたシェーダーは以下のようになります。
#version 320 es
precision mediump float;
in vec2 uv;
uniform sampler2D inputTexture;
out vec4 outputColor;
void main()
{
vec4 color = vec4(0.0);
// 各mipmapレベルの重み付きサンプリング
color += textureLod(inputTexture, uv, 11.0) * 0.000000;
color += textureLod(inputTexture, uv, 10.0) * 0.000000;
color += textureLod(inputTexture, uv, 9.0) * 0.000000;
color += textureLod(inputTexture, uv, 8.0) * 0.000000;
color += textureLod(inputTexture, uv, 7.0) * 0.000000;
color += textureLod(inputTexture, uv, 6.0) * 0.000000;
color += textureLod(inputTexture, uv, 5.0) * 0.089084;
color += textureLod(inputTexture, uv, 4.0) * 0.739691;
color += textureLod(inputTexture, uv, 3.0) * 0.156954;
color += textureLod(inputTexture, uv, 2.0) * 0.013316;
color += textureLod(inputTexture, uv, 1.0) * 0.000898;
color += textureLod(inputTexture, uv, 0.0) * 0.000057;
outputColor = vec4(color.rgb, 1.0);
}
このように、レンダーパスが元の2回から1回に減少し、単一ピクセルのサンプリング頻度も12回にまで削減されています。
NonuniformBlurのさらなる最適化
最適化されたシェーダーからわかるように、一部の重みが0に近い値です。したがって、これらの重みのサンプリングを除外することで、さらにパフォーマンスの消費を削減できます(0.0039未満を除外できます。これは8ビットサブピクセルの正規化後の1色階のサイズ、つまり1/256に相当し、重みが0.0039未満の場合、最終結果への影響は1色階未満となり、無視できます)。 そのため、最適化されたシェーダーは以下のようになります。
#version 320 es
precision mediump float;
in vec2 uv;
uniform sampler2D inputTexture;
out vec4 outputColor;
void main()
{
vec4 color = vec4(0.0);
// 有効な重みのみを使用してサンプリング
color += textureLod(inputTexture, uv, 5.0) * 0.089084;
color += textureLod(inputTexture, uv, 4.0) * 0.739691;
color += textureLod(inputTexture, uv, 3.0) * 0.156954;
color += textureLod(inputTexture, uv, 2.0) * 0.013316;
outputColor = vec4(color.rgb, 1.0);
}
このように、サンプリング頻度は4回にまで削減されています。
参考文献
[1] Xu T, Ren X, Wu E. The Power of Box Filters: Real-time Approximation to Large Convolution Kernel by Box-filtered Image Pyramid[M]//SIGGRAPH Asia 2019 Technical Briefs. 2019: 1-4.