Multi-Layer Alpha Blendingについて

順不同の半透明オブジェクトを描画する方法ひとつである、Multi-Layer Alpha Blendingを見てみたいと思います

参照
Multi-Layer Alpha Blending

正しくAlphaBlendingの計算を行うためには、ご存知のとおり、Z値の順番に沿って計算を実行していく必要があります。従来では、これを行うために、オブジェクト単位、プリミティブ単位のソートを行ってきました。
近年では、GPUによって、Fragment単位のソートを行う事により、順不同の半透明オブジェクトを正しく描画する方法が提案されています。しかし、これによって消費されるGPUの計算リソースと、メモリリソースは、描画する半透明オブジェクトの量によって大きく変動します。特に、メモリの使用量が変動するため、実際のアプリケーションで、簡単にこれを利用することは出来ませんでした。
本手法は、GPU上で順不同の半透明オブジェクトを描画する手法のひとつです。特に、メモリの使用量が固定できるのが大きな特長です。その代わり、AlphaBlendingのレンダリング結果は必ず正しいものになるとは限らず、使用するレイヤーの数と、描画順序の影響を受けます。この点を正しく理解して使う必要があります。

AlphaBlendingの式の一般化

まずは、AlphaBlendingの式の一般化を見てみます。参照論文のeq(4)の部分です。pre-multiplied alphaのカラー(A)と、透過率(t)と深度値(z)の式になります。この式は、AlphaBlendingの計算は、合成するFragmentの深度値の比較結果に応じて、2つの式に分かれることを示しています。深度値が小さいものに対して大きいものを合成する場合、また、その逆も正しく計算する方法があることを示しています。
しかし、この式によって、多数のFragmentを順不同に合成できるわけではありません。たとえば、1番目と2番目のFragmentを合成した後に、1番目と2番目の間の深度値を持つFragmentを正しく合成する術はありません。

この問題に対処するため、まず、1Pixel上で、合成するFragmentの数を、mとします。すると、AlphaBlendingを計算する式は、eq(5)のように書くことが出来ます。
この式を計算するためには、1Pixelあたりm個の半透明Fragment情報の配列を確保すればよいことになります。Fragmentを描画する際は、この配列に対して、挿入ソートを行えば、z値の手前からm個のFragmentを保存することが出来ます。
大きなmを設定すれば、レンダリングするシーン全体を正しく描画できるはずですが、これを行うと、GPUのメモリを多く消費することになります。

消費メモリの問題に対処するためには、配列の長さmは小さな値を使用する必要があります。しかし、小さなmを採用すれば、1Pixel上で多数のFragmentが重なると、Fragmentが配列から溢れることになります。
Fragmentが溢れた場合は、情報の喪失を最小限に留めるため、破棄するのではなく、隣接するFragmentを合成してマージします。

Fragmentのマージによるエラーの考察

Fragmentをマージすることにより、発生するエラーに関して考えます。エラーが発生する条件は、マージしたFragmentの中間の深度値に、新たなFragmentが描画された場合です。AlphaBlendingの式の一般化の項で説明したとおり、合成したFragmentの間のFragmentを後から合成する術はありません。これを配列に挿入すれば、本来のAlphaBlendingの結果と異なる結果になります。
論文中で、マージするFragmentに関して幾つかの方法が考察されていますが、空間的、時間的に、最も一貫した結果が得られるのが、視点から見て、最も透過率の低いFragmentを合成するものとされています。つまり、z値の最も大きな2つのFragmentを合成するということです。

実装

アルゴリズムは、参照論文のListing 1 を参照して下さい。pseudocodeが書かれています。バブルソートを行い、最後尾のFragmentをマージして格納するだけです。
問題は、GPU上のPixelShaderでこれを正しく実行する方法です。既存のGPUでは、AlphaBlend描画時は、PixelShaderの出力順序は、描画APIの呼び出し順序と一致するはずでが、PixelShader上で、メモリに対して、Read/Modify/Writeを行なった場合に、これがAPIの呼び出し順序と一致する保障はありません。加えて、Read/Modify/Writeを行う最中に、他のPixelShaderがバッファに対してアクセスしないようにするには、Atomicを用いる必要があります。しかし、ピクセル単位でAtomic処理を用いるのは、パフォーマンスの側面で好ましくありません。

GL_INTEL_fragment_shader_ordering拡張について

このOpenGLの機能拡張は、上記の処理を行うために、GLSLシェーダー内に、同一Pixelを処理するPixelShader同士のインターロックを設定する事が出来ます。これを用いることによって、Atomic処理を用いることなく、メモリに対して、read/modify/writeの処理を、描画APIの呼び出し順序を守って実行する事が可能となります。具体的には、シェーダーコード内で、下記の組み込み関数を呼び出すことで、この呼び出し以降の処理は、同一Pixelを対象とした、PixelShaderの処理はブロックされます。

void beginFragmentShaderOrderingINTEL();

endFragmentShaderOrderingINTEL()に相当する関数は今のところ存在しません。PixelShaderの処理の終了を以って、ブロックが解除されるようです。

考察

まず、この手法は、必ずしも正しいAlphaBlendingの結果をもたらすものではありません。ただし、エラーが発生する条件を理解すれば、誤差を最小限に留めつつ描画する方法が自然と分かってきます。
先ほども述べたとおり、誤差を発生させるには、マージしたFragmentの中間の深度のFragmentを描画する必要があります。加えて、マージされるFragmentは、z値が一番大きな2つのFragmentです。
もし、近景を先に描画して、近景で配列を充填した後に、遠景を描画すれば、自ずと遠景のFragment同士でマージされ、遠景のFragment同士をソートする機会が失われます。
もし、遠景を先に描画して、遠景で配列を充填した後に、近景を描画すれば、配列内でソートされた、遠景のFragmentから順にマージされ、近景のFragmentが配列に充填されることになります。
したがって、精密なソートは行わないとしても、なるべく遠景より描画すれば、この手法の誤差は少なく保つことが出来るはずです。
また、この手法では、深度値の比較を行って、処理を変更しているため、MSAAとの相性は良くないです。SubSample単位でこの処理を行えば実装可能ですが、メモリの使用量や、PixelShaderの負荷的に、相当のGPUリソースを必要とすると思われます。
最後に、これを実装するにあたって、GPUに、PixelShaderの同期機構が備わっていることが必要だと思います。Atomicを用いてこれを実装することも可能と思われますが、Atomic処理によって、ブロックされる処理は長く、パフォーマンスを考慮すると、あまりお勧めできません。