M-V-P变换与透视投影矩阵的推导

M-V-P变换即Model(模型变换)、View(视图变换)、Projection(投影变换)的结合变换。我们知道我们在屏幕上看见的画面其实都是二维的,那么是怎么做到把三维空间中所展示的内容显示到一个二维空间上呢?这里就需要用到 M-V-P 变换与视口变换。让我们通过一个摄影的例子来理解到底这四个步骤的具体含义是什么:

3D transformations

三维空间中的 Affine Transformation

三维空间中的变换其实也可使用二维的思路,引入另一个变量用于同一三维空间中的仿射变换:

所以我们用 4×4 的矩阵描述 Affine Transformation $$\left.\left(\begin{array}{c}x^{\prime}\\y^{\prime}\\z^{\prime}\\1\end{array}\right.\right)=\left(\begin{array}{cccc}a&b&c&t_x\\d&e&f&t_y\\g&h&i&t_z\\0&0&0&1\end{array}\right)\cdot\left(\begin{array}{c}x\\y\\z\\1\end{array}\right)$$ 同样的对于三维变换来讲,也是先应用线性变换,再应用平移变换。

三维空间中的缩放

$$ \mathbf{S}\left(s_{x}, s_{y}, s_{z}\right)=\left(\begin{array}{cccc} s_{x} & 0 & 0 & 0 \\ 0 & s_{y} & 0 & 0 \\ 0 & 0 & s_{z} & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) $$

三维空间中的平移

$$ \mathbf{T}\left(t_{x}, t_{y}, t_{z}\right)=\left(\begin{array}{cccc} 1 & 0 & 0 & t_{x} \\ 0 & 1 & 0 & t_{y} \\ 0 & 0 & 1 & t_{z} \\ 0 & 0 & 0 & 1 \end{array}\right) $$

三维空间中的旋转

分别绕x、y、z轴进行的旋转,右图是说任意复杂的绕轴旋转都可以分解成为绕 x、y、z 轴的旋转:

$$ \mathbf{R}_{x}(\alpha)=\left(\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos \alpha & -\sin \alpha & 0 \\ 0 & \sin \alpha & \cos \alpha & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) $$

$$ \mathbf{R}_{y}(\alpha)=\left(\begin{array}{cccc} \cos \alpha & 0 & \sin \alpha & 0 \\ 0 & 1 & 0 & 0 \\ -\sin \alpha & 0 & \cos \alpha & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) $$

$$ \mathbf{R}_{z}(\alpha)=\left(\begin{array}{cccc} \cos \alpha & -\sin \alpha & 0 & 0 \\ \sin \alpha & \cos \alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right) $$

任意复杂的绕轴旋转都可以分解成为绕 x、y、z 轴的旋转,所以出现了欧拉角的概念: $$ \mathbf{R}_{x y z}(\alpha, \beta, \gamma)=\mathbf{R}_x(\alpha) \mathbf{R}_y(\beta) \mathbf{R}_z(\gamma) $$ 具体可以参考我之前专门写的一篇关于旋转描述的文章: 《四元数、欧拉角与旋转矩阵的推导》

视图变换 (view transformation)

思考怎么样拍一张好的照片?哈哈,很形象的例子:

1、Find a good place and arrange people (model transformation) 模型变换,被拍摄目标摆好位置

2、Find a good “angle” to put the camera (view transformation) 视图变换,摄像机找好角度

3、Cheese! (projection transformation) 投影变换,茄子!拍照!

模型变换 - 视图变换 - 投影变换的过程就是平时简称的MVP变换!

模型变换之前已经用过了,就是线性变换 + 平移变换。重点就是视图变换 (view transformation)了,其实视图变换的核心用途就是将Camera的坐标系与原世界坐标系重合,这样计算的时候都是相对坐标。

我们先定义摄像机的参数,无非就是三个:摄像机的位置 (eye postion)e,观察方向 (gaze postion) g,视点正上方向 (view-up vector ) t,为了接下来的物体投影到 x-y 平面方便,在右手系中,我们假定摄像机的观察方向是朝着 -z 的,视点正上方向是朝着 y 的。

其实要做的就是两步:将相机位置移动至原点以及通过旋转矩阵将二者坐标系重合。

$$ M_{\text {view }}=R_{\text {view }} T_{\text {view }} $$ 第一步:平移到原点: $$ T_{\text {view }}=\left[\begin{array}{cccc} 1 & 0 & 0 & -x_{e} \\ 0 & 1 & 0 & -y_{e} \\ 0 & 0 & 1 & -z_{e} \\ 0 & 0 & 0 & 1 \end{array}\right] $$ 第二步:应用旋转矩阵:

