Loading

Real-Time Rendering 第二章

第二章 图形渲染管线 The Graphics Rendering Pipeline

“链条的强度取决于其最弱的环节。”

本章介绍实时图形的核心组件,即图形渲染管线,也简称为“管线”。管道的主要功能是生成或渲染二维图像,给定虚拟相机、三维对象、光源等。因此,渲染管线是实时渲染的基础工具。使用管道的过程在图 2.1中描述。图像中对象的位置和形状由它们的几何形状、环境特征以及相机在该环境中的位置决定。对象的外观受材料属性、光源、纹理(应用于表面的图像)和着色方程的影响。

图 2.1。在左图中,一个虚拟相机位于金字塔的顶端(四条线会聚的地方)。仅渲染视图体积内的图元。对于透视渲染的图像(如这里的情况),视图体积是一个平截头体,即具有矩形底部的截棱锥。右图显示了相机“看到的”。请注意,左图中的红色甜甜圈形状不在右侧渲染中,因为它位于视锥之外。此外,左图中扭曲的蓝色棱镜被剪裁对着平截头体的顶平面。

我们将解释渲染管道的不同阶段,重点是功能而不是实现。应用这些阶段的相关细节将在后面的章节中介绍。

2.1 体系结构 Architecture

在现实世界中,管道概念以许多不同的形式表现出来,从工厂装配线到快餐厨房。它也适用于图形渲染。管道由几个阶段组成 [715],每个阶段执行一个更大任务的一部分。

管线的各阶段是并行执行的,每个阶段都取决于上一阶段的结果。理想情况下,将非管线系统划分为 n 个管线阶段,可以使速度提高 n 倍。因此提升性能是使用管线的主要原因。举个例子,三个人以管线方式工作,快速制作大量的三明治:第一个准备面包,第二个加肉,第三个加佐料。每个人都将完成后的结果传递给下一个人,并立即开始处理下一个三明治。如果每个人都是花 20 秒执行他所在阶段的任务,那么每 20 秒就可以制作 1 个三明治,一分钟就能做 3 个三明治。管线的各阶段是并行执行的,但是如果最慢的阶段没有完成,其对应的下一个阶段就会等待。举个例子,假设情况有变,三明治的加肉阶段变得更加复杂,耗时变为 30 秒。那么整个三明治生产管线可以达到的最快速度就是每分钟 2 个三明治。那么对于这个管线来说,加肉阶段就是它的瓶颈(bottleneck),因为它决定了整个管线的生产速度。由于加肉阶段耗时较多,加佐料阶段必须等待加肉阶段完成才能继续工作,就这么干等着,顾客此时大概已经在挨饿了。

这种管道构建也可以在实时计算机图形的上下文中找到。实时渲染管线粗略划分为四个主要阶段——应用程序、几何处理、光栅化和像素处理—如图 2.2 所示。这种结构是核心—渲染管线的引擎—用于实时计算机图形应用程序,因此是必不可少的基础

图 2.2。渲染管线的基本构建,包括四个阶段:应用程序、几何处理、光栅化和像素处理。这些阶段中的每一个本身都可以是一个流水线,如几何处理阶段下方所示,或者一个阶段可以(部分)并行化,如像素处理阶段下方所示。在这个例子中,应用程序阶段是一个单一的进程,但这个阶段也可以是流水线或并行的。请注意,光栅化查找图元内部的像素,例如三角形。

后续章节讨论。这些阶段中的每一个通常本身就是一个管道,这意味着它由几个子阶段组成。我们区分此处显示的功能阶段及其实现结构。一个功能阶段有一个特定的任务要执行,但没有指定任务在管道中的执行方式。一个给定的实现可以将两个功能阶段组合成一个单元或使用可编程内核执行,同时它将另一个更耗时的功能阶段分成几个硬件单元。

渲染速度可以用每秒帧数(FPS)来表示,即每秒渲染的图像数量。也可以用赫兹 (Hz) 来表示,它只是 1/秒的表示法,即更新频率。仅说明渲染图像所需的时间(以毫秒 (ms) 为单位)也很常见。生成图像的时间通常会有所不同,具体取决于每帧期间执行的计算的复杂性。每秒帧数用于表示特定帧的速率或一段时间内的平均性能。 Hertz 用于硬件,例如设置为固定速率的显示器。

