「图形基础」笔记1. 图形渲染管线

时间:2024-04-04 13:59:43

图形渲染管线(The Graphics Rendering Pipeline)

注:渲染管线,或者你也可以叫它渲染流水线,本篇主要对渲染管线的工作流程进行介绍。


1. 体系结构(Architecture)

渲染管线的工作任务在于由一个三维场景出发,生成一张二维图像,也就是说,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。这个工作通常是由CPU和GPU共同完成的。

渲染管线的基本结构包括3个阶段:应用阶段(Application Stage)几何阶段(Geometry Stage)光栅化阶段(Rasterizer Stage)。每个阶段本身内部还有各自的管线。

「图形基础」笔记1. 图形渲染管线
Figure 1 渲染管线的基本结构

最慢的管线阶段决定渲染速度。管线允许并行处理,如果能够确定其中的瓶颈阶段,同时知道所有数据通过该阶段需要花费的时间,就可以计算出渲染速度,前提是输出设备能够以这个速度不断更新。有时可以用“吞吐量”这个术语来代替渲染速度。


2. 应用阶段(Application Stage)

本阶段任务: 应用阶段主要负责将需要渲染的几何体传递到几何阶段。可以划分为如下三步。

  1. 准备好场景数据,例如摄像机的位置,视锥体、场景中包含了哪些模型,使用了哪些光源等等。所有渲染所需的数据都需要从硬盘(HDD)中加载到系统内存(RAM),然后,网络和纹理等数据又被加载到显卡上的存储空间——显存(VRAM)中。这是因为,显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。

  2. 粗粒度剔除(Culling),把不可见的物体剔除掉,这样这些物体就不用交给几何阶段处理了。

  3. 设置好每个模型的渲染状态,如使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。设置好渲染状态后,CPU需要调用Draw Call来指导GPU进行渲染工作。一个Draw Call会指向本次调用需要渲染的图元列表。

这一阶段最重要的输出是渲染所需的几何信息,即渲染图元(Rendering Primitives)。渲染图元可以是点、线、三角形等,这些渲染图元将会被传递到下一个阶段——几何阶段。

应用阶段通过软件方式实现,通常由CPU负责实现,开发者具有这个阶段的绝对控制权,因此我们可通过改变实现方法来提升性能,例如,减少三角形数量,多处理器并行执行等。


3. 几何阶段(Geometry Stage)

本阶段任务: 几何阶段主要负责把顶点坐标变换到屏幕空间中,再交给光栅化阶段进行处理,在这过程中它会和每个渲染图元打交道,进行逐顶点、逐多边形的操作,这一阶段计算量很高,通常在GPU上进行。它可进一步划分为更小的功能阶段。

「图形基础」笔记1. 图形渲染管线
Figure 2 几何阶段内部的功能阶段

通过对输入对渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段——光栅化阶段。

3.1 模型与视点变换(Model & View Transform)

模型和视图变换阶段分为模型变换(Model Transform)视点变换(View Transform)。模型变换的目的是将模型变换到适合渲染的空间当中,而视图变换的目的是将摄像机放置于坐标原点,方便后续步骤的操作。

在屏幕上的显示过程中,模型通常需要变换到若干不同的空间或坐标系中。模型变换的变换对象一般是模型的顶点和法线。物体的坐标称为模型坐标。世界空间是唯一的,所有的模型经过变换后都位于同一个空间中。

实际上我们只需对相机(或者观察者)可以看到的模型进行渲染。相机在世界空间中有一个位置和方向,用来放置和校准相机。

为了便于投影和裁剪,必须对相机和所有模型进行视点变换。变换的目的是把相机放在原点,然后进行视点校准,使其朝向z轴的负方向,y轴指向上方,x轴指向右边,此时的空间称为相机空间观察空间。Figure 3显示了视点变换对相机和模型的影响。

「图形基础」笔记1. 图形渲染管线
Figure 3 视点变换对相机和模型的影响

3.2 顶点着色(Vertex Shading)

