いつも忘れるので,忘れないようにメモをしておこうと思います。
Reverse-Z
DirectXの右手座標系の透視投影行列は[Microsoft 2018 a]で次のように記載されています。
xScale 0 0 0 0 yScale 0 0 0 0 zf/(zn-zf) -1 0 0 zn*zf/(zn-zf) 0 where: yScale = cot(fovY/2) xScale = yScale / aspect ratio
D3DXの行列は行優先(row-major)形式なので,列優先形式として表すと…
\begin{eqnarray}
M_{perspectiveRH} = \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}
となります。ただし,\(s_y = \cot(\frac{fovY}{2})\),\(s_x = \frac{s_y}{aspect}\),\(z_n\)はニアクリップ平面までの距離,\(z_f\)はファークリップ平面までの距離,\(fovY\)は垂直画角で,\(aspect\)は画面のアスペクト比とします。
今,\(z\)の範囲を\([0, 1] \rightarrow [1, 0]\)に変換したいので,\(z\)に対して-1倍して1を足せば範囲が変換できることになります。この変換を行うための行列を左からかけてReverse-Z形式にします。
\begin{eqnarray}
M_{reverse} M_{perspectiveRH} &=& \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & -1 & 1 \\
0 & 0 & 0 & 1 \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}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & \frac{z_n}{z_f – z_n} & \frac{z_f z_n}{z_f – z_n} \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}
[Reed 2015]に記載されていますが,ファーを無限遠にするとちょっとだけ結果が良くなるケースがあるので,\(z_f \rightarrow \infty\)の極限をとります。\( z_n /(z_f – z_n)\)は分母が\(\infty\)になるのでゼロに近づくことと,\(z_f z_n / (z_f – z_n)\)は\(\infty/\infty\)の不定形になるので,分子・分母を\(z_f\)で割って極限をとると,次のように整理できます。
\begin{eqnarray}
M_{reverseInf} = \lim_{z_f \rightarrow \infty} \left( M_{reverse} M_{pervespectiveRH} \right) = \begin{bmatrix}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & 0 & z_n \\
0 & 0 & -1 & 0 \end{bmatrix}
\end{eqnarray}
この行列を使ってベクトルを変換します。
\begin{eqnarray}
p = \begin{bmatrix}
s_x & 0 & 0 & 0 \\
0 & s_y & 0 & 0 \\
0 & 0 & 0 & z_n \\
0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} v_x \\ v_y \\ v_z \\ 1 \end{bmatrix}
\end{eqnarray}
興味があるのは,深度だけなので射影変換後のz値(\(p_z\))とw値(\(p_w\))を求めます。
\begin{eqnarray}
p_z &=& z_n \\
p_w &=& -v_z
\end{eqnarray}
z値をw値で割ったのが深度バッファに出力されるデプス\(d\)になるので,
\begin{eqnarray}
d &=& \frac{p_z}{p_w} \\
&=& \, – \frac{z_n}{v_z}
\end{eqnarray}
よって,深度バッファの値\(d\)からビュー空間の深度\(v_z\)を逆算出する場合は,
\begin{eqnarray}
v_z = -\frac{z_n}{d}
\end{eqnarray}
で求めればよいことになります。
コードに落とし込むと…
// farClip = ∞とするリバースZ深度バッファの値からビュー空間深度値を求めます. float ToViewDepthFromReverseZInfinity(float hardwareDepth, float nearClip) { return -nearClip / hardwareDepth; }
となります。
ちなみに,\(z_f\)の\(\infty\)を取らない場合のビュー空間深度は
\begin{eqnarray}
v_z = -\frac{z_f z_n}{d(z_f – z_n) + z_n}
\end{eqnarray}
で求められます。よって,リバースZを用いた場合の線形深度値は
\begin{eqnarray}
d_{linear} = -\frac{z_n}{d(z_f – z_n) + z_n}
\end{eqnarray}
となります。
コードに落とし込むと
// リバースZ深度バッファの値からビュー空間深度値を求めます。 float ToViewDepthFromReverseZ(float hardwareDepth, float nearClip, float farClip) { return -farClip * nearClip / (hardwareDepth * (farClip - nearClip) + nearClip); } // リバースZ深度バッファの値から[0, 1]の線形深度値を求めます。 float ToLinearDepthFromReverseZ(float hardwareDepth, float nearClip, float farClip) { return -nearClip / (hardwareDepth * (farClip - nearClip) + nearClip); }
となります。
線形深度
ついでに標準の射影行列(D3DXMatrixPerspectiveFovRH)を使った場合の線形深度の算出方法についてもメモしておきます。
まず,先ほどと同様に\(p_z\)と\(p_w\)を求めます。
\begin{eqnarray}
p_z &=& v_z (\frac{z_f}{z_n – z_f}) + \frac{z_f z_n}{z_n – z_f} \\
p_w &=& – v_z
\end{eqnarray}
除算して深度を求めます。
\begin{eqnarray}
d &=& \frac{p_z}{p_w} \\
&=& – \frac{v_z z_f}{v_z(z_n – z_f)} – \frac{z_f z_n}{v_z(z_n – z_f)} \\
\end{eqnarray}
上記の式を\(v_z\)について解き,ビュー空間深度を求めます。
\begin{eqnarray}
d(v_z(z_n – z_f)) &=& -v_z z_f – z_f z_n \\
d v_z z_n – d v_z z_f + v_z z_f &=& – z_f z_n \\
v_z(d(z_n – z_f) + z_f) &=& -z_f z_n \\
v_z &=& -\frac{z_f z_n}{d(z_n – z_f) + z_f}
\end{eqnarray}
線形深度は,ビュー空間深度をファークリップまでの距離で割り\([0, 1]\)の範囲内におさめたものになるので,
\begin{eqnarray}
d_{linear} &=& \frac{v_z}{z_f} \\
&=& -\frac{z_n}{d(z_n – z_f) + z_f}
\end{eqnarray}
となります。
コードに落とし込むと…
// 深度バッファの値からビュー空間深度値を求めます. float ToViewDepth(float hardwareDepth, float nearClip, float farClip) { return -nearClip * farClip / (hardwareDepth * (nearClip - farClip) + farClip); } // 深度バッファの値から[0, 1]の線形深度値を求めます. float ToLinearDepth(float hardwareDepth, float nearClip, float farClip) { return -nearClip / (hardwareDepth * (nearClip - farClip) + farClip); }
となります。
正射影
正射影についてもリバースZを一応求めてみます。
D3DXMatrixOrthoOffCenterRH行列は[Microsoft 2018 b]より
2/(r-l) 0 0 0 0 2/(t-b) 0 0 0 0 1/(zn-zf) 0 (l+r)/(l-r) (t+b)/(b-t) zn/(zn-zf) 1
と記載されています。この行列を列優先形式で書くと次のようになります。
\begin{eqnarray}
M_{orthoRH} = \begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & s_z & t_z \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{eqnarray}
ただし,\(s_x = \frac{2}{r – l}\),\(s_y = \frac{2}{t – b}\),\(s_z = \frac{1}{z_n – z_f}\),\(t_x = \frac{l + r}{l – r}\),\(t_y = \frac{t+b}{b-t}\),\(t_z = \frac{z_n}{z_n – _zf}\)とします。
この行列に対してリバースZ形式にするために左から行列を掛けます。
\begin{eqnarray}
M_{reverse}M_{orthoRH} &=& \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & -1 & 1 \\
0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & s_z & t_z \\
0 & 0 & 0 & 1
\end{bmatrix} \\
&=& \begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & -s_z & -t_z + 1 \\
0 & 0 & 0 & 1
\end{bmatrix} \\
&=& \begin{bmatrix}
\frac{2}{r – 1} & 0 & 0 & \frac{l+r}{l-r} \\
0 & \frac{2}{t – b} & 0 & \frac{t+b}{b-t} \\
0 & 0 & \frac{1}{z_f – z_n} & \frac{z_f}{z_f – z_n} \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{eqnarray}
結果として,正射影の場合のリバースZ形式は単純にニアクリップ平面とファークリップ平面を入れ替えしたものになります。
参考文献
・[Microsoft 2018 a] Microsoft, “D3DXMatrixPerspectiveFovRH function”, https://docs.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh
・[Microsoft 2018 b] Microsoft, “D3DXMatrixOrthoOffCenterRH function”, https://docs.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh
・[Reed 2015] Nathan Reed, “Depth Precision Visualized”, https://developer.nvidia.com/content/depth-precision-visualized