0%

光栅化渲染中的相机投影变换

基本数学与理论工具

计算机图形学(CG,computer graphics)是一门高度数学化的学科,包含了非常多的理论概念,下面会以坐标系为切入点,讨论一些常用的数学和理论工具,这对于理解文中内容来说足够了。

坐标系

如果想要描述某个三维空间中,所有物体的空间位置,最简单的做法就是以空间中的某个点作为原点(origin),为这个空间建立起一个三维的笛卡尔坐标系(Cartesian coordinate system),这是一种正交坐标系统,存在 三个维度(dimension)主轴(axis)彼此垂直,在确定了每个主轴的方向(direction)以及尺度(scale)后,这个坐标系就建立完成了。

根据正轴方向的组合方式,有两种不同的约定,即左手坐标系(left-handed coordinate system)右手坐标系(right-handed coordinate system),它们的差异主要在于如何选择 Z 轴的正方向,并没有优劣之分,只是习惯的不同。

3D_Cartesian_Coodinate_Handedness

from: Cartesian coordinate system - Wiki

可以摆出图中左手坐标系的手势,然后转动手腕使中指指向屏幕,此时 Z 轴的正方向是我们的前方,如果切换为右手坐标系的话,需要保持右手食指和拇指的方向与左手的一致,即 X、Y 轴的正方向保持不变,此时右手中指就指向了我们的后方,这使得 Z 轴的正方向反了过来,因此左手坐标系更符合人眼在现实世界的观察习惯,而右手坐标系则更适合于纸面上的一些数学描述,可从下图理解这一点。

left_right_handed_coordinate
left_right_handed_coordinate

根据惯例,DirectX 使用的是左手坐标系,而 OpenGL 使用右手坐标系,不过 OpenGL 的归一化设备坐标系(NDC)会采用左手坐标系

坐标系分层

上面仅定义了一个全局坐标系(global coordinate),或者说是绝对坐标系(absolute coordinate),这实际上并不足够灵活

假设空间中存在一个物体,物体中的所有点可看作是一个整体,即使物体在空间中平移或旋转,它们之间的相对距离都保持不变,如果仅使用全局坐标系来描述这个物体的话,不能直观地从各个点的绝对坐标值来理解它们之间所保持的相对关系,当物体发生了平移或旋转,这些点的绝对坐标也会随之变化。

一种灵活的做法是,选取物体局部空间中的某个位置作为原点(比如物体的中心),来为物体定义一个局部坐标系(local coordinate),也可称为物体坐标系(object coordinate),物体的所有点将使用相对坐标(relative coordinate)来描述,即相对物体原点的坐标值。

local_coordinate
local_coordinate
《对比顶点采用绝对坐标或相对坐标存储时,物体整体平移后的坐标值变化》

这种做法不仅可以直观地体现出物体的内部联系,当物体整体发生平移或旋转时,即使点的绝对坐标值发生了变化,相对坐标值也还是保持不变。

将整个物体映射到全局坐标系的方法非常简单,只需确定物体原点的绝对坐标以及物体在全局坐标系的旋转状态,就可以将所有点的相对坐标映射为绝对坐标。

这在为三维物体进行建模时非常有用,可以在物体的局部坐标系(模型坐标系)中进行建模,并把物体的原点信息以及所有顶点的相对坐标导出,在使用的时候再映射到三维空间中,即使物体在空间中移动或旋转,只需更新原点位置和旋转状态即可,不用直接更新顶点所存储的相对坐标,这可以节省大量存储写入开销,并且兼容性非常好,能轻易地迁移到别的应用场景中。

这种坐标系的局部化,是可以无限制地继续分层(level)的,特别是对于像人物模型之类的复杂三维模型来说,分层所带来的灵活性,使问题被大大简化。

在三维渲染中,按照惯例也定义多种不同的坐标系,比如下面这些:

  • 模型坐标系(model space)或称物体坐标系(object space)
  • 世界坐标系(world space)
  • 相机坐标系(camera space)观察者坐标系(view space)
  • etc.

这里简要说一下,我们的每个物体都处于世界坐标系下, 但物体存在局部空间,因此物体内的其它对象想要渲染,就需要先变换到世界坐标系下,这属于从局部空间到全局空间的映射。但我们在渲染之前,还需要根据相机在世界坐标系下的视野范围,将范围内的对象从世界坐标系变换到相机坐标系下,这属于从全局空间到局部空间的映射,映射到相机坐标系之后就可以进行后续的变换了,这里不继续讨论。

向量与矩阵

有了坐标系之后,就可以用向量(vector)来描述物体的空间坐标,由于前面我们定义的是一个三维坐标系,所以实际使用的应该是三维向量

我们还可以通过矩阵(matrix)来为向量进行诸如平移(translate)旋转(rotate)缩放(scale)投影(projection)之类的变换(transformation)

单位矩阵(identity matrix)是一种特殊的矩阵,其主对角线上的元素全部为 ,而其余元素全为 ,类似于数字 在乘法中的作用,任何矩阵或向量与单位矩阵相乘,都等于原来的矩阵或向量,一个 的单位矩阵如下: 合法的矩阵乘法(matrix multiplication),需要左边矩阵的列数右边矩阵(或列向量)的行数匹配,而得到的结果中,行数等于左边矩阵列数等于右边矩阵,每一个元素相当于左边矩阵对应的行向量(row vector)与右边矩阵对应的列向量(column vector)点积(dot product),比如:

