计算机图形学中的渲染流水线概述摘录

2.1 综述

2.1.2 什么是渲染流水线(概念流水线)

ü  计算机的渲染流水线:

计算机的图像渲染工作流水线和工厂的运作的流水线是有一定的相似性的。渲染流水线的工作任务在于从一个3D的场景出发,生成(或者说渲染)我们屏幕上看到的一张2D的图像。计算机从一系列的Mesh网格数据(3D场景中的模型展开后一般包括纹理贴图和在建模软件中以线框形式显示的模型文件,而这个文件就是模型的网格数据,就是我们所说的Mesh)、纹理等信息出发,把这些信息转换为一张我们人眼可以看到的图像,而这个工作通常是由计算机的CPUGPU共同完成的。

ü  渲染的流程:

一个渲染流程一般可以大致地分为3个概念性的阶段:应用阶段、几何阶段、光栅化阶段。而每个阶段本身通常也是一个流水线系统。

ü  应用阶段(在CPU上进行):

该阶段是由我们的应用主导的,开发者在该阶段拥有绝对的控制权。在该阶段中,开发者的任务有3个:13D场景的组建:我们需要准备好数据来组建场景,例如摄像机的位置、场景中包含了哪些场景、使用了那些光源等等。2、粗粒度剔除:为了提高渲染的性能,我们需要进行一个粗粒度剔除(culling)工作,将不可见的物体从场景中移除,以把那些不可见的物体剔除出去,这样就不需要在移交给集合阶段进行处理。3、设置模型的渲染状态:这些渲染状态包括但不限于模型使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。在应用阶段中输出渲染所需的几何信息,即渲染图元。通俗的来讲,这些渲染图元将会被传递给几何阶段。

ü  几何阶段(在GPU上进行):

该阶段的重要任务就是对上一阶段传过来的渲染图元进行逐顶点,逐多边形操作,将顶点坐标转换为屏幕空间中的坐标,再交给光栅器(光栅化阶段)进行处理。通过对输入渲染图元进行多步处理以后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传给光栅化阶段。

ü  光栅化阶段(在GPU上进行):

该阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。光栅化的主要任务是决定每个渲染图元中的哪些像素应当被绘制在屏幕上。它对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。

 

2.2 CPUGPU之间的通信:CPU流水线,概念流水线中的应用阶段

渲染流水线的起点是CPU,即应用阶段。应用阶段大致可以分为下面3个阶段:

1)     把数据加载到显存中;

2)     设置渲染状态;

3)     调用Draw Call(会有更详细的解释);

 

ü  把数据加载到显存中:

所有渲染所需的数据都需要从硬盘加载到系统的内存中,然后Mesh(即前面所提到的网格数据)和纹理等数据又被加载到显卡的显存中。这是因为显卡对于显存的访问速度更快,而且大多数显卡对于RAM并没有直接的访问权限(Intel的核心显卡貌似是共享系统内存)。当数据加载到显存中后,RAM中的数据就可以移除了,但对于一些数据来叔,CPU仍然需要访问它们(例如,我们希望CPU可以访问网格数据来进行碰撞检测),那么我们就不希望这些数据被移除,因为从硬盘加载到RAM中是十分耗时的。

ü  设置渲染状态:

渲染状态定义了场景中的Mesh是怎样被渲染的。例如,使用某个指定的顶点着色器或片元着色器,光源的属性,材质等。如果我们没有更改渲染状态,那么所有的Mesh都将使用同一种渲染状态。

ü  调用Draw Call

1)     Draw Call是一个命令,它的发起方是CPU,接受方是GPUCPU通过调用Draw Call来告诉GPU开始一个渲染的过程。一个Draw Call指向本次调用所需要渲染的图元列表。当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质,纹理,着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上的像素。而这个计算过程,就是GPU的流水线(本节为CPU的流水线)。

2)     在每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据状态和命令等等。在这一个阶段,CPU需要完成很多工作,例如检查渲染状态等。一旦CPU完成了这些准备工作,GPU就可以开始本次的渲染。GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没有什么太大的区别,因此渲染的速度往往快于CPU提交命令的速度。如果Draw Call的数量太多,CPU就会把大量的事件花费在提交Draw Call上造成CPU的过载。

