本章描述了实时渲染中最核心的部分——渲染管线。它根据给定的场景设定、摄像机等参数渲染二维图像。
总体架构
渲染管线中 pipeline 一词的含义和 CPU 流水线相近,各个部分只依赖前一部分的结果工作,因此在理论上一个分为 n 级的流水线可以至多有 n 倍的加速效果。
在 RTR 中渲染管线被分为四个部分:Application, Geometry Processing, Rasterizatioin, Pixel Processing 。他们将在接下来的章节中被一一讲解。
第一个阶段,应用阶段,正如其名所表示的,是应用程序驱动的。程序开发者拥有着对这一阶段的完全控制权。
接下来的几何处理阶段则负责处理几何体的变换、投影等一系列操作。这一阶段负责计算哪些几何体应该被绘制、这些几何体会被如何绘制、以及它们是否会被绘制。
第三个光栅化阶段通常会以 3 个顶点形成的一个三角形作为输入,接着找到三角形内的像素,并将这些像素交给下一阶段(RTR3 中这两个阶段并没有分开)。
最后,像素处理阶段负责在每个汇报的像素上运行着色、深度测试等工作。
一般而言,应用阶段主要运行在 CPU 上,而后三个阶段则主要运行在 GPU 上。而且在一个阶段中有时还会被切分为子阶段以更好地完成任务。
应用阶段(Application Stage)
虽然应用阶段在大部分情况下是由 CPU 执行的,但它也可以利用 compute shader 将 GPU 视作一个高度并行的通用运算器以提高效率。另一个提高效率的做法是利用 CPU 的多个核心和线程,或者某些特殊指令集特性做一定程度的并行计算。
在应用阶段中最常处理的工作包括了碰撞检测、物理模拟、动画等等。这一阶段同样负责处理输入、实现加速算法(如 culling )等其它阶段无法处理的工作。
在这一阶段的结尾,需要被渲染的几何体会被传入几何处理阶段,他们被称为 rendering primitives 。
几何处理阶段(Geometry Processing Stage)
在几何处理阶段,GPU 主要负责的就是基于三角形和顶点的一系列操作。它一般被分为以下四个阶段:
Vertex Shading
这一阶段的主要人物有两个:
- 确定顶点的位置
- 根据程序员的要求指定顶点附加的数据
传统的做法是在每个顶点上计算光照并保存着色结果在顶点上,接着通过插值各个顶点的颜色以填充三角形。因此,负责这一过程程序名为 vertex shader 。随着 GPU 的发展,这一过程已经变得更为通用,并通常不再计算着色结果了。现在的 vertex shader 更多用于生成和记录顶点的数据,如生成部分着色用的数据、应用动画等。
一般而言,投影的过程也在这一阶段发生,程序会先用 Model - View 变换矩阵得到相机空间的物体表示,接着经由 Projection 矩阵得到投影位置。投影后视锥体内的场景被变换为一个对角线在 的立方体,称为 canonical view volume 。
Optional Vertex Processing
在上一流程结束后,根据硬件平台和软件设定的不同可能有以下的几个可选的流程,按调用顺序排列如下:
- 曲面细分(tessellation):这一流程可以结合场景和相机的设置,为几何体生成合适数量的三角形,以保证在显示和效率上达到一个最优点
- geometry shader:这一流程与曲面细分有些相似——它们均通过输入信息生成新的顶点和三角形。但这一部分负责的则是更简单的内容,如为基于点的粒子生成负责渲染的几何体等
- stream output:这一流程会截获将要输入接下来的管线的数据并写入一个序列中以便 CPU 或 GPU 在接下来的自定义流程中用到
Clipping
只有在视锥体,或投影后在裁剪空间(等效于之前的 canonical view volume )内的三角形才需要被渲染,这一步骤会剔除该空间外的三角形,并通过算法将部分在空间外的三角形裁剪至只包含空间内的部分。
需要注意的是,在裁剪发生时,生成的新顶点的各项属性并不都是被线性插值得到的。对于透视投影,需要除以一个投影系数以纠正数值误差。
除此之外,用户也可以自己指定裁剪平面以生成需要的效果。
Screen Mapping
几何处理阶段的最后一件事是将裁剪后的坐标投影到对应的屏幕坐标上。这一点在不同 API 的实现有所不同,主要是因为它们对屏幕坐标的定义有所差别。但基本上都会完成从浮点数的坐标转换到整数的像素坐标上这一步。像素的中心点一般定义在对应区域的中心位置,即:
光栅化阶段(Rasterization)
给定了屏幕空间的顶点和它们对应的着色信息,这一阶段的目标在于找到所有在几何体内的的像素以进行着色。他拥有三角形初始化和三角形遍历两个子步骤(虽然名为「三角形」,但实际上可以处理如线段等其它几何体)。
在 RTR 3ed 中,上述四个步骤均属于 Rasterization 阶段,本版将后两者进行了合并
Triangle Setup
在这一步骤中,微分、边函数以及其它与三角形有关的数据被计算。它们会在三角形遍历和像素插值中被使用到。
Triangle Traversal
在这一步中,所有在三角形内的像素会被找到(根据设置的不同,定义一个像素在三角形内部的方法也不同)。对于每个判断在三角形内部的像素(或采样),会根据三个顶点的插值生成一份顶点数据,接着被送入下一步骤中进行着色。
像素处理阶段(Pixel Processing)
这一阶段的处理并行于每个像素或采样之上,并如上一节图中的右侧一样被分为了着色和合并两个子阶段。
Pixel Shading
任何像素级别的操作均在这一阶段完成。利用插值后的顶点数据,最终输出一个或者多个颜色值给接下来的流程。与此前的几何处理和光栅化通常由 GPU 中预先设置好的物理模块运行不同,这一模块会使用 GPU 中的可编程模块以便程序员使用 pixel/fragment shader 自定义着色流程。
Merging
每个像素的信息会被存放在一个或者数个 buffer 之中,这一阶段就决定了哪些信息会被写入 buffer 中。和着色阶段不同,这一部分可被自定义的内容就少很多了。
这一部分的另一个作用是处理可见性。当整个场景被渲染时,应该只有可见的物体会最终被写入 buffer 中。大部分可见性的处理均是通过 z-buffer 或深度缓存完成的。
模板(stencil)buffer 是另一个常用的缓冲区,它一般会用于记录对应像素分别对应了哪个和模板测试有关的物体。这一缓冲区通常会在需要生成一些特殊效果(如以某一物体的区域作为蒙版)时用到。这些操作又被称为 blending 。
最终,所有使用到的缓冲区一般统称为 framebuffer 。
当这一流程结束后,如果没有后续的进一步处理,frame buffer 中的 color buffer 会被显示在屏幕上。为了防止对正在显示的图像进行直接操作,会使用双缓冲方法,交替地使用两个缓冲分别作为显示缓冲和写入缓冲。
总结
这一渲染管线经历了数十年的 API 和 GPU 的针对实时渲染应用发展,最终演进至今。但它并不是唯一的渲染管线。离线渲染具有着另一套流程,如影视制作中曾经使用过的 micropolygon 管线和光线追踪管线。
多年来,开发者大多数使用一个由图形 API 定义的 fixed-function pipeline 进行研发,这种性质一直持续到 2006 年发布的任天堂 Wii 管线。随着可编程 GPU 的出现,程序员得以深入定制管线中的部分子部分以实现更丰富和优秀的效果。