将这三个方向旋转至标准坐标系是十分困难的,不妨反过来,考虑将标准坐标系转换为这三者,之后再利用逆操作变化一下即可。可以得到旋转的逆变换矩阵(因为之前得出所有的旋转矩阵都是正交矩阵,转置矩阵就是逆矩阵) $$ R_{\text {view }}^{-1}=\left[\begin{array}{cccc} x_{\hat{g} \times \hat{t}} & x_{t} & x_{-g} & 0 \\ y_{\hat{g} \times \hat{t}} & y_{t} & y_{-g} & 0 \\ z_{\hat{g} \times \hat{t}} & z_{t} & z_{-g} & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] \quad R_{v i e w}=\left[\begin{array}{cccc} x_{\hat{g} \times \hat{t}} & y_{\hat{g} \times \hat{t}} & z_{\hat{g} \times \hat{t}} & 0 \\ x_{t} & y_{t} & z_{t} & 0 \\ x_{-g} & y_{-g} & z_{-g} & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] $$

投影变换 (projection transformation)

投影变换时除了摄像机之外另外一个重点,在经过摄像机变换之后得到的依然时三维空间中的顶点坐标,如何将其映射至二维空间坐标就交给投影变换完成,也就是上面例子中的拍照例子的最后一步。正如我们所熟知的那样投影又主要分为正交投影和透视投影。

透视投影和正交投影的对比:

正交投影 Orthographic Projection Transformation

用于工程制图,正交投影是相对简单的一种,坐标的相对位置都不会改变,所有光线都是平行传播,我们只需将物体全部转换到一个 $[-1, 1]^{3}$ 的空间中即可。

这边值得注意的是为什么要限定范围为 $[-1, 1]^{3}$,其实这只是为了之后的计算更加的方便而已,在转换到屏幕坐标的时候就会重新拉伸回来,不必太做纠结,只需抓住正交投影的变化核心是,所有物体的相对大小位置都不会有任何变化:

第一步先移动到中心,第二步执行缩放:

$$ M_{\text {ortho }}=\left[\begin{array}{cccc} \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1 \end{array}\right]\left[\begin{array}{cccc} 1 & 0 & 0 & -\frac{r+l}{2} \\ 0 & 1 & 0 & -\frac{t+b}{2} \\ 0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1 \end{array}\right] $$

透视投影 Perspective Projection Transformation

近大远小,平行线不再平行,在计算机图形学、美术、视觉系统中最常见,也是最接近人眼看到的效果:

先回顾一下齐次坐标系的性质:$(x, y, z, 1)$、$ (kx, ky, kz, k != 0)$、$(xz, yz, z^2, z != 0) $ 在3D中都表示一个点,比如 (1, 0, 0, 1) 和 (2, 0, 0, 2) 都表示一个点 (1, 0, 0)。虽然简单,但是有用。

我们可以这样想象:将Frustum的后半部分进行压缩至与前面最终的投影界面大小相同,这样就成了一个立方体,之后进行一次假设的正交投影得到每条线应该投影到的位置,最后在进行一次真正的正交投影得到最终的透视投影。下面是整个过程对于挤压的定义:

1、近平面永远不变;

2、远平面的中心点不变;

投影过程可用下图解释,将 $(x,y,z)$ 一点投影至投影屏幕之后,他的坐标变为$( x ′ , y ′ , z ′ )$,可以通过相似三角形算出来 “挤压” 后的坐标:

此时只考虑 y 的变换。由相似三角形可以得到 $\mathrm{y}^{\prime}=\frac{\mathrm{n}}{\mathrm{z}} \mathrm{y}$,类似地,我们可以得到 $\mathrm{x}^{\prime}=\frac{\mathrm{n}}{\mathrm{z}} \mathrm{x}$ , 根据齐次坐标的性质,我们可以知道 $(1,0,0,1)^{\mathrm{T}}$ 与 $(k,0,0,k)^{\mathrm{T}}$ 表示同一个点,所以我们可以得到:

$$ \left[\begin{array}{l} \mathrm{x} \\ \mathrm{y} \\ \mathrm{z} \\ 1 \end{array}\right] \Rightarrow\left[\begin{array}{c} \frac{\mathrm{nx}}{\mathrm{z}} \\ \frac{\mathrm{ny}}{\mathrm{z}} \\ \text { unkown } \\ 1 \end{array}\right]==\left[\begin{array}{c} \mathrm{nx} \\ \mathrm{ny} \\ \text { unknown } \\ \mathrm{z} \end{array}\right] $$ 所以 “挤压” (persp to ortho)投影做了这个,我们要做的就是找出下面这个M矩阵 $$ M_{\text {persp } \rightarrow \text { ortho }}^{(4 \times 4)}\left(\begin{array}{l} x \\ y \\ z \\ 1 \end{array}\right)=\left(\begin{array}{c} n x \\ n y \\ \text { unknown } \\ z \end{array}\right) $$ 所以得到的透射到正交的变换矩阵是什么呢? $$ \mathrm{M}_{\text {persp } \rightarrow \text { ortho }}=\left[\begin{array}{cccc} \mathrm{n} & 0 & 0 & 0 \\ 0 & \mathrm{n} & 0 & 0 \\ ? & ? & ? & ? \\ 0 & 0 & 1 & 0 \end{array}\right] $$ 只有第三行不知道是什么,但是却可以通过两个特殊情况进行推导:

