Unity-渲染批处理

BDFFZI / 2023-05-05 / 原文

图形渲染

工作方式

显卡的工作方式并非连续的,一般分三步。

  1. 上传更新渲染数据至显存
  2. 设置显卡的渲染管线状态
  3. 启动绘制并等待绘制结果

性能瓶颈

由于显卡是大规模并行计算,绘制几个或几千个三角面在速度上基本没有区别,这时候瓶颈就容易出现在工作方式中第一第二点上。

  1. 内存到显存的传输是有代价的
    如果对比过Unity的作业系统就会发现一点,对应一些数据量大但计算量小的任务,使用计算着色器的速度反而比作业系统要慢的多。原因就是因为计算量小导致显卡的强大计算能力没法体现,结果时间全花在了大量数据的传输上,不如直接用CPU就地解决。
  2. 处理器与显卡的沟通是有代价的
    此时数据已经传输到了显存中,但具体使用那些数据,哪种绘制方式,这些都需要进行设置,这就需要CPU与GPU进行沟通。同第一点,这是两种不同的硬件,交流本就有延迟,再加上GPU因此切换内部状态的时间,那就更长了。

优化方案

由此可见我们应在两个方向上进行优化

  1. 减少显存中数据的更新频率
  2. 减少显卡中工作状况的切换

诸如网格,纹理,着色器,常量缓冲区等这些都是需要存储在显存中的,因此应仅可能减少其内容的修改。如果可以,尽可能多物体共用这些资源,这不仅仅可以减少缓存的使用,还可以减少调整渲染状态的次数。

实际例子

最原始的渲染方案,我们可能每个物体都要单独渲染一次,即使不考虑显存大小的问题,我们每次也都要重新设置渲染用的目标数据和状态,还没正式让GPU工作,仅设置GPU的环境就花费了很多时间。然后当我们正式启动GPU,结果因为单个物体的计算量较小,GPU瞬间就好了,这时又要进入缓慢的准备阶段,让GPU一直空等着。
这很类似早期的单道批处理操作系统的问题,IO时间与处理速度不匹配,所以我们要增加处理速度的时间并减少IO的次数。
比如我们可以合并多个物体的渲染数据,并确保他们可以使用同样的着色器和渲染管线状态,从而将其看成一个新的大型单一物体。
此时渲染一个物体就可以变相实现渲染多个物体的效果了,但每个物体间不再有准备时间,GPU也能跑更长的时间了,CPU不再需要动不动停下来指挥GPU做事,并行能力也大幅提高。

渲染批处理

将有共通属性的多个物体看成同一组甚至同一个,以批为单位进行渲染,这就是图形批处理。在Unity中渲染模型需要两个图形资源,材质球+网格:材质球包含了常量缓冲区,渲染状态,着色器的数据;网格包含了顶点索引缓冲区的数据。物体能被一起渲染,说明他们用到的数据都一样,也因此为希望物体能被识别为同一批,则只要确保他们用到的材质球和网格一样即可。

材质球的共用

  • 使用纹理图集功能
    对于因纹理不同而导致差异的情况,可以尝试用该功能将图片进行合并,这样只要在网格的UV上进行特殊处理即可。另外因为UI和Sprite是自动生成的网格,所以可以自动兼容Unity中的图集设置,简单设置就可轻松获得性能优势。
  • 使用GPU实例化功能
    GPU实例化是一种特殊的渲染方式,由底层图形引擎提供,可以实现一次绘制调用中多次绘制同一网格,同时会在着色器阶段传递这些网格的对应编号,利用编号做区分我们便可为不同的网格采用不同的绘制数据,从而实现一次绘制却有过去多材质球的效果。

网格的共用

  • 静态批处理
    提前自动将选择的多个网格合并成一个“大网格”,所有原本的模型将共享该网格中的顶点索引缓冲区等,绘制时直接绘制一次这个“大网格即可”,当然该网格的渲染和传统网格还是略有区别,因为还要考虑剔除等问题,要是看不见的三角面也都渲染,那结果可能得不偿失。
  • 动态批处理
    和静态批处理的原理一样,但区别是实时的,所以必须每帧都要考虑重新生成用于批处理的大网格,也因此存在性能问题:生成网格的消耗可能比渲染批处理节省的开销要大,因此只能用于小型网格的合并,主要用于对过去旧低端设备的进行优化。