要产生逼真的场景,仅仅渲染物体的形状和位置是不够的。还应考虑材质,以及任何光源照射到该物体上的效果。

这种确定光对材质影响的操作称为着色(Shading)。它需要在物体的各个点上计算一个着色方程。通常情况下,这些计算都是在模型顶点的几何阶段进行的,但也可能在逐像素光栅化的时候进行。顶点存储着各种数据,例如点的位置、法线、颜色或计算着色方程所需的其他数值信息。顶点着色的结果(可以是颜色,向量,纹理坐标,或任何其他种类的着色数据)然后被送到光栅化阶段进行插值。

着色计算通常被认为是在世界空间中发生的。实际上,有时将相关实体(如照相机和光源)转换到其他空间(例如模型或视点空间)执行计算是很方便的。这是因为如果将着色计算中包含的所有实体转换到相同的空间,光源、摄像机和模型之间的相对关系将得到保持。

注:关于顶点着色的详细内容将在下一章顶点着色器部分继续讨论。

3.3 投影(Projection)

在着色处理后,渲染系统开始进行投影操作,目的是将视体变换为一个单位立方体(对角顶点分别是(-1,-1,-1)和(1,1,1)),通常也称单位立方体为规范立方体(Canonical View Volume)。主要的投影方法有正投影(Orthographic Projection)(也称平行投影(Parallel Projection))和透视投影(Perspective Projection),如下图所示:
「图形基础」笔记1. 图形渲染管线
Figure 4 正投影和透视投影

正投影的视体通常是一个矩形,正投影可以把这个视体变换为单位立方体。正投影的主要特性是平行线在变换之后依然平行,这种变换只相当于平移与缩放的组合

透视投影的视体是一个平截锥体(Frustum)也可以变换为单位立方体。透视投影中物体距离相机越远,投影之后就越小,平行线可以在地平线相交。

两种投影都可以通过4x4的矩阵来实现,在这两种变换之后,都可以认为模型位于归一化处理之后的设备坐标系中。投影之后产生的图像中的z坐标系将消失,模型被从三维投影到二维。

3.4 裁剪(Clipping)

当图元完全或部分地存在于视体内部时,才需要将其送至光栅阶段。
当一个图元完全位于视体内部时,直接进入下一个阶段。完全在视体外部的图元不会进入下一个阶段。部分位于视体内部的图元则需要进行裁剪处理。如下图所示:
「图形基础」笔记1. 图形渲染管线
Figure 5 裁剪示意图

3.5 屏幕映射(Screen Mapping)

只有在视体内部经过裁剪的图元以及之前完全位于视体内部的图元才可以进入屏幕映射阶段。进入这个阶段的时候,坐标依然是三维的(范围在单位立方体内),但显示状态在经过投影阶段后已经成了二维。每个图元的x和y坐标系都被变换到屏幕坐标系(Screen Coordinates)中,屏幕坐标系连同z坐标一起称为窗口坐标系(Window Coordinates)。窗口的范围是从最小的窗口坐标(x1,y1)到最大的窗口坐标(x2,y2),如下图:
「图形基础」笔记1. 图形渲染管线
Figure 6 屏幕映射示意图

屏幕映射阶段的主要目的,就是将之前步骤得到的坐标映射到对应的屏幕坐标系上。

一个令人困惑的点是整数和浮点值如何与像素坐标(或纹理坐标)相关联。这个问题可以用转换公式进行解决。这里不多赘述。


4. 光栅化阶段(Rasterizer Stage)

本阶段任务: 给定经过变换和投影后的点转化和相关着色数据(从几何阶段),光栅化阶段的任务是计算每个图元覆盖了哪些像素以及为这些像素计算颜色。这个过程称为光栅化(rasterization),最终将屏幕空间中每一个带有z值(即深度值)和各种着色信息的二维顶点转换为屏幕上的像素。

这一阶段同样也是在GPU上运行。光栅化阶段可被划分为几个功能阶段:三角形设置、三角形遍历、像素着色和合并。
「图形基础」笔记1. 图形渲染管线
Figure 7 光栅化阶段的内部功能阶段