3)     如何减少Draw Call的使用?尽管减少Draw Call的方法有很多,但是我们这里仅仅讨论批处理的方法,即把很多个小的DrawCall打包成一个大的Draw Call,这就是所谓的批处理思想。但是合并的过程往往需要很长的时间,所以批处理更适合用于哪些静态的物体,例如不会移动的石头和地面等。这就启发我们在游戏开发的过程中避免使用大量很小的网格,避免使用过多的材质,尽量在不同的网格之间使用同一个材质。

 

2.3 GPU流水线概念流水线中的几何阶段和光栅化阶段

GPUCPU那里得到渲染命令后,就会进行一系列的流水线操作,最终把图元渲染到屏幕上。

 

2.3.1 概述

GPU的渲染过程就是GPU流水线。对于概念的后两个阶段,即几何阶段和光栅化阶段,开发者是没有绝对的控制权的,其实现的载体是GPU。几何阶段和光栅化阶段可以分成若干更小的流水线细节,每个阶段GPU提供了不同的可配置性和可编程性。

计算机图形学中的渲染流水线概述摘录

几何阶段:

ü  GPU的渲染流水线接收网格数据(Mesh)和常数属性数据(Properties)作为输入,这些数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器。(联想:在Unity着色器训练营(1):入门篇(上)_1笔记所编写的最基本的Shader中,定义了一个从网格数据(Mesh)和常数属性数据(Properties)到顶点函数的结构体appdata,用于向顶点函数传递数据)

ü  2.3.2 顶点着色器(Vertex Shader):

1)     顶点着色器是GPU流水线的第一个部分。它处理的单位是顶点,也就是说输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何定点,而且无法得到顶点与顶点之间的关系,我们无法知道两个顶点是否来自于同一个三角网格。但是因为这样的独立性使得GPU可以快速处理每一个顶点。

2)     顶点着色器是完全可编程的,它通常用于实现顶点的空间变换,逐顶点光照。GPU在输入每隔输入的Mesh顶点上都会调用顶点着色器,顶点着色器必须进行顶点坐标变换,需要时还可以计算和输出颜色。除了这两个主要的任务外,顶点着色器还可以输出后续阶段所需要的数据。

3)     坐标变换就是对顶点的坐标进行某种变换,顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。例如我们可以通过改变顶点位置来模拟水面、布料等。

ü  曲面细分着色器(Tessellation Shader):曲面细分着色器是一个可选的着色器,它用于细分图元。

ü  几何着色器(Geometry Shader):几何着色器同样是一个可选的着色器,它可以用于逐行逐图元的着色操作,或者用于产生更多的图元。

ü 2.3.3 裁剪(Cliping):

1)      这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片,这个阶段是可配置的。例如我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令来控制裁剪三角图元的正面还是背面。

2)      一个图元和摄像机的关系一共有三种:完全在视野内、部分在视野内和完全在事业外。完全在视野内的图元就继续传给下一个流水线的阶段,完全在视野外的图元不会继续向下传递,因为他们不需要渲染。而那些部分在视野内的图元需要进行裁剪处理。例如一条线段的一个顶点在视野内另一个顶点在视野外,那么在视野外的顶点应当使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点处。

ü 2.3.4 屏幕映射(Screen Mapping):

1)      这一步输入的仍然是三维坐标系下的坐标,这一阶段是不可配置和编程的阶段,它负责把每个图元的xy坐标从(-11)范围转换到屏幕的坐标系中(屏幕坐标系是一个二维的坐标系,这个坐标系和我们用于显示画面的分辨率有很大的关系)。屏幕映射得到的坐标决定了这个顶点对应屏幕上的哪个像素及距离这个像素有多远。

