
齐次坐标和齐次向量
所谓“齐次坐标”是一系列关于坐标和向量表示方法的规定。
通常,三维坐标和向量都可以表示为:
⎣⎢⎡xyz⎦⎥⎤
但齐次坐标表示这个点是:
⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤
齐次坐标表示这个向量是:
⎣⎢⎢⎢⎡xyz0⎦⎥⎥⎥⎤
这样的表示可以方便很多东西,尤其是方便表示三维空间的线性变换。
比如我要对整个空间中的点进行一次线性变换,包含旋转和位移操作。
那么类似下图所示,一个旋转操作R可以表示为矩阵相乘,位移操作t可以表示为向量相加,于是对于空间中的任意点[x,y,z]T,求变换后的坐标[x′,y′,z′]T就是:
⎣⎢⎡x′y′z′⎦⎥⎤=R⋅⎣⎢⎡xyz⎦⎥⎤+t=⎣⎢⎡r11r21r31r12r22r32r13r23r33⎦⎥⎤⋅⎣⎢⎡xyz⎦⎥⎤+⎣⎢⎡t1t2t3⎦⎥⎤=⎣⎢⎡r11r21r31r12r22r32r13r23r33t1t2t3⎦⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=[R∣t]⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤
所以这样写成齐次坐标可以直接写成一个矩阵乘一个向量,看着更加清爽。

除此之外,齐次坐标还可以方便区分坐标和向量,并且方便计算。
比如“坐标-坐标=向量”就是直接能计算出来,因为最后一项1-1=0:
⎣⎢⎢⎢⎡x1y1z11⎦⎥⎥⎥⎤−⎣⎢⎢⎢⎡x2y2z21⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡x1−x2y1−y2z1−z20⎦⎥⎥⎥⎤
再比如“坐标±向量=坐标”也直接能计算出来,因为最后一项1±0=1:
⎣⎢⎢⎢⎡x1y1z11⎦⎥⎥⎥⎤±⎣⎢⎢⎢⎡x2y2z20⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡x1±x2y1±y2z1±z21⎦⎥⎥⎥⎤
又比如“向量±向量=向量”,因为最后一项0±0=0:
⎣⎢⎢⎢⎡x1y1z10⎦⎥⎥⎥⎤±⎣⎢⎢⎢⎡x2y2z20⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡x1±x2y1±y2z1±z20⎦⎥⎥⎥⎤
其他性质
齐次坐标的四个项乘同一个值,所表示的点不变:
⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡kxkykzk⎦⎥⎥⎥⎤
“坐标-坐标=两坐标连线的中点”:
⎣⎢⎢⎢⎡x1y1z11⎦⎥⎥⎥⎤+⎣⎢⎢⎢⎡x2y2z21⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡x1+x2y1+y2z1+z22⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡2x1+x22y1+y22z1+z21⎦⎥⎥⎥⎤
正交投影和透视投影
来两张图就看懂了:


正交投影是透视投影中相机无限远的特殊情况,没有了近大远小的效应。正交投影渲染方式也很简单,直接把z轴扔掉就行了:

透视投影
透视投影可以理解为将远处的平面挤压之后再做正交投影:

我们可以从直觉上规定这种挤压的规则:
- 近平面z=n上的点坐标不变
- 远平面z=f压缩后中心点坐标不变
这里注意,我们无法再规定“各平面压缩后z轴坐标不变”,虽然这样很符合直觉,但是规定了这个的变换就不是线性变换了,没法用一个矩阵表示。
设这个线性挤压的操作的变换矩阵为Mpersp→ortho,如何求?
如图所示,设近平面的z轴坐标为n,要将远处某平面上的坐标[x,y,z]T进行挤压,仅从y轴坐标看,挤压操作会将y压缩到y′=zny:

同理,挤压操作会将x压缩到x′=znx,由于没有规定“各平面压缩后z轴坐标不变”,变换后的z轴坐标不知道会变成什么样,先用“?”代替一下,于是可写出变换的结果:
⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡znxzny?1⎦⎥⎥⎥⎤z∈[n,f]
由于是齐次坐标,所以全部乘上z也表示同一个点:
⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡znxzny?1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡nxny?z⎦⎥⎥⎥⎤z∈[n,f]
再看规则“近平面z=n上的点坐标不变”,很显然可以表示为z=n平面上的点映射关系,为方便后面计算,这里也利用齐次坐标性质,全部乘上n:
⎣⎢⎢⎢⎡xyn1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡xyn1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡nxnyn2n⎦⎥⎥⎥⎤
再看规则“远平面z=f压缩后中心点坐标不变”,很显然可以表示为z=n平面上的原点映射关系,为方便后面计算,这里也利用齐次坐标性质,全部乘上f:
⎣⎢⎢⎢⎡00f1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡00f1⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡00f2f⎦⎥⎥⎥⎤
所以上面三个坐标映射关系可以写成Mpersp→ortho的一个方程:
⎣⎢⎢⎢⎡nxny?znxnnynn2n00f2f⎦⎥⎥⎥⎤=Mpersp→ortho⋅⎣⎢⎢⎢⎡xyz1xnynn100f1⎦⎥⎥⎥⎤
很容易推导出Mpersp→ortho,在近远两平面之间的点映射后的z轴坐标也知道了:
⎣⎢⎢⎢⎡nxnyz(n+f)−nfznxnnynn2n00f2f⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡n0000n0000n+f100−nf0⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1xnynn100f1⎦⎥⎥⎥⎤
这种变换又被称为“透视变换”。
注:透视变换近大远小,xy坐标和z有关,显然应该不是一个仿射变换,但是这里居然能用矩阵乘表示?这就是齐次坐标的好处,让齐次坐标的最后一项与z关联上,因为矩阵向量乘法有Akx=kAx,所以可以把投影过程全部算完再除以z,避免了直接算非线性的各种难题。
视锥体
人的视野是有限的,摄像机也是一样,在计算机中,摄像机的视野范围被一个叫视锥的东西决定着,它决定了我们能看到的范围,在视锥外,因为视野受限我们看不到任何东西。而看到的大小都落在视锥内。
视锥体由六个面组成,分别为上下平面(topper/bottom),左右平面(left/right),以及近裁剪平面(Near Clipping Plane)和远裁剪平面(Far Clipping Plane)。
上文中介绍的透视投影决定于视锥体的近裁剪平面和远裁剪平面位置n和f。
正交投影
在经过透视投影后,还需要确定哪些区域需要渲染,才能开始光栅化计算。
要渲染的就是视锥体范围,视锥体外的物体不需要进行渲染。
透视投影后视锥体范围就对应一长方体,视锥体各平面就是长方体的各平面,正交投影就是在透视投影后把这个长方体内的点全部缩放到x,y,z∈[−1,1]3的正方体区域内以方便光栅化计算。
注:理论上可以转换到任意的大小,但是转换到立方体内对于后续的计算最为方便。

很简单,移动长方体中点到坐标原点后在xyz轴上等比例缩放即可。
设透视投影后视锥体左右平面位置(x轴坐标)为l和r、上下平面位置(y轴坐标)为t和b、近裁剪平面和远裁剪平面位置(z轴坐标)为n和f,则易得(x,y,z)正交投影后的坐标(x′,y′,z′)为:
xoyozo=r−l2(xp−2r+l)=t−b2(yp−2t+b)=n−f2(zp−2n+f)
相机内参
透视投影中的近平面距离n属于相机的参数,相关的相机参数还有水平和垂直可视角度fx,fy

在真正的游戏引擎中,相机内外参通常会以MVP变换的形式进行处理。

Model Transformation 矩阵将物体顶点在模型空间下的坐标转换为在世界空间下的坐标。
模型空间
模型空间是以模型某一点为原点建立坐标系而形成的空间,模型的个顶点最开始是以模型空间坐标系的坐标。