顾名思义,应用程序阶段由应用程序驱动,因此通常在通用 CPU 上运行的软件中实现。这些 CPU 通常包括能够并行处理多个执行线程的多个内核。这使 CPU 能够有效地运行由应用程序阶段负责的各种任务。一些传统上在 CPU 上执行的任务包括碰撞检测、全局加速算法、动画、物理模拟等,具体取决于应用程序的类型。下一个主要阶段是几何处理,它处理变换、投影和所有其他类型的几何处理。此阶段计算要绘制的内容、应如何绘制以及应在何处绘制。几何阶段通常在包含许多可编程内核和固定操作硬件的图形处理单元 (GPU) 上执行。光栅化阶段通常将三个顶点作为输入,形成一个三角形,并找到该三角形内的所有像素,然后将这些像素转发到下一个阶段。最后,像素处理阶段对每个像素执行一个程序以确定其颜色,并可能执行深度测试以查看它是否可见。它还可以执行逐像素操作,例如将新计算的颜色与先前的颜色混合。光栅化和像素处理阶段也完全在 GPU 上处理。所有这些阶段及其内部管道将在接下来的四节中讨论。第 3 章提供了有关 GPU 如何处理这些阶段的更多详细信息。

2.2 应用程序阶段 The Application Stage

开发人员可以完全控制应用程序阶段发生的事情,因为它通常在 CPU 上执行。因此,开发人员可以完全确定实现,然后可以对其进行修改以提高性能。此处的更改也会影响后续阶段的性能。例如,应用层算法或设置可以减少要渲染的三角形数量。

综上所述,一些应用程序工作可以由 GPU 执行,使用称为计算着色器的单独模式。此模式将 GPU 视为高度并行的通用处理器,忽略其专门用于渲染图形的特殊功能。

在应用程序阶段结束时,要渲染的几何图形被馈送到几何图形处理阶段。这些是渲染图元,即点、线和三角形,它们最终可能会出现在屏幕上(或正在使用的任何输出设备)。这是应用阶段最重要的任务。

该阶段基于软件实现的一个结果是它没有被划分为子阶段,几何处理、光栅化和像素处理阶段也是如此。但是,为了提高性能,这个阶段通常在几个处理器内核上并行执行。在 CPU 设计中,这被称为超标量构造,因为它能够在同一阶段同时执行多个进程。第 18.5 节介绍了使用多个处理器内核的各种方法。

在这个阶段通常实施的一个过程是碰撞检测。在检测到两个物体之间发生碰撞后,可以生成响应并将其发送回碰撞物体以及力反馈设备。应用程序阶段也是处理来自其他来源的输入的地方,例如键盘、鼠标或头戴式显示器。根据这个输入,可以采取几种不同的动作。加速算法,例如特定的剔除算法 (第 19 章),也在这里实现,以及管道的其余部分无法处理的任何其他内容。

2.3 几何处理阶段 Geometry Processing

GPU 上的几何处理阶段负责大多数每个三角形和每个顶点的操作。该阶段进一步分为以下功能阶段:顶点着色、投影、裁剪和屏幕映射(图2.3)。

图 2.3。几何处理阶段分为一系列功能阶段。

2.3.1 顶点着色 Vertex Shading

顶点着色有两个主要任务,即计算顶点的位置和评估程序员可能喜欢的顶点输出数据,例如法线坐标和纹理坐标。传统上,对象的大部分阴影是通过将灯光应用到每个顶点的位置和法线并仅将结果颜色存储在顶点来计算的。然后将这些颜色插入整个三角形。出于这个原因,这个可编程的顶点处理单元被命名为顶点着色器 [1049]。随着现代 GPU 的出现,以及每个像素发生的部分或全部着色,这个顶点着色阶段更加通用,并且可能根本不评估任何着色方程,这取决于程序员的意图。顶点着色器现在是一个更通用的单元,专门用于设置与每个顶点关联的数据。例如,顶点着色器可以使用 4.4 和 4.5 节中的方法为对象设置动画。

我们首先描述如何计算顶点位置,一组始终需要的坐标。在到达屏幕的路上,模型被转换成几个不同的空间或坐标系。最初,模型驻留在自己的模型空间中,这仅意味着它根本没有被转换。每个模型都可以与模型变换相关联,以便可以对其进行定位和定向。可以将多个模型转换与单个模型相关联。这允许同一模型的多个副本(称为实例)在同一场景中具有不同的位置、方向和大小,而无需复制基本几何体。

