图形渲染管道被认为是实时图形渲染的核心,简称为管道。管道的主要功能是由给定的虚拟摄像机、三维物体、灯源、光照模型、纹理贴图或其他特效来产生或渲染一个二维图像。由此可见,渲染管线是实时渲染技术的底层工具。图像中物体的位置及形状是通过它们的几何描述、环境特征、以及该环境中虚拟摄像机的摆放位置来决定的。物体的外观受到了材质属性、灯源、贴图以及渲染模式(sharding models)的影响。很多计算机图形学的书籍都把渲染管线分为三个阶段:应用程序阶段、几何阶段、光栅化阶段。
应用程序阶段,通过高级编程语言(C、C++、JAVA)进行开发,与CPU、内存打交道,主要任务是识别出潜在可视的网格实例,并把它们及其材质呈交给图形硬件以供渲染。在该阶段的末端将产生几何体数据,包括顶点坐标、法向量、纹理坐标、纹理等,通过数据总线传送到图形硬件以供渲染(时间瓶颈),进行几何阶段。应用程序阶段负责驱动GPU管道,在该阶段有三个操作:
(1)可见性判别
仅把可见(或至少潜在可见)的物体提交GPU, 以免浪费宝贵的资源去渲染看不见的物体。(对象裁剪)
(2)提交几何图元至GPU以供渲染
像DirectX中的渲染调用接口Drawlndexed(), 该接口把子网格材质对传送至GPU进行下一步操作,类似的,在OpenGL中的接口glDrawArrays()也有相同功能。另一种提交方法是建立GPU命令表。场景如果需要多步骤渲染,则需要多次提交,所提交的几何图元应有适当的排序以优化性能。
(3)控制着色器参数以及渲染状态。
很多经典的算法都是在这个阶段中进行的,诸如碰撞检测、场景图建立、空间八叉树更新、视锥体裁剪等。
1. 视锥体裁剪
视锥体裁剪算法是在应用程序阶段执行的。在把场景中的物体提交给GPU进行下阶段操作之前,需要把对最后影像没有任何作用的物体裁剪掉,只需要把可见区域数据传送给GPU。这个建立可见区域实例表的过程即为可见性判断。锥体由虚拟摄像机来定义。虚拟摄像机制定了对观察者可见部分的场景,即我们将依据哪部分 3D场景来创建 2D图像。在世界坐标系中,虚拟摄像机有一定的位置和方向属性,定义了可见的空间体积即视锥体。如图 2.1 所示,一个虚拟摄像机的模型:
图 2.1
用几何术语来讲,上述的空间体积是一个平截头体。在平截头体剔除里,三维物体与平截头体有三种位置关系,如图 2.2 所示。
图 2.2
三维物体完全位于平截头体外。
三维物体部分位于平截头体内。
三维物体完全位于平截头体内。
对于第一种情况,物体会被排除在渲染表之外。给定一个网格模型,我们可以通过一些简单的算法来判断网格模型是否位于平截头体内,这些算法会用到物体的包围体积(包围物体的一个球体)及平截头体的六个平面。把平截头体六个平面同时往里缩进物体包围体积的半径距离,如果六个修改后的平面都包含在球体里,那么物体就是完全位于平截头体内部,这种情况下三维物体将被保留并进入下一个阶段进行处理。对于第二种情况,三维物体的三角形单元将被分为两个部分,视域体内的将被保留,视域体外的将被剔除,从而提升处理性能。
2. 场景图
现在的游戏地图能够达到很大的规模,在多数场景中,大量的物体处于上文所说的平截头体之外,如果这些物体的剔除皆使用平截头体,会造成大量的时间资源消耗。所以我们希望能够设计一种数据结构来处理大场景的裁剪问题,它能够迅速丢弃大量完全位于平截头体外的场景部分,这样才能进行更加详细的平截头体剔除,这数据结构更可以帮助对场景中的几何物体排序。这种数据结构就是场景图。场景图不一定是图,更多可能是某种树:四叉树、八叉树、BSP树、kd树等等。这种理念在于把三维空间以某种形式划分为区域,使位于平截头体外的区域尽快丢弃,而无须把所有物体进行平截头体剔除。
3. 四叉树与八叉树
四叉树使用递归算法的方式把空间划分成象限,因此四叉树每个节点都有四个子节点。象限的划分通常是由轴对称的平面切割而成,所以每个象限是正方形或长方形的,不过也有部分四叉树使用任意形状来细分空间。四叉树这种数据结构出现的目的就是提高平截头体的裁剪效率,那么它是如何办到的呢?我们从根节点往叶子节点遍历,如果某个节点区域不与平截头体相交,则该节点的四个孩子节点区域也不与平截头体相交,所以我们可以停止遍历该分支。八叉树是四叉树的三维版本,每层递归细分都把八叉树空间划分为 8 个子区域,子区域通常是正方体或者长方体,不过也可以是任意三维区域。
几何阶段主要负责顶点坐标变换、光照、裁剪、投影以及屏幕映射,该阶段基于GPU进行计算,该阶段的末尾得到经过变换和投影之后的顶点坐标、颜色以及纹理坐标。其主要工作可以概括为“变换三维顶点坐标”和“光照计算”。我们的显卡信息中通常会有一个标示为“T&L”的硬件部分,T即是Transform,L即是Lighting。那么三维顶点坐标为什么需要变换?如何变换?要知道,我们输入到计算机中的是一系列三维坐标点,但我们最终看到的从视点出发观察到的特定点。我们电脑显示器是二维的,GPU所需要做的,就是把三维顶点数据经过转换绘制到二维屏幕上,并让二维画面看起来有3D效果。顶点的变换涉及一系列的坐标系统,顶点变换过程,就是通过各个变化矩阵,把一个坐标系统下的顶点信息,变化到另外一个坐标系统上,从而实现 3D的顶点数据最终可以在 2D屏幕上进行显示。我们来了解一下变换过程中的各个坐标系统。
根据顶点坐标变换的顺序,主要有如下几个坐标空间:局部坐标系(或称自身坐标系、建模坐标系)、世界坐标系、观察坐标系、视口坐标系(屏幕坐标系)。
1. 局部坐标系
局部坐标系用于定义构成物体的三角形单元列表的坐标,它描述的是模型文件本身的顶点与顶点值之间的关系,顶点值是在模型建模时得到的。局部坐标系与场景中的其他物体没有任何的参照关系,这也是局部坐标系与世界坐标系区分的关键,如图 2.3 所示。
图 2.3
2. 世界坐标系
构建各种模型时,每个模型都位于其自身的局部坐标系中,而无论在现实世界还是在计算机的虚拟空间中,物体都必须和一个固定的坐标原点进行参照才能够确定自己所在的位置,这是世界坐标系的实际意义所在。位于局部坐标系中的物体通过一个称为世界变换的运算过程变换到世界坐标系中,该变换通常包括平移、旋转、以及比例运算,分别用于设定该物体在世界坐标系中的位置、方向及模型的大小。这变换过程由一个四阶矩阵控制,通常称为世界矩阵(world matrix)。
另外,光照计算通常也是在世界坐标系中进行的,这是因为光照效果受到了物体之间关系的影响(如距离、是否遮挡、有无相互投影等)。当然,在观察坐标系中也可以得到相同的光照效果,因为同一观察空间中物体之间的相对关系是保存不变的。有一点值得注意,顶点法向量模型文件属于局部坐标系描述,GPU的顶点程序必须将法向量转换到世界坐标系才能使用。这种转换同样是通过一个矩阵,这矩阵是上文所提的世界变化矩阵的逆矩阵。
3. 观察坐标系
在现实世界中,每个人都是通过自己的眼睛来观察世界,同样的,在虚拟世界中,虚拟摄像机就是我们的“眼睛”,计算机每次只能从唯一的视角出发来渲染物体。例如当我们玩CS游戏时,屏幕显示的内容随着视点的变化而变化,这是因为GPU将物体的坐标从世界坐标系变换到了观察坐标系。实际上所谓的观察坐标系,也就是我们在上文中提到的摄像机的视锥体,它以摄像机为原点,由摄像机观察方向、视角、远近裁剪平面,共同构成一个梯形体的三维空间,如图 2.4 所示。
图 2.4
近裁剪平面也即是梯形体较小的矩形面,在Directx中,为了简化绘制工作,通常将近裁剪平面和投影平面合二为一。在观察者坐标系中,我们的任务是获取 3D场景的 2D表示,这种从N维到N-1 维的操作在数学上称为投影,实现投影有多种方式,如正投影(也称平行投影)和透视投影。由于透视投影更加符合人类的视觉习惯,它会产生近大远小的效果,所以我们采用这种投影来执行视锥中的三维数据到投影平面的投影。Directx中通过一个称为投影矩阵来将视域体中的几何体投影到投影窗口中。
4. 视口坐标系(屏幕坐标系)
从视点坐标系到视口坐标系的转换是通过视口变换操作来进行的。视口变换的任务是将顶点坐标从投影平面转换到屏幕的一个矩形区域中,该区域称为视口。在游戏中,视口通常是整个矩形屏幕区域,当然也可以将视口描述为屏幕的一个子区域,视口的坐标是相对于窗口来描述的,如图 2.5 所示。
图 2.5
经过一系列坐标的转换,我们输入计算机的一系列三维坐标点已经转换为 2D屏幕的三维显示数据。
图元装配,几何阶段处理结束后,送到光栅化阶段的是一堆三角形面片,所以几何阶段需要对顶点进行图元装配。所谓的图元装配,即根据顶点原始的连接关系,还原出模型的网格结构。网格由顶点和索引组成,在之前的流水线中是对顶点的处理,而在这阶段是根据索引将顶点连接中一起,组成线、面单元。然后对超出视口外的三角形进行裁剪(视口裁剪),如果有一个三角形其中一个顶点位于画面外,另外两个顶点位于画面内,我们看到的将是一个四边形,而这个四边形又被划分为两个小的三角形。
这里提到了视口裁剪,实际上裁剪是个很大的概念,裁剪包括了视域裁剪(应用程序阶段)、视口裁剪、背面剔除、遮挡剔除(光栅化阶段)。背面剔除涉及三角形的顶点绕序问题。每个多边形都有两个侧面,我们将其中一个标记为正面,另一个侧面标记为背面,通常,多边形的背面是不可见的,通过背面剔除操作可以不对物体的背面进行渲染,减少需要绘制的顶点个数。一般来说我们根据右手定则来决定三角形的法向量,如果法向量朝向视点(三角形顶点顺时针绕序)即为正面,反之为背面。在Directx3D中,默认顶点排列顺序为顺时针的三角形单元是正面朝向。但也可以通过SetRenderState方法来修改剔除方式。
管道的最终阶段为合并阶段或混合阶段,NVIDIA称之为光栅运算阶段,光栅化的目的是计算出每个像素的颜色值。这个阶段把几何阶段送过来的三角形转化为片段,并对片段进行着色。片段经过裁剪测试、alpha测试、模板测试、深度测试、融合等处理后,最终和帧缓冲混合。光栅化过程大致如图 2.6 所示。
图 2.6
5. 背面剔除
对于实时交互的图形应用程序而言,图形渲染速度和效率是非常重要的。渲染的时候应该尽量减少不必要的操作。剔除是一种通过避免渲染背对观察者的几何体面来提高性能的优化措施。所有几何体都包含正面和反面。剔除基于大多数对象都是封闭的,如果你有一个立方体,你不会看到背离你的那一面(总是只有一面在你的前方),因此我们不需要绘制出背面。因此也被称作背面剔除。
6. alpha测试
纹理的颜色中含有alpha分量,alpha分量主要用于指定像素的透明度。假定我们为每个像素的alpha分量保留了 8 位,则该alpha分量的合法区间是[0,255],其中,[0,255]对应透明度[0%,100%]。当像素的alpha值为 0 时,该像素是完全透明的。如果像素的alpha值为 128,其透明度就是 50%,而alpha值为 255 则表示完全不透明。alpha测试指的是将一个像素点的alpha值和一个固定值比较。如果比较的结果失败,像素将不会被写到显示输出中,如图 2.7 所示。
图 2.7
7. 模板测试
在说明模板测试之前,我们需要先介绍一下模板缓存。模板缓存与深度测试缓存、后台缓存(或颜色缓存,最终显示在屏幕上的缓冲区)的大小(分辨率)完全一致,模板缓存中的像素点与后台缓存的像素点是一一对应的。模板缓存允许我们动态地、有针对性地决定是否将某个像素写入后台缓存中。模板缓存用与获得某种特效,如镜面效果或阴影效果。在实现镜面效果时,我们在“镜子”这块区域中绘制某个特定物体的映像,而使用模板缓存来阻止物体映像在“非镜子”的区域中进行绘制。为了进行这种阻止,就需要使用模板测试。判断是否将某个像素写入后台缓存的决策过程,称为模板测试,如图 2.8 所示。
图 2.8
8. 深度测试
当两个物体有前后位置关系时,位于前面的物体会将后面的物体部分或全部遮挡。这时为了优化考虑,GPU不应该绘制被遮挡的片段,这种行为称为遮挡剔除。为了更好地了解遮挡剔除与深度测试,我们先来看看深度缓存。深度缓存是一个只含有特定像素的深度信息(不含图像表面的数据)。深度缓存为最终绘制图像中的每一个像素都保留了一个深度项。所以,当所绘制的图形的分辨率为 640*480 时,深度缓存中将有 640*480个深度项。深度缓存用于计算每个像素的深度值并进行深度测试,深度测试的基本内容是依据深度值让处于同一位置的不同像素进行竞争,以选出该写入该位置的像素,距离摄像机最近的像素获胜,并被写入深入缓存的对应位置上。这样做是合理的,因为距离摄像机最近的像素一定会将位于其后方的像素遮挡,如图 2.9 所示。
图 2.9
9. alpha融合
融合技术能使我们将当前要进行的光栅化的像素的颜色与先前已经光栅化并处于同一位置的像素的颜色进行合成,即将正在处理的图元颜色值与存储中后台缓存中的像素颜色值进行合成。利用该技术,我们可以获得各种各样的效果,尤其是透明效果。不过值得注意的是,为了在场景中绘制透明物体,通常需要对物体按照由后到前的顺序进行混合处理,如果按照任意顺序进行处理将会产生严重的失真。所以在blending(混色)操作之前要来一次Depth test,如图 2.10 所示。
图 2.10