世界空间
世界空间顾名思义就是以世界原点为原点建立的坐标系。显然可以知道,这与模型空间是两套坐标系,那么此时计算时就要进行相对的计算很麻烦,所以用Model Transformation 矩阵把模型顶点坐标从模型空间都转到世界空间有利于简化后面的计算。
Model Transformation 矩阵将模型空间下的齐次坐标[x,y,z,1]⊤转换为在世界空间下的齐次坐标[x′,y′,z′,1]⊤,包括平移矩阵、旋转矩阵、缩放矩阵:
⎣⎢⎢⎢⎡x′y′z′1⎦⎥⎥⎥⎤=M⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡100001000010txtxtx1⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡R11R21R310R12R22R320R13R23R3300001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡kx0000ky0000kz00001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤
View Transformation 矩阵将物体顶点在世界空间下的坐标转换为视图空间下的坐标。
视图空间
视图空间是以摄像机为原点建立坐标系的空间。我们最终想要的显示在屏幕上的结果是由摄像机决定的,所以世界空间显得不那么主要,而转到以摄像机为原点的视图空间有利于我们计算投影到屏幕上的图像。
View Transformation 矩阵包括旋转矩阵,平移矩阵,其来自于上文中讲过的相机位姿[R∣t]:
⎣⎢⎡x′y′z′⎦⎥⎤=R⋅⎣⎢⎡xyz⎦⎥⎤+t
全部写成齐次就是View Transformation:
⎣⎢⎢⎢⎡x′y′z′1⎦⎥⎥⎥⎤=[R0t1]⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤
这里的
[R0t1]
就是View Transformation 矩阵。
Projection Transformation 矩阵将物体顶点在视图空间的坐标转换为裁剪空间的坐标。
裁剪空间
裁剪空间是一个抽象的齐次坐标空间,是在视图空间坐标到标准设备坐标(Normalized Device Coordinate, NDC)之间的过渡空间。
我们在观察空间经过正射/透视投影变换后,得到的就是裁剪空间。
标准设备坐标 Normalized Device Coordinate, NDC

标准设备坐标能通过对裁剪空间内的 xyz 坐标通过除以齐次坐标 w 得到,这个除以齐次坐标 w 的过程就是透视除法,在OpenGL渲染管线中,透视除法是顶点着色器最后的步骤,往往是自动进行的,然后使得顶点着色器最后的输出就是裁剪坐标经过透视除法后得到的标准设备坐标。
在透视除法得到NDC,转换到屏幕空间就十分方便了,从一个 [−1,1]2 的NDC范围区间变换到屏幕视口的 [0,1920]×[0,1080] 只需要简单线性变换即可。
Projection Transformation 矩阵包括正交投影矩阵和透视投影矩阵。其来自于上文中的正交投影和透视投影。
回忆上文中的透视投影Mpersp→ortho:
⎣⎢⎢⎢⎡xpypzp1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡n0000n0000n+f100−nf0⎦⎥⎥⎥⎤⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤
回忆上文中的正交投影Mortho:
xoyozo=r−l2(xp−2r+l)=t−b2(yp−2t+b)=n−f2(zp−2n+f)
可以写成正交投影矩阵Mortho:
Mortho=⎣⎢⎢⎢⎡r−l20000t−b20000n−f200001⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡100001000010−2r+l−2t+b−2n+f1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡r−l20000t−b20000n−f20−r−lr+l−t−bt+b−n−fn+f1⎦⎥⎥⎥⎤
Projection Transformation 矩阵就是他们的合体:
⎣⎢⎢⎢⎡xoyozo1⎦⎥⎥⎥⎤=⎣⎢⎢⎢⎡r−l20000t−b20000n−f20−r−lr+l−t−bt+b−n−fn+f1⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡n0000n0000n+f100−nf0⎦⎥⎥⎥⎤⋅⎣⎢⎢⎢⎡xyz1⎦⎥⎥⎥⎤
化成一个矩阵:
M=Mpersp→ortho⋅Mortho=⎣⎢⎢⎢⎡r−l2n0000t−b2n00−r−lr+l−t−bt+bn−fn+f100−n−f2nf0⎦⎥⎥⎥⎤
从投影到光栅化
相关概念准备
如何定义“屏幕”?“Raster”的词源?

