《Unity Shader入门精要》随笔

对于整个渲染流程来说我们可以大概分成三大阶段,也就是CPU阶段、过渡阶段、GPU阶段。

1.CPU阶段

首先我们准备好场景数据,例如摄像机的位置,视锥体,场景中包含了哪些模型,使用了那些光源等等,以及把那些不可见或者不想见的物体剔除出去(用户手动剔除),这样就不需要再移交给几何阶段进行处理,最后我们设置好每个模型的渲染状态比如说材质,颜色,纹理等等,这些全都是用户手动设置也是渲染的第一步,用户设置过后会输出渲染所需的几何信息,即(渲染图元),既然是几何信息那便是点,线,三角面等等。

2.过渡阶段

既然CPU输出了渲染所需的几何信息(就是渲染图元),这些渲染图元数据从硬盘中加载到系统内存中,然后图元里包含的网格和纹理等数据被加载到了显卡上的存储空间中——显存,这是因为显卡对于显存的访问速度更快,而且大多数显卡对于内存没有直接访问的权力。而且除了被加载到显存里面的数据也有一部分留在了系统内存中以便CPU访问他们,比如我们希望CPU可以访问网格数据进行碰撞检测。过后我们需要通过CPU来设置渲染状态,“指导”GPU如何进行渲染,比如说实用哪个顶点着色器/片元着色器,光源属性,材质等这就是调整Shader的操作,在准备好上述工作后CPU调用一个渲染命令(Draw Call),这个命令发给了GPU。

3.GPU阶段 (可以细分8个小阶段)

1.顶点着色器阶段

GPU得到渲染命令后,将渲染图元里面的顶点数据从显存里面传给顶点着色器,输入进来的每个顶点都会调用一次顶点着色器,顶点着色器本身不可以创建或者销毁任何顶点,无法得到顶点与顶点之间的关系,例如我们无法知道两个顶点是否属于同一个三角形网格,正是因为这样的独立性,GPU可以利用本身的特性迅速处理每一个顶点而不用操心关系问题,顶点着色器可以的进一步改变顶点的位置,把顶点坐标从模型空间转换到齐次剪裁空间。

2.裁剪阶段

由于我们的场景可能会很大,而摄像机的视野范围很可能不会覆盖所有的场景物体,所以我们可以不处理摄像机视野范围外的物体,这就是剪裁阶段,图元里面的不在视野范围的点线面不会继续处理。

3.屏幕映射

这一步输入的坐标仍然是三维坐标系下的坐标,范围在单位立方体内。屏幕映射就是把图元的X和Y坐标转换到屏幕坐标系下,屏幕是一个二维坐标系,会根据不同屏幕分辨率来同比例显示出来。

4.三角形设置

这一个进入了光栅化阶段,上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和他们相关的的额外信息,如深度值、法线方向、视角方向等。光栅化阶段会计算每个图元覆盖了哪些像素,并计算他们的颜色。

5.三角形遍历

这个阶段会检查每个像素是否被一个三角网络覆盖,如果被覆盖就会生成一个片元。每个片元并不是像素,只是很多状态的集合,比如屏幕坐标,深度信息,以及法线,纹理坐标等。

6.片元着色器

这是另外一个非常重要的可编程着色器阶段,有时被称为像素着色器,前面的光栅化阶段并没有影响到屏幕上每个像素的颜色值,而是产生了一系列的数据信息用来表述一个三角网络是怎样覆盖每个像素的,片元着色器的输入是上阶段对顶点信息插值得到的结果,他的输出是一个或多个颜色值。

7.逐片元操作

这是渲染流水的最后一步,这一步是高度可配置性的,这个阶段首先需要解决每个片元的可见性问题,先开始模版测试,如果开启了模版测试,那么GPU会首先读取模板缓冲区的该片元的位置的模板值,然后将该值和读取到的参考值进行比较,这个比较函数是可以由开发者指定的,比如小于时就舍弃,模板测试通常用于限制渲染的区域。如过这个测试通过就进入深度测试,这个测试GPU会把该片元的深度值和以已经存在于深度缓冲区的深度值进行比较。这个比较函数也是可以有开发者设置的,比如小于时就舍弃该片元,通常这个比较函数是小于等于的关系,因为我们总是只想显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上,和模版测试有些不同的是如果一个片元没有通过深度测试,他就没有权力更改深度缓冲区的值,如果他通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。透明效果和深度测试以及深度写入的关系非常密切。

8.合并

如果一个片元通过了上面所有测试,他就可以自豪的来到这里,渲染的过程是一个物体接着一个物体画到屏幕上面的,而每个像素的颜色信息被储存到颜色缓冲里面,当我们执行这个渲染时,颜色缓冲往往已经有了上次渲染之后的颜色结果,对于不透明的物体,我们可以关闭混合操作,这样片元着色器会直接覆盖颜色缓冲区的像素值,对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。混合操作也是可以高度可配置的,如果没有开启混合功能就会直接就使用片元的颜色覆盖掉颜色缓冲区的颜色,如果开启混合功能GPU会取出元颜色和目标颜色将两种颜色进行混合。源颜色值指的是片元着色器得到的颜色值,而目标颜色是已经存在于颜色缓冲区中的颜色值,之后就可以使用一个混合函数来进行混合操作。这个混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加,相减,相除相除等。扇面给出的测试顺序并不唯一,虽然从逻辑上来说这些测试是在片元着色之后进行的,但对于大多数GPU来说,他们会尽可能执行这些测试,这是可以理解的,当GPU在片元着色器阶段活了很大力气计算出来以后却发现没有通过这些测试,这就很浪费算力了,所以可以先判断哪些片元是会被舍弃的,对于这些片元就不需要在使用片元着色器来计算他们的颜色了,这个通常被称为Early-Z技术。但是如果将这些测试提前的话其检验结果可能会有片元着色器的一些操作冲突。例如我们在片元着色器进行了透明度测试,而这个片元没有通过透明度测试,我们将会在着色器中调用API来手动将舍弃掉。这就导致GPU无法提前进行执行各种测试。因此吸纳带的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突就会禁用提前测试,但是这样会造成性能上的下降,因为有更多的片元需要被处理了。这也就是透明度测试会导致性能下降的原因。当模型的图元经过上面层层计算后测试后就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区中的颜色值,但是为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲的政策,这意味着对场景的渲染实在幕后发生的,,就是后置缓冲,一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲中的内容,而前置缓冲区是之前显示在屏幕上的图像,由此保证我们看到的图像是连续的。

总结

虽然渲染流水线比较复杂,但Unity作为一个非常出色的平台为我们封装了很多功能,更多的时候我们只需在Unity Shader设置一些输入,编写顶点着色器和片元着色器,设置一个状态就可以达到大部分常见的屏幕效果。这样的缺点在于封装性会导致编程自由度的下降,无法掌握背后的原理,出现问题无法找到错误原因。