模型变换所变换的是模型的顶点和法线。对象的坐标称为模型坐标,在对这些坐标应用模型变换后,模型被称为位于世界坐标或世界空间中。世界空间是唯一的,模型经过各自的模型变换后,所有的模型都存在于同一个空间中。

前面有提到,只有相机(或观察者)看到的模型才会被渲染。相机在世界空间中是有位置与朝向数据的,这些数据用于放置和对准相机。为了便于投影和裁剪,相机和所有模型都会进行观察变换(view transform)。观察变换的目的是将相机放置在原点并将其对准目标,使其沿负 z 轴方向看,y 轴指向上方,x 轴指向右侧。我们使用 -z 轴约定; 有些书籍会更偏向使用 +z 轴。区别主要是语义上的,因此彼此之间的转换很简单。应用观察变换后的实际位置和方向取决于底层应用程序编程接口(API)。如此划定的空间称为相机空间(camera space),或更普遍地称为观察空间(view space)或眼空间(eye space)。观察变换影响相机和模型的方式示例如图 2.4 所示。模型变换和观察变换的操作都可以用 4×4 矩阵进行计算,这是第 4 章的主题。但是,重要的是要认识到——可以用程序员喜欢的任何方式来计算顶点的位置和法线。

图2.4. 左边的俯视图显示了在 +z 轴朝上的世界中,用户自定义摆放的相机。观察变换可使世界重定向,以使相机位于其原点,沿其负 z 轴看,而相机的 +y 轴朝上,如右图所示。这样做是为了使裁剪(clipping)和投影(projection)操作更简单,更快捷。浅蓝色区域是视景体(view volume)。因为视景体即为视锥体(frustum),所以这里假定为透视视图。类似的技术适用于任何类型的投影。

接下来,我们将描述顶点着色的第二种类型的输出。要生成逼真的场景,仅渲染对象的形状和位置是不够的,还必须对它们的外观进行建模。此描述包括每个对象的材质,以及任何光源照射在对象上的效果。材质和灯光可以通过多种方式建模,从简单的颜色到物理描述的精细表示。

这种确定光对材料效果的操作称为着色。它涉及计算对象上不同点的着色方程。通常,这些计算中的一些是在模型顶点的几何处理期间执行的,而其他计算可能是在逐像素处理期间执行的。可以在每个顶点存储各种材质数据,例如点位置、法线、颜色或评估着色方程所需的任何其他数字信息。顶点着色结果(可以是颜色、矢量、纹理坐标以及任何其他类型的着色数据)然后被发送到光栅化和像素处理阶段进行插值并用于计算表面的着色。

在本书中,尤其是在第3章和第5章,将更深入地讨论 GPU 顶点着色器形式的顶点着色。

作为顶点着色的一部分,渲染系统先进行投影变换,然后进行裁剪,这会将视景体(View Volume)变换为单位立方体(a unit cube),其极点位于(-1,-1,-1)和(1、1、1)。我们可以定义相同体积的不同范围,例如 0 ≤ z ≤ 1。此单位立方称为规范视景体(the canonical view volume)。投影是最先完成的,并且在 GPU 上它是由顶点着色器完成。有两种常用的投影方法,即正交投影(orthographic,也称为平行投影,parallel)和透视投影(perspective)。参见图 2.5。事实上,正交投影只是平行投影的一种。还有一些其他投影在建筑领域会特别用到,例如斜投影(oblique)和轴测投影(axonometric)。有一个老的街机游戏 Zaxxon 的名字就来源于后者。

图 2.5. 左边是正交投影或平行投影; 右边是透视投影。

请注意,投影操作可以表示为矩阵(第 4.7 节),因此有时可以将它与其余的几何变换连接在一起。

正交视图的视景体(view volume)通常是一个矩形框,而正交投影会将此视景体变换为单位立方体。正交投影的主要特征是平行线在变换后仍然保持平行。这种变换是平移和缩放的组合。

透视投影有点复杂。在这种类型的投影中,物体离相机越远,投影后看起来就越小。此外,平行线可能会聚在地平线上。因此,透视变换模仿了我们感知物体大小的方式。在几何上,称为截锥体的视图体积是一个具有矩形底面的截棱锥。平截头体也转化为单位立方体。正交变换和透视变换都可以用 4 x 4 矩阵构造,并且在任一变换之后,模型都被认为是在剪辑坐标中。这些实际上是齐次坐标,讨论过,所以这发生在除以 w 之前。 GPU 的顶点着色器必须始终输出这种类型的坐标,以便下一个功能阶段(裁剪)正常工作。