矩阵乘法可总结为以下表达式: 使用矩阵为向量做变换的方法就是让它们相乘,可先把向量变成一个列向量,即仅有一列的矩阵,然后再与矩阵相乘,比如一个 矩阵与一个三维向量相乘的方法如下: 可以把多个不同变换的矩阵通过相乘拼接为一个复合变换矩阵(combined transformation matrix),这样就可以一次性为向量做多个不同的变换,不过矩阵拼接的顺序需要注意,因为矩阵乘法不满足交换律(commutative property),两个矩阵左乘和右乘所得到的结果是不同的,即 ,不过矩阵乘法满足结合律(associative property),即 ,此时不管从左边还是右边开始进行乘法,结果都是相等的,只要保证矩阵结合的先后顺序即可。

有的矩阵变换是可逆的,如果计算出原本矩阵的逆矩阵(inverse matrix),就可通过逆矩阵来执行与原本矩阵相反的变换,这样就可以在不同的空间之间来回映射,比如一个 TRS 变换的逆矩阵为以下形式: 需要注意的是,并不是所有的矩阵都可逆,计算逆矩阵的其中一种方法如下:

  1. 先求出原本矩阵的余子式矩阵(cofactor matrix)
  2. 把余子式矩阵转换为代数余子式(余因子)矩阵后,为其执行矩阵转置(transpose),从而得到伴随矩阵(adjugate matrix)
  3. 计算原本矩阵的行列式(determinant),把伴随矩阵与行列式的倒数相乘,从而得到原本矩阵的逆矩阵

矩阵变换与齐次坐标系

本篇文章主要关注平移(translate)旋转(rotate)缩放(scale)以及投影(projection)四种变换,而要想把这些变换组合到一起,去给一个三维向量执行变换的话,仅采用一个 矩阵是做不到的,所以我们一般会使用 矩阵来组合它们,但是这样就无法与三维向量相乘了。

因此,我们还需要将原本欧氏坐标系中的三维向量(3D vector)转换到齐次坐标系(homogeneous coordinates)中,变为一个四维向量(4D vector),即给原本的三维向量增加一个维度 其它三个分量再乘以 ,不过一般我们会把 初始化为 ,所以此时的乘法可以省略。 将齐次坐标再转换回原本欧氏坐标的方法很简单,即将所有分量除以 ,并去掉 分量,从而投影回三维向量采用这种齐次坐标系的做法,不仅可以确保为透视执行变换的过程是线性(linear)的,而且通过矩阵相乘的方式,可以将平移旋转缩放透视等各种变换都连接起来,把变换链(transformation chains)组合到单个矩阵中一起执行,从而提供了一种非常简洁的方法来表达和操作变换。

投影矩阵会放到后面讨论,而旋转矩阵由于较为复杂,绕不同轴进行旋转的构造方式都不一样,在文章中其实也不需要考虑其具体构造方法,所以这里不讨论旋转矩阵的构造,以下给出平移缩放变换的 矩阵形式: 它们给齐次坐标执行变换前后的变化如下:

矩阵的拼接顺序

这里先抛开投影变换不谈,来看下平移(Translate)旋转(Rotate)以及缩放(Scale)组合顺序问题,一般有 TRSSRT 两种,不过这指的仅是矩阵的相乘顺序,并不是与向量的结合顺序,由于矩阵与向量相乘时,我们一般采用列向量,形式为 ,因此向量是从右边开始与矩阵结合的,也就是说变换的顺序也是从右边开始的,那么 TRSSRT 执行变换的顺序分别为:

  • TRS先缩放再旋转后平移

  • SRT先平移再旋转后缩放

trs_srt
trs_srt

这个顺序的先后非常重要,主要原因是像旋转和缩放这样的变换通常都是相对于坐标系“原点”进行的,组合顺序不一样会导致变换的结果不同。

这个顺序其实可以换一个角度来看,比如说 ,如果以物体自身坐标系的角度来看的话,就是先将自身坐标系缩放、旋转,而在平移的时候,也是根据自身坐标系进行,那么结果跟以父坐标系的“先平移再旋转后缩放”是一样的

有时候也可以直接以变换的顺序来称呼这些组合,到底怎么称呼其实是习惯问题,不过很容易混淆,比如: 此外,TRS 的逆矩阵会变成 SRT,而SRT 的逆矩阵则会变成 TRS,如下: 对于模型坐标系中的一些变换、将模型映射到世界坐标系(world space)所做的世界变换(world transformation)等,通常都是采用 TRS 的矩阵拼接顺序,即 SRT 的变换顺序,因为一般在为对象执行旋转或缩放的时候,我们更倾向于使这些变换相对于对象自身而言,这更符合我们的习惯,可以从上图体会到这一点。

不过,在执行视变换(viewing transformation)时,即将世界坐标系的对象映射到相机坐标系(camera space)时,有些不一样,首先,视变换并不用考虑缩放的问题,只需要考虑平移和旋转

在为相机构造视变换矩阵(view matrix)的时候,我们为其提供的是相机的世界坐标,如果只是把相机自身映射到世界坐标的话,那视变换矩阵与模型矩阵(model matrix)的构造方式是一样的,只是不用插入缩放矩阵。

但视变换的目标是要把世界坐标系中的对象映射到相机的局部空间中,因此视变换其实是相机执行世界变换的逆变换,那视变换矩阵的构造方法就是,先求出相机的世界变换矩阵,再求这个矩阵的逆矩阵,如下: 从上面这个角度来看的话,如果我们想从子坐标系映射到父坐标系,一般会选择用 TRS 拼接顺序,而从父坐标系映射到子坐标系的话,由于是一个逆变换,所以会是一个 SRT 拼接顺序的矩阵。

