RTR4图形渲染管线
本章展示了实时图形的核心组成部分,图形渲染管线,也可以简称为管线.管线的主要功能是生成或者说渲染一个二维图像,通过给定的摄像机,三维物体,光源等资源.渲染管线因此也是实时渲染的基本工具.渲染管线的过程如图所示.图像中的物体的位置和形状是通过它们的几何,环境特征和虚拟空间中摄像机的位置决定的.物体的外观受材质属性,光源,纹理和阴影方程影响.
我们会解释渲染管线的不同阶段,更加关注功能而不是具体实现.这些阶段应用上的相关细节会在后面几章介绍.
2.1 架构
在现实世界,管线的概念以许多不同的形式出现,从工厂装配流水线到快餐厨房.它同样被应用于图形渲染.一个管线包含许多阶段,每个阶段负责这个巨型任务的一部分.
各个管线阶段是并行执行的,每个阶段都依赖前一阶段的结果.理想情况下,一个非管线化的系统被划分为了n条管线,那么效率会提升n倍.由此带来的性能提升是使用管线的主要原因.举例来说,一个人负责做面包,一个人负责加香肠,另一个人负责浇料,就能很快地准备很多三明治.每个人在流水线作业中将结果迅速传给下一个人,并且马上开始做下一个三明治.如果每个人都需要20秒完成工作,那么整个三明治的制作最短速度就是20秒.管线各个阶段并行处理,但是它们会等待最慢的阶段完成工作.例如,假设加香肠需要花费30秒,那么现在整个三明治制作的最短速度就是30秒.对于这个管线来说,加香肠这步就是瓶颈,因为它决定了整个生产过程的速度.后面的浇料的步骤就会等待.
这种类型的管线结构也在计算机实时渲染中有体现.一种粗糙的划分方式是将实时渲染管线划分成四个主要阶段:应用(application),几何处理(geometry processing),光栅化(rasterization)和像素处理(pixel processing),如图所示.这个架构是计算机实时图形应用的核心,并且也是后面几节话题的基础.每个阶段内部也都是一个管线,也就意味着还有许多小阶段.我们将图中展示的阶段的功能和他们各自的实现分开.功能阶段拥有一个具体的执行任务,但是不会指定管线中执行这个任务的方式.一种实现的方式可能是将两个功能阶段,都由一个单元执行或通过可编程的核心执行,如果将它们分成许多硬件单元执行,就会消耗更多时间.
渲染速度会通过每秒帧率(frames per second,FPS)表示.也可以用赫兹(Hz)表示.也可以用时间表示,以毫秒为单位的渲染一个画面的时间.生成一幅图像的时间是会变化的,取决于每帧计算的复杂度.FPS既能表示具体一帧的生成速度,也能描述使用期间的平均表现.Hz被用于硬件,如显示器,它们被设定为固定的频率.
顾名思义,应用阶段是被应用驱动的,是被CPU上运行的软件程序实现的.这些CPU通常拥有多个核心,能够并行处理多线程的执行.这也确保了CPU能高效地运行应用阶段的大量的任务.一些任务传统上是由CPU处理的,包括碰撞检测,全局加速算法,动画,物理模拟和其他,取决于应用的类型.下一个主要阶段是几何处理,需要处理变换,投影和其他的几何处理.这个阶段的任务是计算"该绘制什么","怎么绘制"和"绘制在哪".几何阶段就是典型的由图形处理单元(Graphics Processing Unit,GPU)运行的,GPU其中包括众多可编程单元和固定操作单元.光栅化阶段一般输入三个顶点,组成一个三角形,找到三角形内部会包含的所有像素,然后将这些信息传递到下个阶段.最终是像素处理阶段,逐像素地执行一个程序,来决定像素的颜色并可能执行深度测试看这个像素是否可见.这个阶段也会执行一些逐像素操作比如颜色混合.光栅化和像素处理阶段是完全在GPU上执行的.所有的阶段和他们内部的小管线会在接下来四节中讲述.GPU如何处理这些阶段的更多信息在第三章.
2.2 应用阶段
在应用阶段开发者拥有完全的控制权,因为完全在CPU上执行.因此,开发者能完全的决定如何实现并且也能为了更好的性能而修改.应用阶段的改变也能影响后续阶段的性能.例如,应用阶段的算法或设置能降低需要渲染的三角形数量.
也就是说,一些应用工作也能通过GPU执行,使用区分的模式,叫计算着色器(compute shader).这个模式将GPU视作高度并行的通用处理器,忽略它为渲染图形特别设置的功能.
在应用阶段的最后,需要渲染的几何体会被送到几何处理阶段.这些几何体被称为渲染面元(rendering primitives),也就是最终会在屏幕里显示的点,线和三角形(无论使用什么显示设备).这是应用阶段最重要的任务.
应用阶段基于软件的实现的结果就是它不会被分为子阶段.而后续三个阶段会.然而,为了提升性能表现,这个过程也会并行地在一些处理核心上执行.在CPU设计中,被称为超级标量体系建设(superscalar construction),因为它能够同时执行多个进程.18.5节展示了用于多处理核心的许多方法.
在应用阶段被广泛实现的处理过程是碰撞检测.在两个物体间的碰撞被检测出来后,会有一些反应反馈到两个物体中,也会给设备反馈.应用阶段也是要处理其他源的输入的,比如键鼠或头戴式设备.由于输入的不同,会进行许多不同的动作.加速算法,像具体的剔除算法,还有其他管线不能处理的东西也在这里执行.
2.3几何处理
几何处理阶段负责大多数逐三角形和逐顶点的操作.这个阶段也被划分为如下的功能阶段:顶点着色,投影,裁剪和屏幕映射.
2.3.1 顶点着色
顶点着色有两个主要任务,一是计算顶点的位置,二是决定期望的顶点数据的输出形式,比如法线和纹理坐标.传统上很多物体的着色都是通过在每个顶点位置和法线应用光照并且储存在顶点处的颜色.这些颜色接下来会在三角形中插值.因此,可编程的顶点处理单元被叫做顶点着色器.随着现代GPU的到来,随着更多的逐像素着色的出现,顶点着色阶段更加的一般,并且可能不会再计算着色结果了,这取决于开发者的目的.顶点着色器现在成为了一般的单元,能够设置每个顶点相关的数据.举个例子,顶点着色器可以通过4.4章和4.5章的方式让物体动起来.
我们从描述顶点位置是如何计算的开始,我们需要一系列的坐标.在从顶点位置转换到屏幕上的过程中,模型需要转换到许多不同的空间和坐标系统中.起初,模型拥有自身的模型空间,这也意味着它没有进行变换.每个模型都能与一个模型变换相关联,这样它能被定位和定向.一个模型能够有多个模型变换,这使得一个模型的诸多复制(也叫实例)能在一个场景中拥有不同位置,指向和大小,而不需要重新复制基本的几何图形.
模型的顶点和法线会通过模型转换变换.对象的坐标被称为模型坐标,经过模型变换后,模型被定位在世界空间的世界坐标上.世界空间是唯一的,在模型通过各自的模型变换后,所有的模型都存在于相同的世界空间中.
如前面所说,只有被摄像机看到的模型才会被渲染.相机在世界空间有个坐标和方向,这被用于放置相机和瞄准相机方向.为了便于投影和裁剪,相机和所有的模型都会通过视图变换.视图变换的目的是将相机放置在原点并使它看向z轴负半轴,y轴指向上,x轴指向右.我们使用-z轴,一些文章可能倾向于看向+z轴.区别主要是语义上的,因为变换是相同的.视图变换后真正的位置和方向取决于底层API.这个空间被称作摄像机空间,或者说观察空间或视点空间.视图变换如何影响相机和模型的例子如图所示.模型变换和视图变换会使用4x4的矩阵,这是第4章的话题.我们需要认识到顶点的位置和法线可以用程序员习惯的任何方式计算.
接下来,我们介绍顶点着色的第二种输出.为了呈现一个真实的场景,渲染物体的形状和位置是不够的,还需要对外观进行建模.这种描述包含每个物体的材质.也包含光源在物体上的效果.材质和灯光能够以多种方式建模,从简单的颜色到对物理描述精细的表示.
决定灯光在材质上的影响的操作被称为着色.它包括计算计算物体上不同点的着色方程.通常,一些计算是在模型顶点的几何处理阶段,而另一些是在逐像素处理阶段.每个顶点能存储许多材质数据,例如顶点位置,法线,颜色,或者其他数字信息等计算着色方程需要的.顶点着色结果(可以是颜色,向量,纹理坐标等以及其他着色数据)会接下来发送到光栅化和像素处理阶段,来通过插值计算表面的着色.
GPU顶点着色器的顶点着色的讨论会特别在本书的3-5章.
作为顶点着色的一部分,渲染系统执行投影然后裁剪,将可见范围转换为一个单位立方体,端点为(-1,-1,-1)和(1,1,1).可以不同的范围来使用和定义相同体积,比如[0,1].单位立方体也被称为标准视图空间(canonical view volume).投影是首先完成的,在GPU上是通过顶点着色器完成的.有两种通常被使用的投影方法,称为正交投影(也叫平行投影)和透视投影.如图所示.事实上,正交投影是透视投影的一种平行形式.其他的投影也会有用,特别是在建筑领域,比如斜轴(oblique)和轴测(axonometric)投影.老式游戏街机Zaxxon是以后者的名字命名的.
注意,透视是用矩阵表示的,因此有时它也可能与几何变换的剩余部分连接在一起.
正交投影视图的视图体(view volume,指投影前的视体空间)通常是一个矩形盒子,正交投影变换将这个视图体转换成单位正方体.正交投影的主要特征是平行的线转换后依旧平行.变换过程包含了平移和缩放.
透视投影就有一些复杂,在这个投影中,物体离相机越远,经过投影后它就越小.另外,平行线可以在地平线上相交.因此,透视变换模仿了我们感知物体大小的方式.几何上来讲,视图体也叫视锥体,是底为矩形的截顶棱锥.视锥体也被投影成了单位立方体.正交变换和透视变换都可以通过4x4的矩阵来构造(第四章),并且在任何一个变换后,模型所在的坐标系称为裁剪坐标系.这些坐标实际上是齐次坐标,发生在除之前.GPU的顶点着色器必须总是输出这种类型的坐标,因为要让下一个功能阶段准确运行,比如裁剪.
尽管这些矩阵将一个体积转换为了另一个体积,他们称为投影是因为在显示后,z坐标并不存储在生成的图像中,而是在深度缓冲中,2.5章会介绍.也就是说,模型从3维空间投影到了2维空间.
2.3.2 可选的顶点处理
每个管线都拥有上文所说的顶点处理,一旦处理结束,GPU也会有一些可选的操作步骤.按顺序分别是:细分曲面,几何着色,流输出.它们的使用与否取决于硬件(不仅是GPU)的支持,也取决于开发者的意愿.它们都各自独立,一般来说用得不多.第三章会介绍更多.
第一个可选阶段是曲面细分.想象一下你拥有一个反射球物体.如果你仅仅将它以三角形集合的方式展现的话, 你会遇到表现的问题.你的球可能在5米远以外看得不错,但是靠近后就看到独立的三角形了,特别是沿着轮廓的部分.如果你把球用更多的三角形表示来改善图像质量,那在球离摄像机很远的时候,就会浪费更多的处理时间和内存来处理仅仅是几个像素大小的球.有了曲面细分,一个曲面可以生成相当数量的三角形.
我们已经讨论一些三角形了,但是到此为止我们的管线还只能处理顶点.顶点能够用来表现点,直线,三角形或其他物体.顶点也能用来表示一个曲面,例如球体.这些表面能通过一系列的patch表现出来,每个patch都是通过一组顶点来描述的.曲面细分阶段内也包含了一组阶段:外壳着色器(hull shader),曲面细分,域着色器(domain shader).这些步骤将一系列的patch顶点(一般情况下)转换为更多的顶点,并接下来用来形成一系列新的三角形.画面的摄像机能用来决定需要生成多少个三角形,patch离得近就生成得多,patch离得远就生成得少.
下一个可选阶段是几何着色.这个着色器早于曲面细分着色器并且在GPU上应用地更广泛.就像曲面细分着色器,几何着色也是接收各种类型的图元,然后产生新的顶点.这是一个非常简单的阶段,因为创建顶点的范围非常有限,输出的图元类型也非常有限.几何着色器有几个用途,其中最著名的是粒子生成.设想模拟烟花爆炸的过程.每个火球都能用一个顶点来表示.几何着色器能够将每个顶点转换成一个面对着观众的平面(由两个三角形组成),并覆盖一定数量的像素.这种图元着色毫无疑问更有说服力.
最后的可选阶段是流输出.这个阶段能让我们把GPU当做几何引擎来使用.不把处理过的顶点之间传入接下来的管线步骤从而将其渲染在屏幕上,而是我们可以选择将这些顶点输出到数组中做进一步处理.在后续环节这些数据能被CPU或GPU自身使用.这个阶段也经常用来做粒子模拟,正如上文的烟花例子.
这三个阶段按曲面细分,几何着色,流输出的顺序执行,每个阶段都是可选的.不管使用了哪个选项,如果我们继续沿着管线进行,会得到一组其次坐标的顶点,然后会检查这些顶点是否会被摄像机看到.
2.3.3 裁剪
只有完全或部分在视体(view volume)内的图元才会进入光栅化阶段(和后面的像素处理阶段),然后它们会被绘制在屏幕上.完全在视体内的图元会被原封不动地传递到下一阶段.完全在视体外部的图元不会被传递到下一阶段,所以它们不会被渲染.部分在视体内部的图元是需要裁剪的.例如,一条直线有一个顶点在视体外,一个顶点在视体内,就会被根据视体裁剪,于是在外部的顶点就会被位于直线和视体角点处的新顶点所取代.投影矩阵的使用意味着经过投影转换的图元会被单位立方体所裁剪.在裁剪之前应用视图变换和投影变换的好处是它能让裁剪问题保持一致,图元总是根据单位立方体裁剪.
裁剪过程如图所示.除了这六个视体的裁剪平面以外,用户能定义额外的裁剪平面来明显地裁剪对象.图19.1展示了这种裁剪,称作切片(sectioning).
裁剪的步骤使用经过投影变换的4个值的其次坐标.在透视空间,沿着三角形的插值并不是线性的.其次坐标的第四个值的存在使应用了透视投影后的插值更正确.最后会执行透视除法,这会把三角形的位置放置在三维的归一化设备坐标系(normalized device coordinates,NDC)中.正如前文所说,视体的坐标范围是从到的.几何处理的最后一步是将这个空间转换为窗口坐标.
2.3.4 屏幕映射
只有处于视体内部并经过裁剪的图元才会进入屏幕映射阶段,进入这个阶段时的坐标亦然是3维的.每个图元的x与y坐标会转换成屏幕空间坐标.屏幕空间坐标以及z坐标也会被称为视窗坐标.设想画面会被渲染至一个最小角落最大角落, .屏幕映射先进行平移然后缩放.新的x和y坐标被称作屏幕坐标,而z坐标(OpenGL是[-1,+1],DirectX是[0,1])也会被映射到默认[0,1]之间.这可能根据API而不同,然而,屏幕坐标和深度坐标会一起进入光栅化阶段.屏幕映射的过程如图所示.
接下来,我们描述整数值和浮点数值是怎样和像素(以及纹理坐标)值相联系的.给定水平像素数组并使用笛卡尔坐标,最左边的像素的左边缘以浮点数表示是0.0f.OpenGL一直使用此方案,而DX10以后才应用这版本.这个像素的重心是0.5,所以,像素0-9覆盖了0.0-10.0的范围.可以简单表示为:
d是像素的整数索引,c是像素内连续的浮点数值.
尽管所有的API的像素位置都是从左向右递增的,但是从上到下是递增递减是不一致的.OpenGL全程使用笛卡尔系统,将左下角作为最小的元素值,而DX有时根据上下文将左上作为第一个像素元素值.这种差异在从一个API迁移到另一API时需要注意.
2.4 光栅化
有了变换和投影后的顶点以及相应的着色信息(都是来自几何处理),下一个阶段的目的是找到图元(也就是三角形)内部的所有的像素(pixels是picture elements的缩写).我们把这个过程叫做光栅化,并且它被分成了两个子阶段:三角形装配(也叫图元装配)和三角形遍历.如图所示.注意这些过程同样能处理点和线,但是因为三角形的处理更加普遍,所以才叫做三角形装配和三角形遍历.光栅化也被叫做扫描转换(scan conversion),是将屏幕空间中的二维点坐标(带有深度信息,和许多其他着色信息)转换到屏幕中的像素上.光栅化也能被认为是连接几何处理和像素处理的同步点,因为通过光栅化,三角形从三个顶点最终转换到了像素处理阶段.
三角形该如何覆盖一个像素取决于你的管线是如何设置的.比如,你可能通过对点采样来决定像素在不在三角形内.最简单的情况是每个像素使用一个位于像素中心的采样点,并且如果中心点在三角形内部,则对应的像素就被认为在三角形的中心.也可以使用每像素多于一个的采样点,使用超采样或MSAA技术(见5.4.2章).还有一种方法是使用保守光栅化,它的定义是只要像素内有部分处于三角形中,那么就认为这个像素位于三角形中.
2.4.1 三角形装配
在这个阶段,会计算差分,边缘函数和其他三角形的数据.这些数据可能被用于三角形遍历,也可能用于计算几何阶段的各种着色数据的插值.会有固定功能的硬件来做这个工作.
2.4.2 三角形遍历
在这个阶段,每个像素会确认它和三角形的覆盖情况,并且被覆盖的像素会生成片元.其他精巧的采样方法见5.4章.找到那个像素或采样点位于三角形中的过程叫做三角形遍历.每个三角形中的片元属性通过在三角形三个顶点之间插值生成.这些属性包括片元深度以及其他从几何处理阶段传递过来的着色数据.McCormack等人对三角形遍历提供了更多的信息.这里也会进行透视校正插值.图元内所有的像素或采样点接下来会被送进像素处理阶段.
2.5 像素处理
在这里,因为前文讲过的阶段,所有的像素都被认为在三角形或其他图元内部.像素处理阶段被划分为像素着色和合并,如图所示.像素处理是对图院内的像素和采样点进行逐像素和逐采样点的计算和操作的阶段.
2.5.1 像素着色
各种逐像素着色的计算会在像素着色环节执行,以经过插值的着色数据作为输入.最终的结果是需要传递到下个阶段的一个或几个颜色.不像三角形装配和三角形遍历阶段由特定的硬件部分执行,像素处理阶段是通过可编程的GPU核心执行的.程序员会为像素着色器(片元着色器)提供程序,其中包含想要执行的操作和计算.大量各种各样的技术会在这里应用,其中最重要的是纹理.第6章会详细介绍纹理.简单来说,在一个物体上应用纹理相当于将一张或多张图片"粘"到物体上,出于各种不同的目的.一个简单的例子,如图所示.纹理图片可以是一维的,二维的或三维的,二维最为常见.最简要地概括,最终的产品是为每个片元生成一个颜色值,这个颜色值会应用在接下来的阶段中.
2.5.2 合并
每个像素的信息储存在颜色缓冲(color buffer)中,颜色缓冲是关于颜色的二维数组(每个颜色有红绿蓝三个分量组成).合并阶段的任务是将像素着色阶段产生的片元颜色值与在颜色缓冲中存储的颜色进行组合.这个阶段也称做ROP,指管线光栅操作(raster operation (pipeline))或渲染输出单元(render output unit),取决于你询问的对象.与着色阶段不同,执行合并的GPU子单元不是充分可编程的.然而是高度可配置的,可以实现不同的效果.
这个阶段也负责处理可见性的问题.这意味着当整个场景被渲染完毕后,颜色缓冲中应该包含场景中能被摄像机看到的图元的颜色(也就是片元遮挡的问题).对于大多数甚至全部图形硬件来说,这步通过z-buffer算法(也就是深度缓冲算法)解决.深度缓冲和颜色缓冲具有相同的大小,并且对于每个像素都储存了当前距离摄像机最近的片元的深度.这意味着当图元被渲染到像素上,图元上这个像素的深度就会计算并和深度缓冲中相同位置的像素深度进行比较.如果新的深度比缓冲中的深度小,那么当前渲染的图元更接近摄像机.于是,这个位置的深度缓冲和颜色缓冲将会被更新为当前图元的深度.如果正在计算的像素的深度比缓冲中的深度大,那么这个像素的颜色和深度将被抛弃.z-buffer算法很简单,是O(N)复杂度(N是被渲染的图元的数量),并适用于可以为每个(相关)像素计算z值的任何图形图元。同样需要注意,z-buffer算法允许大多数图元以任何顺序渲染,这也是它好用的原因之一.然而,深度缓冲仅仅为屏幕上的每个点储存一个深度值,所以它不能用于部分透明的图元.透明图元必须在不透明图元之后渲染,并且以前后顺序呈现,或者单独使用独立于顺序的算法(5.5章介绍).透明度是z-buffer算法的主要弱点.
我们提到了颜色缓冲用来储存颜色,深度缓冲储存每个像素的深度.然而,也有其他的通道和缓冲能够用来过滤和捕获片元信息.透明度通道(alpha channel)和颜色缓冲相关并且储存了每个像素的透明度值(5.5章介绍).在旧的API中,透明度通道也被用来通过透明度测试丢弃像素.如今我们可以在像素着色器中插入丢弃的操作,或者其他的计算也能用来激活丢弃操作.这种类型的测试能用来确保完全透明的片元不会影响深度缓冲.
模板缓冲(stencil buffer)是一个幕后缓冲,用来记录已渲染的图元的位置.它一般每个像素包含8位.图元可以通过各种操作被渲染进模板缓冲,然后模板缓冲的内容能够被用来控制颜色缓冲和深度缓冲的写入.举例来说,设想一个实心圆形被画入了模板缓冲.随后能够进行操作,只允许在这个圆形出现的位置,将后续图元绘制进颜色缓冲.模板缓冲对于生成某些特别的效果十分有用.这些管线最后的操作被称为光栅操作(raster operations, ROP)或混合操作(blend operations).可以做把当前正在处理的像素颜色和颜色缓冲中已经存在的颜色进行混合的操作.这中操作可以实现透明度或颜色采样叠加的效果.如上所说,混合操作在API中是可配置的而不是可编程的.然而,一些API也提供对于光栅顺序视图(raster order views),也称为像素着色器排序(pixel shader ordering)的支持,它支持可编程的混合能力.
帧缓冲一般就包括上面所说的缓冲区.
当图元到达并且通过了光栅化阶段,对于摄像机的视角可见的图元就会被展示在屏幕上.屏幕展示的是颜色缓冲中的内容.为了避免观众看到图元正在进行光栅化和发送到屏幕的过程,通常会使用双缓冲.这意味着屏幕图像的渲染是发生在后台的,在后缓冲中(back buffer).一旦图像在后缓冲中被渲染完成,后缓冲中的内容就会和正在展示的前缓冲进行切换.交换的过程发生在垂直回溯(vertical retrace)期间,此时进行是安全的.
有关不同缓冲区的内容见5.4.2,23.6和23.7节.