Direct3D 10 ShaderModel4.0 ピクセル単位の半透明ソートを行う

追記 2007/11/02: Direct3D 10 ShaderModel4.0 Stencil Routed A-Buffer とお詫び も見てください。

半透明描画時に、ピクセル単位でソートを行うシェーダーを考案しました。

wheelhandle_ss10 per pixel alpha sorting

とりあえずデモプログラムをご覧ください。(ソース付)
実行には DirectX10 環境 (Vista+GPU) と
DirectX SDK November 2007 の Runtime が必要です。

wheelhandle_ss10t.zip

[SPACE]   pause 一時停止
[3]       3 layer mode
[6]       6 layer mode
[S]       sort ありなし切り替え
[U]       + teapot 追加
[D]       - teapot 削除

ピクセル単位でソートしているので、順番が正しく保たれるだけでなく
半透明ポリゴンが交差しても矛盾しません。

こちらがソートありで

wheelhandle_ss10 per pixel alpha blending with sort

ソートなしにするとこうなります。

wheelhandle_ss10 per pixel alpha blending without sort

ソートしないと、teapot のオブジェクト間だけでなく、自分自身の取っ手や
ふたの重なりにも矛盾が生じていることがわかるかと思います。

●特徴

この手法の特徴は本当にソートしてブレンドしていることと、GPU だけで処理が
完結していることです。CPU は何もしていません。

またシーンのレンダリングは一回で済み、レイヤー単位でレンダリングしなおす
必要はありません。

●欠点

ピクセル単位で最大 layer 数(重なり回数)制限があります。
2種類のシェーダーを作成しています。それぞれ pixel 単位で 3枚までのものと、
6枚までです。

MultiRenderTarget かつ 128bit pixel を使っているため、それなりにメモリと
帯域を消費します。速度についてはまだ未評価です。

今の方法では格納できる値に制限があるため、ソート時に参照する Z の精度に
限界があります。14~16bit なので、交差面の境界が荒くなる可能性があります。

●ソートが必要な理由

Direct3D 10 では GeometryShader の導入によって、より自由度が高まりました。
ポリゴンの動的な追加削除が可能で、また StreamOutput を使ってジオメトリ
の更新も GPU だけでできるようになっています。

反面ジオメトリの決定や描画において CPU を介さないということは、CPU に
よる半透明ソートができなくなることも意味しています。

これを解決する手段として、ソートが不要な加算半透明だけ用いる方法があります。

また Direct3D では、MultiSample + AlphaToCoverage を使う方法が有望と
されています。これはアルファブレンドの代わりに高密度の点描を行い平均化
するものです。

今回のデモ ss10 のアルゴリズムは全く異なるアプローチで、本当にピクセルを
ソートしてしまいました。

● 3 layer シェーダー

・8bit のフルカラー画素値を用いることができます。
・半透明描画の重なりはピクセルごとに 3枚までです。
・透明度(Alpha)は各ピクセル単位で独立した値を持つことができます。
・3枚を超える重なりは、最後に描画された 3pixel が用いられます。Z 値を
 考慮した選択ではないので、重なりが多いと不定の色に見えることがあります。
・MRT が 2枚なので 6 layer より効率はよくなっています。
・ソート時の Z 精度が 16bit あるためで 6 layer より実用的です。

● 6 layer シェーダー

・R G B A 各チャンネルは 7bit カラーに制限されます。
・その代わり半透明描画の重なりはピクセルごとに 6枚まで対応可能です。
・透明度(Alpha)は各ピクセル単位で独立した値を持ちます。
・6枚を超える重なりは、最初に描画した 3pixel と最後に描画された 3pixel
 が用いられます。6枚を超えると 3layer 同様に不定の色に見えることがあります。
・MRT が 4枚必要で、それなりにメモリを帯域を消費します。
・ソート時の Z 値が 14bit なので、交差したポリゴンの境界などの精度が
 3layer に劣ります。

●原理とアイデア

MRT (MultiRenderTarget) と Blend 機能だけを使い、レンダリングした
ピクセルの蓄積を行っていることが最大の特徴です。

レンダリング時に、直前の結果を即時反映させることができるのは
RenderTarget か DepthStencil しかありません。これらの機能の組み合わせ
だけで、描画したピクセルの選択と判定、蓄積のためのエンコードを実現する
必要があります。いくら Shader に自由度があっても、同じパスで出力を
受け取ることができないからです。
なお今回は DepthStencil は使用していません。

Direct3D 10.0 (ShaderModel4.0) では、128bit 浮動少数フォーマット
R32G32B32A32_FLOAT による Blend が可能です。これを利用しています。

RTc= RTc + Pc * RTa
RTa= RTa * Pa

RTc = RenderTarget color
RTa = RenderTarget alpha
Pc = Input Pixel color
Pa = Input Pixel alpha (U:2^8, D:2^-8)

下記の表は、CPU で Pixel 値のシミュレーションを行ったものです。
0x10 から順番に 0x11, 0x12 ~ とカラー値を同じピクセルに重ねていきます。
pixel ~ が書き込まれたフレームバッファの画素値、decode ~ がデコード
して取り出したカラーを表しています。

step 0
pixel U: hex=41800000 float=16
pixel D: hex=41800000 float=16
decode U:  10
decode D:  10

step 1
pixel U: hex=45888000 float=4368
pixel D: hex=41808800 float=16.0664
decode U:  11 10
decode D:  11 10

