本文将要介绍图形渲染管线,它被认为是实时图形学中的核心部分。图形渲染管线的主要功能是在给定了的虚拟摄像机,三维的对象物体,光源,着色方程,纹理等情况下,去生成或者是渲染成一张二维的图像。渲染管线因此是实时渲染的底层工具。图2.1展示了使用渲染流水线的过程。画面中对象物体的位置和形状是由它们的几何形状,环境的特性以及在环境中摄像机的位置所共同决定的。对象物体的外观表现则是由材质的属性,光源,纹理贴图,以及着色模型(shading model)所影响。
现在我们将要讨论并介绍渲染管线中的不同阶段,其中我们只关注方法而不关注实现。实现的细节要么留在后续章节去讨论,要么是有些是程序员无法控制的元素。通常这些渲染管线中的一些阶段是用不可编程的硬件所实现的,这使得对其实现进行优化和改进是不可能的。例如,对于某个使用线的人来说,他最在意的是线的特性,例如其顶点数据格式、颜色、线型类型以及所谓深度提示是否可用,而非该线是使用Bresenham直线绘制算法实现或是使用symmetric double-step算法实现。一些书深入地介绍了基本的绘制和填充算法的细节,例如Rogers的书。尽管我们可能对底层硬件的控制是有限的,但是算法以及编程的方法会对生成的图像在速度和质量上产生重要的影响。
2.1 架构
在现实世界中,管线的概念以许多不同的方式所呈现,从工厂的组装流水线到运送滑雪者的上山吊车。它同样应用于图形渲染领域。一条管线一般包含了若干阶段。例如,在输送石油的管线中,在第一阶段的石油不能在第二阶段石油进入第三阶段前进入第二阶段,这预示着管线的速度取决于最慢的阶段,无论其它的阶段有多快。
在理想状况下,个非管线系统若分成n条管线阶段,速度上会提高n倍。性能上的提升是使用流水线的主要原因。例如,例如一雪橇吊车只有一张椅子是低效率的;增加更多的椅子可以使运送到山上的滑雪者数量成比例地增加。虽然管线各阶段是并行执行的,但是在最慢的阶段完成所做的工作之前,其它的阶段会处于停滞状态。例如,在汽车装配线上的方向盘附件装配阶段需要花费3分钟,但是所有其它阶段只花费2分钟,那么可以达到的最好的完成速度是每三分钟一辆车;当方向盘附件完成之前,其它的阶段需要等待一分钟。对于这种管线来说,方向盘装配阶段是整个管线的瓶颈,因为它决定了整个生产过程的速度。这种管线结构也同样存在于计算机实时图形渲染中。实时渲染管线可粗略划分为三个阶段:应用程序阶段,几何阶段,以及光栅化阶段,如图2.2所示。
这种结构是渲染管线引擎的核心机制-它被用于计算机实时图形的应用,因此它是后续章节讨论的重要基础。每一个阶段通常本身也是一条管线,这意味着它由一些子阶段组成。我们在概念性的阶段(应用程序阶段,几何阶段和光栅化阶段),功能性阶段以及管线阶段之间进行区分。功能性阶段规定了运行某个特定的任务,但并未指定任务在管线中是如何执行的。另一方面,一个管线阶段是和其它管线阶段同时运行的。为了高性能的需求,管线阶段也可以是并行运行的。例如,几何阶段可能被分成5个功能阶段,但是图形渲染系统的实现决定了它被分成管线阶段。一个给定的实现可能会将两个功能阶段合并为一个管线阶段,同时一个时间消耗比较大的功能阶段,也可划分为为一系列管线阶段,甚至对其并行化。最慢的管线阶段决定了渲染速度、图像的更新速率。这个速度可以用帧率(fps)来表达,这是每秒渲染图像的数目。它也可以用更新频率-赫兹(Hz)来表示,单位是1 /秒。一个应用程序生成一张图像所花费的时间通常是不同的,这取决于每帧图像执行计算的复杂度。帧率所表达的要么是某一帧的速率,要么是一段时间内的平均速率。赫兹单位用于硬件方面,例如被设置为固定速率的显示器。因为我们正处理一条管线,通过整个渲染管线去渲染我们所需的数据所花费的时间,将它们简单地相加是不够的。当然,这是管线结构的结果,它使各阶段并行地执行。如果我们能够定位到瓶颈的位置-管线中最慢的阶段,并且测量出通过那个阶段所花费的时间,那么我们就可以计算出渲染的速度。例如,假设执行瓶颈阶段所花费的时间为20ms;那么渲染速度将会是1/0.020 = 50 Hz;否则,真实的输出速率会更慢。在其它流水线的环境中,使用吞吐量这个术语来代替渲染速度。
例子:渲染速度。假设我们输出设备的最大更新频率是60HZ,并且渲染管线的瓶颈已经被找到了。执行这个瓶颈阶段所花费的时间为62.5ms。那么渲染速度按下面的方式计算。首先,忽略掉输出设备,我们能得到最大的渲染速度是1/0.0625 = 16 fps。第二步,调整这个值到输出设备的频率:60HZ说明了渲染速度可以是60 Hz, 60/2 = 30 Hz, 60 /3 = 20 Hz, 60 /4 = 15 Hz, 60/5 = 12 Hz,等等。这意味者我们可以预期的渲染速度为15HZ,这是输出设备可以管理的小于16 fps的最大恒定速度。
顾名思义,应用阶段是由应用程序所驱动的并且因此是在通用CPU上运行的软件中实现的。这些CPU通常包含多个具有并行处理多个线程的核心。这使得CPU能够高效地运行在应用程序阶段中负责的大量任务。一些传统地在CPU上执行的任务,包括碰撞检测,全局加速算法,动画,物理模拟,以及许多其他方面,取决于应用的类型。下一阶段是几何阶段,它的任务是几何变换和投影等,这个阶段计算了绘制了什么,如何被绘制,以及应该在哪里绘制。几何阶段通常是在图形处理单元(GPU)上执行的,图形处理单元包含了许多可编程核心,同时也包含了一些固定操作的硬件。最后,光栅化阶段通过使用了上一阶段生成的数据进行所需像素的计算,最终绘制(渲染)了一张图像。光栅化阶段完全地在GPU上执行。这些阶段以及它们内部的管线将会在接下来的三个部分被讨论。
2.2 应用程序阶段
由于应用程序阶段是在CPU上运行的,开发人员可以完全地控制在应用程序阶段发生的一切。因此,开发人员可以完全地决定如何实现并且为了提高性能,可以在之后对其进行修改。在这里的改变也可以影响到后续阶段的表现性能。例如,一个在应用程序阶段的算法或者设置可以减少被渲染的三角形数量。在应用程序阶段的最后,将被渲染的几何体会输入到几何阶段。这些几何体都是绘制图元,例如点、线和三角形等可能最终会在屏幕上显示(或者被输出设备所用)。这是应用程序阶段中最重要的任务。这个阶段基于软件实现的后果就是它没有像几何阶段和光栅化阶段那样,被划分为多个子阶段。然而,为了提升性能,这个阶段经常在一些线程或核心中被并行地执行。在CPU的设计中,这叫超标量体系结构,因为它能够在同一阶段同时执行多个过程。通常在这个阶段实现的过程是碰撞检测。挡在两个对象物体之间的碰撞被检测到之后,反应将会被生成并发送到碰撞的对象和力反馈的设备上。应用程序阶段也是处理例如键 盘、鼠标、头盔等设备输入的地方。跟据这种输入,作出不同的反应。在这个阶段实现的其它过程包括纹理动画,位移动画,或者任何其他阶段不被执行的计算。
2.3 几何阶段
几何阶段是用来负责大多数逐图元以及逐顶点的操作。这个阶段进一步地划分为以下几个功能阶段:模型和视图变化,顶点着色,投影,裁剪,以及屏幕映射(图2.3).再一次注意,取决于实现的不同,这些功能阶段可能会也可能不会等价于管线阶段。在某些情况下,一系列连续功能阶段组合成一个单独的管线阶段(它与其它管线阶段并行运行),在另外一些情况下,一个功能阶段也可能被分割成多个更小的管线阶段。
例如,在一种极端情况下,在整个渲染管线的所有阶段可能在单一处理器上以软件运行,那么你可以说你的整个管线只包含一个管线阶段。当然,这就是在单独的图形加速芯片和显卡出现前去生成图形的方法。在另一种极端情况中,每个图形阶段可以被细分为一些更小的管线阶段,并且每个这样的管线阶段可以在一个指定的处理器核心元件上执行。
2.3.1 模型及视图变换
在被渲染到屏幕上之前,一个模型会被转换到几个不同的空间或坐标系下。在最初,一个模型处于它自己所在的模型空间中,简单地说就是它根本没有被转换。每个模型可以与一个模型变换相关联,以便对其定位和定向。这使得不需要复制基本的几何物体的情况下,相同模型的一些副本(称为实例)在同一场景下可以有不同的位置,方向,以及大小。模型变换(model transform)转换的是模型上的顶点和法线。对象物体的坐标系被称为模型坐标系,当对象物体的坐标系应用了模型变换后,这时模型就处于世界坐标系或世界空间下。世界空间是唯一的,在模型用它们对应的模型变换转换后,所有的模型处于相同的空间下。
就像前面描述的那样,只有被摄像机(或者观察者)看见的模型才会被渲染。摄像机在世界空间下有一个位置和方向,用来放置和对准摄像机。为了便于投影和裁剪,摄像机和其它所有的模型都会用视图变换进行转换。视图变化的目的是将摄像机放置在原点并对准它,让它方向朝Z轴负向,Y轴指向上,并且X轴指向右。在视图变换之后的实际位置和方向取决于底层的应用程序编程接口(API)。
因此这个空间所描述的被叫作摄像机空间,或者更普遍地说,叫视角空间。图2.4展示了一个视图变换影响摄像机和模型的例子。所有的模型变换和视图变换都是由4×4矩形阵实现的。
2.3.2 顶点着色
为了生成一个真实的场景,仅渲染对象物体的位置和形状是不够的,它们的外观表现也需要被模拟。这些描述包括每个对象物体的材质,以及光源照射到对象物体上的效果。材质和光可以用许多种方式来模拟。包括从最简单的颜色到精细物理的表现效果。
决定了光在一个材质上所呈现出的效果的操作被称为着色。它包含了对在对象物体上各种不同的点进行着色方程的计算。通常情况下,其中一些计算会在逐顶点的几何阶段执行,其它的计算可能会在逐像素的光栅化阶段被执行。各种材质的数据,例如点所在的位置,法线,颜色或者着色方程需要计算的其它数字信息,可以被存储在每个顶点中。顶点着色的结果(可以是颜色,向量,纹理坐标,或者任何其它种类的着色数据)之后会被送到光栅化阶段进行插值。
着色计算通常被认为是在世界空间下进行的。在实践中,有时则将相关的实体(例如摄像机和光源)变换到其它空间(例如模型或视觉空间)以及在那里执行计算,会更为方便。因为如果所有包含在着色计算中的实体对象都被变换到同一空间,则光源、摄像机和模型的相对关系是相一致的。
2.3.3 投影
在着色之后,渲染系统会执行投影操作,将可视体转换到一个单位立方体中,这个单位立方体的最小点最大点分别是(−1,−1,−1) 和 (1,1,1)。单位立方体被叫做规范可视体。有两种常用的投影方法,即正交投影(也称为平行投影)和透视投影。如图2.5
正交视图的可视体通常是一个矩形的盒子,并且正交投影变换将这个可视体转换带一个单位立方体中。正交投影的主要特性是变换后平行线仍然保持平行。这个变换是位置变换和比例变换的组合。透视投影有一点复杂。在这种类型的投影中,越远离摄像机的物体,它在投影后看起来越小。除此之外,平行线将会在地平线汇聚。透视变换以这种方式模拟了我们感知对象物体尺寸大小。从几何上讲,被叫做视椎体的可视体,是一个有着矩形底座的截头型金字塔。视椎体也将被变换到单位立方体中。正交变换和透视变换可以用4×4的矩阵来构建,而在进行任意一种变换后,模型被认为处于规范设备坐标系中。虽然这些矩阵变换是从一个可视体变换到另一个,但它们仍被称为投影。因为在完成显示后,Z坐标不会再保存在图片中。通过这样的方法模型从三维空间投影到二维空间中。
2.3.4 裁剪
只有完全或部分处于可视体中的图元才会被传递到光栅化阶段,该阶段将在屏幕绘制这些图元。完全处于可视体的图元会传递到下一个阶段。完全在可视体外面的图元不会被传递到下一阶段,因为它们没有被渲染。部分处于可视体内的图元需要被裁剪。例如,一个顶点在可视体外而另一个在可视体内的线段将被可视体裁剪,所以在外面的顶点会被一个位于线段与可视体相交的顶点所代替。使用投影矩阵意味着已转换的图元会被单位立方体所裁剪。在裁剪之前执行视图变换和投影变换的好处是使得裁剪问题比较一致;图元总是被单位立方体所裁剪。图2.6描述了裁剪的过程。除了可视体的6个裁剪平面外,用户可以定义额外的裁剪平面去显式地裁剪对象物体。不像前面的几个几何阶段,它们通常被可编程处理单元所执行,裁剪阶段(后续的屏幕映射阶段也是)通常是被固定操作的硬件所处理。
2.3.5 屏幕映射
只有(被裁剪过的)在可视体内的图元才会被传递到屏幕映射阶段,并且当进入这个阶段时坐标还仍然是三维的。每个图元的x和y坐标被转换到屏幕坐标系中。屏幕坐标和Z坐标组合在一起也被成为窗口坐标。假设场景应该被渲染到一个窗口中,这个窗口的最小角是(x1,y1),最大角是(x2,y2),并且X1<X2,Y1<Y2。然后屏幕映射首先进行平移随后进行缩放操作。Z坐标不会受这种映射的影响。新的x和y坐标被称为是屏幕坐标,它们随着z坐标(−1 ≤ z ≤ 1)被传送到光栅化阶段。图2.7展示了屏幕映射的过程。
一个困惑是整型和浮点型的点值如何与像素(纹理)坐标相关联。DirectX 9 和它之前的版本使用了这样的坐标系统,它的像素的中央是0.0,这意味着一片范围为[0,9]的像素所覆盖的跨距为[-0.5,9.5]。Heckbert[520]给出了一个理论上更一致的策略。给出一个水平的像素数组并且使用了笛卡尔坐标系,最左边像素的左边缘在浮点坐标中为0.0。OpenGL一直以来都使用这种策略,而DirectX10及其后继版本也使用这种方式。在中央的像素为0.5。所以一系列范围为[0,9]的像素覆盖的范围为[0.0,10.0]。转换公式如下:
d = floor(c), (2.1)
c = d +0 .5, (2.2)
d是离散(整数)的像素索引,而c是连续的(浮点)像素值。虽然所有API的像素位置的值都是由左向右递增,但在关于零点的位置是位于最顶边缘还是最底边缘的问题上,OpenGL与DirectX是不一致的。OpenGL更倾向于笛卡尔坐标系,将左下角设定为数值最低的点,而DirectX有时定义左上角为这个点,这依赖于周边环境。每个做法都有其背后的逻辑,关于它们的不同并没有正确的答案。例如在OpenGL中,(0,0)位于图像的左下角而在DirectX中位于图像的左上角。DirectX采用如此做法的原因是屏幕上的很多的现象均从上到下的:微软的窗体使用这种坐标系,我们阅读的顺序,多种图片格式储存缓冲数据的方式。重要的是它们的不同是客观存在的,并且当从一种API换为另一种API时应该重点考虑它们的不同。
2.4 光栅化阶段
给定了已经变换且投影过的顶点,以及和它们相关的着色数据(均来自几何阶段),光栅化阶段的目标是为被对象物体覆盖的像素计算和设置颜色。这个过程叫做光栅化或者扫描变换,即将屏幕空间下的二维顶点(所有顶点都包含Z值即深度值,及各种与相关的着色信息)转换到屏幕上的像素。
和几何阶段相似,这个阶段被分成了几个功能阶段:建立三角形阶段,遍历三角形阶段,像素着色阶段,以及融合阶段(图2.8).
2.4.1 三角形建立阶段
在这个阶段,三角形表面的差异和其它数据将会被计算。这个数据被用来扫描转换,及几何阶段产生的各种着色数据的插值。这个过程是在专门用于这项工作的有着固定操作的硬件上执行。
2.4.2 三角形遍历阶段
在这个阶段将检查三角形是否覆盖每个像素的中心,并且与三角形重叠的像素部分会生成片段。寻找哪些样本或者像素在一个三角形中经常被成为三角形遍历或者扫描转换。用三角形的三个顶点插值的数据来生成三角形片段的属性,每个三角形片元的属性是通过使用三角形三个顶点插值后的数据生成的。这些属性包括片段的深度,以及从几何阶段传递过来的其他着色数据。Akeley和Jermoluk和Rigers提供了更多关于三角形遍历方面的信息。
2.4.3 像素着色阶段
这里使用插值过的着色数据作为输入,进行逐像素的着色计算。最终的结果就是一个或更多的颜色会被传递到下一个阶段。不像三角形建立和三角形遍历阶段那样,通常是在专用的硬件上执行,像素着色阶段是被可编程GPU核心执行的。在这里可以使用各种各样的技术,其中最重要的技术之一为纹理贴图。简单地说,纹理贴图即将一张图像贴在那个对象物体身上。图2.9描述了这一过程。图像可以是一维、二维、三维的,而二维图像的使用是更为普遍的。
2.4.4 融合阶段
在颜色缓存中存储了每一个像素的信息,它是一个颜色的矩形阵列(每个颜色包含了红、绿和蓝的成分)。融合阶段的任务,是将由像素着色阶段生成的片段颜色与当前存储在颜色缓存中的颜色合并。不像像素着色阶段那样, 执行这个阶段的GPU子单元通常不是完全可编程的。然而,它是可高度配置的,可实现多种效果。
这个阶段也负责解决可见性的计算。这意味着当整个场景已经被渲染了,颜色缓存应该包含了在摄像机视角上可见的场景中图元的颜色。对于大多数图形硬件而言,这项工作是由Z-深度缓存算法完成的。Z-深度缓存的形状及尺寸大小和颜色缓存一样,并且对于每个像素,它储存了从摄像机到离摄像机最近的图元的Z值。这意味着当图元被渲染到某个像素时,其图元像素的Z值会被计算并与Z深度缓存中相同像素的内容比较,如果新的Z值小于Z深度缓存中的z值,那么将被渲染的图元将会是离摄像机更近的那个。因此,像素的z值以及颜色会随着被绘制图元的z值和颜色进行更新。如果被计算的z值比在Z深度缓存中的z值大,那么颜色缓存和Z深度缓存将保持不变。Z深度缓存算法是非常简单的,其收敛性为O(n)(n指将被渲染的图元的数目),而且它对于所有可计算出每个(相关的)像素z值的绘制图元都是有效的。并且注意到这个算法允许大多数图元按任意顺序被渲染,这也是其流行的原因。然而,部分半透明的图元不能按任意顺序去渲染。它们必须在所有不透名的图元渲染后,并且按从后到前的顺序去渲染。这是Z-深度缓存的另一个主要的弱点。我们已经注意到颜色缓存用来存储每个像素的颜色,Z-深度缓存用来存储每个像素的深度z值。
然而,还有一些其它的通道和缓存可以用来过滤和捕获片段的信息。Alpha通道与颜色缓存相关联并且为每个像素存储了一个相关的不透明度值。在Z-深度测试被执行以前,一个可选的Alpha测试可以在传入片段上运行。片段的alpha值与与一个参考值作一些特定的测试(如等于,大于等等),如果片断未能通过测试,它将不再进行进一步的处理。这种测试通常用于保证完全透明的片段不影响Z-深度缓存。
模板缓存是用于记录渲染图元位置的离屏缓存。模板缓存通常每个像素占8个bit。可以使用多种方法将图元渲染到模板缓存中,并且缓存的内容之后可以被用于控制渲染到颜色缓存和Z-深度缓存中。举个例子,假设一个被填充的圆环已经被渲染到模板缓存中。这可以与一种操作组合,这个操作允许将后续的图元仅在圆形所出现之处进行渲染。模板缓存对于生成特殊效果是一个强大的工具。所有这些在管线末尾的方法都被叫做光栅化操作或者融合操作。
帧缓存通常包含一个系统的所有缓存,但是它有时用来指颜色缓存和Z-深度缓存所组成的集合。在1990年,Haeberli和Akeley [474]提出了对帧缓存的另一种补充,叫累积缓存。在这种缓存中,图像可通过一系列操作进行累积。例如,图为了生成运动模糊效果,一系列展示物体运动的图像可被累积和平均。其它可以被生成的效果包括:景深,反锯齿,软阴影,等等。
当图元已经到达并且通过了光栅化阶段,那些从摄像机视角为可见的图元将被显示到屏幕上。屏幕显示了颜色缓存中的内容。为了避免人们看到图元正在被光栅化以及被送到屏幕的过程,使用了双缓存技术。这意味着场景的渲染在后置缓存中离屏进行。一旦场景在后置缓存中被渲染完毕,后置缓存中的内容就会与之前显示在屏幕上的前置缓存中的内容交换。交换发生在垂直回扫时,这时候执行这个操作是安全的。
2.5 管线纵览
点,线,三角形是构成一个模型或者一个对象物体的渲染图元。想像一下用户正在使用一个交互的计算机辅助设计系统,检查一个手机的设计。在这里我们将跟随这个模型通过整个图形渲染管线,包括三个主要的阶段:应用程序阶段,几何阶段,以及光栅化阶段。场景是以透视投影渲染到屏幕上的。在这个简单的例子中,手机模型引入了直线段(用于展示边缘部分)和三角形(用于展示表面)。一部分的三角形以一张二维图像纹理贴图去表现键盘和屏幕。在这个例子中,着色计算全部在几何阶段,除了发生在光栅化阶段中纹理贴图的应用。
2.5.1应用程序阶段
CAD应用程序允许用户去选择和移动模型上的部分。例如,用户可能选择手机的顶部并移动鼠标翻开手机。应用程序阶段必须将鼠标的移动转换为相对应的旋转矩阵,然后务必确认渲染时那个矩阵正确地应用于手机翻盖上。另一个例子:播放一个摄像机沿预定路径运动的从不同角度去展示手机的动画。摄像机取决于时间的参数,例如位置和视角方向,必须由应用程序阶段更新。对于被渲染的每一帧,应用程序阶段将摄像机位置,光,以及模型的图元传送到管线的下一个主要阶段——几何阶段。
2.5.2几何阶段
视图变换矩阵在应用程序阶段被计算,与此同时的还有关于每个模型位置和朝向的模型变换矩阵。对于每一个被传送到几何阶段的对象物体,这两个矩阵通常会相乘,合并为一个单一的矩阵。在几何阶段,对象物体的顶点和法线被这个合并的矩阵转换到视角空间。然后通过使用材质和光源属性,进行顶点着色的计算。投影之后被执行,将对象物体转换到一个单位立方体空间下,这个单位立方体空间代表了能看到的一切事物。所有在立方体外面的图元会被丢弃。为了得到完全位于单位立方体内的一系列图元,所有与单位立方体相交的图元会被立方体裁剪。之后顶点会被映射到屏幕上的窗口。在所有这些逐图元操作被执行之后,由此产生的数据会被传送到管线中最后一个重要的阶段-光栅化阶段。
2.5.3光栅化阶段
在这个阶段,所有的图元都被光栅化,被转化为窗体中的像素。每个对象物体上可见的线段和三角形进入屏幕空间下的光栅器,准备被转化。与纹理相关联的那些三角形将使用该纹理(图像)进行渲染。可见性计算通过Z缓存算法解决,随同的还有可选的alpha测试和模板测试。每个对象就这样依次处理,而最终的图像随后会显示在屏幕上。
结论
管线是图形API以及图形硬件数十年来以实时渲染应用程序为目标进行演化的结果。重要的是要注意,这不是唯一可能的渲染管线,离线渲染管线已经经历了不同的演化过程。电影产业的渲染通常使用微型图元渲染管线。学术研究以及预测性的渲染应用例如建筑的视觉预览通常使用射线追踪渲染。许多年以来前,应用开发者使用这里所描述的过程的唯一方法是使用图形API所定义的固定功能管线。固定功能管线之所以这么命名是因为实现它的图形硬件是由不可以被灵活编程的元件所组成。管线的各部分可以被设置成不同的状态。Z-深度测试可以被开启或关闭,但是无法通过编写程序去控制应用到各阶段的功能的顺序。最新(很可能是最后)的固定功能管线机器例子是任天堂的Wii。可编程GPU使得它可以准确地决定管线中各种子阶段进行什么样的运算。尽管研究固定功能流水线可以合理地介绍一些基本原理,但大多数最新的发展是针对可编程GPU的。