跳转至

透视投影变换推导

透视投影是3D固定流水线的重要组成部分,是将相机空间中的点从视锥体(frustum)变换到规则观察体(Canonical View Volume)中,待裁剪完毕后进行透视除法的行为。在算法中它是通过透视矩阵乘法透视除法(perspective division)(又叫齐次除法 homogeneous division)两步完成的。

透视投影变换是令很多刚刚进入3D图形领域的开发人员感到迷惑乃至神秘的一个图形技术。其中的理解困难在于步骤繁琐,对一些基础知识过分依赖,一旦对它们中的任何地方感到陌生,立刻导致理解停止不前。

没错,主流的3D APIs如OpenGL、D3D的确把具体的透视投影细节封装起来,比如

gluPerspective(…)就可以根据输入生成一个透视投影矩阵。而且在大多数情况下不需要了解具体的内幕算法也可以完成任务。但是你不觉得,如果想要成为一个职业的图形程序员或游戏开发者,就应该真正降伏透视投影这个家伙么?我们先从必需的基础知识着手,一步一步深入下去(这些知识在很多地方可以单独找到,但我从来没有在同一个地方全部找到,但是你现在找到了)。

我们首先介绍两个必须掌握的知识。有了它们,我们才不至于在理解透视投影变换的过程中迷失方向(这里会使用到向量几何、矩阵的部分知识,如果你对此不是很熟悉,可以参考《向量几何在游戏编程中的使用》系列文章)

1.齐次坐标

这是在图形学中普遍使用的基本技巧,我们在很多地方都会用到,比如2D位图的放大、缩小,Tweening变换,以及我们即将看到的透视投影变换等等。基本思想是:给一个x属于[a, b],找到y属于[c, d],使得x与a的距离比上ab长度所得到的比例,等于y与c的距离比上cd长度所得到的比例,用数学表达式描述很容易理解:

这样,从a到b的每一个点都与c到d上的唯一一个点对应。有一个x,就可以求得一个y。

此外,如果x不在[a, b]内,比如x < a或者x > b,则得到的y也是符合y < c或者y > d,比例仍然不变,插值同样适用。

投影变换(推导方案一)

个人更倾向这种推导

转自:https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/opengl-perspective-projection-matrix

我们从相机的原点到要投影的点 P 画一条线,该线与图像平面的交点表示投影点 Ps 的位置。在OpenGL中,图像平面位于附近的剪切平面上(而不是与相机原点精确地相差一个单位),这里是近剪切面。

( 假设相机空间下任意一点 P ,首先投影到近裁剪面所在平面的点为 Ps,然后将 Ps 变换到NDC立方体空间)

图1:我们使用相似三角形的属性来找到 Ps 的位置。

相似的三角形ΔABC 和 ΔDEF是相似的。因此我们可以这样写:

如果我们用n 替代AB,近裁剪平面,DE用 Pz替代 (P的z坐标)和EF用 Py替代(P的y坐标),我们可以将该等式重写为(等式1):

根据类似的原理得到方程式(等式2)

图2:相机的视锥范围或观看量由相机的视场,近和远裁剪平面以及图像纵横比定义。在OpenGL中,点投影在视锥的正面(靠近剪切平面)上。

计算 x和y的范围([-1,1])


现在我们有两个值 Psx 和 Psÿ我们仍然需要解释它们与OpenGL透视矩阵的关系。投影矩阵的目标是将投影到图像平面上的值重新映射到一个单位立方体(一个最小和最大范围分别为(-1,-1,-1)和(1,1,1)的立方体)。但是,一旦将点P投影到图像平面上,如果其x和y坐标包含在x的[left,rigtht]和y [bottom,top]的范围内,则Ps是可见的。如图2所示。这些坐标定义了可见点(包含在视锥体中并投影在图像平面上的所有点)在图像平面上的界限或边界。如果我们假设PsX 是可见的,那么我们可以这样写:

20210422231355 然后对该不等式进行变换, 20210422231700 再进行变换 20210422232350

20210422232410

20210422232427

20210422232503

20210422232533

20210422232555

