“A chain is no stronger than its weakest link.”
—Anonymous
本章主要介绍实时图形学中公认为最核心的部分,称为graphics rendering pipeline,也可以简称为管线。管线的主要功能是根据场景中给定的虚拟相机,三维物体,光源,阴影计算公式,纹理以及更多的元素,生成一幅二维的图像。因此渲染管线是实时渲染的底层核心工具。使用管线执行渲染图像的过程如图2.1所示。在该图像中,物体所处的位置和形状由物体的几何图形,环境特征以及环境中相机的位置共同决定。物体最终呈现的外观受材质属性,光源,纹理以及阴影模型的影响。
图2.1 左图中有一个虚拟相机位于金字塔的顶端(也就是四条线的交集)。只有位于视域体内部的图元才会被渲染。对于透视渲染的图像(即图中所示的情况),视域体是一个frustum(平截头体),即一个矩形基底的被截断的金字塔。右图中显示了相机“看到”的内容。其中左图中的红色圆圈形状的物体没有被渲染到右图中,因为该物体位于视域体之外。同样,左图中蓝色的弯曲棱柱体也被截头体的上平面剪裁了。
现在开始讨论渲染管线的各个不同阶段,我们重点讲解每个阶段的功能,而不是具体的实现细节。这些实现细节中的部分会在后面的章节进行讲解,要么就是属于程序开发人员无法控件的部分。例如,对于使用直线的开发人员来说,重要的是关注一些如顶点数据格式,颜色和模式类型的特点,以及depth cueing是否可用,而不是考虑直线是通过Bresenham直线绘制算法还是通过一种对称的两步算法实现的。通常管线中的部分阶段是使用不可编程的硬件实现的,因为无法对这些阶段的实现过程进行优化或改进。关于几何图形的基本绘制和填充算法的细节,在其他的书籍如Rogers中进行了深入的讲解。虽然我们无法控制底层硬件的部分实现,但是应用层的算法和编码方法也会对生成图像的速度和质量产生重大的影响。
在实际的物理世界中,管线的概念具有多种不同的表现形式,从工厂装配线到滑雪运送机都是一种管线形式。另外,管线的概念还应用于描述图形渲染。
管线由多个阶段组成。例如,在石油管线中,石油无法从管线的第一个阶段流动到第二个阶段直到第二阶段的石油已经流到了第三个阶段,依此类推。这种流动过程意味着管线的速度由最慢的阶段决定,而不管其他阶段的速度有多快。
理想情况下,把一个非管线系统划分成 n 个管线阶段可以使管线的执行速度提高 n 倍。这种方法带来的性能提升是使用管线的主要原因。例如,一辆滑雪缆车只包含一个座位是非常低效的,增加更多的座位可以成比例的快速增加载上山的滑雪人数。虽然管线的每一个阶段是并行执行的,但是它们需要停顿以等待最慢的管线阶段执行完成。例如,在一辆汽车的组成生产线上如果方向盘安装阶段需要执行3分钟,而其他的阶段都只用两分钟,那么组装完成一辆车的最快速度是3分钟;在执行过程中,其他阶段会有1分钟的空闲状态以等待方向盘安装完成。对于这种管线,方向盘阶段就是管线的瓶颈,因为这个阶段决定了整个生产过程的速度。
这种管线结构也用于表示实时计算机图形的执行过程。实时渲染管线可以粗略的划分为三个conceptual stages——application,geometry以及rasterizer,如图2.2所示。这种划分结构是实时计算机图形应用程序的核心,即渲染管线的引擎,因此是讨论后续章节必不可少的基础。此外,这三个阶段本身还是一个管线,即每一个阶段又包括一些子阶段。 根据功能的不同,我们可以把conceptual阶段(application,geometry,rasterizer)区分为functional阶段和pipeline阶段。在一个functional阶段执行某个具体的任务,但是不会指定在管线中该任务的执行方式。另一方面,一个pipeline阶段会与其他的pipeline阶段同步执行。另外,为了满足高性能需求,一个pipeline阶段还可能会并行执行。例如,geometry阶段可能会被划分为5个functional阶段,但是这是一个图形系统确定管线划分阶段的实现方式。一种给定的实现方法是把两个耗时较少的functional阶段合并为一个pipeline阶段,而把一个耗时的functional阶段划分为多个pipeline阶段,甚至对该阶段并行执行。
图2.2 渲染管线的基本结构,由三个阶段组成:application,geometry以及rasterizer。每一个阶段本身又可以是一个管线结构,如geometry阶段下面描述了这种子阶段的情况,或者一个阶段可以是(部分)并行的,如rasterizer阶段的下面所示。在该图中,application阶段是单个进程,但是该阶段也可以进行管线划分或并行执行。
管线中速度最慢的阶段决定了渲染速度,即图像的刷新速率。可以使用单位frames per second(fps)进行表示,即每秒钟渲染的图像数量。另外,也可以使用单位Hertz(Hz)表示,简单地记为 1/seconds ,即刷新频率。通常一个应用程序生成一幅图像所用的时间是不定的,主要由每一帧执行过程中计算的复杂性决定的。Frames per seconde通常用于表示一个具体帧的速度,或者在一段时间之内的平均性能。而Hertz用于表示硬件,比如一个显示器的刷新频率,一般设置为一个固定的速率。由于我们要处理的是一个管线,因此把我们想要渲染的所有数据在每个管线中传递完成所花费的时间进行相加是远远不够的。当然,这是由管线结构导致的,因为管线允许各个阶段并行执行。如果我们能找到管线的瓶颈,即管线中最慢的阶段,并衡量数据在该阶段传递完成所花的时间,那么我们就可以计算出渲染的速度。例如,假设管线的瓶颈阶段需要执行20ms(毫秒),那么渲染速度为 1/0.020=50 Hz。但是,只有在输出设备的刷新频率能够达到该速度的情况,才是真正的渲染速度;否则,真正的输出速率就会更慢。在其他的管线描述中,使用术语throughput(吞吐量)而不是渲染速度表示管线执行的速度。
EXAMPLE:RENDERING SPEED。假设输出设备的最大刷新频率为60Hz,并且已经找到了渲染管线的瓶颈。通过计时得到该阶段执行时间为62.5ms。那么可以使用以下方法计算渲染速度。首先,在不考虑输出设备刷新频率的情况下,可以得到最大的渲染速度为 1/0.0625=16 fps。其次,根据输出设备的刷新频率调整该速度值:刷新频率为60Hz表示渲染速度可以为60Hz,60/2=30Hz,60/3=20Hz,60/4=15Hz,60/5=12Hz,等等。这意味着我们可以预期渲染速度达到15Hz,因为这是输出设备在保证速度小于16fps的情况下,能够支持的最大的恒定速度。
顾名思义,application阶段是由应用程序驱动的,以软件的实现方式运行在通用CPUs中。这些CPUs通常含有多个内核单元支持以并行的方式处理多个线程的执行。因此CPUs可以高效地处理需要由application阶段完成的多个不同的任务。一般地,在CPU中执行的任务主要包括碰撞检测,全局加速算法,动画,物理仿真,以及根据应用程序类型对应的大量其他的任务。下一步是geometry阶段,主要处理变换,投影等运算。在这个阶段主要计算将要绘制的内容,如何绘制,以及要保存绘制数据的载体。Geometry阶段通常是在一个图形处理单元(GPU)中进行处理的,与固定管线功能的显卡一样,GPU中包含大量的可编程内核。最后在rasterizer阶段,使用前面阶段产生的数据绘制(渲染)一幅图像,与任何per-pixel(针对每个像素)进行计算所期望的结果一致。另外,rasterizer阶段完成在GPU中处理。在接下来的三节中,我们将分别讨论这三个阶段以及每个阶段的内部管线。在第3章会更详细地详解GPU如何处理每一个阶段。
由于application阶段是在CPU端运行的,因此开发人员可以完全控制该阶段的执行过程。于是,开发人员可以确定application阶段的完整实现,并在以后对其进行修改以提高程序的性能。对这个阶段的修改也会影响后续阶段的性能。例如,application阶段的算法或设置可以减少要渲染的三角形数量。
在application阶段的最后,需要把要渲染的几何图形传递给geometry阶段。这些几何图形也就是rendering primitives(图元),即points,lines和triangles,最终会显示到屏幕上(或者任何其他的输出设备)。这一过程是application阶段最重要的任务。
由于application阶段是基于软件实现的,因此不需要像geometry和rasterizer阶段那样把该阶段划分为多个子阶段。但是这个阶段通常会在多个处理器内核中并行执行,以提高程序性能。在CPU设计中,这种方式称为superscalar(超标量)架构,支持在同一个阶段同时执行多个指令。在第15章的15.5节讲述了多个处理器内核的各种不同的使用方法。
注:由于CPU本身就是一种规模非常小的流水线,因此说application阶段可以进一步划分为多个流水线阶段,但是这种流水线与本章所讨论的管线是毫不相干的。
在application阶段最常用实现的一种操作过程是collision detection(碰撞检测)。与一种压力反馈的设备一样,一旦在两个物体之间检测到了碰撞,就会产生一种响应并反馈给相互碰撞的物体。另外,在该阶段还会处理一些其他的输入源,比如键盘,鼠标,头盔等。根据不同的输入,需要采取不同种类的处理措施。最后,在这个阶段实现的其他计算过程包括纹理动画,通过变换得到的动画,或者任何只在该阶段执行的运算。一些常用的加速算法,如分层视域体剔除(见14章),也会在这一阶段执行。
在geometry阶段主要处理大多数的per-polygon和per-vertex(针对每个多边形和每个顶点)的操作运算。该阶段会被进一步划分为以下几个functional阶段:model和view transform(模型和视图变换),vertex shading(顶点着色),projection(投影),clipping(裁剪),以及screen mapping(屏幕映射)(如图2.3所示)。需要再次提醒的是,根据这些functional阶段的实现方式,这些阶段可能等同于pipeline阶段,也可能不同。在某些情况下,一连串的functional阶段形成单个pipeline阶段(与其他的pipeline阶段保持并行执行)。在另一些情况下,functional阶段可能会被划分为多个规模较小的pipeline阶段。
图2.3把Geometry阶段划分为由多个functinal阶段组成的管线。
例如,在某种极端情况下,整个渲染管线的所有阶段都可能以软件的方式运行在单个处理器上,这时你可以认为整个渲染管线只有一个pipeline阶段组成。这正是在独立的加速芯片和主板出现之前,所有显卡的生成方式。在另一种极端情况下,每一个functional阶段都可以被划分为多个小的pipeline阶段,并且每一个小的pipeline阶段可以在一个指定的处理器内核单元上执行。
在一个模型显示到最终屏幕的过程中,需要把该模型变换到到多个不同的空间或坐标系中。一开始,模型位于自身的model space中,意味着还没有进行任何变换。每一个模型可以与一个model transform进行关联,用于设置模型的位置和方向。另外,一个模型还可能与多个model transforms进行关联。这种方法允许同一个模型的多个副本(称为instances实例)在同一个场景中具有不同的坐标位置,方向和大小,而不需要复制多个基本的几何图形。
模型的变换主要是指使用与模型关联的model transform对模型的顶点和法线进行变换。最开始一个物体所在的坐标称为model coordinates,把model transform应用于这些坐标之后,就称模型位于world coordinates或处于world space中。对于所有的模型来说,world space是唯一的,在所有的模型使用各自的model transforms进行变换之后,这些模型都位于同一个空间中。
如前文所述,只有相机(或观察者)能够看到的模型才会被渲染。相机在world space中有一个位置和方向表示,用于放置相机并指定相机的观察目标。为了方便计算投影和裁剪,需要使用view transform对相机和所有的模型进行变换。View transform的目的是把相机放置在坐标原点,并设置观察方向,使得相机向前的方向为坐标的负z轴,y轴指向向上的方向,x轴指向向右的方向。
在这里我们约定使用-z轴表示正向,在有些资料中更偏向于使用+z轴。这两种表示方法主要是语义的不同,因为从一种表示变换到另一种是非常简单的。
使用view transform进行变换之后的实际位置和方向依赖于底层的图形应用编程接口(API)。因此使用这种方式划分的空间称为camera space,或更通用的,称为eye space。在图2.4中显示了一个使用view transform如何影响相机和模型的位置和方向的示例。Model transform和view tansform都是使用 4×4 大小的矩阵实现的,在第4章将会详解讨论该主题。
图2.4 在左图中,相机位于用户指定的位置并朝向用户指定的方向。在右图中,使用view transform把相机转移到原点,并指向负z轴。这种变换是为了更简单,更快速的执行裁剪和投影操作。其中浅灰色区域是相机的视域体。在这里,我们假设使用透视投影,因为视域体是一个锥体。同样的技术可以用于处理任意类型的投影。
要生成一个逼真的场景,仅仅渲染场景中物体的几何形状和坐标位置是远远不够的,还需要模拟物体的显示外观。这种描述包括每一个物体的材质,以及任意光源照射在物体上呈现的效果。模拟材质和光源的方法多种多样,从设置简单的颜色到精心设计物体的外观描述的表示形式。
这种确定一种光源照射到某种材质上的呈现效果的操作过程称为shading(着色)。该过程涉及到计算物体上各个点的shading equation(着色公式)。通常情况,一部分计算是在geometry阶段针对一个物体的顶点进行处理的,其他部分的计算在per-pixel(逐点)光栅化时处理的。每一个顶点中可以存储多种不同的材质数据,比如点坐标位置,法向量,颜色值或者任何其他的用于计算着色公式的数据信息。然后,把顶点着色的结果值(包括颜色值,向量,纹理坐标或任何其他类型的着色数据)发送到光栅化阶段进行插值运算。
通常我们认为在world space中执行着色计算。在实际操作中,有时把相关实体(比如相机和光源)变换到其他的坐标空间中(比如model或eye space)执行这些计算。之所以可以在这些空间中进行计算是因为只要把着色计算中所有实体变换到同一个空间中,就可以使光源,相机和模型物体的相对关系保持不变。
在本书上将会深入讨论着色计算,特别是在第3章和第5章。
完成顶点着色之后,渲染系统就开始处理projection(投影),投影是指把视域体变换成一个极值点为(-1,-1,-1)和(1,1,1)的单位立方体。这种单位立方体称为canonical view volume(规范视域体)。有两种常用的投影方法,分别称为orthographic(正交)(也称为平行)投影和perspective(透视)投影。如图2.5所示。
注1:规范视域体可以使用不同的极值点,比如 0≤z≤1 。Blinn在一篇文章中[102]讨论其他的间隔值。
注2:实际上正交投影只是平行投影的一种类型。例如,还有一种不常用的斜平行投影方法[516]。
图2.5 左边是一个正交(或平行)投影的示例,右边是透视投影的示例。
正交观察视图的视域体通常为长方盒,正交投影把该矩阵视域体变换为单位立方体。正交投影的主要特点是平行线经过投影变换之后依然是平行的。这种变换是由平移和缩放变换组合得到的。
透视投影比正交投影更复杂一点。在透视投影情况下,离相机距离越远的物体,在投影之后看起来越小。另外,平行线经过投影后可能会在地平线端点相交。通过这种方法,透视投影可以模拟我们感知物体大小的方式。从几何意义上来说,透视投影的视域体称为锥体,是一种以矩阵为基底顶部被截断的金字塔。正交投影和透视投影变换都可以使用 4×4 的矩阵构建(见第4章),经过任意一种变换后,我们称模型位于normalized device coordinates(归一化设备坐标)内。
尽管这些矩阵把一种包围体变换为另一种包围,但是我们把这个过程称为投影是因为在变换之后生成的图像中不会存储 z 坐标值。通过这种方式,模型由三维表示投影成二维表示。
注:不是完全不保存 z 坐标值,而是存储到了一个Z-buffer中。见2.4节。
只有全部或部分被包含在视域体之后的图元才需要传递下光栅化阶段,然后在显示器上绘制这些图元。完全位于视域体之内的图元可以直接传递到下一个阶段。而完全在视域体之外的图元就不会再传递到下一阶段,因为不会渲染这些图元。只有部分位于视域体之内的图元才需要clipping(裁剪)。例如,对于一条直接其中一个顶点位于视域体外,另一个顶点在视域体内部,需要根据视域体边缘对其进行裁剪,使用线段与视域体相交得到的新顶点替换位于视域体外部的顶点。使用了投影矩阵就意味着经过投影变换后的图元针对单位立方体进行裁剪。在执行裁剪操作之前先进行视图变换和投影变换的好处是可以使得裁剪操作保持一致;因为图元总是会针对单位立方体进行裁剪。裁剪操作的过程如图2.6所示。除了视域体的6个裁剪面之外,用户还可以增加自定义的裁剪面用于根据可见性剔除物体。在本书646页的图14.1中描述了这种可视化类型,称为sectioning(区域划分)。在前面的geometry阶段,通常使用可编程的处理单元执行,与此不同的是在clipping阶段(还有接下来要讨论的screen mapping阶段)通常使用固定功能的硬件进行处理。
图2.6 经过投影变换之后,只有位于单位立方体内部的(对应的位于视域体内部)图元才需要进一步继续处理。因此,位于单位立方体外部的图元直接被丢弃,整个位于内部的图元都被保留。对于那些与立方体相交的图元,则针对立方体进行裁剪,最终生成新的顶点并丢弃裁剪之前旧的顶点。
只有位于视域体内部的(裁剪后)图元会被传递到screen mapping阶段,在进入这个阶段的过程中依然是三维坐标。每一个图元的 x 和 y 坐标会被经过变换以形成screen coordinates(屏幕坐标)。屏幕坐标与 z 坐标结合一起也称为window coordinates(窗口坐标)。假设要把场景渲染到一个窗口中,该窗口最小坐标位置为 (x1,y1) ,最大坐标位置为 (x2,y2) ,其中 x1<x2 并且 yx<y2 。那么screen mapping就是先执行一次平移变换,接着执行一次缩放变换。而 z 坐标值不会受到screen mapping操作的影响。变换后的 x 和 y 坐标称为为屏幕坐标。最后,再加上 z 坐标值 (−1≤z≤1) 一起传递到rasterizer阶段。Screen mapping的处理如图2.7所示。
图2.7 经过投影变换之后的图元位于单位立方体中,screen mapping阶段通过计算确定屏幕上的坐标值。
关于screen mapping主要令人疑惑的是,如何把整数值和浮点值与像素(纹理)坐标进行关联。在DirectX 9以及之前的版本中,使用一种以 0.0 作为像素中心的坐标系,这种情况下位于范围[0,9]的一系列像素会覆盖[-0.5,9.5)的范围跨度。Heckbert[520]提出了一种在逻辑上更一致的方案。给定一个水平的像素数组并使用笛卡儿坐标,定义浮点数坐标中最左边像素的左边界为0.0。OpenGL中总是使用这种方案,在DirectX 10以及后续版本中也使用了这种方案。在这种情况下,像素的中心为0.5。因为处于范围[0,9]内的像素覆盖的范围坐标范围为[0.0,10.0)。这种变换方法可以简单地表示为
其中 d 是像素的离散(整数)索引值, c 为像素的连续(浮点)值。
虽然在所有APIs中,像素的位置值都是从左向右进行递增的,但是在OpenGL和DirectX的某些情况下,表示顶边和底边的 0 点值是不一致的。
“Direct3D”是DirectX的三维图形API组件。DirectX还包括其他的API组件,如输入和音频控制组件。在本书中我们不区分针对其他的组件使用“DirectX”,而对这里特定的API使用“Direct3D”表示,而是遵行通用的表示,统一使用“DirectX”。
OpenGL中倾向于在整个坐标表示中都使用笛卡尔坐标,把左下角作为最小值坐标,而DirectX则会根据实际情况使用左上角表示最小值。对于每种API这只是一种逻辑的不同,对于具体的不同点没有确切的答案。例如,在OpenGL中(0,0)位于图像的左下角,而在DirectX中则是左上角。DirectX之所以使用这种表示,是因为有大量的屏幕坐标表示是从顶部到底部:Microsoft Windows就是使用这种坐标系统,我们阅读的时候也是这种方向,还有大量的图像文件格式使用这种方式存储像素数据。当从一种API迁移到另一种API时,关键是要知道存在这些差异,并重点考虑这些问题。
给定了变换和投影后的顶点,以及与顶点对应的着色数据(全部来自geometry阶段),rasterizer阶段的主要目的是计算并设定物体涵盖的全部像素(图片元素的简称)的颜色值。这个过程称为raterization(光栅处理)或scan conversion(扫描转换),主要是把screen space中的二维顶点—每一个顶点包含一个 z 值(深度值)以及对应的各种各样的着色信息—转换为屏幕上的像素点。
图2.8 把rasterizer阶段划分为多个functional阶段的管线。
类似于geometry阶段,rasterizer阶段也被划分为多个functional阶段:triangle setup(组装三角形),triangle traversal(遍历三角形),pixel shading(pixel着色),以及merging(合并)(如图2.8所示)。
在这个阶段主要计算三角表面的差异以及其他数据。然后使用该数据执行扫描转换,同时还用于执行由geometry阶段产生的各种着色数据的插值运算。这个过程是由固定功能的硬件处理的,固定功能的硬件正是用于完成这些计算任务。
在这个阶段,检查每一个中心(或采样)坐标被三角形覆盖的像素点,并生成一个fragment(片段)用于表示覆盖三角形的部分像素点。查找哪些像素点或像素点位于三角形内部的操作过程称为triangle traversal或scan conversion。每一个三角形片段的属性值是通过在三角形三个顶点之间执行插值运算生成的(见第5章)。这些属性值主要包括片段的深度值,以及从geometry阶段传递过来的任意着色数据。Akeley和Jermoluk[7]和Rogers[1077]提供了有关三角形遍历的更多详细的信息。
在pixel shading阶段使用作为输入的插值后的shading数据处理任意的per-pixel shading(针对每一个像素进行着色)计算。最终计算的结果是一种或多种颜色值,并被传递到下一个阶段。Triangle setup和traversal阶段通常是使用特定的硬件晶体硅进行处理,与此不同地是,pixel shading阶段是在可编程的GPU内核上执行。在这个阶段可以使用多种技术,其中最重要的一种是texturing(纹理贴图)。在第6章我们将会详细讲解texturing。简单来讲,对一种物体进行纹理贴图就是把一张图片“粘贴”到物体上。图2.9描述了这个过程。纹理图片可以是一维、二维或者三维的,其中二维图片是最常用的一种。
图2.9 左上角显示了一个dragon模型没有进行纹理贴图的情况。把右边的纹理图片一块块“粘贴”到该模型上,可以得到左下角所示的结果。
每一个像素的信息都存储在color buffer(颜色值缓存)中,该buffer是一个颜色值(每一个颜色值由红,绿,蓝分量组成)的矩阵数组。在merging阶段主要负责把shading阶段产生的片段颜色值与color buffer中当前存储的颜色值进行合并。与shading阶段不同的是,通常用于执行merging阶段的GPU子单元不是完全可编程的。但是,该阶段是高度可配置的,允许实现各种各样的效果。
另外,在merging阶段还要解决可见性问题。这意味着,当整个场景都被渲染了,从相机的观察角度,color buffer中需要包含场景中可见图元部分的颜色值。对于大多数的显卡,这个过程是由Z-buffer(也称为depth buffer,深度缓存)算法实现的[162]。Z-buffer是一种与color buffer具有相同大小的形状的缓存,对于每一个像素存储了从相机到当前最近的图元之间的z-value(距离)。意思是当把一个图元渲染到某个像素点时,需要计算位于该像素点的图元的z-value,并与Z-buffer中位于同一个像素点的z-value进行比较。如果新的z-value小于Z-buffer中的z-value,那么要进行渲染的图元比之前该像素点上距离相机最近的图元到相机的距离更近。因此,该像素点的z-value和color值会被更新为当前正在绘制的图元的z-value和color值。如果计算的z-value大于Z-buffer中的z-value值,那么color buffer和Z-buffer保持不变。Z-buffer算法非常简单,算法的复杂度趋近于 O(n) (其中n为正被渲染的图元的数量),并且可以用于计算任意的绘制图元,该图元对于每一个像素点都可以计算得到一个z-value。此外,还需要注意的是该算法支持以任意顺序绘制大部分图元,这也是Z-buffer算法非常流行的另一个原因。但是,部分透明的图元无法以任意的顺序进行绘制,这些图元必须在绘制完所有不透明的图元之后再绘制,并且以back-to-front(距离相机由远及近,见第5章5.7节)的顺序绘制。这是使用Z-buffer的最主要的问题之一。
在无法使用Z-buffer的情况下,可以使用BSP tree以back-to-front(距离相机由远及近)的顺序绘制场景。关于BSP trees的详细信息见第14章14.1.2节。
前面我们已经讨论过color buffer用于存储颜色值,Z-buffer用于存储每一个像素点的z-value。除此之外还有其他的channels和buffers,可以用于筛选和捕获片段的信息。比如,在color buffer中除了红,绿,蓝通道值,还有alpha channel(通道),用于存储每一个像素点的相对透明度(见第5章5.7节)。在对传入的片段执行深度测试之前可以先执行一次alpha test。片段的alpha测试是把片段的alpha值通过一些特定的测试(相等,大于等等)与一个参考值进行比较。如果片段无法测试通过,就在下一步的处理过程中移除。这种测试通常用于保证完全透明的片段不会影响Z-buffer(见第6章6.6节)。
在DirectX 10中,alpha测试不再是merging阶段的一部分,而是pixel shader的其中一个function阶段。
Stencil buffer(模板缓存)是一个离屏缓存,用于记录渲染图元的位置信息,该buffer中每个像素点通常包含8-bits。可以使用多种functions把图元渲染到stencil buffer中,然后就可以使用该buffer中的数据控制渲染到color buffer和Z-buffer中的图元。比如,假设有一个实心圆圈被渲染到stencil buffer中,只有在该圆圈可见的位置对应的图元才会被渲染到color buffer中,结合这种方法我们能够控制绘制到color buffer中的图元了。另外,stencil buffer还是一种用于生成特殊效果的强有力的工具。在管线最后阶段的所有functions都称为raster operations(ROP)或blend operations。
Frame buffer(帧缓存)通常包括一个渲染系统中的所有buffers,但有时候也会仅使用frame buffer表示color buffer和Z-buffer的集合。1990年,Haeberli和Akeley[474]提出了frame buffer的另一个补集,称为accumulation buffer(累加缓存)。在该buffer中,可以使用一组操作对图像进行累加处理。例如,可以把用于显示一个运动中的物体的一组图像进行累加,再计算平均值,以生成运动模糊的效果。此外,还可以生成其他的效果,比如depth of field(景深),antialiasing(抗锯齿),soft shadows(软阴影)等等。
当基本图元到达并传递到rasterizer阶段,那些从相机的观察角度来说是可见的图元就会被显示到屏幕上。屏幕显示了color buffer中的存储的内容。为了避免让人眼看到图元被渲染并发送到屏幕上的过程,需要使用double buffering(双缓存技术)。意思是说场景渲染的过程发生在离屏阶段,在一个back buffer(后备缓存)中。一旦场景被渲染到back buffer中,位于back buffer中的数据内容就会与front buffer(前置缓存)进行交换,front buffer是屏幕上显示的之前的场景。这两个buffers的交换发生在vertical retrace(垂直回扫)期间,这是安全处理buffers交换的时间段。
关于不同的buffers以及各种buffering方法的更多信息,请阅读第5章5.6.2节以及第18章18.1节。
点、线和三角形是构建一个模型或物体的基本渲染图元。设想用户正在使用一个交互式的computer aided design(计算机辅助设计)应用程序设计一部手机。在这里我们将会通过整个图形渲染管线讨论该模型,管线由三个主要的阶段组成:application,geometry和rasterizer。通过透视投影到屏幕上的一个窗口对场景进行渲染。在这个简单的示例中,手机模型同时包含图元lines(显示手机的边缘部分)和triangles(显示手机的表面)。通过一个二维的图像对部分三角形进行纹理贴图,以显示手机的键盘和屏幕。对于这种示例,shading计算全部在geoemtry阶段执行,但是纹理的计算例外,该过程发生在rasterization阶段。
CAD应用程序允许用户选中模型的部分并移动。例如,用户可能会选择手机的顶部,然后移动鼠标翻转盖子并打开手机。在application阶段必须把鼠标的移动转化为一个对应的旋转矩阵,然后在渲染时把该矩阵正确地应用到手机盖的翻转操作中。示例二,通过把相机沿着预设的路线进行移动,从不同的观察角度显示手机可以形成一种动画的播放效果。其中相机的参数,如位置和观察方向,必须在application阶段根据运行的时间段进行更新。对于每个要渲染的帧,application阶段把相机的位置,光照,以及模型的基本图元传递到管线的下一个主要阶段—geometry阶段。
在application阶段计算了view transform,同时还计算了一个model matrix用于指定每一个物体的位置和方向。对于每一个传递到geometry阶段的物体,通常把这两个矩阵相乘以得到一个单一的矩阵。在geometry阶段,首先使用这个concatenated matrix(由一系列矩阵相乘得到的连接矩阵)对物体的顶点和法向量执行变换。然后使用材质和光源属性计算在顶点上的着色。接着执行投影运算,把物体变换到一个单位立方体空间中,该空间表示眼睛能看到的范围。所有处于该立方体之外的图元都会被丢弃。所有与该立方体相交的图元都会被裁剪,以得到一组完全位于单位立方体内部的基本图元。然后把顶点一一映射到屏幕上的窗口中。在执行完所有per-polygon(针对单个多边形)的操作之后,就把处理的结果传递到rasterizer阶段—管线中的最后一个主要的阶段。
在这个阶段,所有基本图元都被光栅化,即把图元转换为窗口中的像素点。进入到rasterizer阶段的每一个物体的可见的line和triangles都位于sceen space,以便于光栅化的转换。对于那些关联到纹理的triangles,使用应用到triangles上的纹理(图片)进行渲染。关于图元的可见性问题,可以使用Z-buffer算法处理,以及可选的alpha测试和stencil测试。依次处理每一个物体,然后把最终的图像显示到屏幕上。
这种管线由API和图形硬件几十年的演化而产生,致力于实时渲染应用程序。需要重点注意的是,这并不是唯一的渲染管线;离线渲染管线经历了不同的发展路线。用于电影制作的渲染管线最常用的是micropolygon管线[196,1236]。此外,在学术研究和predictive rendering的应用中,如建筑的可视化预览通常使用ray tracing(光线追踪)渲染器(见第9章9.8.2节)。
多年来,应用程序开发人员使用本书描述的处理方法的唯一途径是,使用图形API定义的一种fixed-function pipeline(固定功能的管线)。之前称为fixed-function pipeline是因为在图形硬件中该管线的实现包含一些不能进行灵活编程的部件。在该管线中,各个部分可以被设置成不同的阶段,比如Z-buffer测试可以打开或关闭,但是无法编写程序来控制把functions应用于各个阶段的顺序。最新的(也可能是最后一个)使用fixed-function的硬件机器是Nintendo(任天堂)的Wii。可编程GPUs的诞生,使得确定在整个管线的各个子阶段到底执行什么操作成为可能。虽然fixed-function pipeline的学习提供了对一些基本原理的合理介绍,但是大部分新的发展都是针对可编程GPUs。这种可编程性是本书第三版默认采用的方法,因为这是充分利用GPU的现代编程方法。
Blinn编写的A Trip Down the Graphics Pipeline[105]是一本讲解从头开始编写软件渲染器的书,虽然这本书已经比较旧了,但不失为学习一种渲染管线的的部分精妙之处,较好的参考资料。对于fixed-function pipeline,经典书籍(但是不断更新)OpenGL Programming Guide(又称为红宝书)[969]完整的讲述了fixed-function pipeline以及用到的相关算法。在本书的配套网站,http://www.realtimerendering.com,给出了多种渲染引擎实现的链接。