转换
转换
转换是一个三维场景中的物体变化到屏幕二维空间上的一个过程,这个过程大致如下:

我们常说的MVP矩阵转换。
- Modeling : 模型变换
- View : 视图/相机变换,也就是图中的Camera transformation
- Projection : 投影变换
这里在加上一个视口变换,就完成了所有转化。
视图/相机变换
这里我们先说视图变换,这样会好理解一些。
定义相机
首先我们要知道怎么去定义一个相机,我们要想知道它在场景中的信息,可以通过以下几个向量来知道:
- 相机的位置坐标 e
- 相机的照射方向 g
- 相机的向上方向 t
相机定义图:

我们经常为了方便,在拍照的时候,先把相机固定了,构好图,然后再调整姿势。在渲染图片的时候我们也为方便也先固定好相机,所以约定相机:
- 我们需要把相机移动到世界坐标原点,
- 看向的方向为负z轴方向
- 向上方向与y轴正方向一样。
那我们要把相机设置成上面的状态。这个过程我们也可以认为是把相机从自己的坐标系转移到世界坐标系中,因为相机在世界坐标系原点。
转换过程
我们就可以先把相机移动到原点,然后再把相机旋转到指定的方向。这样会方便计算旋转。
我们可以得到公式:

Tips
因为使用矩阵(Mview)最后乘以相机坐标,所以这里是需要反过来相乘。
平移
只需把相机坐标e移动到原点即可得到平移矩阵(视角变换):
旋转
我们要旋转任意的角度,可以分别沿想x、y、z轴旋转即可。获得旋转:
- t x g 旋转到x轴正方向
- t 旋转到y轴正方向
- g 旋转到z轴负方向
Tips
我们这里定义一下 t x g 这个方向为 r 方便用
这样子我们并不是很好的去求解旋转矩阵,我们可以反向旋转。先求到结果点旋转到原始点的旋转矩阵,即世界坐标轴旋转到相机在世界坐标的方向,然后对矩阵求逆即可。
- 旋转 x 轴正方向 r (1,0,0) 到 (rx,ry,rz)
- 旋转 y 轴正方向 t (0,1,0) 到 (tx,ty,tz)
- 旋转 z 轴正方向 -g (0,0,1) 到 (-gx,-gy,-gz)
从原点旋转得到相机场景中的矩阵:
我们可以很清晰的看出上面的旋转矩阵为正交矩阵,又因为正交矩阵的逆矩阵等于转置矩阵。
所以我们的旋转矩阵为:
模型变换
我们把相机移动到世界坐标原点位置并且方向都改变了,物体被相机渲染出来的位置都不正确了吧。这里我们就要用到初中物理
相对运动。我们在移动相机的同时也移动物体,这样就可以保持相对运动,这样一来就可以使相机渲染出来的结果不变。这个过程我们常常叫模型变换(modeling transformation)。所以我们也把场景中的所有物体模型的位置坐标乘上上面我们求到 Tview 矩阵。
投影变换
我们把相机和物体都摆放好了,我们就需要开始把场景中的物体映射到一个图片上了。这个过程叫做相机投影,这里我们把投影分为两种:

- 透视投影(Projective Projection):把相机看做一个点形成一个锥体。
- 正交投影(Orthographic Projection):把相机放的无限远后,投影出近处物体与远处的物体的大小差不多相同。
正交投影
我们可以想象把物体坐标的z值去掉,然后平行投影到 [-1,1]² 的一个矩形中。投影到屏幕坐标范围 [0,1](NDC Normalized Device Coordinates)

Tips
这里的 [-1,1]² 矩形是约定俗成。
有点像俯视图(平面图)的感觉。我们先把物体移动到世界坐标原点,然后对其进行缩放。我们具体怎么去实现这个过程呢?如下图:

