ガウスぼかしのパフォーマンス最適化

あるプロジェクトにおいて、広範囲で高頻度に適用される高斯ぼかし処理が、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) ここで xy は、サンプリングされるピクセルが中心ピクセルからの横方向、縦方向の距離です。したがって、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.

タグ: ガウスぼかし パフォーマンス最適化 GLSL mipmap GPU

8月2日 21:49 投稿