2)      我们把场景渲染到一个窗口上,窗口的范围是从最小窗口坐标(x1,y1)窗口左下角到嘴的窗口坐标(x2,y2)窗口右上角,其中x1<x2y1<y2。由于我们输入的坐标范围是在(-11)之间的,因此屏幕映射的过程实际上是一个缩放的过程。需要注意屏幕映射不会对z坐标进行任何的处理。而实际上屏幕坐标和z坐标一起构成了一个坐标系——窗口坐标系。这些值会一起被传递到光栅化阶段。

3)      需要额外注意的是OpenGLDirectX之间的屏幕坐标是具有细微的差异的。OpenGL的最小窗口坐标系是在窗口的左下角,而DirectX的最小窗口坐标实在窗口的左上角,这样的差异极有可能使得在屏幕上的图像是倒转的,在开发的过程中需要多加注意。

 

光栅化阶段:

从上一阶段输出的是屏幕坐标系下的顶点的位置及和它们相关地额外信息(如深度值(z坐标),法线方向,视角方向等),光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。

ü  三角形设置(Traingle Setup)和三角形遍历(Traingle Travel):这两个阶段是固定函数阶段。

1.     2.3.5 三角形设置:

这个阶段将会计算光栅化一个三角网格所需的信息。具体的说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角形网格每条边的两个端点,但是如果我们想要得到每个三角网格对像素的覆盖情况就需要计算每条边上的像素的坐标。为了能够计算出边界像素的坐标信息,我们就需要得到三角形边界的表示方式,这样一个计算三角形网格表示数据的过程就叫做三角形设置。它的输出是为下一个阶段做准备。

2.     2.3.6 三角形遍历:

1)     该阶段将会检查每一个像素是否被一个三角形网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角形覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。

2)     三角形遍历阶段会根据上一个阶段计算的结果来判断一个三角网格覆盖了哪些像素,并使用三角网格的三个顶点的顶点信息对整个覆盖区域的像素进行插值。(可以理解为原先的图元是一个只有三个顶点的三角形,现在这个阶段就是向三角形中填充像素,而最终的结果是得到一个片元)需要注意的是一个片元并不是真正意义上的像素而是包含了很多状态的集合,这些集合用于计算每个像素的最终颜色。这些状态包括但不限于它的屏幕坐标、深度信息,以及其它从几何阶段输出的顶点信息,如法线纹、理坐标等。

ü 2.3.7 片元着色器(FragementShader):

该阶段是完全可编程的,它用于实现逐片元的着色操作。

1)      前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的存储信息用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列的数据。真正会对像素产生影响的是下一个流水线阶段——逐片元操作。

2)      片元着色器的输入是上一个阶段对顶点信息插值得到的结果。这一阶段可以完成许多很重要的渲染操作,其中最重要的就是进行纹理采样。为了在片元着色器中进行纹理的采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的三个定点对应的问福利坐标进行插值后,就可以得带器覆盖的片元的纹理坐标了。

ü  2.3.8 逐片元操作(Per-Fragement Operation):

该操作执行修改颜色、深度缓冲、进行混合等,它不可编程但具有很高的可配置性。这一阶段主要有以下任务:

l  决定每个片元的可见性:对片元进行深度测试和模板测试等。

l  当一个片元通过了以上所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区的颜色进行合并或者说是混合。

计算机图形学中的渲染流水线概述摘录

1.     模版测试:如果开启了模板缓冲,GPU会首先读取模板缓冲区中该片元位置的模板值,然后将该值和读取到的参考值进行比较,这个比较函数可以由开发者指定,例如小于该值时舍弃片元,或者大于等于时舍弃该片元。如果这个片元没有通过模版测试,该片元就会被舍弃。不管一个片元有没有通过模版测试,我们都可以通过模版测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者进行指定的。例如,在失败后,模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模版测试通常用于限制渲染的区域。另外模版测试还有另外一些更高级的用法,如渲染莹莹,轮廓渲染等。

2.     深度测试:同理如果开启了深度测试,GPU会首先读取深度缓冲区中该片元位置的深度值,然后将该值和读取到的参考值进行比较,这个比较函数可以由开发者指定,例如小于该值时舍弃片元,或者大于等于时舍弃该片元。通常这个比较函数时小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值就舍弃当前的片元。这是因为我们总想只显示离摄像机最近的物体,而那些被其他物体遮挡的就不需要显示在屏幕上。和模板测试有些不同的时,如果当前片元没有通过深度测试,它就没有权利去修改深度缓冲区中的值。而如果它通过了测试,,开发者还可以通过开启深度值写入的方法使用当前片元的深度值覆盖掉原有的深度值。

