DirectXの話 第103回
LUV色空間を用いたLight Pre-Pass Renderer
以前やった Light Pre-Pass Renderer ですが、どうも理解不足でした。
書いていること自体はだいたい正しいようなのですが、作成したサンプルが全く駄目です。
Light Pre-Pass Renderer は Z Pre-Pass Renderer と同様にモデル1つを最低2回描画します。
1回目は法線と深度値を G-Buffer に書き込むパスで、これは Z Pre-Pass Renderer と同様です。
2回目はライトバッファを元にマテリアル計算するパスで、これも Z Pre-Pass Renderer と同じと言って良いでしょう。
違いは、Z Pre-Pass Renderer はマテリアル計算を通常の Forward Renderer と同じように行うのに対し、Light Pre-Pass Renderer はライトの成分だけをライトバッファから持ってくるという点です。
また、Deferred Shading との違いは、マテリアル計算をスクリーン空間で行うかモデル描画の際に行うかです。
Deferred Shading の場合、モデルの描画は最低1回で済みます。この1回の時にできる限りの情報をMRTを使って G-Buffer に描画します。
最終パスでは通常、これらの情報のみを用いて描画を行います。このとき、モデルの描画は行いません。
つまり、モデル描画においては Light Pre-Pass Renderer は Deferred Shading より不利になります。
これについては Wolfgang氏も認めていますが、Z Pre-Pass Renderer も同じだからいいよね、ってことらしい。
しかし、Deferred Shading はある程度マテリアルの種類を限られてしまいます。G-Buffer に保存できる情報にも限界はあります。
マテリアルIDを G-Buffer に保存する手段もありますが、モデル単位でパラメータが必要な場合はやっぱり G-Buffer に保存しておかなければなりません。
Light Pre-Pass Renderer の場合、この問題は2回目のパス(つまり最終のマテリアル描画)で必要なパラメータを設定すればいいのでこのような問題は解決できます。
ただし、Forward Renderer に比べるとやはりマテリアルの数は制限されます。特にディフューズ関係はライトバッファパスで計算されてしまうので1種類くらいしか使えません。
多分あり得ないと思いますが、Lambert と Half Lambert と Toon Shader をすべて同時に使いたいといった場合はその分の G-Buffer を用意しなければなりません。
また、Deferred Shading も Light Pre-Pass Renderer も半透明のオブジェクトは取り扱えないのはわかりきっています。
しかし、Light Pre-Pass Renderer はライトバッファ以外はほぼ Z Pre-Pass Renderer と同じなので、Forward Renderer で実装した半透明と一緒に使用することは問題なくできます。
たとえば不透明→半透明→不透明→半透明と描画しなければならない場合でも問題はないわけです。
ただ、Forward Renderer の問題点を解消するために Light Pre-Pass Renderer を実装したのに結局 Forward Renderer が必要ってのもどうかと思いますがね。
特に日本のゲームは半透明を多用し、しかもその質にこだわる人が多いので Deferred Shading や Deferred Lighting は実装する会社が少なそうですね。
ここから、前回のサンプルの問題が見えてきます。
前回のサンプルは Deferred Shading をやっているのです。Light Pre-Pass Renderer のサンプルとしては大きく間違っています。
一応消しはしないのですが、あまり参考にされないことをオススメします。
で、本題。今回はLUV色空間を用いて Light Pre-Pass Renderer をやってみます。
これは『ShaderX 7』に掲載されている記事で、TorqueX等で有名な Garage Games の Pat Wilson氏によって書かれています。
LUV色空間は輝度を基本とする色空間の一種で、LogLUVというHDRテクスチャフォーマットなんかが有名ですね。
ライトバッファをこのLUV色空間で実装することにより、32ビットカラーフォーマットのバッファでもHDRが(やろうと思えば)可能です。
ただ、ちょっと面倒なことが多い技術でもあるので、実用に耐えられるかどうかは甚だ疑問です。
まず、RGB色空間からLUV色空間に変換する計算式は以下のようになります。
ここで [M] はRGBをXYZに変換する行列で、以下のものを用います。
最終的に保存される値は Ue、Ve、L で、それぞれライトバッファのRGBに保存されます。
RGBに戻す時はこの逆を計算すればよいのですが、『ShaderX 7』の記事の元に戻す計算は多分間違っています。計算し直しても書かれてるようにはならないし、実際にあの計算式で描画するとおかしな色になります。
さて、ここで問題となるのはライトバッファはアキュムレーションバッファであるという点です。
つまり複数のライトの結果を累計加算していかなければならないのですが、LUV色空間は単純に足していけばいいのでしょうか?
答えは残念ながら NO です。以下の計算式によって求めることが可能です。
final が結果、dest がライトバッファに書き込まれている値、src がこれから加算する値です。L については通常通り加算で問題ありません。
当然、このような特殊な計算はアルファブレンディングでは行えません。この計算はピクセルシェーダで行う必要があり、それがこの技術の最大の問題点です。
ライトボリュームを描画する場合、以下のような順番で描画します。
1.ライトボリューム裏面描画
コピー用バッファに現在のライトバッファをコピーする
2.ライトボリューム表面描画
カラー描画なし。ステンシルへの書き込みのみ
3.ライトボリューム裏面描画
ライトバッファに加算したライトカラー(LUV色空間)を書き込む
dest にはコピー用バッファにコピーした値を用いる
見ての通り、コピー用のバッファが必要なのと、そのバッファにコピーするためのパスが必要になります。
ここで、テクスチャ2枚を交互にレンダリングターゲットにすればいいと考える人もいるかと思います(自分がまさにそれ)が、それではうまくいきません。
なぜなら、ライトボリュームの描画は範囲が限定されているため、範囲外の値は次のレンダリングターゲットにコピーされないのです。
ライトボリューム描画ではなく全画面描画にすればこの問題は解決できますが、ボリュームサイズによる部分があるとはいえ、そちらの方が多分重いです。
LUV色空間は32ビットカラーでも実現は可能なのですが、L の値に16ビットを使用します。
もしもライトバッファを32ビットカラーにしてしまうと N・H の項目を保存することができなくなってしまいます。
結局、N・H を使用するにはMRTを用いるか浮動小数点バッファを使用せざるをえないわけで、MRTが使えるなら浮動小数点バッファは使えるだろうし、浮動小数点バッファを使うならLUV色空間に置き換える理由もないし…という問題が発生します。
こう書いてしまうと…この技術って使えないんじゃないかと思うわけですが、どうなんでしょうね? 個人的には使わないですね。
でも、HDRテクスチャをLUVで実装するのは悪くないと思います。多分。
ソースの解説は行いませんが、やっていることは上記の技術をそのまま実装しているだけです。
サンプルは添付ファイルを参照してください。
いつもどおりにマウスでカメラの操作、Q/WでPSSMのOFF/ONを行います。
ドワーフのモデルはDirectXサンプルから拝借しています。今回のサンプル専用の独自フォーマットに変換している理由は趣味以外の理由がありません。
まあ、ちょっとしたオープンソースライブラリを使ってみたかったというだけです。なかなか良かったのでこれからも使っていこうと思っています。
ドワーフのマテリアルはテクスチャの名前によっていくつかのマテリアルを適用しています。Light Pre-Pass Renderer はこうやるらしいぞ、と言うサンプルになれば幸いです。
次回もやっぱり Deferred Rendering 関係をやる予定です。