一、参考资料

投影(矩阵变换)是将3D转化2D的核心过程,可以把这个过程想象成这样:

OpenGL_投影变换矩阵 - 图2
把摄像机比作人的眼睛,它决定了我们看向何处,还有看的方位,举个例子,我们躺在床上看天花板的灯,头在床头、头在床尾,还有横躺着看,在我们眼里灯好像旋转了一样。

六个面分别是:上下、左右、远近平面(Near/Far Plane)
眼睛所能看到的范围是近平面(Near Plane)、远平面(Far Plane)、上平面(Top Plane)、下平面(Bottom Plane)、左平面(Left Plane)、右平面(Right Plane)包围的的空间,叫做视景体(View Volume)、视体(viewing frustum)、平截头体(Frustum)、或者视锥。在视景体范围之外的物体,我们是看不到的(视野有限,被裁剪掉)。

物体成像在近平面(NearPlane)上,物体模型的顶点和眼睛的连线相交于近平面上的一点,该点就是顶点在我们眼睛里的成像。

有两种投影变换效果,正交投影(Ortho Projection)和透视投影(Perspective Projection)。这两种变换用于不同的场景。

二、正交投影(Ortho Projection

也叫正射投影,Orthographic Projection Matrix。
正交投影的视景体是长方体形状。物体模型的顶点是以垂直于近平面的方向,投影到近平面。
OpenGL_投影变换矩阵 - 图3
最终的投影效果是:不管物体位置的远近,物体的投影(成像)大小都是物体的实际大小,这会造成很不真实的感觉。比如你看很远和很近的人都是一样大。适用于一些没有远近概念的场景,要表现物体真实大小的场景,比如:

  • 2D游戏
  • 几何图形展示

    三、透视投影(Perspective Projection

    透视投影的视景体,我们一般叫做平截头体,就像一个削了顶的金字塔形状。
    OpenGL_投影变换矩阵 - 图4
    FOV(Filed of View),视野,在投影矩阵构建函数中的参数,一般设置为OpenGL_投影变换矩阵 - 图5,可以达到比较真实的效果。
    投影效果最显著的特征就是近大远小(远的物体,在x、y、z方向上都会被一定程度的压缩),叫做透视缩略,符合人类的视觉模型。
    一般用于3D模拟。
    OpenGL_投影变换矩阵 - 图6

下图展示了正交投影和透视投影的投影效果。

OpenGL_投影变换矩阵 - 图7

四、求解投影矩阵

(一)透视投影矩阵

image.png

坐标系为观察(视图)坐标系,摄像机位置在坐标系原点,摄像机朝向-Z轴方向。
视景体是一个平截头体(削了顶的金字塔)。
OpenGL_投影变换矩阵 - 图9
OpenGL_投影变换矩阵 - 图10
近平面方程:OpenGL_投影变换矩阵 - 图11
远平面方程:OpenGL_投影变换矩阵 - 图12

设观察空间(View Space)的某点坐标为OpenGL_投影变换矩阵 - 图13,这是投影矩阵上一步的视图变换输出的坐标,投影到近平面上的投影坐标为OpenGL_投影变换矩阵 - 图14
下面两图分别是视景体俯视/侧视图:
OpenGL_投影变换矩阵 - 图15OpenGL_投影变换矩阵 - 图16
根据相似三角形关系有:

OpenGL_投影变换矩阵 - 图17

OpenGL_投影变换矩阵 - 图18

为了让投影坐标可以灵活映射到指定大小的显示区域(视口,ViewPort)上,科学家们想到一个很好的办法,在中间加入一个适配环节(归一化),就是说提出一个标准设备坐标(NDC,Normal Device Coordinates)的概念,将显示区域的OpenGL_投影变换矩阵 - 图19坐标设定在OpenGL_投影变换矩阵 - 图20的范围,即显示区域的左下角坐标为OpenGL_投影变换矩阵 - 图21,右上角坐标为OpenGL_投影变换矩阵 - 图22。用数学来表达就是:
OpenGL_投影变换矩阵 - 图23,有OpenGL_投影变换矩阵 - 图24,其中OpenGL_投影变换矩阵 - 图25为线性函数,且有:
OpenGL_投影变换矩阵 - 图26
OpenGL_投影变换矩阵 - 图27
我们可以求出OpenGL_投影变换矩阵 - 图28如下:

OpenGL_投影变换矩阵 - 图29
OpenGL_投影变换矩阵 - 图30

代入等式1、2,有:

OpenGL_投影变换矩阵 - 图31

OpenGL_投影变换矩阵 - 图32

设裁剪坐标OpenGL_投影变换矩阵 - 图33(先别管为啥叫裁剪),投影矩阵OpenGL_投影变换矩阵 - 图34

OpenGL_投影变换矩阵 - 图35(等式4.5),我们现在要从结果逆一步步推出矩阵OpenGL_投影变换矩阵 - 图36的结构,这个裁剪坐标就是投影变换后的结果,然后我们需要再将裁剪坐标转换成OpenGL_投影变换矩阵 - 图37坐标,我们从等式3、4的结果中令

OpenGL_投影变换矩阵 - 图38

OpenGL_投影变换矩阵 - 图39

则有OpenGL_投影变换矩阵 - 图40

我们可以用齐次坐标的第四个分量来保存一个数据,令OpenGL_投影变换矩阵 - 图41,有:

OpenGL_投影变换矩阵 - 图42

OpenGL_投影变换矩阵 - 图43

我们可以逆向推出矩阵OpenGL_投影变换矩阵 - 图44的一部分:

OpenGL_投影变换矩阵 - 图45

仔细查看等式5、6,结合矩阵与向量乘法特性,我们又可以逆推出矩阵OpenGL_投影变换矩阵 - 图46的一部分:

OpenGL_投影变换矩阵 - 图47

我们还需要解出矩阵的第三行,我们可以填充假设变量,如下:

OpenGL_投影变换矩阵 - 图48

根据矩阵与向量乘法规则,有:

OpenGL_投影变换矩阵 - 图49

我们可以知道,对于视景体内任意点,它的x、y坐标的值不应该影响到投影后坐标的z值,所以OpenGL_投影变换矩阵 - 图50,有:

OpenGL_投影变换矩阵 - 图51

然后有OpenGL_投影变换矩阵 - 图52

根据等式7、8,为了保持计算的统一,显然我们很容易联想到:OpenGL_投影变换矩阵 - 图53,将等式12代入,有:

OpenGL_投影变换矩阵 - 图54

我们知道,OpenGL_投影变换矩阵 - 图55,且有:

OpenGL_投影变换矩阵 - 图56时,OpenGL_投影变换矩阵 - 图57
OpenGL_投影变换矩阵 - 图58时,OpenGL_投影变换矩阵 - 图59

分别代入等式13,同时我们可以令OpenGL_投影变换矩阵 - 图60有:

OpenGL_投影变换矩阵 - 图61

OpenGL_投影变换矩阵 - 图62

解二元一次方程,我们可以得到:OpenGL_投影变换矩阵 - 图63OpenGL_投影变换矩阵 - 图64,代入等式13得到:
OpenGL_投影变换矩阵 - 图65
OpenGL_投影变换矩阵 - 图66
同上面等式9的逆推技巧,我们可以令OpenGL_投影变换矩阵 - 图67,则有:

OpenGL_投影变换矩阵 - 图68,我们可以逆推出矩阵的OpenGL_投影变换矩阵 - 图69元素:

OpenGL_投影变换矩阵 - 图70

至此,我们已经推算出针对一般情况的投影矩阵:

OpenGL_投影变换矩阵 - 图71

OpenGL_投影变换矩阵 - 图72
OpenGL_投影变换矩阵 - 图73
近平面方程:OpenGL_投影变换矩阵 - 图74
远平面方程:OpenGL_投影变换矩阵 - 图75

设,NDC坐标OpenGL_投影变换矩阵 - 图76
观察空间(View Space)任意某点坐标的齐次坐标OpenGL_投影变换矩阵 - 图77
裁剪坐标OpenGL_投影变换矩阵 - 图78的齐次坐标OpenGL_投影变换矩阵 - 图79,有:

OpenGL_投影变换矩阵 - 图80OpenGL_投影变换矩阵 - 图81

假如视景体的坐标是对称的,即OpenGL_投影变换矩阵 - 图82OpenGL_投影变换矩阵 - 图83,投影矩阵可简化为:

OpenGL_投影变换矩阵 - 图84

深度值函数

我们知道NDC坐标的z坐标值其实就是后续深度测试(Depth Test)要用到的深度值(Depth)
回到上面的等式14,我们有:

OpenGL_投影变换矩阵 - 图85

OpenGL_投影变换矩阵 - 图86

我们可以看到观察空间的点的z坐标OpenGL_投影变换矩阵 - 图87和深度值depth之间并不是线性函数,根据该函数的曲线分布特性我们知道,当ze越靠近近平面,深度值有更好的精度,而越靠近远平面,精度越低。如果远近平面的距离越大,即OpenGL_投影变换矩阵 - 图88的范围越大,在靠近远平面的地方越容易发生Z冲突(Z-fighting,在靠近远平面的点投影变换后得到的深度值的差异非常小,如果数据值超过了支持的精度,则会认为这两个深度值相等)。我们应该尽可能地的缩小OpenGL_投影变换矩阵 - 图89的范围,如下图解释:
OpenGL_投影变换矩阵 - 图90

(二)正交投影矩阵

image.png

坐标系为观察(视图)坐标系,摄像机位置在坐标系原点,摄像机朝向-Z轴方向。
视景体为一个长方体。
OpenGL_投影变换矩阵 - 图92
OpenGL_投影变换矩阵 - 图93
近平面方程:OpenGL_投影变换矩阵 - 图94
远平面方程:OpenGL_投影变换矩阵 - 图95

视景体内的点是垂直投影到近平面上,因此我们有:
OpenGL_投影变换矩阵 - 图96,很显然z坐标也应该是线性映射关系,我们可以令OpenGL_投影变换矩阵 - 图97

同上透视投影矩阵求解过程中关于投影坐标与NDC坐标之间的映射关系,我们有:

OpenGL_投影变换矩阵 - 图98,有如下关系:

OpenGL_投影变换矩阵 - 图99
OpenGL_投影变换矩阵 - 图100
OpenGL_投影变换矩阵 - 图101

其中OpenGL_投影变换矩阵 - 图102为线性函数,且有:

OpenGL_投影变换矩阵 - 图103
OpenGL_投影变换矩阵 - 图104
OpenGL_投影变换矩阵 - 图105

我们可以求出OpenGL_投影变换矩阵 - 图106如下:

OpenGL_投影变换矩阵 - 图107

OpenGL_投影变换矩阵 - 图108

OpenGL_投影变换矩阵 - 图109

将投影坐标替换成被投影的点的坐标(观察坐标),有

OpenGL_投影变换矩阵 - 图110

OpenGL_投影变换矩阵 - 图111

OpenGL_投影变换矩阵 - 图112

同上面等式9的推理,我们可以推出裁剪坐标、正交投影矩阵、观察坐标之间的关系:

OpenGL_投影变换矩阵 - 图113

其中OpenGL_投影变换矩阵 - 图114

OpenGL_投影变换矩阵 - 图115
OpenGL_投影变换矩阵 - 图116
OpenGL_投影变换矩阵 - 图117

同样的,如果视景体的坐标是对称的,即OpenGL_投影变换矩阵 - 图118,则正交投影矩阵结构如下:

OpenGL_投影变换矩阵 - 图119