3.     合并:渲染的过程是一个物体接着一个物体被画到屏幕上的过程,而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行当前渲染时,颜色缓冲中往往已经拥有了上一次渲染之后的颜色结果,那么我们可以使用这次渲染得到的颜色完全覆盖点上次渲染的结果,也可以进行其他的处理。

对于不透明的物体,,开发者可以关闭混合操作。这样片元着色器中计算得到的颜色便直接覆盖掉颜色缓冲区中的像素值。但对于半透明的物体,我们就需要使用混合操作让这个物体看起来是透明的。如果开启了混合,GPU会取出源颜色和目标颜色,将两种颜色进行混合。源颜色指的是片元着色器得到的颜色值,而目标颜色则是已经存在于颜色缓冲区中的颜色值。之后,GPU就会调用一个混合函数来进行混合操作。这个混合操作通常和透明度通道息息相关,例如通过透明度通道进行加减乘等操作。

 

上面给出的测试顺序并不是唯一的,虽然从逻辑上来说这些测试是在片元着色器之后进行的,但对于绝大多数的GPU来说它们会尽可能的在片元着色器之前就进行测试操作。因为当GPU在花费了大部分的计算性能计算出片元的颜色之后而这些片元在后续的测试中没有通过检验,也就是说这个片元最终还是被舍弃了,那之前花费的计算成本就会全部浪费了。为了提高GPU的性能,应当提早地让GPU知道哪些片元是会被舍弃地,对于这些片元就不需要在片元着色器中计算他们的颜色。在Unity给出地渲染流水线中,我们可以发现它给出的深度测试实在片元着色之前进行的。这种提前将深度测试进行地操作名为Early-Z技术。(顶点坐标的z坐标表示深度)

但是如果将测试提前的话,其检验的结果可能会与片元着色器中的一些操作发生冲突。例如我们在片元着色器进行了透明度测试,而这个片元没有通过透明度测试,我们就会调用着色器中的API(例如Clip函数)将其手动的舍弃调。因为着色器中的操作,那么在着色器之前地检测操作就会与之发生冲突,因此,GPU会判断当前的提前测试是否和着色器中的操作发生冲突,如果发生了冲突就会关闭提前测试。但是这样也造成了性能上的下降,因为有更多的片元需要被处理了,这也是为什么透明度测试会造成性能下降的原因。

当模型的图元进行了上面层层的计算和测试之后就会显示到我们的屏幕上,我们屏幕显示的就是颜色缓冲区域中的颜色值,但是为了避免我们看到正在光栅化的图元,GPU会使用双重缓冲的策略。一旦场景被渲染到了后置缓冲中,GPU就会交换当前正在显示图像的前置缓冲中的内容,由此保证了我们所看到的图像是连续的。

 

2.3.9 小结

实际渲染的流水线往往比上面提到的内容更加复杂,但是Unity为我们封装了很多的功能。更多的时候我们只需要在一个Unity Shader设置一些输入,编写顶点着色器和片元着色器,设置一些状态就可以实现大部分的屏幕效果。但是高度封装性会导致变成*度的下降,使得很多初学者迷失方向并无法掌握其背后的原理,出现问题时往往很难找到问题的原因。我们之所以要花费大量的篇幅来面熟GPU的渲染流水线是因为Shader所在的阶段就是渲染流水线的一部分,更具体的说就是:

1)     GPU流水线上一些可高度编程的阶段,而由着色器编译出来的代码最终是会在GPU上运行的。

2)     有一些特定类型的编译器如顶点着色器、片元着色器是可供我们编程和配置的。

3)     依靠着色器我们可以控制流水线渲染中的具体细节,例如使用顶点着色器来进行顶点的变换以及传递数据,用片元着色器来进行逐像素的渲染。