4.1 三角形设置(Triangle Setup)

该阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们需要得到三角形边界的表达方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。这个过程由专门的硬件完成。

4.2 三角形遍历(Triangle Traversal)

该阶段检查每个像素是否被三角网格覆盖,对于有三角网格部分重合的像素,将在其重合部分生成片元(fragment)。这样一个寻找哪些像素被三角网格覆盖的过程就是三角形遍历,也称扫描转换(scan conversion)

三角形遍历阶段会根据上一个阶段的计算结果来判定一个三角网格覆盖了哪些像素,并使用三角网格3个顶点信息对整个覆盖区域的像素进行插值。

这一步的输出是一个片元序列,需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了但不限于屏幕坐标、深度信息以及其他从集合阶段输出的顶点信息,例如法线、纹理坐标等。这个过程也由专门的硬件完成。

4.3 像素着色(Pixel Shading)

像素着色也称片元着色(Fragment Shading)。任何逐像素着色的计算都是在该阶段执行,使用插值后的着色数据作为输入,最终输出的是一个或多个颜色值,被传递到下一个阶段。与三角形设置与遍历使用专用硬件不同,像素着色阶段是在可编程的GPU核心上执行,各种各样的技术可以被应用在这里,其中一个最重要的技术是贴图(texturing)。简单地说,给一个物体贴图就是将一张图片粘到物体上,下图详细描述了这个过程。贴图用的图片可以是一维,二维和三维的,最常用的是二维的。
「图形基础」笔记1. 图形渲染管线
Figure 8 像素着色示意图,左上角为一没有纹理贴图的飞龙模型。左下角为一贴上图像纹理的飞龙。右图为所用的纹理贴图。

注:关于像素着色的详细内容将在下一章像素着色器(也称片元着色器)部分继续讨论。

4.4 合并(Merging)

合并阶段在OpenGL中被称为逐片元操作(Per-Fragment Operations),可见这个阶段的操作对象是片元。

这一阶段的主要任务是:

  1. 决定每个片元的可见性。这涉及到很多测试工作,例如模板测试(Stencil Test)深度测试(Depth Test)等。

  2. 如果一个片元通过了所有测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区(Color Buffer)中的颜色进行合并。颜色缓冲区是一个存储每个像素颜色值的矩形阵列(红、绿、蓝三个分量),另外还有一个与之相关的Alpha通道(Alpha Channel),储存每个像素的不透明值。

简化的合并操作(逐片元操作)过程如下:
「图形基础」笔记1. 图形渲染管线
Figure 9 简化的合并操作(逐片元操作)序列
「图形基础」笔记1. 图形渲染管线
Figure 10 NVDIA官方OpenGL手册的合并操作(逐片元操作)序列

只有通过了所有测试后,新生成的片元才能和颜色缓冲区已经存在的像素颜色进行混合,最后再写入颜色缓冲区中。需要注意的是,Figure 10中最后写入的不是颜色缓冲区而是帧缓冲区(Frame Buffer),实际上帧缓冲区通常指一个系统上的所有缓冲区,不过有时仅仅是颜色缓冲区及Z缓冲区的组合。

测试顺序并不是唯一的。虽然从逻辑上讲这些测试是在片元着色器之后进行的,但对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试,这样可以尽可能早地知道哪些片元是会被舍弃的,以避免无谓的计算。然而需要注意的是。这些测试被提前有时会与片元着色器中一些操作发生冲突。

Alpha测试(Alpha Test)
上文说到的与颜色缓冲区相关联的alpha通道,为每个像素存储相应的不透明值。片元可以在执行模板测试和深度测试之前选择Alpha测试(Alpha Test),这一测试是可选的。片元的Alpha值与参考值进行指定的测试(比如等于,大于),如果片元测试失败,将被从后续的处理中移除,该测试通常用于保证完全透明的片元不会影响到Z缓冲区。

