搬砖笔记 (2) —— OpenGL 投影矩阵

本文翻译自 OpenGL Projection Matrix,仅用于个人学习记录。

概述

计算机显示器是一个 2D 表面。由 OpenGL 渲染的 3D 场景必须被投影为计算机屏幕上的 2D 图像。GL_PROJECTION 矩阵被用于这个投影变换。首先,它将所有顶点数据从视觉坐标 (Eye Coordinates) 转换到裁剪坐标 (Clip Coordinates)。然后,将这些裁剪坐标分别除以裁剪坐标的 \(w\) 分量,转换为标准化设备坐标 (Normalized Device Coordinate, NDC)。

因此,GL_PROJECTION 矩阵包含了裁剪(平截头体剔除 (Frustum Culling))和标准化设备坐标变换。下面描述如何用左 (left)、右 (right)、底 (bottom)、顶 (top)、近 (near) 和远 (far) 的边界值这 6 个参数来建立投影矩阵。

被裁剪的三角形

需要注意的是,裁剪发生在裁剪坐标中,且在除以 \({w}_{c}\) 之前。裁剪坐标 \({x}_{c}\)\({y}_{c}\)\({z}_{c}\)\({w}_{c}\) 进行比较:如果任何裁剪坐标小于 \(-{w}_{c}\) 或大于 \({w}_{c}\),则该顶点被丢弃。

\[ -{w}_{c} < {x}_{c},\ {y}_{c},\ {z}_{c} < {w}_{c} \]

之后,OpenGL 将重建被裁剪的多边形的边缘。

透视投影 (Perspective Projection)

在透视投影中,平截头体(视觉坐标)中的一个 3D 点被映射到标准化设备坐标。\(x\)\(y\)\(z\) 坐标的取值范围分别从 \(\left[l,\ r\right]\)\(\left[b,\ t\right]\)\(\left[-n,\ -f\right]\) 被映射到 \(\left[-1,\ 1\right]\)

透视平截头体和标准化设备坐标

视觉坐标是在右手坐标系中定义的,但是标准化设备坐标使用的是左手坐标系。因此,原点的相机在视觉空间中是看向 \(Z\) 轴负方向 (\(-Z\)) 的,而在标准化设备坐标中是看向 \(Z\) 轴正方向 (\(+Z\)) 的。

glFrustum() 只接受 nearfar 距离的正值,因为需要在构建 GL_PROJECTION 矩阵时取其相反数。

在 OpenGL 中,一个视觉空间中的 3D 点被投影到近平面(投影平面)。下图展示了视觉空间中的一点 \(\left({x}_{e},\ {y}_{e},\ {z}_{e}\right)\) 是如何被投影到近平面上点 \(\left({x}_{p},\ {y}_{p},\ {z}_{p}\right)\) 的。

平截头体的俯视图 平截头体的侧视图

如上左图所示为平截头体的俯视图,通过计算相似三角形的比例,视觉空间的 \(x\) 坐标 \({x}_{e}\) 被映射到 \({x}_{p}\)

\[ \frac{x_p}{x_e} = \frac{-n}{z_e} \Rightarrow x_p = \frac{n \cdot x_e}{-z_e} \]

如上右图所示为平截头体的测试图,同样可以计算 \(y\) 坐标的映射:

\[ \frac{y_p}{y_e} = \frac{-n}{z_e} \Rightarrow y_p = \frac{n \cdot y_e}{-z_e} \]

注意 \({x}_{p}\)\({y}_{p}\) 都依赖于 \({z}_{e}\),它们都与 \(-{z}_{e}\) 成反比关系。换句话说,它们都被 \(-{z}_{e}\) 所除。这是构建 GL_PROJECTION 矩阵的第一条线索。将视觉坐标通过乘以 GL_PROJECTION 矩阵进行转换后,得到的裁剪坐标仍然是一个齐次坐标。通过除以裁剪坐标的 \(w\) 分量,它最终被转换为标准化设备坐标(更多细节见 OpenGL 变换)。

\[ \begin{bmatrix} {x}_{clip} \\ {y}_{clip} \\ {z}_{clip} \\ {w}_{clip} \end{bmatrix} = {M}_{projection} \cdot \begin{bmatrix} {x}_{eye} \\ {y}_{eye} \\ {z}_{eye} \\ {w}_{eye} \end{bmatrix} ,\ \begin{bmatrix} {x}_{ndc} \\ {y}_{ndc} \\ {z}_{ndc} \end{bmatrix} = \begin{bmatrix} \dfrac{x_{clip}}{w_{clip}} \\ \dfrac{y_{clip}}{w_{clip}} \\ \dfrac{z_{clip}}{w_{clip}} \end{bmatrix} \]

因此,我们可以把裁剪坐标的 \(w\) 分量设为 \(-{z}_{e}\),则 GL_PROJECTION 矩阵的第四行变为 \(\begin{bmatrix}0 & 0 & -1 & 0\end{bmatrix}\)

