GPU结构被设计的非常适合矩阵运算,把透视投影过程用矩阵表达出来可以加速计算。
Unit Cube
在讨论如何构造透视投影矩阵之前,我们先了解透视投影的作用(效果)。空间中一点 $P$ 乘以透视投影矩阵后,我们会得到一点 $P^\prime$:
- 它的 x’ 坐标和 y’ 坐标的范围是 [-1, 1],即定义在 NDC 空间下。
- 它的 z’ 坐标的范围同样被压缩到 [-1, 1] 的范围内,边界分别为近剪切平面和远剪切平面。
如(Figure 1)所示,这样做的效果从空间中来看,其实是把相机坐标系下的视棱台(viewing frustum)压缩成了立方体(unit cube)。
而这样做的目的是为了方便做剪切(clipping)。一方面去除那些在视野范围之外的图元(不必浪费资源渲染它们),另一方面对那些一部分在视野内一部分在视野外的图元做修剪(Figure 2)。
这里有一个剪切空间(Clip Space)的概念,与 NDC 空间不同。我们稍后再做介绍 。
透视投影矩阵
现在我们可以开始构造透视投影矩阵了。回顾一下之前的透视投影的内容,相机空间中的点投影到成像平面上(Figure 3)的变化为:
$$
P_{S_x} = \cfrac{n * P_x}{-P_z} \\
P_{S_y} = \cfrac{n * P_y}{-P_z}
$$
我们发现计算投影要除以 $P_z$ 坐标,但这是矩阵的线性变换做不到的,矩阵只能线性的组合 $P_x$,$P_y$,$P_z$ 来得到 $P_{S_x}$,$P_{S_y}$,$P_{S_z}$,于是我们引入其次坐标,将空间点扩充为 $[P_x, P_y, P_z, P_w = 1]$。
同时对矩阵做特定设定,使其将转换后的 $P_{S_w}$ 分量设置为 $-P_z$,这样通过 $[P_{S_x}, P_{S_y}, P_{S_z}, P_{S_w}]$ 整体除以 $P_{S_w}$ 分量便实现了除法变换。
$$
\left[
\begin{matrix}
… & … & … & … \\
… & … & … & … \\
… & … & … & … \\
0 & 0 & -1 & 0
\end{matrix}
\right]
*
\left[
\begin{matrix}
P_x \\
P_y \\
P_z \\
1
\end{matrix}
\right]
=
\left[
\begin{matrix}
P_{S_x} \\
P_{S_y} \\
P_{S_z} \\
-P_z
\end{matrix}
\right]
$$
接下来继续构造其它分量,假设成像平面坐标系的 $x$ 坐标范围为 $[l, r]$,$y$ 坐标范围为 $[b, t]$,近剪切平面和远剪切平面分别为 $n, f$。
由透视投影的拓展内容中我们知道,若要使相机坐标 $P_x$ 变换到屏幕坐标 $P_{S_x}$ 且范围为 $[-1, 1]$,那么
$$
P_{S_x} = \cfrac{2nP_x}{-P_z(r-l)}-\cfrac{r+l}{r-l}
$$
由于 $\cfrac{1}{-P_z}$ 部分可以除以 $P_{S_w}$ 分量得到,所以矩阵的形式可以构造为:
$$
\left[
\begin{matrix}
\cfrac{2n}{r-l} & 0 & \cfrac{r+l}{r-l} & 0 \\
… & … & … & … \\
… & … & … & … \\
0 & 0 & -1 & 0
\end{matrix}
\right]
$$
同理
$$
P_{S_y} = \cfrac{2nP_y}{-P_z(t-b)}-\cfrac{t+b}{t-b}
$$
进一步扩充矩阵
$$
\left[
\begin{matrix}
\cfrac{2n}{r-l} & 0 & \cfrac{r+l}{r-l} & 0 \\
0 & \cfrac{2n}{t-b} & \cfrac{t+b}{t-b} & 0 \\
… & … & … & … \\
0 & 0 & -1 & 0
\end{matrix}
\right]
$$
现在只需要构造出 $P_z$ 分量部分的矩阵,使变换后的 $P_{S_z}$ 的范围为 $[-1, 1]$,由于 $P_{S_z}$ 和 $P_x, P_y$ 分量都无关,因此矩阵可以写成如下形式:
$$
\left[
\begin{matrix}
\cfrac{2n}{r-l} & 0 & \cfrac{r+l}{r-l} & 0 \\
0 & \cfrac{2n}{t-b} & \cfrac{t+b}{t-b} & 0 \\
0 & 0 & A & B \\
0 & 0 & -1 & 0
\end{matrix}
\right]
$$
$A$ 和 $B$ 是我们待求解的部分,它们满足:
当 $P_z = -n$ 即点 $P$ 在近平面上时,$P_{S_z} = -1$;当 $P_z = -f$ 即点 $P$ 在远平面上时,$P_{S_z} = 1$。所以可以得到下面的方程组:
解方程组得:
$$
A = -\cfrac{f+n}{f-n} \\
B = -\cfrac{2fn}{f-n}
$$
于是我们就得到了最终的透视投影矩阵:
$$
\left[
\begin{matrix}
\cfrac{2n}{r-l} & 0 & \cfrac{r+l}{r-l} & 0 \\
0 & \cfrac{2n}{t-b} & \cfrac{t+b}{t-b} & 0 \\
0 & 0 & -\cfrac{f+n}{f-n} & -\cfrac{2fn}{f-n} \\
0 & 0 & -1 & 0
\end{matrix}
\right]
$$
由于 $P_z$ 映射到 $[-1, 1]$ 的变化不是线性的,靠近近剪切平面的点的精度高,靠近远剪切平面的点的精度低(Figure 4),图中 $n = 1$,$f = 5$。
当精度过低时,可能会出现 z-fighting 的现象,即不同 $P_z$ 坐标的点映射到 $P_{S_z}$ 是相同的。这个问题无法被真正解决,我们只能通过使两个剪切平面之间的距离在包含所有物体的前提下尽可能的接近,来尽量避免 z-fighting 的问题。(Figure 5)
视场(Fov)和图像纵横比(Aspect Ratio)
求解透视投影矩阵时用到了6个参数 near,far,left,right,top,bottom。其中 near 和 far 是人为指定的,而剩下的4个参数则是由视场(Fov)以及图像纵横比(Aspect Ratio)决定的(Figure 6)。
由简单的比例关系可以得到:
$$
tan(\cfrac{FOV}{2}) = \cfrac{opposite}{adjacent} = \cfrac{BC}{AB} = \cfrac{top}{near}
$$
因此
$$
\begin{align}
&top = tan(\cfrac{FOV}{2})*near \\
&bottom = -top
\end{align}
$$
这里假设了 filed-of-view 是纵向的,OpenGL 就是默认 fov 为纵向,但是同时需要注意的是其它渲染器有可能默认 fov 为横向,如 Maya 和 RenderMan。
如图(Figure 7)图像的纵横比有可能相等也有可能不等:
若纵横比相等,由对称关系可得:
$$
\begin{align}
&top = tan(\cfrac{FOV}{2})*near \\
&right = top \\
&left = bottom = -top
\end{align}
$$
若纵横比不等,借助 aspect ratio 可得:
$$
\begin{align}
&aspect ratio = \cfrac{width}{height} \\
&top = tan(\cfrac{FOV}{2})*near \\
& bottom = -top \\
&right = top * aspect ratio \\
&left = -right
\end{align}
$$
参考链接
Scratchapixel-Projection Matrices: What You Need to Know First
Scratchapixel-The OpenGL Perspective Projection Matrix