Z什么时候是线性的

在看separable-sss的shader的时候,关注了一下其中点光源生成shadowmap的投影矩阵的处理,方法来自于Linearized Depth using Vertex Shaders,生成[0, 1]的线性深度。这也让我意识到,对于z在管线每一步的计算中,什么时候是线性的,什么时候变得不线性,我并没有掌握所有细节。

1
2
3
4
5
6
7
8
9
10
/**
* This is for rendering linear values:
* Check this: http://www.mvps.org/directx/articles/linear_z/linearz.htm
*/
D3DXMATRIX linearProjection = projection;
float Q = projection._33;
float N = -projection._43 / projection._33;
float F = -N * Q / (1 - Q);
linearProjection._33 /= F;
linearProjection._43 /= F;

同时需要vertex shader中配合,解除掉后续的透视除:

1
2
3
4
5
6
float4 ShadowMapVS(float4 position : POSITION0,
uniform float4x4 worldViewProjection) : SV_POSITION {
float4 pos = mul(position, worldViewProjection);
pos.z *= pos.w; // We want linear positions
return pos;
}

不经过特殊处理,经过透视投影,最终管线输出的深度是非线性的。世界变换、相机变换和视口变换对于z的操作,自然结果还是线性的;透视除法后,z就不线性了;那么投影变换后的clip space(裁剪空间)坐标的z还是线性的吗?

\(z_{clip} = A \cdot z_{view} + B,w_{clip} = \pm z_{view}\)

\(A\)\(B\) 是由近裁剪面 \(n\) 和远裁剪面 \(f\) 决定的常数,这个线性函数对于相机空间的z只有平移和缩放操作,随意结果\(z_{clip}\)依然保持线性。

但是透视除法,\(z_{ndc} = \frac{z_{clip}}{w_{clip}} = \frac{A \cdot z_{view} + B}{-z_{view}} = -A - \frac{B}{z_{view}}\),就是线性函数了,导致z的非线性化。

虽然有不同的手系,但是相机和投影的手系是一致的。

1. OpenGL(NDC 深度区间为 \([-1, 1]\)

在右手坐标系中(摄像机看向 \(-Z\) 轴,即 \(z_{view} \in [-f, -n]\)):

  • \(z_{view} = -n\) 时,映射后的 \(z_{ndc} = -1\)
  • \(z_{view} = -f\) 时,映射后的 \(z_{ndc} = 1\)

\[P_{\text{OpenGL}} = \begin{bmatrix} \frac{\cot(\frac{\theta}{2})}{\text{aspect}} & 0 & 0 & 0 \\ 0 & \cot(\frac{\theta}{2}) & 0 & 0 \\ 0 & 0 & A & B \\ 0 & 0 & -1 & 0 \end{bmatrix} = \begin{bmatrix} \frac{\cot(\frac{\theta}{2})}{\text{aspect}} & 0 & 0 & 0 \\ 0 & \cot(\frac{\theta}{2}) & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix}\]

\[A = -\frac{f + n}{f - n}, \quad B = -\frac{2fn}{f - n}\]

\(z_{clip} = A \cdot z_{view} + B\)\(w_{clip} = -z_{view}\)

透视除法 \(z_{ndc} = \frac{z_{clip}}{w_{clip}} = \frac{A \cdot z_{view} + B}{-z_{view}}\)

  • 当顶点在近平面 \(z_{view} = -n\) 时:\(z_{ndc} = \frac{-\frac{f+n}{f-n}(-n) - \frac{2fn}{f-n}}{n} = \frac{n(f+n) - 2fn}{n(f-n)} = \frac{n(n-f)}{n(f-n)} = -1\)
  • 当顶点在远平面 \(z_{view} = -f\) 时:\(z_{ndc} = \frac{-\frac{f+n}{f-n}(-f) - \frac{2fn}{f-n}}{f} = \frac{f(f+n) - 2fn}{f(f-n)} = \frac{f(f-n)}{f(f-n)} = 1\)

2. DirectX / Vulkan(NDC 深度区间为 \([0, 1]\)

在左手坐标系中(摄像机看向 \(+Z\) 轴,即 \(z_{view} \in [n, f]\)):

  • \(z_{view} = n\) 时,映射后的 \(z_{ndc} = 0\)
  • \(z_{view} = f\) 时,映射后的 \(z_{ndc} = 1\)

\[P_{\text{DX/VK}} = \begin{bmatrix} \frac{\cot(\frac{\theta}{2})}{\text{aspect}} & 0 & 0 & 0 \\ 0 & \cot(\frac{\theta}{2}) & 0 & 0 \\ 0 & 0 & A & B \\ 0 & 0 & 1 & 0 \end{bmatrix} = \begin{bmatrix} \frac{\cot(\frac{\theta}{2})}{\text{aspect}} & 0 & 0 & 0 \\ 0 & 0 & \cot(\frac{\theta}{2}) & 0 \\ 0 & 0 & \frac{f}{f-n} & -\frac{fn}{f-n} \\ 0 & 0 & 1 & 0 \end{bmatrix}\]

\[A = \frac{f}{f - n}, \quad B = -\frac{fn}{f - n}\]

  • 当顶点在近平面 \(z_{view} = n\) 时:\(z_{ndc} = \frac{\frac{f}{f-n}(n) - \frac{fn}{f-n}}{n} = \frac{fn - fn}{n(f-n)} = 0\)
  • 当顶点在远平面 \(z_{view} = f\) 时:\(z_{ndc} = \frac{\frac{f}{f-n}(f) - \frac{fn}{f-n}}{f} = \frac{f^2 - fn}{f(f-n)} = \frac{f(f-n)}{f(f-n)} = 1\)

XMMatrixPerspectiveFovLH

透视投影,在透视除法导致了非线性化,那么正交投影呢?

XMMatrixOrthographicLH

DirectX 正交矩阵 Left-Handed

d3dxmatrixortholh \[P_{\text{DX}} = \begin{bmatrix} \frac{2}{w} & 0 & 0 & 0 \\ 0 & \frac{2}{h} & 0 & 0 \\ 0 & 0 & \frac{1}{f-n} & 0 \\ 0 & 0 & \frac{-n}{f-n} & 1 \end{bmatrix}\]

矩阵的\(P_{33}\)元素才是1,导致正交变换后的\(w_clip\)是1,那么透视除法就相当于没有了,正交投影矩阵中即完成了深度的变换,所以ndc仍然保持了裁剪坐标的z线性,最终的深度输出依然是线性,而且是[-1,1]的线性深度。就不需要像透视投影Linearized Depth using Vertex Shaders那样麻烦,在vertex shader和透视投影矩阵中同时做修改。正交投影下,直接用视口变换后的z输出,乘以\(f\)平面,就得到separable-sss需要的距离了。

参考