\[ \begin{bmatrix} {x}_{c} \\ {y}_{c} \\ {z}_{c} \\ {w}_{c} \end{bmatrix} = \begin{bmatrix} & & & \\ \vdots & \vdots & \vdots & \vdots \\ & & & \\ 0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} {x}_{e} \\ {y}_{e} \\ {z}_{e} \\ {w}_{e} \end{bmatrix} ,\ \therefore {w}_{c} = -{z}_{e} \]

下一步,我们以线性关系把 \({x}_{p}\)\({y}_{p}\) 映射到标准化设备坐标的 \({x}_{n}\)\({y}_{n}\)\(\left[l,\ r\right] \Rightarrow \left[-1,\ 1\right]\) 以及 \(\left[b,\ t\right] \Rightarrow \left[-1,\ 1\right]\)

x_p 映射到 x_n

\[ \begin{aligned} {x}_{n} & = \dfrac{1 - (-1)}{r - l} \cdot {x}_{p} + \beta \text{(代入}{x}_{p} = r,\ {x}_{n} = 1\text{)} \\ \Rightarrow \beta & = -\dfrac{r + l}{r - l} \\ \therefore {x}_{n} & = \dfrac{2 x_p}{r - l} - \dfrac{r + l}{r - l} \end{aligned} \]

y_p 映射到 y_n

\[ \begin{aligned} {y}_{n} & = \dfrac{1 - (-1)}{t - b} \cdot {y}_{p} + \beta \text{(代入}{y}_{p} = t,\ {y}_{n} = 1\text{)} \\ \Rightarrow \beta & = -\dfrac{t + b}{t - b} \\ \therefore {y}_{n} & = \dfrac{2 y_p}{t - b} - \dfrac{t + b}{t - b} \end{aligned} \]

然后,我们将 \({x}_{p} = \dfrac{n x_e}{-z_e}\)\({y}_{p} = \dfrac{n y_e}{-z_e}\) 代入上式:

\[ \begin{aligned} {x}_{n} & = \dfrac{\frac{2 n}{r - l} \cdot x_e + \frac{r + l}{r - l} \cdot z_e}{-z_e} = \dfrac{x_c}{-z_e} \\ {y}_{n} & = \dfrac{\frac{2 n}{t - b} \cdot y_e + \frac{t + b}{t - b} \cdot z_e}{-z_e} = \dfrac{y_c}{-z_e} \end{aligned} \]

注意我们让上式中的每项都除以 \(- {z}_{e}\) 以进行透视除法 (\(\frac{x_c}{w_c},\ \frac{y_c}{w_c}\))。并且我们之前令 \({w}_{c} = -{z}_{e}\),则上式中的分子分别变为裁剪坐标 \({x}_{c}\)\({y}_{c}\)

由这些方程,我们可以得到 GL_PROJECTION 矩阵的第一行和第二行:

\[ \begin{bmatrix} {x}_{c} \\ {y}_{c} \\ {z}_{c} \\ {w}_{c} \end{bmatrix} = \begin{bmatrix} \frac{2 n}{r - l} & 0 & \frac{r + l}{r - l} & 0 \\ 0 & \frac{2 n}{t - b} & \frac{t + b}{t - b} & 0 \\ \vdots & \vdots & \vdots & \vdots \\ 0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} {x}_{e} \\ {y}_{e} \\ {z}_{e} \\ {w}_{e} \end{bmatrix} \]

现在,我们只剩 GL_PROJECTION 矩阵的第三行需要求解。计算 \({z}_{n}\) 和其他的略有区别,因为视觉空间的 \({z}_{e}\) 总是投影到近平面上的 \(-n\),但是我们需要唯一的 \(z\) 值来进行裁剪和深度测试。此外,我们应该能够反投影(逆变换)它。由于我们知道 \(z\) 不依赖于 \(x\)\(y\) 的值,我们借用 \(w\) 分量来寻找 \({z}_{n}\)\({z}_{e}\) 的关系。因此,我们令 GL_PROJECTION 矩阵的第三行形如 \(\begin{bmatrix}0 & 0 & A & B\end{bmatrix}\),则有

\[ {z}_{n} = \dfrac{z_c}{w_c} = \dfrac{A z_e + B w_e}{-z_e} \]

在视觉空间中,\({w}_{e} = 1\),因此,方程变为

\[ {z}_{n} = \dfrac{A z_e + B}{-z_e} \]

为了计算系数 \(A\)\(B\),我们将 \({z}_{e} = -n,\ {z}_{n} = -1\)\({z}_{e} = -f,\ {z}_{n} = 1\) 代入上式

