概述


渲染流水线的最终目的在于生成或者说是渲染一张二维图像,即我们在电脑屏幕上看到的所有效果。
渲染流程大体可以分为三个概念阶段:

CPU流水线

  • 应用阶段

开发者具有这个阶段的绝对控制权。
大致分为三个阶段:

  1. 把数据加载到显存中

渲染所需的数据从硬盘最终加载到显存中。在渲染时,GPU可以快速访问这些数据

  1. 设置渲染状态
    渲染状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器/片元着色器、光源属性、材质等。

  2. 调用Draw Call
    Draw Call只是一个命令,从CPU发送向GPU。这个命令仅仅会指向一个需要被渲染的图元列表,并且不会包含任何材质信息。

GPU流水线

GPU的渲染流水线实现。颜色表示了不同阶段的可配置性或可编程性:绿色表示该流水线阶段是完全可编程控制的,黄色表示该流水线阶段可以配置但不是可编程的,蓝色表示该流水线阶段是由GPU固定实现的,开发者没有任何控制权。实线表示该shader必须由开发者编程实现,虚线表示该Shader是可选的

GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。

  • 几何阶段

顶点着色器是完全可编程的,它的处理单位是顶点。顶点着色器需要完成的工作主要有:坐标变换、逐顶点光照和输出后续阶段所需的数据。必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间。

undefined
顶点着色器会将模型顶点的位置变换到齐次裁剪坐标空间下,进行输出后再由硬件做透视除法得到NDC下的坐标

裁剪是为了不处理那些不在摄像机视野范围内的物体。这一步不可编程。裁剪过程:
undefined
只有在单位立方体的图元才需要被继续处理。因此,完全在单位立方体外部的图元(红色三角形)被舍弃,完全在单位立方体内部的图元(绿色三角形)将被保留。和单位立方体相交的图元(黄色三角形)会被裁剪,新的顶点会被生成,原来在外部的顶点会被舍弃

屏幕映射的任务是把每个图元的*x*和*y*坐标转换到屏幕坐标系下。 将*x、y *坐标从(-1,1)范围转换到屏幕坐标系中:
undefined

注意:屏幕坐标系在OpenGL和DiretX之间存在差异:
undefined

  • 光栅化阶段

三角形设置阶段会计算光栅化一个三角形网格所需的数据。

三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖,会生成一个片元。片元是包含了很多状态的集合,这些状态包括屏幕坐标、深度信息、法线、纹理坐标等。三角形遍历过程:
undefined

根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。

片元着色器是另一个非常重要的可编程着色器阶段。这一阶段可以完成很多重要的渲染技术,技术之一是纹理采样。在执行片元着色器时,它不能将自己的任何结果直接发送给它的邻居。这一阶段的输出是一个或多个颜色值。

逐片元操作这一阶段有几个主要任务:

  1. 决定每个片元的可见性。涉及许多测试工作:深度测试、模板测试等。
  2. 如果片元通过测试,需要把源颜色(通过测试的片元颜色)与目标颜色(颜色缓冲区中的颜色)进行混合。


    此阶段所做的操作:
    undefined
只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中

模板测试和深度测试简化流程图:
undefined

混合操作简化流程图:
undefined

Unity的渲染流水线中,深度测试是在片元着色器之前。
屏幕显示的就是颜色缓冲区的值,为了避免看到正在进行光栅化的图元,GPU会使用双重缓冲:即对场景的渲染发生在后置缓冲中。

Draw Call

Draw Call本身的含义很简单,就是CPU调用图像编程接口,如OpelGL中的glDrawElements命令。
Draw Call中造成性能问题的元凶是CPU

CPU和GPU实现并行工作

让CPU和GPU可以并行工作,解决方法是使用一个命令缓冲区:
undefined

CPU通过图像编程接口向命令缓冲区中添加命令,而GPU从中读取命令并执行。黄色方框内的命令就是Draw Call,而红色方框内的命令用于改变渲染状态。我们使用红色方框来表示改变渲染状态的命令,是因为这些命令往往更加耗时

Draw Call多了影响帧率

如果Draw Call的数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU过载。
undefined

命令缓冲区中的虚线方框表示GPU已经完成的命令。此时,命令缓冲区中没有可以执行的命令了,GPU处于空闲状态,而CPU还没有准备好下一个渲染命令。

减少Draw Call
  1. 避免使用大量很小的网格。
  2. 避免使用过多的材质。

以便于Unity中利用批处理进行优化。