こんるる~、Pocolです。
また,前回は深度について書きましたが,今回はスクリーンスペース系のテクニックなどで有用な位置座標の復元について忘れないようにメモしておこうと思います。
D3DXの行列は行優先(row-major)形式なので,列優先形式として表すと
\begin{eqnarray}
M_{RH} = \begin{bmatrix} s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_f}{z_n – z_f} & \frac{z_n z_f}{z_n – z_f} \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}
と書けます。
ビュー空間の位置座標を\((v_x, v_y, v_z, 1)^{\mathrm T}\)として,透視投影を行い,透視投影後の位置座標を\((p_x, p_y, p_z, p_w)^{\mathrm T}\)とすると,次のように書くことができます。
\begin{eqnarray}
\begin{bmatrix} p_x \\ p_y \\ p_z \\ p_w \end{bmatrix} &=& \begin{bmatrix} s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_f}{z_n – z_f} & \frac{z_n z_f}{z_n – z_f} \\
0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} v_x \\ v_y \\ v_z \\ 1 \end{bmatrix} \\
&=& \begin{bmatrix} s_x v_x \\ s_y v_y \\ v_z \left( \frac{z_f}{z_n – z_f} \right) + \frac{z_n z_f}{z_n – z_f} \\ -v_z \end{bmatrix}
\end{eqnarray}
ここで,\(p_x\),\(p_y\),\(p_z\)を\(p_w\)で除算し,正規化デバイス座標系での位置座標\(P_x\), \(P_y\), \(P_z\)を計算すると
\begin{eqnarray}
P_x &=& \frac{p_x}{p_w} &=& -\frac{s_x v_x}{v_z} \tag{1} \\
P_y &=& \frac{p_y}{p_w} &=& -\frac{s_y v_y}{v_z} \tag{2} \\
P_z &=& \frac{p_z}{p_w} &=& -\frac{z_f}{z_n – z_f} \left( 1 \, + \, \frac{z_n}{v_z} \right) \tag{3}
\end{eqnarray}
となります。
今,\(v_x\),\(v_y\)を復元する方法を考えたいので\(P_x\)と\(P_y\)についての式を\(v_x\),\(v_y\)について解きます。
\begin{eqnarray}
v_x &=& -\frac{v_z}{s_x} P_x \\
v_y &=& -\frac{v_z}{s_y} P_y
\end{eqnarray}
DirectXの正規化デバイス座標系は\(x\)成分と\(y\)成分が\([-1, 1]\)で,\(z\)成分が\([0, 1]\)であるのでテクスチャ座標\(s\),\(t\)とすると,\(P_x\)と\(P_y\)との関係は次のように書くことができます。
\begin{eqnarray}
s &=& 0.5 P_x + 0.5 \tag{4} \\
t &=& -0.5 P_y + 0.5 \tag{5}
\end{eqnarray}
DirectXのテクスチャ座標を求めるために上下反転させていることに注意してください。
この式を\(P_x\),\(P_y\)について書くと,
\begin{eqnarray}
P_x &=& 2s – 1 \\
P_y &=& -2t + 1
\end{eqnarray}
となります。よって,\(v_x\)と\(v_y\)をこの式を使って書き直すと次のようになります。
\begin{eqnarray}
v_x &=& -v_z \frac{1}{s_x} (2s – 1) \\
v_y &=& -v_z \frac{1}{s_y} (-2t + 1)
\end{eqnarray}
ここで,前回の記事で導出した深度バッファから\(v_z\)を求める式は
\begin{eqnarray}
v_z = -\frac{z_f z_n}{d(z_n – z_f) + z_f}
\end{eqnarray}
であったので,深度バッファの値\(d\)と,ニアクリップ平面までの距離\(z_n\),ファークリップ平面までの距離\(z_f\),\(s_y = \cot(\frac{fovY}{2})\),\(s_x = s_y / aspect\),そしてテクスチャ座標\(s\)と\(t\)があればビュー空間の位置座標が復元することができることになります。
定数バッファとして渡せるようにパラメータをfloat4にパックします。成分の内訳は
\begin{eqnarray}
{\rm param} = \left(\frac{1}{s_x}, \frac{1}{s_y}, z_n, z_f \right)
\end{eqnarray}
とします。
\(s_x\)と\(s_y\)はもともと射影行列の要素なので,射影行列があれば簡単に計算することができます。以上を踏まえてコードに落とし込むと
param.x = 1.0f / projection._11; // matrix[0][0]成分. param.y = 1.0f / projection._22; // matrix[1][1]成分. param.z = nearClip; // ニアクリップ平面までの距離. param.w = farClip; // ファークリップ平面までの距離.
シェーダ側ではparamを使って次の復元処理を実装すればよいです。
// 深度バッファの値からビュー空間深度値を求めます. float ToViewDepth(float hardwareDepth, float nearClip, float farClip) { return -nearClip * farClip / (hardwareDepth * (farClip - nearClip) + farClip); } // テクスチャ座標と深度値からビュー空間位置座標を求めます。 float3 ToViewPos(float2 st, float hardwareDepth, float4 param) { float z = ToViewDepth(hardwareDepth, param.z, param.w); float2 p = st * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); return float3(-z * param.x * p.x, -z * param.y * p.y, z); }
そんなわけでビュー空間深度さえ求めれば,単純な計算だけでビュー空間が復元できます。
ビュー空間から射影空間に戻す場合は,式(1), 式(2),式(3)を使って求められるので…
// ビュー空間から射影空間に変換します. float3 ToProjPos(float3 viewPos, float4 param) { return float3( -viewPos.x / (viewPos.z * param.x), -viewPos.y / (viewPos.z * param.y), -(param.w / (param.z - param.w)) * (1.0f + (param.z / viewPos.z) ); }
となります。
ビュー空間から,テクスチャ座標を求めたい場合は,さらに式(4),式(5)を使えばよいので
// ビュー空間からテクスチャ座標に変換します. float2 ToTexCoord(float3 viewPos, float4 param) { float2 p; p.x = -viewPos.x / (viewPos.z * param.x); p.y = -viewPos.y / (viewPos.z * param.y); return p * float2(0.5f, -0.5f) + float2(0.5f, 0.5f); }
で求められるはずです。
もし、書き間違いとか「そもそも計算あってないよ!」という箇所あれば遠慮なく指摘してください (…というのも若干自信無いからです)。