尽管这些矩阵将一个体积转换为另一个体积,但它们被称为投影,因为在显示之后,z 坐标不存储在生成的图像中,而是存储在 z 缓冲区中,如第 2.5 节所述。通过这种方式,模型从三个维度投影到两个维度。

2.3.2 可选顶点处理 Optional Vertex Processing

每个管道都有刚刚描述的顶点处理。完成此处理后,可以在 GPU 上进行几个可选阶段,按顺序是:曲面细分、几何着色和流输出。它们的使用取决于硬件的能力——并非所有 GPU 都有它们——以及程序员的愿望。它们相互独立,一般不常用。关于每个中的更多内容。第三章有关于每个中的更多内容。

第一个可选阶段是曲面细分。假设您有一个弹跳球对象。如果用一组三角形表示它,则可能会遇到质量或性能问题。您的球在 5 米外可能看起来不错,但近距离观察单个三角形,尤其是沿着轮廓,就会变得可见。如果你用更多的三角形制作球来提高质量,当球很远并且只覆盖屏幕上的几个像素时,你可能会浪费大量的处理时间和内存。通过曲面细分,可以使用适当数量的三角形生成曲面。

我们已经讨论了一些三角形,但在管道中的这一点上,我们只处理了顶点。这些可用于表示点、线、三角形或其他对象。顶点可用于描述曲面,例如球。这样的表面可以由一组面片指定,每个面片由一组顶点组成。镶嵌阶段由一系列阶段本身组成——外壳着色器、镶嵌器和域着色器——将这些补丁顶点集转换为(通常)更大的顶点集,然后用于制作新的三角形集。场景的相机可用于确定生成了多少个三角形:面片靠近时会产生很多三角形,面片很远时会产生很少的三角形。

一个可选阶段是几何着色器。该着色器早于曲面细分着色器,因此在 GPU 上更常见。它类似于曲面细分着色器,因为它接受各种类型的图元并可以生成新的顶点。这是一个更简单的阶段,因为此创建的范围有限,输出图元的类型也更有限。几何着色器有多种用途,其中最流行的一种是粒子生成。想象一下模拟烟花爆炸。每个火球都可以用一个点、一个顶点来表示。几何着色器可以将每个点变成面向观察者并覆盖多个像素的正方形(由两个三角形组成),从而为我们提供更令人信服的图元进行着色。

最后一个可选阶段称为流输出。这个阶段让我们使用 GPU 作为几何引擎。与将我们处理过的顶点沿着管道的其余部分发送到屏幕上不同,此时我们可以选择将这些顶点输出到数组中以供进一步处理。这些数据可以由 CPU 或 GPU 本身在以后的过程中使用。此阶段通常用于粒子模拟,例如我们的烟花示例。

这三个阶段按此顺序执行—曲面细分、几何着色和流输出—每个阶段都是可选的。无论使用哪个(如果有)选项,如果我们继续沿着管道向下走,我们就会得到一组具有齐次坐标的顶点,这些顶点将被检查相机是否能看到它们。

2.3.3 裁剪 Clipping

只有那些全部或部分位于视景体(view volume)内的图元(primitives)才需要传递到光栅化阶段(以及随后的像素处理阶段),然后将其绘制在屏幕上。完全位于视景体内的图元将保持原样并传递到下一个阶段。完全不在视景体之内的图元不会进一步传递,因为它们不会被渲染。部分位于视景体内的图元只需要裁剪后的那部分。举个例子,在视景体内有一条线段,线段的一个顶点在视景体外部,那么这条线段就应该相对于视景体进行裁剪,以便将外部顶点替换为位于该线段和视景体交点处的新顶点。投影矩阵的使用意味着将变换后的图元裁剪到单位立方体上。在裁剪之前执行观察变换和投影变换的优点是可以使它们的裁剪问题保持一致,同时进行;另外的优点是,图元总是被裁剪到单位立方体上。

裁剪过程如图 2.6 所示。除了视景体的六个裁剪平面之外,用户还可以定义其他的裁剪平面以可视方式裁剪对象。这种可视化的操作被称作切片(sectioning),见第 818 页的图 19.1。