关于像素的一些约定和定义:

视口变换
屏幕左下角定义为原点,屏幕长宽分别为height,width,所以光栅化之前要把x,y,z∈[−1,1]的正方体区域再缩放到屏幕区域内,称为视口变换:

简单,变换矩阵直接写一下不用费时间解释了:
Mviewport=⎣⎢⎢⎢⎡2width00002height0000102width2height01⎦⎥⎥⎥⎤
成像设备
CRT靠电子打像素发光

LCD靠液晶排布控制光偏振方向进而控制光是否可透过平面偏振片

LED自己发光

电子墨水控制像素的黑白

光栅化
- 何时开始光栅化:已经通过透视投影、正交投影和视口变换将场景中的所有对象都变换到x∈[0,width],y∈[0,height],z∈[−1,1]的视口区域内
- 光栅化的目标是:将上述正方体区域内的目标画在屏幕上
- 光栅化的基本思想:光栅扫描,逐像素上色
- 光栅化的基本元素:三角形


三角形的好处:
- 任何平面都可以拆成三角形
- 三个顶点组成的面必是平面
- 很容易定义三角形的“内”和“外”
- 定义了三个顶点的属性,再三角形内的任意点的属性很容易插值得到
采样
直接一个一个像素问过去是什么颜色

实践中,对屏幕空间中的像素的采样就是确定每个像素是否在三角形内

判断像素是否在三角形内
基本思想:判断点是否在三角形内=判断点在三角形三条边的左侧还是右侧=叉乘大于1还是小于1

比如,点Q在三角形内⇒
⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧P0P1×P0QP1P2×P1QP2P0×P2Q>0>0>0
优化采样效率:包围盒 (Axis Aligned Bounding Box, AABB)
包围盒就是三角形的外接四边形,三角形不可能覆盖到包围盒外面的像素,可以限制采样范围

优化采样效率:Incremental Triangle Traversal

适用于很窄的三角形,因为窄三角形会有很大的包围盒但是很少的像素
光栅化的缺点:走样(Aliasing)、锯齿(Jaggies)


如何解决:《反走样、抗锯齿(Antialiasing)》
深度缓存和z-buffering算法
画家算法:先画远处再画近处,近处盖住远处
先把所有三角形按距离相机的深度排序,由远及近绘制

缺点:三角形跨一段距离,多个三角形距离有交叉,不能简单的用一个覆盖另一个

解决方法:深度缓存 z-buffering
深度缓存的结构
就是除了RGB图片之外另外一张“图片”记录了像素所显示的颜色与像素之间的距离:

- 深度缓冲区(Depth/Z-buffer):每个像素记录了离相机最近的片元的深度(即物体三角面上离相机最近的某点的z坐标)。顾名思义,深度就是片元对应的三角面的z坐标,因此深度测试既叫 Z-Test 也叫 depth test。
- 颜色缓冲区(Color/Frame buffer),也叫帧缓冲区 :每个像素记录了当前该位置对应的颜色;
z-buffering算法

- 屏幕上的每个像素的深度缓存初始化为∞
- 对场景中的每个三角形,同时计算其覆盖范围和相对于覆盖范围内像素的深度
- 如果深度小于深度缓存内记录的深度,则更新像素颜色和深度缓存中的深度

算法复杂度
- 画家算法复杂度:排序,n个三角形复杂度O(nlogn)
- z-buffering算法复杂度:无排序,n个三角形复杂度O(n)
z-buffering算法非常棒,无关顺序,简直完美。
几乎所有光栅化都需要z-buffering算法,几乎所有的GPU都有在硬件层面实现了z-buffering算法。
z-buffering算法盲区
算法盲区:透明物体处理不了。对于半透明物体,有α-blending算法。