\[ \begin{cases} \dfrac{-A n + B}{n} & = -1 \\ \dfrac{-A f + B}{f} & = 1 \end{cases} \Rightarrow \begin{cases} A & = -\dfrac{f + n}{f - n} \\ B & = -\dfrac{2 f n}{f - n} \end{cases} \]

我们得到了 \(A\)\(B\),因此 \({z}_{e}\)\({z}_{n}\) 的关系变为

\[ {z}_{n} = \dfrac{-\frac{f + n}{f - n} z_e - \frac{2 f n}{f - n}}{-z_e} \]

最终我们得到了 GL_PROJECTION 矩阵所有的元素,完整的投影矩阵为

\[ \begin{bmatrix} \frac{2 n}{r - l} & 0 & \frac{r + l}{r - l} & 0 \\ 0 & \frac{2 n}{t - b} & \frac{t + b}{t - b} & 0 \\ 0 & 0 & -\frac{f + n}{f - n} & \frac{-2 f n}{f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix} \]

这个投影矩阵是针对一般平截头体的,如果视觉体积 (Viewing Volume) 是对称的,即 \(r = -l,\ t = -b\),则矩阵可以简化为

\[ \begin{bmatrix} \frac{n}{r} & 0 & 0 & 0 \\ 0 & \frac{n}{t} & 0 & 0 \\ 0 & 0 & -\frac{f + n}{f - n} & \frac{-2 f n}{f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix} \]

在我们继续之前,请在此看看 \({z}_{e}\)\({z}_{n}\) 之间的关系。注意它是一个有理函数,并且是 \({z}_{e}\)\({z}_{n}\) 之间的非线性关系。这意味着在近平面 (near) 有非常高的京精度,但在远平面 (far) 精度非常低。如果范围 \(\left[-n,\ -f\right]\) 变大,就会引起深度的精度问题 (Z-fighting):远平面附近 \({z}_{e}\) 微小的变化不会影响 \({z}_{n}\) 的值。\(n\)\(f\) 之间的距离应该尽可能地短,以减少深度缓冲的精度问题。

深度精度的比较

正射投影 (Orthographic Projection)

构建正射投影的 GL_PROJECTION 矩阵比透视投影简单得多。

正射体积和标准化设备坐标

视觉空间的所有 \({x}_{e}\)\({y}_{e}\)\({z}_{e}\) 分量都被线性地映射到标准化设备坐标,我们只需要将一个长方体缩放到一个立方体,然后将其移动到原点。让我们用线性关系找出 GL_PROJECTION 矩阵的元素。

x_e 映射到 x_n

\[ \begin{aligned} {x}_{n} & = \dfrac{1 - (-1)}{r - l} \cdot {x}_{e} + \beta \text{(代入}{x}_{e} = r,\ {x}_{n} = 1\text{)} \\ \Rightarrow \beta & = -\dfrac{r + l}{r - l} \\ \therefore {x}_{n} & = \dfrac{2 x_e}{r - l} - \dfrac{r + l}{r - l} \end{aligned} \]

y_e 映射到 y_n

\[ \begin{aligned} {y}_{n} & = \dfrac{1 - (-1)}{t - b} \cdot {y}_{e} + \beta \text{(代入}{y}_{e} = t,\ {y}_{n} = 1\text{)} \\ \Rightarrow \beta & = -\dfrac{t + b}{t - b} \\ \therefore {y}_{n} & = \dfrac{2 y_e}{t - b} - \dfrac{t + b}{t - b} \end{aligned} \]

z_e 映射到 z_n

\[ \begin{aligned} {z}_{n} & = \dfrac{1 - (-1)}{-f - (-n)} \cdot {z}_{e} + \beta \text{(代入}{z}_{e} = -f,\ {z}_{n} = 1\text{)} \\ \Rightarrow \beta & = -\dfrac{f + n}{f - n} \\ \therefore {z}_{n} & = \dfrac{-2 z_e}{f - n} - \dfrac{f + n}{f - n} \end{aligned} \]

由于 \(w\) 分量对于正射投影是不必要的,故 GL_PROJECTION 矩阵的第四行为 \(\begin{bmatrix}0 & 0 & 0 & 1\end{bmatrix}\)。因此,完整的正射投影 GL_PROJECTION 矩阵为

\[ \begin{bmatrix} \frac{2}{r - l} & 0 & 0 & -\frac{r + l}{r - l} \\ 0 & \frac{2}{t - b} & 0 & -\frac{t + b}{t - b} \\ 0 & 0 & \frac{-2}{f - n} & -\frac{f + n}{f - n} \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

如果视觉体积是对称的,即 \(r = -l,\ t = -b\),则它可以被进一步简化为

\[ \begin{bmatrix} \frac{1}{r} & 0 & 0 & 0 \\ 0 & \frac{1}{t} & 0 & 0 \\ 0 & 0 & \frac{-2}{f - n} & -\frac{f + n}{f - n} \\ 0 & 0 & 0 & 1 \end{bmatrix} \]