图 2.6. 投影变换后,只有单位立方体内的图元(对应视锥体内的图元)需要进行后续处理。因此,单位立方体外部的图元将被丢弃,完全位于内部的图元将被保留。与单位立方体相交的图元将被裁剪到单位立方体上,并且因此生成新的顶点,丢弃旧的顶点。

裁剪步骤使用投影产生的 4 值齐次坐标执行裁剪。在透视空间中,值通常不会跨单个三角形进行线性插值。我们需要第四个坐标,以便在使用透视投影时能够正确地进行插值和裁剪。最后,执行透视划分(perspective division),将所得三角形的位置放入三维归一化的设备坐标中(three-dimensional normalized device coordinates)。正如之前提到的,此视景体的范围是从(-1,-1,-1)到(1、1、1)。几何阶段的最后一步是从该空间转换为窗口坐标(window coordinates)。

2.3.4 屏幕映射 Screen Mapping

只有视景体内的(裁剪)图元会被传递到屏幕映射阶段(screen mapping stage),并且当进入该阶段时坐标仍然是三维的。每个图元的 x 坐标和 y 坐标都将转换为屏幕坐标(screen coordinates)。屏幕坐标和 z 坐标也称为窗口坐标(window coordinates)。假设场景应被渲染到窗口中,窗口边角坐标最小在(x1,y1)处,最大在(x2,y2)处,其中 x1 < x2 并且 y1 < y2。然后,进行屏幕映射,即一个平移操作后接一个缩放操作。得出的新的 x 和 y 坐标被称为屏幕坐标。z 坐标(OpenGL中为 [−1,+1],DirectX 中为 [0,1])也映射到 [z1,z2],其中 z1 = 0 和 z2 = 1 作为默认值。但是这些是可以使用 API 进行更改的。之后窗口坐标以及这个重新映射的 z 值将传递到光栅化阶段。屏幕映射的过程如图 2.7 所示。

图 2.7. 图元位于投影变换后的单位立方体中,屏幕映射过程负责在屏幕上找到相应坐标。

接下来,我们将描述整数、浮点值与像素(以及纹理坐标)的关系。给定一个水平像素数组并使用笛卡尔坐标系,那么最左像素的左边缘在浮点坐标系中为 0.0。OpenGL 一直使用此方案,DirectX 10 及其后续版本使用此方案。 该最左像素的中心为 0.5。 因此,像素 [0,9] 的范围覆盖了 [0.0,10.0)的范围。这个转换很简单:

\[d=\operatorname{floor}(c) \tag{2.1} \]

\[c=d+0.5 \tag{2.2} \]

其中 \(d\) 是像素的离散 (整数) 索引, \(c\) 是像素内的连续 (浮点) 值。

尽管所有 API 的像素位置值都是从左到右增加的,但是在某些情况下,OpenGL 和 DirectX=中上下边缘的零的位置不一致。整个 OpenGL 都偏爱笛卡尔系统,将左下角视为最小值,而 DirectX 有时会根据上下文将左上角定义为最小值。这两种方式都有逻辑可循,两者间并没有正确答案。举个例子,(0,0)位于 OpenGL 中图像的左下角,而在 DirectX 中则位于左上角。 因此,当我们从一个 API 迁移到另一个 API 时,必须考虑到这一差异。

“ Direct3D”是DirectX的三维图形API组件。DirectX包括其他API元素,例如输入和音频控件。除了在指定特定版本时编写“ DirectX”和在讨论该特定API时编写“ Direct3D”之间,我们没有区别,而是通篇编写“ DirectX”来遵循常用用法。

2.4 光栅化阶段 Rasterization

给定经过变换和投影的顶点及其关联的着色数据之后(数据全部来自几何处理过程),下一阶段的目标是找到需要渲染的图元(例如一个三角形)内的所有像素(像素即图片元素 picture elements 的缩写)。我们称这种过程为光栅化,它被分为两个功能子阶段:三角形设置(triangle setup,也称为基本装配)和三角形遍历(triangle traversal)。这些显示在图 2.8 的左侧。请注意,它们也可以处理点和线,但是由于三角形最常见,因此子阶段的名称中带有“ 三角形”。光栅化,也称为扫描转换,是从屏幕空间中的二维顶点——每个顶点具有 z 值(即深度值)以及与每个顶点相关的各种着色信息——到屏幕上像素的转换。光栅化也可以被视为几何处理和像素处理之间的同步点,因为在这里,三角形是由三个顶点形成的,并且最终将往下发送以进行像素处理。