模板测试(Stencil Test)
与模板测试相关的是模板缓冲区(Stencil Buffer)。模板缓冲区是一个离屏的缓冲区,为每个像素保存了一个“模板值”,当像素需要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的通过测试,不符合条件的则被丢弃,不进行绘制。条件的设置与Alpha测试中的条件设置相似。但注意Alpha测试中是用浮点数来进行比较,而模板测试则是用整数来进行比较。举个例子,假设在模版缓冲区中绘制出了一个实心圆形,那么可以使用一系列操作符来将后续的图元仅在圆形所出现的像素处绘制,类似一个mask的操作。模板缓冲区是生成特殊效果的强大工具,类似于PhotoShop中的蒙板操作。

深度测试(Depth Test)
深度测试在绘制三维场景的时候特别有用。在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。
如果使用了深度测试,当一个图元被渲染到相应的像素时,GPU会计算像素位置处图元的z值,并与已经存在于Z缓冲区(Z-buffer,也叫深度缓冲区) 中的z值进行比较,如果新的z值比Z缓冲区中的值小,那么说明该图元与相机的距离比原来距离相机最近的图元还要近,所以该图元会被渲染。Z缓冲区是一个与颜色缓冲区形状大小相同的缓冲区,存储着每个像素从相机到当前最近图元的z值。这样,该像素的z值和颜色就由当前图元的z值和颜色进行更新。如果计算得到的z值大于Z缓冲区中的值,就保持不变。Z缓冲区算法非常简单,具有O(n)复杂度(n是需要渲染的图元数量)。对于任意图元,只要可以计算出相应像素的z值,就可以用这种方法。
要注意的是,该算法允许大部分图元以任意顺序进行渲染,这也是该算法流行的另一个原因,然而对于部分透明的图元是不允许的,它们必须在所有不透明体图元渲染之后,并且按照从后向前的顺序进行渲染,这是Z缓冲区算法的一个主要缺点。

混合(Blending)
对于不透明物体,开发者可以关闭混合操作。这样片元着色器计算得到的颜色值就是直接覆盖颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。开启混合功能之后,GPU会取出源颜色(片元着色器得到的颜色值)和目标颜色(已经存在于颜色缓冲区中的颜色值)进行混合,这里会使用一个混合函数来进行混合操作。这个混合函数通常与Alpha通道的值息息相关,例如根据Alpha值进行相加、相减、相乘等。这很类似于Photoshop中对图层的操作。

累积缓冲区(Accumulation Buffer)
1990年Haeberli和Akeley 提出累积缓冲区,是对帧缓存区的一种补充。累积缓冲区允许你把渲染到颜色缓冲区的值,拷贝到累积缓冲区。在多次拷贝操作到累积缓冲区时,可以用不同方式的把颜色缓冲区内容和当前累积缓冲区的内容进行重复混合。如此一来你就可以通过对一系列表示物体运动的图像进行累积和平均来产生运动模糊的效果。此外,还可以生成其他效果,包括景深(DOF),抗锯齿(antialiasing)和软阴影等。

当图元到经过上面层层计算和测试后,那些从相机视点可见的图元就会被显示在屏幕上,屏幕显示的就是颜色缓冲区中的颜色值。为了避免我们看到正在被光栅化以及发送到屏幕上的图元,双重缓冲(Double Buffering)被使用,这也即是说场景的渲染发生在离屏的后置缓冲区(back buffer)中,一旦场景在后缓冲区中渲染完成,后缓冲区的内容与之前正在屏幕上显示的前置缓冲区(front buffer)中内容进行交换,交换发生在一个可以安全执行的垂直回扫(vertical retrace)时间里,保证了我们看到的图像总是连续的。


参考文献
[1] Akenine-Möller T, Haines E, Hoffman N. Real-Time Rendering, Third Edition[J]. Crc Press, 2008.
[2] http://blog.csdn.net/poem_qianmo/article/details/70544201
[3] 冯乐乐. Unity Shader入门精要[M]. 人民邮电出版社, 2016.