step 2
pixel U: hex=49908880 float=1.18402e+006
pixel D: hex=41808890 float=16.0667
decode U:  12 11 10
decode D:  12 11 10

step 3
pixel U: hex=4d989088 float=3.19951e+008
pixel D: hex=41808891 float=16.0667
decode U:  13 12 11 00
decode D:  20 12 11 10

step 4
pixel U: hex=51a09891 float=8.62193e+010
pixel D: hex=41808891 float=16.0667
decode U:  14 13 12 20 00
decode D:  00 20 12 11 10

step 5
pixel U: hex=55a8a099 float=2.3176e+013
pixel D: hex=41808891 float=16.0667
decode U:  15 14 13 20 00 00
decode D:  00 00 20 12 11 10

step 6
pixel U: hex=59b0a8a1 float=6.21563e+015
pixel D: hex=41808891 float=16.0667
decode U:  16 15 14 20 00 00 00
decode D:  00 00 00 20 12 11 10

step 7
pixel U: hex=5db8b0a9 float=1.66354e+018
pixel D: hex=41808891 float=16.0667
decode U:  17 16 15 20 00 00 00 00
decode D:  00 00 00 00 20 12 11 10

デコード結果を見ると、Blend 機能を使った積和演算のみでも、最初の 3値と
最後の 3値が正しく保持されていることがわかります。

アルゴリズム U と D のどちらかを使えば 3 layer が実現可能で、両方組み
合わせることで 6layer が実現できます。
このように、制限があるのは演算アルゴリズム上の問題なので、MRT 数を
増やしたとしても単純にレイヤー数が増えるわけではありません。

画素の割り当て下記のとおり。

Algorithm-U / MRT0,1

MRT-0      X    Y    Z   W
-----------------------------
Pixel0-2   R    G    B  ExpU
-----------------------------

MRT-1      X    Y    Z   W
-----------------------------
Pixel0-2   ZH   ZL   A  ExpU
-----------------------------

Algorithm-D / MRT2,3

MRT-2      X    Y    Z   W
-----------------------------
Pixel3-5   R    G    B  ExpD
-----------------------------

MRT-3      X    Y    Z   W
-----------------------------
Pixel3-5   ZH   ZL   A  ExpD
-----------------------------

●浮動少数演算の丸め問題

Blend の演算機能と浮動小数値の特性を使って、ピクセルの蓄積と適切な
値の選択を行っています。このとき非常に厄介な問題が、浮動少数演算時の
丸め処理です。

入力 3 値まではこの問題が発生しないため、当初実現できたのは 3 layer
のみでした。3 値を越えると桁あふれによって追い出された bit が丸め込まれ、
上位 bit に影響を与える可能性があります。

例えば

Algorithm-U: C B A

と入力された段階で D を入力すると A が切り捨てられます。このとき A の
値の重さによって上位 D C B に影響が出ます。例えばすべて A B C D が
すべて 0xff だった場合、あふれた 0xff の繰り上がりの 1 によって
上位すべて 0 になってしまいます。影響は無視できません。

この問題を回避するために、失われたピクセル値が何であったのか推測する
必要があります。もし丸め込まれていたら decode 時に減算すればいいわけです。

捨てられたピクセルは情報として保持されていませんが、同時に Algorithm-D
を用いることによって、6 値までなら失われた値を相互に補完することが
できそうです。

step3
Algorithm-U: C B A
Algorithm-D: C B A

step4
Algorithm-U: D C B
Algorithm-D: C B A

step5
Algorithm-U: E D C
Algorithm-D: C B A

step6
Algorithm-U: F E D
Algorithm-D: C B A

この場合、step5 なら U の捨てられた値は D の B で参照でき、step6 なら
D の C で参照することができます。

同じように Algorithm-D でも入力値を捨てたことによるまるめこみが入ります
が、Algorithm-U から求めることができます。

一見うまくいきそうですが、双方同時にまるめこみが発生した場合に残念ながら
適切な値をとることができませんでした。おそらく実現できるのは 5 layer
までと考えられます。U と D で反転した値を入力しておくことで、同値の
ずれを割り出すことができます。

この問題を解決ができなかったので、6 layer シェーダーでは値を 7bit に
小さくすることで、とりあえず桁上がりが発生しないようにしています。
8bit フルに格納できなかったのが残念です。

●今後に向けて

Direct3D 10.1 / ShaderModel4.1 では、Blend 機能が大幅に拡張されます。
特に MRT 単位で独立した Blend パラメータを設定でき、また UNORM/SNORM
フォーマットでの Blend も可能となります。
これにより、今よりも実装も実現もずっと楽になると考えられます。
layer を増やせるかもしれません。

とりあえず実現が目標だったので・・速度最適化などはほとんど行っていません。
特に並べ替えの実装はあまりに手抜きなので、まだまだ改善の余地は多く
残されていると思います。

シミュレーションのあと実際にシェーダーとして実装したところ、予想と異なる
挙動がありました。CPU と GPU による浮動少数演算の違いだと考えられます。
ss10 ではごまかしが入っているので・・改善しないと。

layer オーバー時に、Stencil を使って最前面の pixel だけ選択できないか
も考えていました。Stencil Test の結果と Depth Test の結果の組み合わせ
によって Pixel を捨てるかどうか自由に決められればできそうです。
現状は Stencil Test の結果だけで決まり、Depth Test との組み合わせは
Stencil の更新方法の選択のみなのでうまくいきませんでした。