图2.8. 左:光栅化分为两个功能阶段,称为三角形设置与三角形遍历。右:像素处理分为两个功能阶段,即像素着色与合并。

三角形是否被认为与像素重叠取决于你如何设置 GPU 的管线。例如,你可以使用点采样来确定“内部”。最简单的情况是在每个像素的中心使用一个点采样,因此,如果该中心点在三角形内部,则相应的像素也被认为在三角形内部。您还可以使用超级采样或多重采样反走样技术对每个像素使用一个以上的采样(第 5.4.2 节)。还有一种方法是使用保守光栅化,其定义是,只要像素有部分与三角形重叠,则该像素就位于三角形内(第 23.1.2 节)。

2.4.1 三角形设置 Triangle Setup

在这一阶段,计算了三角形的微分、边缘方程和其他数据。 这些数据可用于三角形遍历(第 2.4.2 节)以及几何阶段产生的各种着色数据的插值。 固定功能硬件既适用于此任务。

2.4.2 三角形遍历 Triangle Traversal

在这个阶段,我们将检查每个像素(或样本)中心被三角形覆盖的情况,并为与三角形重叠的像素部分生成一个片段(fragment)。更多详细的采样方法可以在第 5.4 节中找到。找出三角形内的样本与像素的过程通常被称为三角形遍历。 每个三角形片元的属性都是使用在三个三角形顶点之间插值的数据生成的(第 5 章)。这些属性包括片元的深度,以及来自几何图形阶段的任何着色数据。 McCormack [1162] 等等提供了更多关于三角形遍历的信息。三角形上执行的透视校正插值(perspective-correct interpolation)也是在此阶段进行的(第 23.1.1 节)。 然后,图元中的所有像素或样本都会被发送到像素处理阶段,这将在接下来描述。

2.5 像素处理阶段 Pixel Processing

在此阶段,经过之前所有阶段的处理,已经找到了在三角形或其他图元内部应考虑的所有像素。像素处理阶段被划分为像素着色与合并,如图 2.8 右侧所示。像素处理是对图元内部的像素或样本执行逐像素或逐样本计算和操作的阶段。

2.5.1 像素着色 Pixel Shading

使用插值的着色数据作为输入,此处可以执行任何逐像素的着色计算。其最终结果是一种或多种颜色被传递到下一个阶段。与通常由专用的硬连线硅(hardwired silicon)执行的三角形设置和遍历阶段不同,像素着色阶段由可编程 GPU 内核执行。为此,程序员为像素着色器(在 OpenGL 中称作片段着色器或片元着色器)提供了一个程序,该程序可以包含任何所需的计算。该程序里可以使用各种各样的技术,其中最重要的一种技术是纹理(texturing)。在第 6 章中将更详细地讨论纹理。简单地说,对一个对象进行纹理操作意味着出于各种目的将一个或多个图像“粘”到该对象上。此过程的一个简单示例如图 2.9 所示。图像可以是一维,二维或三维图像,其中二维图像是最常见的。以最简单的方式来说,像素着色的最终产物是每个片元的颜色值,并会将它们传递到下一个子阶段。

图 2.9. 左上方显示了没有纹理的龙模型。图像纹理中的部分被“粘”到龙上,其结果显示在左下方。

2.5.2 合并 Merging

每个像素的信息都存储在颜色缓冲区中,颜色缓冲区是颜色的矩形阵列(每种颜色的红色、绿色和蓝色分量)。合并阶段负责将像素着色阶段产生的片元颜色与当前存储在缓冲区中的颜色进行组合。此阶段也称为 ROP,代表“光栅操作(管线)”(raster operations pipeline)或“渲染输出单元”(render output unit)。与着色阶段不同,执行此阶段的 GPU 子单元通常不是完全可编程的。但是,它是高度可配置的,可实现各种效果。

