追記 2007/11/02: Direct3D 10 ShaderModel4.0 Stencil Routed A-Buffer とお詫び も見てください。
半透明描画時に、ピクセル単位でソートを行うシェーダーを考案しました。
とりあえずデモプログラムをご覧ください。(ソース付)
実行には DirectX10 環境 (Vista+GPU) と
DirectX SDK November 2007 の Runtime が必要です。
[SPACE] pause 一時停止 [3] 3 layer mode [6] 6 layer mode [S] sort ありなし切り替え [U] + teapot 追加 [D] - teapot 削除
ピクセル単位でソートしているので、順番が正しく保たれるだけでなく
半透明ポリゴンが交差しても矛盾しません。
こちらがソートありで
ソートなしにするとこうなります。
ソートしないと、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 の更新方法の選択のみなのでうまくいきませんでした。