20210422232623 上面不等式都乘以Pz后得到新的不等式,我们用矩阵的形式对不等式编码。将不等式的第一项系数和第二项系数替换矩阵的第一行第一列和第三列的系数,得到的结果:

20210422232907

请记住,OpenGL矩阵使用colum-major排序,因此我们将不得不使用列向量在矩阵的右边以及点坐标处写入乘法符号:

20210422233100

根据矩阵计算出Psx结果: 20210422233428

然后除以 -Pz

20210422233740

根据上面的推导,我们可以推导出Psy 的不等式:

20210422233859

然后将系数带入矩阵 得出结果:

20210422233957

根据矩阵运算得出:

20210422234048

然后除以 -Pz后: 20210422234104

计算 z的范围([-1,1])


我们剩下要做的就是找到一种将投影点的z坐标重新映射到[-1,1]范围的方法。我们知道P的x和y坐标对投影点z坐标的计算没有帮助。因此,将要与P x和y坐标相乘的矩阵第三行的第一和第二系数必须为零(绿色)。我们在矩阵中剩下两个未知的系数A和B(红色)

如果我们写方程式来计算 Ps然后除以 -Pž后 使用这个矩阵,我们得到(记住 Psž 也除以 Psw 当点从齐次坐标转换为笛卡尔坐标时,同时 Pw = 1):

我们需要找到A和B的值。希望我们知道 Pž 躺在附近的裁剪平面上, Psž 需要重新映射为-1以及当 Pž 躺在远的剪裁平面上, Psž 需要重新映射为1。因此,我们需要替换 Psž 经过 ñ 和 F 在方程中得到两个新方程(请注意,投影在像面上的所有点的z坐标为负,但 ñ 和 F 是正数,因此我们将使用 − n 和 - f 取反):

让我们求解方程式(1)中的B:

  B = − n + A n 。

并用该等式代入方程式(2)中的B:

  -fA − n + A n = f。

然后求解A和B:

我们可以替换在矩阵中为A和B找到的解决方案,最后得到:

这就是OpenGL透视投影矩阵

z-fighting

图3:投影点的z坐标的重新映射是非线性的。此图显示了结果Psž 对于Near = 1和far = 5。

z坐标的重映射(我们选择将z重映射到[-1,1]范围。从技术上讲,您可以将其重新映射为所需的任何内容,但[-1,1]也是常见的选择。)具有以比距离更远的点更高的数值精度来表示距离摄像机更近的点的属性。当缺乏数值精度导致某些相邻样本在投影到屏幕后,当它们的z坐标在世界上时,它们具有相同的深度值时,此属性可能会成为问题。空间实际上是不同的,这个问题称为 Z战 (z-fighting) 。这个问题不能真正解决(我们始终局限于可以存储在单精度浮点数中的精度,尽管如果将近剪切面和远剪切面分别设置得尽可能接近,则可以将问题最小化。场景中最接近和最远的物体,这就是为什么总是建议调整剪切平面的原因)。 最后翻译可能不准确,这里给出原文: (we are always limited to the precision that can be stored in a single-precision floating-point number though the problem can be minimised if the near and flar clipping planes are fit respectively as closely as possible to the nearest and furthest object visible in the scene. This is the reason why adjusting the clipping planes is always recommended.)

简单概括透视投影变换

  • 先变换x,y
  • 将透视视椎体P投影到近裁剪面上P1
  • 将在近裁剪面上P1变换到NDC正方形[-1,1]范围内
  • 再变换z
  • 使用 (A*z+B)/(-Pz) 的代数形式根据取值范围[-1,1]求解A和B

注意求解A和B时:
方程式:
(A(-n)+B)/(-(-n))=-1
(A
(-f)+B)/(-(-f))=1
为啥-n带入后是-1,因为是从相机空间(右手坐标系)变换到ndc空间(左手坐标系),因为坐标系变换所以不等式代入-n后对应ndc空间是-1,不等式代入-f后对应ndc空间是1
20210424144836
20210421231724

Unity中透视投影变换


根据上面推导的结果按照右边yz平面计算视锥体,得出最终Unity中用的透视矩阵:

按照OpenGL计算的,不讨论DirectX

OpenGL Projection Matrix