合并阶段还负责解决可见性。这意味着在渲染了整个场景之后,颜色缓冲区应包含场景中的原始颜色,这些颜色从相机的角度是可见的。对于大多数甚至所有图形硬件,这都是通过 z 缓冲区(也称为深度缓冲区)算法 [238] 完成的。z 缓冲区的大小和形状与颜色缓冲区相同,并且对于每个像素,z 缓冲区将 z 值存储到当前最接近的图元中。这意味着,当将图元渲染到某个像素时,该图元在该像素处的z值将被计算并与同一像素处 z 缓冲区的内容进行比较。如果新的 z 值小于 z 缓冲区中的 z 值,则正在渲染的图元比之前在那个像素处最接近相机的图元更接近相机。因此,该像素的 z 值和颜色将使用所绘制图元的 z 值和颜色进行更新。如果计算的 z 值大于 z 缓冲区中的 z 值,则保持颜色缓冲区和 z 缓冲区不变。z 缓冲区算法很简单,具有 \(O(n)\) 收敛(其中 n 是要渲染的图元的数量),并且适用于可为每个(相关)像素计算 z 值的任意绘图图元。也需要注意的是,该算法允许大多数图元以任意顺序呈现,这是该算法如此流行的另一个原因。但是,z 缓冲区仅在屏幕上的每个点存储一个深度,因此不能用于部分透明的图元。所以必须在所有不透明图元之后,以从后到前的顺序,或使用单独的、与顺序无关的算法(第 5.5 节)呈现这些内容。透明部分也是基本 z 缓冲区的主要弱点之一。

我们已经提到了颜色缓冲区是用于存储颜色的,而 z 缓冲区是用于存储每个像素的 z 值的。然而,还有其他通道和缓冲区可用于筛选和捕获片元信息。例如 Alpha 通道就是与颜色缓冲区关联,并为每个像素存储相关的不透明度值(第 5.5 节)。在较早的API 中,Alpha 通道还用于通过 Alpha 测试功能选择性地丢弃像素。如今,我们可以将该丢弃操作插入到像素着色器程序中,并且可以使用任意类型的计算来触发丢弃。我们可以使用此类测试来确保完全透明的片元不会影响 z 缓冲区(第 6.6 节)。

模板缓冲区是一个屏幕外的缓冲区,它用于记录所渲染图元的位置。通常它的每个像素包含 8 位。我们可以使用各种功能将图元渲染到模板缓冲区中,然后可以使用缓冲区的内容来控制渲染到颜色缓冲区和 z 缓冲区中的内容。举个例子,假设已将填充后的圆绘制到模板缓冲区中。它可以与一个操作符相结合,该操作符只允许将后续图元进入颜色缓冲区中填充圆所在的部分。模板缓冲区是生成某些特殊效果的强大工具。管线末端的所有这些功能都称为光栅操作(raster operations,ROP)或混合操作(blend operations)。可以将当前颜色缓冲区中的颜色与三角形内要处理的像素的颜色混合。这可以实现诸如透明度或颜色样本累积的效果。如前所述,混合操作通常可以使用 API 进行配置,而不是完全可编程的。但是,某些 API 支持光栅顺序视图(raster order views),也称为像素着色器顺序(pixel shader ordering),它可启用可编程混合功能。

帧缓冲区(framebuffer)通常由系统上的所有缓冲区组成。

当图元到达并通过光栅化程序阶段时,从相机的角度可见的图元将显示在屏幕上。 屏幕显示了颜色缓冲区的内容。 为了避免让人类观察者在对它进行光栅化并将它发送到屏幕时看到它们,因此使用了双重缓冲(double buffffering)。 这意味着场景的渲染发生在后台缓冲区的屏幕外。 将场景渲染到后缓冲区(back buffffer)中后,后缓冲区的内容将与先前在屏幕上显示的前缓冲区(front buffffer)的内容交换。 交换通常发生在垂直回扫(vertical retrace)过程中,而这是进行交换操作的安全时间。

有关不同缓冲区和缓冲方法的更多信息,请参见第 5.4.2、23.6 和 23.7 节。

2.6 管线概览 Through the Pipeline

点,线和三角形是用于构建模型或对象的渲染图元。假设该应用程序是交互式计算机辅助设计(CAD)应用程序,并且用户正在检查华夫饼制造商的设计。在这里,我们将遵循此模型并遍历整个图形渲染管线,包括四个主要阶段:应用程序,几何,光栅化和像素处理。然后通过透视视图将场景渲染到屏幕上的窗口中。在这个简单的示例中,华夫饼制作机模型同时包含线(以显示零件的边缘)和三角形(以显示零件的表面)。松饼机的盖子可以打开。一些三角形是由带有制造商徽标的二维图像制成的。对于此示例,除了在光栅化阶段进行的纹理应用操作之外,表面着色都是在几何阶段进行完整计算的。

