欢迎大家围观评论,我每天抽出一个小时以上边看边翻译,绝不太监。当然有翻译不好或者理解不到位的地方,欢迎大家拍砖!!!
第二章 渲染管线
本章描述实时渲染系统中最核心的部分,称之为渲染管线,简称为管线。渲染管线最主要的功能是根据虚拟的相机、三维对象、光源、着色公式和纹理等信息来渲染一张二维图像。渲染管线是实时渲染的背后工具。如图2.1描述了如何使用渲染管线。二维图像中模型的位置和形状是由很多因素决定的,比如说模型的几何形状,环境的特点和相机的位置。模型的外观受到材质、光源、纹理和着色模型等影响。
接下来会讨论渲染管线的各个阶段,主要关注于每个阶段的功能而不是实现。实现细节留在以后讨论,很多细节程序员不能控制。例如,画出一条直线需要注意顶点格式,颜色,样式甚至景深效果,但是不需要关注是使用Bresenham直线算法还是对称Symmetric double-setp算法。管线中很多阶段是由不可编程的硬件实现,所以不可能优化这些硬件实现的阶段。基本的画图和填充算法可参见Rogers[1077]。由于我们对底层硬件的控制力很弱,所以好的算法和编程方式才能渲染的又好又快。
图2.1 左边的图像,虚拟的相机位于金字塔的顶部(由四条直线确定)。只有在视晶体内部的图元才会被渲染。当采用透视投影的时候,视晶体是一个截头锥体,一个截面为矩形的截头锥体。右边的图像为相机所看到的图像。红色的环状线圈没有渲染,因为它落在视晶体外部。另外蓝色的棱柱被视晶体的上平面裁剪过。
2.1 架构
在真实世界当中,管线的概念置身于多种形式当中,比如说工厂的装配线到上山的缆车。这个概念也应用到图形渲染当中。这就意味着,管线的整体速度由最慢的阶段所决定而不是其他阶段有多快。
理论上来说,一个非管线系统改造成为由n个阶段所组成的管线系统后,会带来n倍加速效果。这就是使用管线的最主要原因。例如,一个上山缆车只有一个椅子的效率是不够的,增加更多的椅子意味着运送人员的速度成比的增加。虽然管线的每个阶段是并行的执行,但是整个系统还是受限于最慢的阶段。例如,汽车装配线上,方向盘的安装需要三分钟而其他阶段只需要两分钟,那么装配线上所能达到的最佳速度是三分钟每辆,其他阶段需要停工一分钟等待方向盘的安装。对于这个管线系统来说,方向盘的安装是瓶颈,因为它决定着整个生产系统的速度。
计算机实时图形渲染也使用这种管线结构。可以将实时渲染管线大致的分为三个阶段,应用、几何和光栅化,如图2.2。管线结构是实时渲染的核心,是我们接下来几章要讨论的基础。每个阶段也可能是一个管线结构,也就意味着每个阶段也可能拥有多个子阶段。我们区分概念阶段(应用、几何和光栅化)、功能阶段和管线阶段。功能阶段需要执行特定的操作,但是没有规定如何实现这一操作。另一方面,各个管线阶段是同时执行的。这些管线阶段需要并行才能满足性能要求。例如,几何阶段可分为五个功能阶段,但是这些功能阶段都是图形系统的具体实现。
图2.2 渲染管线的最基本结构,由三个阶段组成:应用,几何和光栅化。每个阶段可能又是一个管线,例如图中所示的几何阶段,或者如图中所示的光栅化阶段,其中的多个子阶段又可以并行。图中的应用阶段也可以管线化或者并行化。
最慢的管线阶段决定着系统的渲染速度,图像的刷新率。可以用每秒帧数(fps)来表示,就是每秒能渲染的图像的数量。也可以用使用Hz来表示,表示更新频率。每个系统渲染出图像所耗的时间各有不同,依赖于渲染出这张图像的计算复杂度。FPS用来表示特定图像的渲染的频率,或者表示一段时间内平均的频率。Hz多为硬件中使用,例如设置为固定不变的显示频率。因为我们在和渲染管线打交道,不能增加一些时间来达到固定频率,渲染频率是由数据经过整个管线的时间来决定的。当然,使用渲染管线结构的结果是可以让这些不同的阶段并行执行。如果我们能够定位到瓶颈,例如管线中最慢的阶段,并且测量出数据通过此阶段所花费的时间,我们就能计算出渲染频率。例如,瓶颈阶段需要花费20ms才能完成,那么渲染速度就是1/0.02=50Hz。当然,只有显示设备能在此频率上工作,这个结果才成立,不然真实的频率还要低一些。在有的管线中,使用术语throughput来替代渲染速度。
例子:渲染速度。假设显示设备的最大更新频率为60Hz,是整个渲染管线的瓶颈。某一阶段需要花费62.5ms,那么渲染速度计算如下。首先忽略显示设备,可以计算出最大渲染速度为1/0.0625=16fps。第二,调整渲染频率以适应显示器的频率:显示器60Hz的更新频率也就意味着渲染频率可以是60Hz,60/2=30Hz,60/3=20Hz,60/4=15Hz,60/5=12Hz。(因为显示频率必须是渲染频率的倍数)这也就意味着整个系统的频率只能是15Hz,因为这是显示设备可以接受的最大的频率(渲染频率小于16的情况下)。
从名字可知,应用阶段是由运行在CPU上的应用程序所驱动的。CPU包含多核,可以并行的处理多线程。这使得CPU可以有效的运行大量的任务,这些任务属于应用阶段。这些运行在CPU上的任务包括碰撞检测、全局加速算法、动画、物理模拟等等。下一阶段是几何阶段,处理变换、投影等。这一阶段机选哪些东西需要被渲染,以及在什么地方渲染。几何阶段基本上在GPU上运行,GPU包含许多可编程单元和固定单元。最后,光栅化阶段根据上一阶段的数据以及逐渲染的计算渲染出最终的图像。光栅化阶段完全是由GPU实现的。接下来的三小结会逐一讨论这些阶段以及其中的信息。更多关于GPU如何处理的信息会在第三章介绍。
2.2 应用阶段
应用阶段工作在CPU上,因此开发者可以控制这一阶段的所有细节。开发者可以决定这一阶段的所有实现并随时可以修改以达到更好的性能。当然这些修改也会影响到后来的阶段。例如,应用阶段的算法或者设置可能会减少需要渲染的三角形的数量。
应用阶段的最后,需要渲染的几何体被传递到几何阶段。这传递过去的东西被称为图元,例如点、线、三角形,这些图元有可能最终显示到屏幕上。这是应用阶段最重要的任务。
由软件实现的应用阶段的问题可能在于,它不能像几何和光栅化阶段那样划分为多个子阶段。但是,为了提高性能,这一阶段经常是由多核并行执行。在CPU设计中,这称之为超标量体系,因为多个处理器可以在同时处理同一个阶段。15.5展示课多种使用多处理的方法。
碰撞检测经常在应用阶段实现。如果检测到两个物体碰撞,需要将反馈送到碰撞的物体,也会反馈到输出设备上。[2016/06/06]应用阶段也会处理来自其他方面的输入信息,例如键盘、鼠标和头盔等。依据这些输入可能有各种不同的反馈。应用阶段还包括纹理动画,和模型动画,还有一些其他阶段不处理的计算。加速算法,例如分层的视晶体裁剪(详见14章)也会在应用阶段实现。
2.3 几何阶段
几何阶段主要是逐图元和逐顶点的操作。这一阶段也细分为如下功能阶段:模型视图变换、顶点着色、投影、裁剪和屏幕映射(如图2.3)。注意,依赖于实现不用,每个功能阶段不一定等于一个管线阶段。有的例子中,多个连续的功能阶段组成一个管线阶段(此管线阶段和其他管线阶段并行执行)。在其他例子中,一个功能阶段可以细分为过个更小的管线阶段。[2016/06/09]
图2.3 几何阶段细分为多个功能阶段
在某些极端的情况下,所有的渲染管线阶段都以软件的方式运行在单个处理器上,可以认为全部的管线只包含一个管线阶段。在独立的图形加速芯片和主板出现之前,就是以只包含一个管线阶段的方式来渲染图形。还有一些极端情况,每个功能阶段可以细分为更小的管线阶段,每一个管线阶段可以在指定的处理单元上运行。
2.3.1 模型视图变换
在将模型渲染到屏幕的途中,模型需要变换到几个不同的空间或者坐标系统中。开始,模型处在自己的模型空间中,这意味着没有经过任何变换。每个模型有一个与之相关的模型变换,这个模型变换将模型变换到指定的位置和朝向。单个模型可能由多个模型变换与之相连。这就允许同一个模型有着多份拷贝(也叫实例),每个拷贝有着自己的位置方向和尺寸,从而不需要复制模型的几何信息。
模型的顶点和法线经过模型变换。模型本身的坐标系被称之为模型坐标系,经过模型变换之后,模型位于世界坐标系或者世界空间之中。世界空间是唯一的,所有的模型经过自己的模型变换之后,都位于这个唯一的世界空间中。
正如前面提到的一样,只有相机(或者观察者)能看到的模型才会渲染。相机位于世界空间之中,有着自己的位置和朝向。为了实施投影和裁剪,相机和所有的模型都会经过视图变换。
模型变换和视图变换是两种操作,模型变换将物体从模型空间变换到世界空间中。视图变换将模型从世界空间变换到视图空间(相机为原点的空间)。视图变换的目的在于,将相机置于原点,朝向某一观察方向,这个观察方向为-z轴,朝上的方向为y轴,x轴指向右边(右手坐标系)。经过视图变换之后实际的位置和方向依赖于底层的API。刚才描述的这个空间(坐标系)称之为相机空间或者眼空间。视图变换的结果如图2.4所示
2.4 左边的示例,相机置于用户想要的位置,右边的示例,视图变换将相机置于原点,朝向-z轴。这么做是便于裁剪和投影操作简单快速的完成。灰色的区域称之为视晶体。这里假设是透视投影,因为视晶体是一个截头锥体。
2.3.2 顶点着色
为了产生真实的场景,渲染模型的形状和位置是不够的,外观也需要被很好的渲染。为了实现这些渲染,需要引入模型的材质和在灯光。材质和灯光有无数种方法来模仿,从简单的颜色到
精心描述的物理方法。
决定材质在灯光下的效果的操作称之为着色。需要在模型的每个顶点上计算着色等式以等到最终的结果。通常而言,有些计算是在几何阶段中逐顶点进行,另外的在光栅化阶段逐像素进行。大量的材质信息存储在顶点上,比如顶点位置、法线、颜色和其他着色等式所需的参数。顶点着色的结果(可能是颜色、向量、纹理坐标和其他类型的着色数据)会送到光栅化阶段进行插值。
着色计算通常是在世界坐标系中进行。实际中,为了方便,相关的实体(相机和光源)会被变换到其他空间中(例如模型和视图空间),然后在此空间中进行计算。这么做是可行的,因为光源、相机和模型之间的关系(着色计算当中的关系)在同一坐标系下保持不变。
着色会在本书中更深入的讨论,详见第三章和第五章。
2.3.3 投影
经过顶点着色之后,接下来进行的是投影,投影是将视晶体转换为一个标准的视晶体(也称之为规范化视晶体) {(x,y,z)|−1<x,y,z<1} 。投影分为两类,正交投影和透视投影。如图2.5所示,左边是正交投影,右边是透视投影。
图2.5 左边的是正交投影,也称之为平行投影。右边的是透视投影。
正交投影的视晶体是一个长方体,正交投影将这个视晶体变换为一个单位立方体。正交投影的结果是平行的线任然平行。这种变换是位移和缩放的结合体。
透视投影要稍微复杂一点。这种投影,离相机越远的物体经过投影后会越小。经过透视投影,平行的直线在远方会相交。透视投影模仿的是我们对物体的感知。几何上,这种视晶体称之为截头锥体,只一个被截取一部分,且截口为矩形的金字塔。这种截头椎体最后也会被变换到单位立方体。正交和透视投影的变换都可以用一个4x4矩阵来表示,经过这类变换后,模型位于标准设备坐标系中。
尽管这些矩阵将模型从一个空间变换到另外一个,但还是被称之为投影,这是因为显示的图像并不保持z值。其实是,将模型由三维投影到二维。
2.3.4 裁剪
只有图元完全或者部分在视晶体内,才需要传送到光栅化阶段,最后显示在屏幕上。完全在视晶体内部的图元会按部就班的传送到下一个阶段。图元完全在视晶体外部不会传送到下一阶段,因为他们不会被渲染。那些部分在视晶体内的图元会被裁剪。例如,一条直线,一个顶点在视晶体内部,而另一个在外部,那个在外部的顶点会被直线和视晶体的交点所取代。使用投影矩阵意味着变换后的图元需要被标准立方体裁剪。[2016/06/11]在裁剪之前进行视图和投影变换的原因是保持裁剪问题一致,图元始终是被标准立方体裁剪。裁剪过程如图2.6所示。除了视晶体的六个裁剪面之外,用户还可以定义额外的裁剪平面来屏蔽不需要的对象。不像几何阶段的操作是由可编程单元完成,裁剪阶段(后来的屏幕映射)是由固定的硬件单元来完成。
2.6 经过投影变换,只有在标准立方体内部的图元(相应也在视晶体内部)才需要进行接下来的处理。因此在标准立方体之外的图元会被丢弃,之内的图元会完整保存。如果图元和标准立方体相交,需要进行裁剪,会产生新的顶点而老的顶点会被丢弃。
2.3.5 屏幕映射
只有裁剪后在视晶体之内的图元才会被传送到屏幕映射阶段,这时候图元的坐标还是三维的。x和y坐标会形成最终的屏幕坐标。屏幕坐标加上z坐标称之为窗口坐标。假设场景渲染到窗口点 (x1,y1) 和点 (x2,y2) 之间的区域。这种屏幕映射使用缩放操作来完成。z坐标不会受到屏幕映射的影响。经过映射之后,x和y坐标位于屏幕坐标系。z坐标 (−1≤z≤1) 也会同时传送到光栅化阶段。[2016/06/12]屏幕映射过程详见图2.7。
图2.7 经过投影变换之后,图元位于标准立方体内,屏幕映射将是将图元最终映射到屏幕上。
很多人对浮点数的坐标如何关联到像素点感到混乱。DirectX9以及其之前的版本认为坐标0.0位于像素点的中心位置,也就意味着像素[0,9]包含实际区间[-0.5,9.5)。还有一种表示方法,认为最左边像素的最左位置为坐标0.0。OpenGL和DirectX10及其后续版本使用后面这种表示方法。那么,像素[0,9]包含实际区间[0.0,10.0)。简单的转换如下:
d=floor(c)(2.1)
c=d+0.5(2.2)
其中d表示像素的索引,是离散的整数,c表示在像素点内的连续的浮点数。
关于像素位置的布局,所有的API中像素的位置都是从左向右增长,但是像素的原点位于屏幕上方还是底部各有不同。OpenGL喜欢使用笛卡尔坐标系,左下角被认为是像素原点,而DirectX将左上角定义为像素原点。两种方式各有缘由。例如,OpenGL中原点(0,0)位于图像的左下角,而DirectX中原点位于左上角。DirectX使用这种方式原因在于,很多场景是至上而下的,例如,微软的窗口也使用这种坐标系,我们的阅读方式也是至上而下的,很多图像格式都是以这种方式存放在缓存当中的(例如bmp格式)。当我们需要从一个API移植到另一个的时候,需要考虑这两者之间的差异。
2.4 光栅化阶段
光栅化的目标是利用经过变换和投影之后的顶点数据(及其从几何阶段带来的着色数据),计算出对象的每个像素的颜色。这个过程称之为光栅化或者扫描转换,这个过程将屏幕空间中的二维顶点(包含z值,也就是深度值,和其他的着色信息)转化屏幕上的一个一个的二维点。
类似于几何阶段,光栅化阶段也可以分为过个功能阶段:设置三角形,遍历三角形,像素着色和合并,如图2.8所示:
图2.8 光栅化阶段细分为一个拥有多个功能阶段的流水线
2.4.1 设置三角形
在这个阶段,会计算三角形的差异数据和其他数据。这些数据用于扫描转换,也会对这些由几何阶段产生的着色数据进行插值。这些操作是由专门的固定硬件完成的。
2.4.2 遍历三角形
这一阶段找到三角形所覆盖的所有像素,并为每一个像素产生一个片源(fragment)。寻找位于三角形内部的像素的操作称之为遍历三角形或者扫描转换。所有的产生的片源属性是根据所在三角形的顶点的属性插值而得到的。这些片源属性包括片源的深度和其他从几何阶段送过来的着色数据。
2.4.3 像素着色
所有的逐像素着色计算都在这个阶段完成,使用经过插值的着色数据作为数据源。这个阶段的输出主要是颜色。不像设置三角形和遍历三角形有专门的硬件来完成,像素着色是由可编程的GPU单元完成。很多技术可以在此阶段使用,最重要的一个就是纹理贴图。纹理贴图详见第6章。简单介绍,纹理贴图就是将一张图像粘贴在物体表面。此过程详见图2.9。这个图像可能是一维、二维或者是三维,最常见的是二维纹理。
图2.9 左上方是一个无贴图的龙模型。左下角就是将纹理粘贴到龙模型上。[2016/06/13]
2.4.4 合并
每个像素的颜色信息存储在颜色缓存中(color buffer),这个缓存是一个颜色值(拥有红绿蓝三个分量)的二维矩阵。合并操作负责合并像素着色所产生的片源的颜色与当前颜色缓存中的颜色。不同于像素着色,这一阶段的GPU单元不是完全可编程的。但是也是可以高度定制化的,产生出各种效果。
合并阶段也负责解决可见性问题。这就意味着,当渲染整个场景时,只有从相机方向能看到的图元才能留到颜色缓存中。很多硬件使用Z-buffer(depth buffer)算法。Z-buffer是一个和颜色缓存大小相同的缓存,其中每个像素存储的是从相机方向看过去最近图元(应该是片源)的Z值。这就意味着当一个图元(应该是片源)渲染到一个像素上时,这个图元(应该是片源)的Z值会被计算出来,图元(应该是片源)的Z值和Z-buffer当中对应像素的Z值做比较。如果图元(应该是片源)的Z值小于Z-buffer中的值,意味着图元(应该是片源)离相机更近。因此使用图元(应该是片源)的Z值和图元的颜色来更新Z-buffer和颜色缓存。如果图元(应该是片源)的Z值比Z-buffer中的值大,那么图元(应该是片源)的颜色值和Z值会被丢弃。Z-buffer算法十分简单,复杂度为 O(n) (n是渲染图元的数量),对任意的图元都有效(只要能计算出它的Z值)。还注意这个算法运行基本上所有的图元以任意顺序渲染,这也是它为什么这么流行的原因。然而,半透明的图元不能以任意顺序渲染。他必须要在不透明物体渲染之后再渲染,以从后到前的顺序。这是Z-buffer算法的主要缺点之一。
我们注意到颜色缓存存储颜色,Z-buffer存储z值。还有其他和片源信息相关的通道和缓存。Alpha通道也很颜色缓存相关,存储的是每个像素的透明值(详见5.7节)。针对片源的Alpha测试工作在深度测试之前。片源的Alpha值和和一个指定的值比较。如果片源没通过Alpha测试,它就不会通过接下来的处理。Alpha测试用来保证全透明的片源不会影响到Z-buffer。
模板缓存是另一种缓存用来记录渲染后图元的位置。模板缓存单像素包含8位(一个字节)。模板缓存有很多功能,可以用来控制渲染到颜色缓存和Z-buffer中的值。例如,一个填充的圆渲染到模板缓存中。可以利用目标缓存实现一个效果,所有片源只有在圆之内才渲染。目标缓存是一个强有力的工具以实现某些特殊的效果。所有的位于渲染管线末尾中的这些操作,称之为光栅操作(ROP)或者混合操作。
帧缓存通常指的是所有的缓存,有时候也单单指的是颜色缓存和Z-buffer的集合。1990,Haeberili和Akeley展现了另外一种buffer,称之为累计buffer(accumulation buffer)。这种缓存中,可以使用一组方法来累加像素颜色。例如,一组关于对象运动的图像可以累加然后求平均,以达到运动模糊的效果。还可以用其实现景深、反走样和软阴影等等。
当图元通过光栅化阶段,那些位于相机视觉范围之内的片源将会在屏幕上显示。屏幕上显示的是颜色缓存中的值。为了避免用户看到渲染过程中的颜色缓存,会使用双缓存。这意味着场景的渲染并不是在当前显示的缓存中进行的,而是在后备缓存中进行的。一旦场景在后备缓存中渲染完成,后备缓存会和当前缓存交换。这个交换操作发生在垂直扫描之间,这期间有足够的时间完成交换。
2.5 贯穿渲染管线
点、线和三角形是最基础的图元,所有的模型都由这些基础图元构建。比方一个交互式的CAD应用,用户检查一个手机的设计。我们顺着整个流水线,包括三部分:应用、几何和光栅化。整个场景通过透视投影到屏幕的窗口中。[2016/06/14]这个例子中,手机模型包括基本图元线(手机的边缘)和三角形(手机表面)。有些图元三角形被贴上二维图像,以此表示键盘和屏幕。这个例子中,所有的着色操作在几何阶段完成,除了光栅化阶段的纹理贴图。
应用
CAD应用运行用户选择和移动模型的部件。例如,用户选择手机的最上面的部件(盖子),然后点击鼠标来打开手机。应用阶段将用户的操作转换对应的旋转矩阵,当矩阵作用到盖子上,正确的渲染结果就出来了。另一个例子,相机随着定义好的路径移动以全方位的展现手机效果。相机的各个参数,例如位置和朝向必须在应用阶段实时更新。每一帧开始绘制前,应用阶段为几何阶段设置好相机位置、光源和模型的图元等信息。
几何
由应用阶段传过来的视图矩阵,加上每个对象都有自己的模型矩阵,保存着模型的位置和朝向信息。对每个传到几何阶段的对象,这两个矩阵相乘变为一个矩阵。在几何阶段,对象的顶点和法线都经由这个级联的矩阵(视图矩阵乘以模型矩阵)变换,从而将对象变换到视图空间(眼空间,相机空间)。然后使用材质和光源等信息,计算每个顶点的着色信息。然后进行投影变换,将对象变变换到标准的立方体空间内,这个空间表示眼睛所见的空间。所有在标准立方体之外的图元都会被丢弃。所有和标准立方体相交的图元都会被裁剪,得到处于标准立方体以内的新图元。然后所有的图元顶点映射到屏幕空间中。经过上述操作之后,所有的图元数据送到光栅化阶段。
光栅化
这个阶段,所有的图元被光栅化,最终变成 一个一个的像素点。所有的可见的图元进入光栅化阶段,等待处理。有纹理与之相关的图元会进行纹理贴图。使用Z-buffer处理可见性问题,与之相关的还有Alpha测试和模板测试。所有的图元依次处理,最终变成屏幕上的图像。
结论
这套渲染管线是API和图形硬件数十年朝着实时渲染进化的结果。注意这不是唯一可行的渲染管线,离线渲染的渲染管线有着不同的进化路径。电影级别的渲染产品使用微多边形(micropolygon)渲染管线。学术研究和predictive rendering应用经常使用光线追踪,比如说建筑可视化。
多年以来,开发者只能利用API来控制固定管线来使用上述功能。称之为固定管线,是因为图形硬件实现了上述功能,不能通过可编程的方式灵活使用。每个阶段可以设置渲染管线的功能,例如Z-buffer测试可以打开或者关闭,但是没有任何办法通过编程方式来控制每个阶段所开启的功能。最新关于固定管线的例子的时Nintendo’s Wii。可编程GPU可以控制每个子阶段的具体操作。学习固定管线只是引入最基本的概念,但是最新的开发都是使用可编程GPU。本书假设GPU是可编程的,这是使用GPU的最先进方式。