1、近平面上的任何点都不会改变

2、远平面上任何点的 z 值都不会改变

将上述 z 替换为 n ,由第一条性质可得: $$ \left[\begin{array}{l} \mathrm{x} \\ \mathrm{y} \\ \mathrm{z} \\ 1 \end{array}\right] \Rightarrow\left[\begin{array}{c} \frac{\mathrm{nx}}{\mathrm{z}} \\ \frac{\mathrm{ny}}{\mathrm{z}} \\ \text { unknown } \\ 1 \end{array}\right]==\left[\begin{array}{c} \mathrm{nx} \\ \mathrm{ny} \\ \text { unknown } \\ \mathrm{z} \end{array}\right] \Rightarrow\left[\begin{array}{l} \mathrm{x} \\ \mathrm{y} \\ \mathrm{n} \\ 1 \end{array}\right] \Rightarrow\left[\begin{array}{l} \mathrm{x} \\ \mathrm{y} \\ \mathrm{n} \\ 1 \end{array}\right]==\left[\begin{array}{c} \mathrm{nx} \\ \mathrm{ny} \\ \mathrm{n}^{2} \\ \mathrm{n} \end{array}\right] $$ 故与透射到正交的变换矩阵的第三行相乘有: $$ \left[\begin{array}{llll} \mathrm{0} & \mathrm{0} & \mathrm{A} & \mathrm{B} \end{array}\right]\left[\begin{array}{l} \mathrm{x} \\ \mathrm{y} \\ \mathrm{n} \\ 1 \end{array}\right]=\mathrm{n}^{2} $$ 从而得出$An + B = n^2$

同理由第二条性质可得: $$ \left[\begin{array}{l} 0 \\ 0 \\ \mathrm{f} \\ 1 \end{array}\right] \Rightarrow\left[\begin{array}{l} 0 \\ 0 \\ \mathrm{f} \\ 1 \end{array}\right]==\left[\begin{array}{c} 0 \\ 0 \\ \mathrm{f}^{2} \\ \mathrm{f} \end{array}\right] $$ 从而得出$Af + B = f^2$

进一步得出:$A=n+f, B=-nf$

所以变换矩阵为:

$$ \mathrm{M}_{\text {persp } \rightarrow \text { ortho }}=\left[\begin{array}{cccc} \mathrm{n} & 0 & 0 & 0 \\ 0 & \mathrm{n} & 0 & 0 \\ 0 & 0 & \mathrm{n}+\mathrm{f} & -\mathrm{nf} \\ 0 & 0 & 1 & 0 \end{array}\right] $$

最后,将这个被压缩过的空间,重新正交投影成标准小立方体,故定义透视投影变换矩阵:

$$ M_{\text {persp }}=M_{\text {ortho }} M_{\text {persp } \rightarrow \text { ortho }}\\ \\ \\ M_{\text {persp }}=\left[\begin{array}{cccc} \frac{2 \mathrm{n}}{\mathrm{r}-\mathrm{l}} & 0 & \frac{1+\mathrm{r}}{\mathrm{l}-\mathrm{r}} & 0 \\ 0 & \frac{2 \mathrm{n}}{\mathrm{t}-\mathrm{b}} & \frac{\mathrm{b}+\mathrm{t}}{\mathrm{b}-\mathrm{t}} & 0 \\ 0 & 0 & \frac{\mathrm{f}+\mathrm{n}}{\mathrm{n}-\mathrm{f}} & -\frac{2 \mathrm{fn}}{\mathrm{f}-\mathrm{n}} \\ 0 & 0 & 1 & 0 \end{array}\right] $$

视口变换(Viewport transformation)

在经过了前面的MVP变换后,空间被转换为一个$[-1, 1]^{3}$ 这么一个立方体,接下来,需要将这个立方体画到屏幕上。这就需要用到视口变。其实目前只需要关心下、y轴的数据,它的z轴数据由深度缓冲来处理。屏幕映射需要将 x、y 轴 $[-1, 1]^{2}$ 映射到屏幕坐标 $[0,width] * [0,height]$ 。 屏幕原点在左下角,所以先缩放,后平移:

$$ \mathrm{M}_{\text {viewport }}=\left[\begin{array}{cccc} \frac{\text { width }}{2} & 0 & 0 & \frac{\text { width }}{2} \\ 0 & \frac{\text { height }}{2} & 0 & \frac{\text { height }}{2} \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] $$

至此历经上述4个变换(模型变换,视图变换,投影变换,视口变换),我们就已经成功的把游戏世界的任意可视物体转换到屏幕上了!回顾一下,其实也不是很难,只要分清步骤就会十分清晰。

总结:$$M=M_\text{viewport }M_\text{per}M_\text{view/cam}M_\text{model}$$