应用程序阶段(Application)

CAD 应用程序允许用户选择和移动模型的各个部分。 例如,用户可以选择盖子,然后移动鼠标将其打开。 应用程序阶段必须将鼠标移动转换为相应的旋转矩阵,然后确认呈现该矩阵时已将其正确应用于盖子。 另一个例子:播放动画,使动画沿预定路径移动,以便从不同的角度显示华夫饼制作机。接下来就必须由应用程序根据时间更新相机参数,例如位置和视图方向。对于要渲染的每个帧,应用程序阶段将相机的位置,照明和模型的图元发送到管线中的下一个主要阶段——几何处理阶段。

几何处理阶段(Geometry Processing)

对于透视视图,我们在此假定应用程序已提供了投影矩阵。同样,对于每个对象,应用程序都已计算出一个矩阵,该矩阵描述了观察变换以及对象本身的位置和方向。在我们的示例中,华夫饼制造商的底座将具有一个矩阵,而盖子则具有另一个矩阵。在几何阶段,我们使用此矩阵来转换对象的顶点和法线,从而将对象放入观察空间(view space)。然后,可以使用材质和光源属性来计算顶点处的着色或其他计算。然后使用单独的用户提供的投影矩阵执行投影,将对象转换为代表眼睛所见的单位立方体的空间。单位立方体外部的所有图元都将被丢弃。将与该单位多维数据集合相交的所有图元都裁剪到该多维数据集合上,以便获得一组完全位于单位立方体内的图元。然后将这些顶点映射到屏幕上的窗口中。在完成所有这些每个三角形和每个顶点操作之后,将所得数据传递到光栅化阶段。

光栅化阶段(Rasterization)

接下来,将所有在上一阶段裁剪后留下的图元光栅化,这意味着将寻找图元内部的所有像素,并将它们进一步发送到管线以进行像素处理。

像素处理阶段(Pixel Processing)

此阶段的目标是计算每个可见图元中每个像素的颜色。那些与任何纹理(图像)相关联的三角形将根据需要来使用这些图像进行渲染。可见性的问题通过 z 缓冲区算法以及可选的丢弃操作和模板测试来解决。每个对象被依次处理,并且将最终的图像显示在屏幕上。

总结(Conclusion)

该管线源于针对实时渲染应用程序的数十年的 API 和图形硬件演变。重要的是我们要注意,这不是唯一可行的渲染管线。脱机渲染管线经历了不同的演进路径。电影制作的渲染通常是通过微多边形管线(micropolygon)来完成的 [289,1734],但是光线追踪和路径追踪近来已被替代。11.2.2 节中介绍的这些技术也可以用于建筑和设计的预可视化。

多年来,应用程序开发人员使用此处描述的过程的唯一方法是通过使用中的图形 API 定义的固定功能管线(fixed-function pipeline)。固定功能管线之所以如此命名,是因为实现它的图形硬件包含无法灵活编程的元素。主流的固定功能设备的最后一个例子是 2006 年推出的 Nintendo 的 Wii。另一方面,可编程的 GPU 可以准确地确定在整个管线的各个子阶段中具体应用了哪些操作。 对于本书的第四版,我们假设所有开发都是使用可编程 GPU 来完成的。

进一步阅读和资源(Further Reading and Resources)

Blinn 的著作《图形管线之旅》(《A Trip Down the Graphics Pipeline》) [165]是一本关于从头开始编写软件渲染器的旧书。它是学习实现渲染管线的一些精妙之处以及解释关键算法(例如裁剪和透视插值)的好资源。古老的(至今仍经常更新)《 OpenGL编程指南》(又称“红皮书”)[885]提供了图形管线的完整描述以及与其使用相关的算法。本书的网站 realtimerendering.com 提供了指向各种管线图以及渲染引擎实现等等的链接。

相关参考

超标量构造(superscalar construction) 英文勘误中修改为:多核(multi-core) FYI:Real-Time Rendering Book Corrigenda (realtimerendering.com)

posted @ 2021-12-14 21:12  straywriter  阅读(86)  评论(0编辑  收藏  举报