此外还需要注意一点,那就是我们在执行 MVP 组合变换的时候,实际的拼接顺序应该是: 而执行变换的顺序为,先执行世界变换,再到视变换,最后投影变换

总的来说,矩阵的拼接顺序需要注意以下几点:

  • 把希望优先执行的变换放到右边,即越早与向量结合的变换,越早执行
  • 如果希望从子坐标系映射到父坐标系,通常采用 TRS 的矩阵拼接顺序、SRT 的变换顺序
  • 如果希望从父坐标系映射到子坐标系,比如视变换,可以先拼接一个从子到父的 TRS 矩阵,然后求逆矩阵,得到一个 SRT 拼接顺序的矩阵

由于后续会用到 ST(scale,translate)的矩阵拼接,这里先给出 STTS 两种拼接结果的对比:

欧拉角与四元数

有些时候,我们需要将平移、旋转、缩放这些变换持久化存储起来,使用一个 矩阵来存储并不是一个好方法,因为其比较大,对于平移和旋转我们可以各自使用一个三维向量来描述,而旋转有两种方式可以选择,即欧拉角(Euler angles)以及四元数(quaternion)

pitch_roll_yaw
pitch_roll_yaw
《欧拉角》
from: Sensors to detect motion in three dimensions

在欧拉角中,通常会把 X、Y、Z 轴分别称为俯仰轴(pitch axis)偏航轴(yaw axis)以及翻滚轴(roll axis),而绕这些轴旋转的角度分别称为俯仰角(pitch angle)偏航角(yaw angle)以及翻滚角(roll angle),不过,这些叫法并非是固定的,使实际使用情况而定。

Euler2a
Euler2a
from: Euler angles - Wiki

欧拉角的描述方法非常简单,即绕各个轴的旋转角度,在执行旋转的时候,会根据一定的顺序每次绕一个轴进行旋转,如上图。

在右手坐标系中,欧拉角逆时针旋转为正角度值,而在左手坐标系中,则是顺时针旋转为正角度值。

