目的
DirectX(10以降)にて、デプスバッファ(深度バッファ/Zバッファ)の値を取得し、次の描画でその値を利用したいときに、どうやって元の座標値に戻すかを試行錯誤したので記録します。
「デプスバッファの値をビュー座標系に変換する」が今回のやることです。
想定
例として「半透明の雲の中に、リジッドな物体がある」ような場合を想定します。雲は半透明で向こう側が透けて見えるように描画しますが、その描画はレイマーチングなどを用います。手前から視線方向(レイ)にそって進みながら、雲の値を積分していきますが、リジッドな物体に当たったら進むのをやめることにします。この終了判定を、デプスバッファの値を使って行いたいわけです。
(以降、この節は読み飛ばしてもOK)
描画は2つのオブジェクトを二度に分けて行うものとします。フローとしては次の流れです。
- 一度目の描画:リジッドな物体をポリゴンとして普通に描画する。
- このとき、デプスバッファは読み込み可能なバッファとして作成し、ResourceViewも作っておく
- 一度目の描画で書き込まれたデプスバッファを取得し、二度目の描画における読み込み可能2次元テクスチャとしてピクセルシェーダに紐づける
- 二度目の描画におけるデプスバッファには別のリソースをセットしておく
- 二度目の描画:雲をレイマーチングで描画する。
- 頂点シェーダは画面全体を覆う数枚の板ポリゴンとし、ピクセルシェーダに必要最低限の情報を送る(例の出発点の座標、レイの方向ベクトル、後述の情報など)
- ピクセルシェーダ内でレイマーチングを行う。このときビュー座標系(カメラ座標系)で行うものとする。レイの開始点は板ポリゴンの位置。
- レイマーチングの終了位置として、一度目の描画で書き込まれたデプスバッファの値を参考に判定したい。
レイマーチングをビュー座標系で行うため、デプスバッファから読み込んだ値をビュー座標系に変換する必要があるというのが、今回のモチベーションです。
デプスバッファに書かれた値
デプスバッファに書きこまれている値は、ビュー座標系の位置ベクトルにさらに射影行列(projection matrix)を作用したものです。
ビュー座標系の位置ベクトル$(x_v,y_v,z_v,1)$は、元のオブジェクトの座標$(x,y,z,1)$に、ワールド行列$W$、ビュー行列(カメラ行列)$V$を作用することで得ていると思います。
$$
(x_v,y_v,z_v,1)=(x,y,z,1)WV
$$
このビュー座標に射影行列$P$を作用した
$$
(x_p,y_p,z_p,w_p)=(x_v,y_v,z_v,1) P
$$
がどのような値になっているかを見ます。
射影行列はレンダリングによって色々あり得ますが、一般的には、非ゼロの成分は次のようになるでしょう。
\begin{align}
P_{00} &= \tan^{-1}\frac{\theta_x}{2} \\
P_{11} &= \tan^{-1}\frac{\theta_y}{2} \\
P_{33} &= \frac{z_f}{z_f-z_n} \\
P_{34} &= 1 \\
P_{43} &= -\frac{z_f z_n}{z_f-z_n}
\end{align}
ただし、$\theta_x$、$\theta_y$はカメラの視野角です。$z_n$および$z_f$は、カメラに映る視体積の手前側の境界と奥側の境界です。
そこで、実際に射影行列$P$を作用した後の$z_p$の値がどうなっているかを見ると、
z_p = z_v P_{33} + P_{43} = \frac{z_f ( z_v - z_n)}{z_f-z_n}
となっています。
この$z_p$の値は、対象の物体が最もカメラに近い($z_v=z_n$)ときに$0$になり、対象の物体が最もカメラから遠い($z_v=z_f$)ときに$z_f$になります。しかし、デプスバッファの値は[0.0,1.0]の値にする必要があります。そのため、デプスバッファに格納される値$d$は$z_p$ではありません。規格化が必要です。
このとき、規格化として視体積の奥境界$z_f$で割る$d=z_p/z_f$という手もありますが、DirectXにおいて実際に行われているのはビュー座標値$z_v$で割る次のような規格化です
$$
d=\frac{z_p}{z_v}
$$
これでも、$d$の値は[0.0,1.0]の範囲にピッタリと収まり、規格化になっています。さらに、近い時ほど解像度が挙がる様になっています。
デプスバッファの値からビュー座標へ変換
さて、事前にレンダリングして得たデプスバッファを、次のレンダリング時に(先のフローの二度目のレンダリングにおいて)ピクセルシェーダでテクスチャとして読む場合を考えます。ここで得られるのは、規格化された$d$の値です。
//ピクセルシェーダのサンプル
float4 main(PS_INPUT input) : SV_TARGET
{
float2 uv = float2(input.pos.x / screen_w, input.pos.y / screen_h);
float d = texture.Sample(sampler, uv);
...
...続く
ここで、デプスバッファを読み込ませたtexture上のuv座標は、前回の記事に沿っています。
次に、読み込んだ$d$の値から、ビュー座標の$z_v$を得るにはどうするか。
方法1:式(8)、(9)を使って逆変換する。
すなわち、
$$
z_v = \frac{z_f z_n}{z_f - (z_f - z_n) * d}
$$
とします。ピクセルシェーダでこれを行うには、定数バッファ等で$z_f$、$z_n$の値を渡してやる必要があります。
//ピクセルシェーダ続き
...
float z_v = z_f * z_n / (z_f - (z_f - z_n) * d);
...
方法2:射影行列の値を使って変換する(頂点シェーダから情報を渡す)
射影行列の値を使って変換することもできます。実際に、
$$
z_v = -\frac{P_{43} }{P_{33} - d}
$$
です。ここで、射影行列は頂点シェーダで必ず使います。もし、頂点シェーダの出力として、ピクセルシェーダに渡すデータの中に、余裕があるならば、頂点毎に$P_{33}, P_{43}$の値を付随しておくることもできます。例えば次のようなコードです。
struct PS_INPUT
{
float4 pos : SV_POSITION;
float3 ray : RAY;
float4 info : Z_VIEW_RAY;
...
};
//頂点シェーダ//
PS_INPUT mainVS(VS_INPUT input)
{
PS_INPUT output;
float4 vpos = float4(input.pos, 1.0f);
vpos = mul(vpos, world);
vpos = mul(vpos, view);
output.pos = mul(vpos, projection);
output.ray = input.ray; //レイマーチングのレイ
output.info.x = projection._33; //P_{33}
output.info.y = projection._43; //P_{34}
output.info.z = mul(mul(float4(input.ray, 0.0f), world), view).z; //レイのビュー座標系でのz成分
output.info.w = vpos.z; //レイのビュー座標系での開始点
return output;
}
//ピクセルシェーダ
float4 mainPS(PS_INPUT) : SV_TARGET
{
float2 uv = float2(input.pos.x / screen_w, input.pos.y / screen_h);
float d = texture.Sample(sampler, uv);
float P33 = input.info.x;
float P43 = input.info.y;
float z_v = -P43 / (P33 - depth);
float t_depth = (z_v - input.info.w) / input.info.z; //レイの終了位置
//以降でレイマーチングを行って雲を描画。
//媒介変数t=[0,t_depth]の範囲で積分する。
...
}
ここでinfo
には$P_{33}, P_{34}$の他に、レイマーチングに必要な情報も渡しています。定数バッファで$z_f, z_n$を渡すのが面倒なときに便利かもしれません。
おわりに
今回の方法で、実際にリジッドな球ポリゴンで原子を描き、その周りに量子力学的に分布した電子の密度を雲としてレイマーチングで描いたものが次の図になります。雲は空間の半分だけ描くようにしてみましたが、雲の中に埋まっている原子も何となく透けて見えると思います。レイマーチングの終了点が原子の表面になっているからです。