我们先把物体移动到坐标原点,然后把物体缩放到一个 [-1,1]³ 的一个立方体中。这个立方体叫做“规范立方体”(canonical cube)。这其中参数:
- l 盒子最左边 (x轴)left
- r 盒子最右边 (x轴)right
- t 盒子最上边 (y轴)top
- b 盒子最下边 (y轴)bottom
- n 盒子最前边 (z轴)near
- f 盒子最远边 (z轴)far
这样我们就可以写出变换矩阵了:

Tips
这里n与f都是在z轴上的,相机的朝向是z轴负方向所以这里n的值是比f大。
因为我们的立方体是 [-1,1] 长宽高都为2,所以我们的缩放是2除以原来物体的长度。
最终矩阵:
透视投影
正常我们人眼看到的构图结构———远小近大,就是透视投影。
透视投影我们需要三维的场景映射到观察窗口,然后把映射到窗口的做一次正交投影。
前面了解到,透视投影是把相机作为一个点,这样我们可以来获得一个横截面:

Tips
观察窗口可以简单的理解为屏幕
各个参数:
- e为相机位置
- g为相机照射方向
- n为相机到屏幕距离
- y场景中一点的高度基于g
- ys我们需要求得屏幕映射点的高度基于g
这里最重要的根据就是相似三角形,可以得到公式:
同时我们也可以得到:
我们可以先写出一个简单的关系:
Tips
这里用到一个齐次坐标的特性:
- (x,y,z,1)
- (kx,ky,kz,k)
- (zx,zy,z*z,z)
以上三个都代表点(x,y,z),当然这里乘上的系数不能为0。如k和z都不能为0。

这里我们的Zs一直不知道是什么,所以在乘上Z后还是不知道。这里我们已经获得一个矩阵的部分,我们只需要求出第三行即可。这个里我们用特殊值得方法来求:
Tips
- n 相机的照射到的近平面的z值,相机到屏幕
- f 相机的照射到的远平面的z值
当我们设的点就是屏幕上的点,这里我们的Zs和Z都为n,公式:Zs = Z = n。这里就可以写出以下等式:
这里的小步骤:
- 先把 [x,y,z,1]Τ 中间的 Z 替换成 n 得到 [x,y,n,1]Τ
- 然后把每一位都乘以 n 得到 [nx,ny,n²,n]Τ
- 这个形式很像上面矩阵计算的第三个 [nx,ny,*,z]Τ,把这其中的 z 也换成 n,得到 [nx,ny,*,n]Τ,所以说这里的*就为n²。
这里因为等于 n² 所以跟 x 与 y 都没有关系的,所以我们就可以写出上面最后一个相乘矩阵第三行的计算等式:
然后我们可以写出一个等式:
当我们设的点为远平面,在越来越远的地方它的 Zs 和 Z 都为 f ,公式:Zs = Z = f 。同理也可以得到下面的等式:
这两个等式解出来A与B如下:

最终写出透视投影到正交投影的矩阵:

这里最后再乘上正交投影即可:

视锥
我们相机是透视照射的话,就可以调整一个参数叫fov也就是视锥。我们可以用 (l, r, b, t) 来定义任意一个窗口, n 作为相机到窗口的距离。

这其中的关系有:
- l = -r
- b = -t
- 宽高比 aspect = r / t
- 这里的Θ也就是我们的fovY可以得到
视口变换
把之前算好的标准立方体的x和y拉伸到屏幕分辨率大小叫做视口变换。
- Z值不变
- width:屏幕宽度 height:屏幕高度
- xy平面 [-1,1]² 转换到 [0,width] x [0,height]
这样就可以得到转换矩阵:

最终结果
我们可以得到简单伪代码:
function view_transform(projection,view,model)
// mvp transformation
mvp = projection * view * model
for vertex in allVertexes do
vertex = mvp * vertex
//Homogeneous division
for vertex in allVertexes do
vertex /= vertex.w
//Viewport transformation
for vertex in allVertexes do
vertex.x = 0.5 * width * (vertex.x + 1.0)
vertex.y = 0.5 * height * (vertex.y + 1.0)
// vertex.z = -vertex.z * f1 +f2
这里的视口变换是直接写的结果试子,我们也可以直接传入矩阵