欧拉角的旋转方式(或组合方式)主要以下两类:

  • 泰特布莱恩角(Tait–Bryan angles):(x-y-zy-z-xz-x-yx-z-yz-y-xy-x-z
  • 经典欧拉角(Poroper/classic Euler angles):(z-x-zx-y-xy-z-yz-y-zx-z-xy-x-y
Gimbal_Lock_Plane
Gimbal_Lock_Plane
《万向锁》
from: Gimbal lock - Wiki

欧拉角可以很直观地描述旋转,可读性非常强,但会受到万向锁(gimbal lock)的影响,当依次施加三个旋转时,第一个或第二个旋转可能导致第三个轴的方向与先前两个轴之一相同,这意味丢失了一个轴上的自由度(degree of freedom),因为不能围绕唯一轴应用第三个旋转值。

除了欧拉角和矩阵外,还可以使用四元数来描述旋转,这本质上是一个四维度超复数(hypercomplex number),拥有三个虚部一个实部,这实际是相当于用三个虚部描述了一个三维向量,即旋转轴(rotation axis),而实部描述了绕这个旋转轴发生旋转的角度(angle)

四元数的定义如下: 其满足以下性质: 我们在实际应用时会定义为以下形式,其 为四元数的三个向量分量,则是四元数的旋转分量: 这四个分量满足以下条件: 而它们的计算方法为: 其中 为规范化的轴向量,它们满足 ,而 则是旋转角。

四元数描述的是绕轴旋转一次,因此不会导致欧拉角那样的万向锁问题,不过其不能表示在任何方向上超过 的旋转

欧拉角、四元数以及旋转矩阵之间可以互相转换,各有优缺点,由于欧拉角更为直观,所以在一些游戏引擎的 inspector 上通常会采用欧拉角来表示,不过在内部将使用四元数描述,其实多数的实现在为旋转变换做持久化的时候,基本也都是采用四元数,而在实际执行变换的时候,视情况可能会直接使用四元数执行,但多数情况应该会转换为矩阵,与其它变换组合在一起。

三维虚拟相机

一个三维场景中所放置的相机(camera)可用来模拟人类的眼睛,其可视范围内所能见到的景象,就是渲染到屏幕上的画面,像游戏这类应用场景,都是采用实时渲染(real-time rendering)的工作方式,相机会跟随着角色一起平移(translation)旋转(rotation)以切换视角,并以锁定帧率(limited FPS,locked frames per second)不锁帧(unlimited FPS)的方式,定期或不断地重新渲染,以更新屏幕上的可视图像。

这些虚拟相机通常都是基于理想化针孔相机模型(pinhole camera model)合成相机模型(synthetic camera model),不过,除非需要模拟真实物理相机,否则在传统三维渲染中,一般情况下并不会使用诸如焦距、成像平面或投影平面之类的概念,这是由于传统三维渲染的机制与真实物理相机的成像机制是有区别的,因此它们在概念的使用上存在一些差异。

物理相机主要考虑的是怎样有效的捕捉光信号并将其转化为电子或其它类型的信号,而传统三维渲染中的虚拟相机更多的是一个数学模型,其考虑的是如何根据透视(perspective)正交(orthographic)的投影关系,把三维场景中的对象投影(projection)到二维平面上。

实际上,在给三维场景中的某一个顶点执行投影变换之前,需要先将其变换到相机的相机坐标系(camera space)或称观察者坐标系(view space)下。

一般来说,一个物体有其自身的物体坐标系(object space)或称局部坐标系(local space),而局部空间内的那些顶点想要进行渲染的话,得先通过世界变换(world transformation)或称模型变换(modeling transformation),映射到世界坐标系(world space)下,涉及的变换有 缩放旋转平移SRT),而矩阵的拼接顺序为 TRS

处于世界坐标系下的顶点,可直接进行取景变换(view space transformation)或称视变换(viewing transformation),通过平移旋转,映射到相机坐标系,该变换矩阵实际是为相机世界变换矩阵的逆矩阵。

当顶点处于相机坐标系后就可以为其执行投影变换了,不过这需要把以下两个操作考虑进去:

  • 根据相机的可视空间体(view volume)对图元进行剔除(culling)裁剪(clipping)
  • 处理各图元之间的遮挡(occlusion)关系,比如说某个物体被另一个物体完全遮挡住了,那么该物体就不应该被渲染出来

由于相机所提供的数学模型本质只是一种几何变换(geometry transformation),所以这两个操作并不是由相机的投影变换所完成的,排除某些优化外,剔除和裁剪通常在投影变换之后立刻进行,而遮挡问题会交给后续的渲染流程来处理,如深度测试(depth testing),不难发现,这些处理其实都依赖顶点坐标 分量所提供的深度(depth)信息,另外,在实现半透明或其它一些特效时也经常会用到深度信息,所以顶点在经过投影变换后的深度信息是需要保留下来的

实际上,相机的投影变换(projection transformation)并不会直接将对象投影在一个二维平面上,而是将顶点映射到一个裁剪空间(clip space)中,这使得顶点的深度信息得以保留并方便后续的处理,因此相机的投影变换也称为裁剪变换(clip transformation)

裁剪空间也称为齐次裁剪空间(homogeneous clip space),这是一个齐次坐标系(homogeneous coordinates),坐标点使用四维向量(4D vector)来描述,并使用一个 投影矩阵(projection matrix)或称裁剪矩阵(clip matrix)来描述投影变换,关于齐次坐标的概念,在前面已经提到过了,这里不再重复。

对于一个正交相机(orthographic camera)来说,其投影变换为正交投影(orthographic projection),所做的变换非常简单,为平移缩放的组合。

  • 平移操作可理解为将整个空间平移,使相机可视体原点(如可视体的中心)与相机坐标系原点重合,这相当于将顶点映射到了可视体的坐标系中
  • 缩放操作可理解为将整个空间缩放,使相机可视体的长、宽、高被缩放成某个单位长度(如 或半长为 ),这相当于将正交相机原本的可视体,从长方体缩放成了单位大小的立方体

之所以结合可视体并以其为主体来描述变换,仅是为了便于理解,实际执行变换的其实是顶点,可结合下图来理解这种变换,现实的实现可能会采用不同的原点位置或立方体大小,图中选择以相机可视体的中心为其原点,缩放后的边长为 ,因此,在变换后,处于可视体内(视野范围内)的每个点,在各个轴上的取值范围都为

orth_camera_projection_transformation
orth_camera_projection_transformation
note:这是在笛卡尔坐标系下的侧视图,绿点代表可视体内的一个顶点,蓝色则位于可视体外

透视相机(perspective camera)比正交相机多了一个透视的变换,因此透视投影(perspective projection)透视平移缩放的组合。

透视相机的可视体是一个平截头体,而透视所做的,就像是将平截头体“挤压”成了跟正交相机可视体一样的长方体,如图所示:

persp_camera_projection_transformation
persp_camera_projection_transformation
note:这是在笛卡尔坐标系下的侧视图

这实际是根据相似三角形的原理,将每个顶点的 X、Y 分量进行了映射,处理了每个对象之间的透视关系,另外,从上图可发现,在透视之后,绿色顶点的 Z 分量稍微往右边挪了一下,这是因为对深度的映射通常有些特殊,一般会采用非线性(non-linear)的映射方式,这主要是为了避免浮点数精度问题引起的深度冲突(z-fighting),详细原因会在之后重新提到。

当透视相机的可视体被压成了跟正交相机一样的长方体后,可看作是将透视相机变成了一个正交相机,不过视体内的顶点已经根据透视处理好了,满足透视关系,而接下来的平移和缩放,就是使其进一步变成一个规范化的正方体,目的跟正交相机的正交投影是一样的,因此,透视相机的透视投影相当于是透视正交投影组合

当对顶点执行完投影变换之后,相机所提供的数学模型其实已经算是完成了其自身的任务,此时顶点已经被映射到了一个齐次裁剪空间中。

但要注意的是,由于以上两种投影变换都是在齐次坐标系下进行的,一个 矩阵与一个四维向量相乘,得到的仍是一个齐次坐标系下的四维向量,在完成剔除和裁剪之后,后续的渲染流程真正需要的是一个欧氏(笛卡尔)坐标系下的三维向量,也就是说,完整的正交投影或透视投影并没有实际完成,投影实际是发生在从四维向三维降维的时候。

假设有一个齐次坐标 ,其变回欧氏坐标的方法是,将所有分量除以 ,并去掉 分量,从而降为三维向量 ,可见齐次坐标的其它三个分量充当了分子(numerator),而 分量充当了分母(denominator)

这个特性非常有用,由于投影变换最终是将相机的可视体变成一个边长为单位长度的立方体,并且所有顶点随着可视体一起变换,这意味着,变回到欧氏坐标系后,位于可视体内的所有顶点,它们在 轴上的取值范围是一样的,如 ,如果选择以可视体的中心为坐标系原点的话,则是

也就是说,将齐次坐标的 分量除以分母 ,就是对坐标进行归一化处理,分量相当于起到了边界(boundary)的作用,当一个顶点变换到裁剪空间后,如果其 某个分量的绝对值大于 ,就意味着该顶点处于相机的可视体之外。

这就是为什么对图元的剔除裁剪是在裁剪空间中进行的原因,也表明了这个空间为啥叫裁剪空间,判断顶点是否位于可视体内的依据非常简单: 剔除和裁剪的操作会由硬件(GPU)自动完成,被保留下来的顶点都位于相机的视野范围内,这些顶点会通过除以 分量的方式,从四维降回到三维,映射到一个三维的归一化设备坐标空间(NDC space,normalized device coordinate space)中,并往渲染管线的后续流程传递下去,这同样也是由硬件来完成的。

  • 对于透视投影来说,这个过程称为透视除法(perspective divide),此时才实际完成了透视投影
  • 对于正交相机所做的正交投影来说,这一步通常是没必要的,直接把第四维分量去掉就行了,因为顶点经过正交相机的投影变换后,在裁剪空间的 分量会是

如果先变换到归一化设备坐标系中,然后再执行剔除和裁剪的话,由于前者需要执行透视除法,这样就还得对那些应该被剔除掉的顶点执行除法,从而导致不必要的开销,所以剔除和裁剪还是应该放在裁剪空间中做

归一化设备坐标空间是一个标准立方体(canonical cube),或称规范视体积(canonical view volume),其三个轴的取值范围都相等,通常为 ,超过这个视体积范围的顶点都已经在裁剪阶段被剔除掉了,所以不会有超过这个范围的顶点出现。

假设场景中有两个顶点被映射到该空间中,如果它们映射后的 分量相同,意味着它们最终可被映射到二维图像的同一个像素点上,而 坐标体现了它们之间的深度差异或遮挡关系。

不同的实现(如 DirectX 和 OpenGL)所采用的投影矩阵构造方式可能会存在差异,这使得它们的规范视体积的取值范围会不同,比如 DirectX 通常是 ,OpenGL 则是 ,不过它们的实现方式大同小异。

顶点被映射到归一化设备坐标系(NDC)后,就可以通过视口变换(viewport transformation,viewport mapping)进一步映射到视口坐标系(viewport coordinate)上了,视口(viewport)是一个对应了窗口某块区域的二维平面,顶点 NDC 坐标的 分量将根据视口大小进行缩放,并对应到视口内某个像素坐标上,通常不用再对 分量进行变换,其仍为深度信息。

至此,就可以对图元进行光栅化(rasterization)了,图元基本都是采用三角形,并由顶点索引(index)来描述,光栅化会对每个三角形进行绘制,为了处理遮挡关系,会配合深度缓冲(z-buffer,depth buffer),对每个三角形着色出的每个像素进行深度测试,只有通过测试,才会将其像素值写入帧缓冲区(frame buffer)中,并最终在屏幕上显示出来。

以上提到的各种操作中,除了剔除、裁剪以及光栅化等操作外,其它操作的本质都属于几何变换,这些变换使得顶点的坐标在各种坐标系之间过渡,进而变换到窗口的某块区域上,可将这部分变换流程总结为下图:

vertex_geometry_transformation_flow
vertex_geometry_transformation_flow
《顶点在渲染管线中的主要几何变换过程》

投影

简单来说,投影(projection)就是降维的过程,比如从三维降到二维

三维渲染主要有透视投影(perspective projection)正交投影(orthographic projection)两种投影方式,它们的可视空间体(view volume)分别为视锥体(view frustum)正交视体(orthographic view volume),前者为一个平载头体(frustum),后者为一个长方体(cuboid),对比可参考下图:

projection_view_volume

《透视投影和正交投影的可视空间体分别为平载头体和长方体》
from: 《Fundamentals of Computer Graphics》 - 7.3. Perspective Projection

透视投影采用的是中心投影法(central projection),所有投影线可会汇聚到一个点上,而正交投影所有投影线相互平行,这造成了它们投影效果的差异,这主要有以下两点:

  • 透视投影不会维持原本的平行关系,平行线在投影后可汇聚到消失点(vanishing point),而正交投影可以在投影后保持原来的平行关系

    perspective_vanishing_point
    perspective_vanishing_point
    《平行线经过透视投影后汇聚而成的消失点》
    from: PERSPECTIVE
  • 透视投影可以产生近大远小、近高远低、近实远虚的视觉效果,具有很强的立体感真实感,而正交投影,无论物体间的距离如何,它们投影后的大小比列关系,都保持与真实的大小比列关系一致

perspective_vs_orthographic

《透视投影和正交投影的成像效果对比》
from: Orthographic

可从下图来理解它们的成像方式差异,也可从中体会为什么透视投影会出现近大远小、近高远低、近实远虚这样的效果,这就像是将物体给挤压到了一个平面上,距离平面越远的物体,被挤压的越严重。

projection_imaging
projection_imaging
《对比透视投影和正交投影的成像特点》

从几何学上看,透视投影利用了相似三角形(similar triangle)的原理,这对于理解透视投影来说,是一个非常重要的基础。

perspective_similar_triangle
perspective_similar_triangle
《利用三角形相似原理进行映射》

上图演示了在一个空间中,将一个点通过透视投影映射到平面上的过程,根据相似三角形原理,可得到投影前后的点坐标之间的关系: 不过,由于场景中的所有点,都会被拉到与图中的眼睛(或相机)在 Z 轴相同距离的平面上,这丢失了每个点的深度信息,消除了点之间的深度差异,相当于破坏了物体之间的遮挡关系,可结合下图来理解这种现象,根据透视关系,蓝色点和绿色点会被映射到平面的同一个点上,假设这些点只能反射光但不能透射光的话,那么绿色点将完全遮挡住蓝色点反射或发出的光,那么眼睛将不能看见蓝色点,而只能看见绿色点。

perspective_occlusion
perspective_occlusion

在现实世界中,驱动投影成像的本质是光线及其与物质之间的相互作用,物体与物体之间的遮挡关系其实已经被处理好了,对于成像面来说,只需要考虑其所接收到的光线即可。

但对于传统的三维渲染来说,其并不是由光线来驱动成像(除非是光追渲染),透视投影所做的,更多是为场景中的几何点进行一种几何变换,这期间不需要考虑对象的着色问题,不过,为了在后续渲染流程计算像素值时能够正确处理对象之间的遮挡关系,需要将几何点的深度信息保留下来。

因此,不能单纯的用一个成像(或投影)平面来建模投影,或者说,几何点在经过透视投影后,不能直接映射在一个平面上,而是应该变换到另一个三维空间上,这样才能将深度( Z 轴)保留下来。

在此逐步来看这个所谓的三维空间是怎样演化出来的,实际应用中的数学建模可能并不如此,一些细节还需要考虑,不过基本思路是差不多的,所以以下过程只是为了能从中理解基本思路和一些要点。

首先,我们并不希望得到的是一个无限大的空间,因此可将上面投影面定义为一个近裁剪面,并在它后面的适当位置上再定义一个远裁剪面,通过这两个面定义一个可视空间体,其从形状上看是一个平截头体,以下图为例,远裁剪面上所有点都能够投影到近裁剪面上,所有超出这个可视空间体的物体,对于眼睛来说不可见。

perspective_projection_0
perspective_projection_0
《透视投影视景体》

然后,我们可以在上述透视关系的基础上,让几何点坐标的 分量保持为投影前的值不变,那么投影前后的点坐标之间的关系可暂时改为以下这样:

虽然让 分量保持为投影前的值不变,可使后续计算的顶点深度信息与其原始 Z 轴距离呈线性关系,也能更真实地还原顶点之间原始的深度差异,但由于计算机浮点数操作存在舍入误差,从而降低深度信息的精度,因此通常还需要分量进行非线性调整,不过这个问题我们留到后续拼接透视投影矩阵时再讨论,这里先让 分量继续保持不变

透视以及两个裁剪面相当于划出了一个新的空间,空间的高度和宽度由近裁剪面的大小决定,而深度( Z 轴)范围则由两个裁剪面的距离决定,如下图所示:

perspective_projection_1
perspective_projection_1
《这里先保持投影后的z分量为投影前的》

这个空间是一个长方体,可看作是由原本的视锥体经过透视所挤压而成,这类似于正交投影的可视体,当几何点被挤压到此空间后,其实它们的透视关系已经被处理好了,假设再对这个空间使用正交投影进行成像,无论投影面放在哪里,平行性和物体之间的大小关系都不会再变了,即保持经过透视之后的关系。

对于透视投影来说,完成透视这一步之后,后续的处理就跟正交投影是一样的,可以认为透视投影只不过是比正交投影多了一步“​透视”处理。

接下来只要将这个长方体空间整体进行平移缩放映射到一个归一化的立方体后,投影就完成了。

perspective_projection_2
perspective_projection_2
《平移》

平移实际上是为了让长方体空间中的所有点的坐标,映射到长方体空间坐标系上,以该空间的原点为坐标系原点。

perspective_projection_3
perspective_projection_3
《缩放,得到归一化的立方体》

至此,可把透视投影的变换总结为以下流程:

  • 【透视】透视:根据透视关系,将顶点的 分量映射在近裁剪平面上,虽然对于 分量,我们这里仅是保持原始值,但在实际处理中会将其非线性化。这使得透视投影的可视空间体从一个视锥体被挤压成了一个长方体,这个长方体与正交投影的可视空间体基本是一样的
  • 【正交】平移:其实就是将顶点的坐标重新映射在长方体空间坐标系中
  • 【正交】缩放:把长方体空间缩放为一个立方体,顶点的坐标被归一化

就像【标记】所描述的那样,以上流程其实蕴含了正交投影的变换过程,透视投影只是比正交投影多了一步透视,以上流程实际也并不算是真实的情况,比如裁剪的问题就没有考虑进去,也没有考虑怎样调整 分量,这只是一个大概的变换流程。

如果从矩阵的组合变换上来描述的话,正交投影和透视投影可表达为以下形式: 其中,分别表示的是缩放平移的变换矩阵,这个顺序不能改变,因为根据我们的流程,先透视再平移后缩放,矩阵通常是从右边开始与向量结合的,因此变换顺序也是从右边开始,以上两个投影矩阵的计算方式会在后续讨论。

透视相机

对于一个采用透视投影(perspective projection)的相机来说,可呈现出近大远小透视效果,其可视空间体(view volume)视锥体(view frustum)或称视景体,这决定了相机的可视范围,在渲染管线中,场景中的对象需要与视锥体进行相交测试(intersection test),处于视锥体外的图元将会被剔除(culling),即不参与渲染,而与视锥体相交但部分位于体外的图元将会被裁剪(clipping)

视锥体从几何形状上看为一个平截头体(frustum),这由两个裁剪平面(clipping planes)所决定,即近裁剪面(near clip plane)远裁剪面(far clip plane),平截头体四条侧边往近裁剪面方向的延长线可汇聚到一个点上,该点通常也是相机的空间坐标点,称为透视中心(center of perspective)投影中心(center of projection)

视锥体的主要参数有纵横比(aspect ratio)近和远裁剪面到相机的 Z 轴距离以及视场角(FOV,field of view - theta),两个裁剪平面的宽高可以通过这些参数间接得到。

其中纵横比指的是裁剪面的宽高之比,而两个裁剪平面到相机的距离决定了视野范围的深度区域,具体来说,近裁剪面和远裁剪面分别决定了可见的最小深度和最大深度,即能看得到多近和多远。

fovs
fovs
《视场角》

视场角可分为水平视场角(HFOV,horizontal field of view)垂直视场角(VFOV,vertical field of view)对角视场角(DFOV,diagonal field of view),在已知其中一个角的情况下,可结合裁剪面的距离以及纵横比来得到其它两个角的大小,因此,只需指定其中某个视场角即可,一般都会选择指定垂直视场角(VFOV)的大小,所以通常视场角都是指垂直视场角,该角为视锥体顶部中心和底部中心分别与透视中心连线所形成的夹角,即 Y 轴视野范围的角度大小。

view_frustum_fov
view_frustum_fov
《VFOV与裁剪面高度的关系》

参考上图,可根据视场角的大小、裁剪面的距离以及三角函数来计算出裁剪面的高,再根据纵横比来得到宽,同时也可得到水平和对角视场角,它们之间的关系如下(对近裁剪面和远裁剪面都适用): 一般情况下,在三维渲染中,视角场的大小都不会调整到超过 ,也就是说 不会超过 函数在 内是单调递增的,所以视场角的角度越大,裁剪面也会越大,不过裁剪面的大小并不能反映出视野范围的广度或深度,因为调整裁剪面到相机的距离也可使裁剪面的大小发生变化。

实际上,视场角决定了视野范围的广度,而两个裁剪面形成的深度区域决定了视野范围的深度

视场角越大,视野范围越广,意味着能够看到的内容越多,如果同一个相机,分别使用较广和较窄的视场角来捕捉画面,并映射到同一个视口上(可理解为映射到大小相同的平面上),对比之下可发现,相机使用较窄视角所得到的画面,就像是将场景给拉近了,或者说画面被放大了,因此,通过调整视场角可以实现缩放/变焦(zoom)的效果。

physical_camera_fov
physical_camera_fov
《物理相机简化模型使用不同fov成像的对比》

可从上图对真实相机的简化模型来理解视场角是怎样导致画面缩放的,此图仅是理想化的情况,真实相机比这要复杂的多。图中的传感器(sensor)为相机的成像传感器(image sensor),其具有固定的物理尺寸,可将其抽象为成像平面(image plane)投影平面(projection plane),面的尺寸大小与传感器相等且固定不变。

对比左右两图采用不同的视场角大小可发现,由于传感器尺寸固定,为了使视野范围内的光线都能落到传感器上,并铺满整个成像面,需要调整传感器到投影中心的距离,即焦距(focal length),这体现了视场角与焦距之间的关系(),真实的相机通过改变焦距来调整视场角(或视野广度)。

这两种情况都需要将视野范围内的景象全部映射到尺寸固定的传感器上,相较于左图,右图的视场角更小,因此视野更窄,传递给传感器的“内容”显然会更少,被“缩放”到大小相同的成像面后,画面中的景象就会显得更大。

在三维渲染中,通常都没必要对成像平面或投影平面进行建模,不过在进行一些讨论或分析时,尽管不常见,有时也会用到投影平面的概念进行辅助。针孔相机模型中的投影平面位于相机的后面,并且成“倒立”的图像,与此不同的是,为了简化分析,通常会将投影平面放到相机的前面,那么成像面所得到的就是“正立”的图像。

将投影平面放置在距离相机多远的位置(焦距),视情况而定,理论上可放在任何位置,不过一般会限制在近裁剪面和远裁剪面之间,一种做法是让投影平面和近裁剪面重合,另一种常见做法则是调整投影平面的焦距使其半高等于单位长度()并保持纵横比,这种做法可以使一些数学运算更为简单。

view_frustum_projection_plane
view_frustum_projection_plane
from: Foundations of Game Engine Development, Volume 1: Mathematics

比如说,在为视场角量化一个规范化的焦距,或反过来通过规范化焦距确认视场角大小时,就可采用使投影平面半高为单位长度的做法,从而将计算进行简化和规范化,如下面两个式子: view_frustum_zoom

也可根据视口的大小,将 转换为以像素为单位,比如视口的宽高为 ,视场角为 约等于 ,根据以下公式,以像素为单位的大小约等于为 不过,类似于在游戏内提供设置界面,让玩家自主调整画面缩放的情景中,无论提供视场角或焦距的设置界面,都很难让人能直观的理解其中的缩放关系,更通俗易懂的做法是使用缩放比(zoom ratio / scaling),即默认高度与缩放后高度的比值,如 0.5x、2x、150%、200%,缩放比越大,缩放后的高度越小,画面内容越被放大,呈线性关系,不过,缩放前后的视场角大小并不是线性关系,可根据默认视场角来确定一个固定焦距且默认半高为单位长度的投影平面,那么通过缩放比来调整投影平面的高度就可以很容易的计算出缩放后的视场角大小。 fov_and_zoom_ratio

《初始30°视场角进行两倍缩小和放大的对比》
《透视投影视锥体演示》
from: WebGL Visualizing the Camera

透视投影变换

当世界坐标系中的顶点映射到相机坐标系后,就可根据正交关系透视关系为其执行投影变换(projection transformation),从而将其映射到一个裁剪空间(clip space)中进行后续处理了,我们在前面已经讨论过正交投影与透视投影的关联性,所以这里只考虑透视相机的投影变换,可称为透视投影变换(perspective projection transformation)

这个变换将在齐次坐标系中完成,因此需要将顶点的坐标转换为齐次坐标: 可以采用一个 大小的矩阵来描述透视投影矩阵(perspective projection matrix)现在要考虑的就是如何将这个矩阵计算出来,变换的执行顺序为透视平移以及缩放,其中透视其实就是把透视相机的视锥体挤压成类似于正交相机的长方体,这个过程我们可看作是从透视到正交的变换,而平移和缩放可看作是进行了一次正交投影,因此透视投影矩阵可以表示为: 为了数值符号上的简便,我们这里将从左手坐标系的角度来拼接这个变换矩阵:

perspective_projection_4
perspective_projection_4
《左手坐标系下透视相机的视锥体》

以下将根据变换的顺序来看,首先是透视,在前面已经得出了以下透视关系(其中 为相机到近裁剪面的距离): 分量我们先不考虑,先根据 两个分量的透视关系,得出向量在透视前后的映射关系: 根据以上关系,可拼接出以下透视矩阵(perspective matrix)先验证下这个矩阵: 这个矩阵还未处理 分量,即深度信息,虽然理想情况下,在这一步让 分量保持不变,使得顶点在完整变换后的深度信息与原始的 Z 轴距离成线性关系,能更真实地还原顶点之间原始的深度差异,但是我们最终是要将深度信息进行归一化处理(normalize)的,如果采用线性映射的话,浮点数在操作时会出现舍入误差(round-off error),这会降低深度信息的精度(precision),当两个表面非常接近的时候就有可能会发生深度冲突(z-fighting),从而产生视觉伪影(visual artifact)

Z-fighting

《z-fighting示例》
from: Z-fighting - Wiki

为了减少这种现象,我们可以采用非线性深度缓存(non-linear depth buffer),即每个顶点的深度信息与其原始 Z 轴距离呈非线性关系(non-linear),这么做并不会影响顶点原先的深度关系(即遮挡关系)。

这种非线性关系中的深度值精度,会在越靠近相机时表现得越高,越远离相机则表现得越低,不过,这符合我们的期望。

此外,有时还可以通过稍微缩短近裁剪面与远裁剪面的距离,来减少这种 z-fighting 现象。

正交相机中,深度值还是采用线性变化的,透视相机之所以需要非线性化,主要还是因为经历过透视后,导致了顶点之间在 分量的关系发生了变化,那么此时深度值的精度就很重要了

下图展示了线性深度和非线性深度的对比:

non_linear_depth
non_linear_depth
note:其中曲线为非线性深度,直线为线性深度
from: The Math behind (most) 3D games - Perspective Projection

为了构造 的非线性化关系,我们会定下三个约束:

  • 近裁剪面上的所有坐标值不变
  • 远裁剪面上的所有坐标的 分量不变,保持为 ,即相机到远裁剪面的距离
  • 远裁剪面的中心点的坐标值不变,固定为

由于 分量对深度值没有影响,可将前面的透视矩阵修改为以下形式: 使用该矩阵变换后将为: 那么 分量在执行透视后的非线性关系就是: 由于上面所定下的约束,对 执行以上映射后,值并不会变,所以可以建立以下方程组: 求解以上方程组可得: 至此就可以得到完整的透视矩阵了,如下: perspective_projection_5

透视矩阵相当于把透视相机的视锥体压成了如上图这样的的长方体,此时远裁剪面的大小可看作与近裁剪面一样。

perspective_projection_6

note:上图中的视锥体和规范视体积都是采用左手坐标系
from: Projection Matrix

而我们最终要做的就是将透视得到的长方体再变为如上图右边这样的规范视体积(canonical view volume),也称为标准立方体(canonical cube),顶点的坐标将被映射为归一化设备坐标(normalized device coordinate),每个轴的取值范围为 ,映射前后都是采用左手坐标系。

perspective_projection_7
perspective_projection_7

接下来,将长方体平移缩放就可以了,为此,我们还需要知道裁剪面的大小(近裁剪面的大小即可),这可通过相机的视锥体参数求出来,前面有讨论过,这里不重复,参考上图中的参数即可。

平移要做的是将长方体的中心与相机坐标系原点对齐,需要注意的是,虽然在上图中,轴实际已经是对齐了的,即 互为相反数,而 也是互为相反数,不过我们需要考虑应对所有的情况,那么每个轴需要平移的量如下: 由此可得到平移矩阵为: 经过平移之后,长方体的中心将与相机坐标系原点重合,如下:

perspective_projection_8
perspective_projection_8

接下来就是把长方体缩放为规范视体积了,由于标准立方体每个轴的取值范围为 ,因此立方体的每条边长实际为 ,那么各个轴的缩放比如下: 缩放矩阵,如下: 经过此变换后,长方体就变为了以下的标准立方体。

perspective_projection_9
perspective_projection_9

正交矩阵,如下: 组合完成的透视投影矩阵,如下: 以上矩阵的结果这里就不算了,另外,此透视投影矩阵得推导过程,是基于左手坐标系的,而且得到的是一个各轴取值范围为 的规范视体积,而不同的实现,比如 OpenGL 和 DirectX,它们做法并不一样,不过思路都是差不多的,只是在计算变换的细节上存在一些差异。

另外,由于我们是在齐次坐标系中执行的变换,因此处于相机坐标系的顶点,在执行透视投影后,实际还未映射到 NDC 坐标系上,而是处于裁剪空间中,在此完成裁剪和剔除,再执行透视除法(perspective divide),才真正映射为 NDC 坐标。

相关文章推荐

请我喝瓶肥仔快乐水?

欢迎关注我的其它发布渠道