Intro

我们知道,在传统的渲染管线中,为了渲染一个物体,CPU需要向GPU发送一个DrawCall命令.
比如下面这段代码:

glDrawElements(GL_TRIANGLES, indices->size(), GL_UNSIGNED_INT, 0);

而在下达DarwCall命令之前,CPU还需要设置好渲染状态,绑定顶点数据,绑定纹理,设置材质等等.比如这样:

glBindVertexArray(VAO); // 设置VAO
material->Use(); // 设置材质
status->Set() // 设置物体各种状态
//...
glDrawElements(GL_TRIANGLES, indices->size(), GL_UNSIGNED_INT, 0);//DrawCall

值得注意的是,设置各种参数的操作费用不低,DrawCall也是一个非常费的操作(虽然现代图形API已经做了很多相应的优化).

这就出现了一个问题,看下面这个场景:

博德之门3

在这个场景中,各种各样的物体十分之多,如果每个物体都需要发送一次DrawCall命令的话,那么帧数将十分难看.

那么游戏中都用了哪些技术来解决这一问题呢?

Batch & DrawCall

对于上面博德之门3的场景,我们可以假设如果不做任何优化按某一顺序绘制场景中所有Mesh,伪代码如下:

// 设置城墙墩子1Mesh各种参数
glDrawElements(城墙墩子1);

// 设置小人1脑袋Mesh各种参数
glDrawElements(小人1脑袋);

// 设置城墙墩子2Mesh各种参数
glDrawElements(城墙墩子2);

//...

我们发现,其实这个场景中有很多相同的Mesh,比如城墙上的墩子.

博德之门3

这些墩子拥有相同的Mesh,纹理,材质,shader.很容易就会发现,我们可以将拥有相同材质等属性的Mesh在同一个地方进行绘制,这样就可以减少设置各种参数了.

// 设置城墙墩子1Mesh各种参数
glDrawElements(城墙墩子1);
glDrawElements(城墙墩子2);

// 设置小人1脑袋Mesh各种参数
glDrawElements(小人1脑袋);

//...

这样我们就减小了设置参数的开销,而这就是Batching的思路和原理,即:
通过合并相同数据,减少状态切换和DrawCall的数量,从而提高渲染效率.

聪明的读者肯定发现了,这里并没有减少DrawCall.别急,那是下文的内容.

Static Batching

回看上文的伪代码:

// 设置城墙墩子1Mesh各种参数
glDrawElements(城墙墩子1);
glDrawElements(城墙墩子2);

// 设置小人1脑袋Mesh各种参数
glDrawElements(小人1脑袋);

//...

自然而然就会想到,是否可以将城墙墩子1和2打包成一个Mesh,这样就可以将两个DrawCall合并为一个DrawCall了.这就是静态合批的思路.

所以我们进化我们的伪代码:

// 设置城墙墩子1Mesh各种参数
Mesh 城墙墩子 = 城墙墩子1 + 城墙墩子2;
glDrawElements(城墙墩子);

太棒了,这样我们又成功减少了DrawCall的数量,提升了性能(因为合并Mesh操作比DrawCall要快)

使用条件

在Unity中,启动静态合批的条件如下:

  1. 静态合批物体材质球相同

使用方法

Unity开启静态合批方法如下:

  1. 标记物体为Batching Static

    标记Batching Static

优缺点

优点:

  1. 减少了状态切换和DrawCall的数量,从而提高渲染效率.

缺点:

  1. 静态合批物体不能移动,旋转等.
  2. 占用更多内存,因为合并Mesh需要更多的内存.

Dynamic Batching

静态合批最大问题在于,它不能动态改变物体的属性.那如何减少场景中动态的物体的DrawCall呢?

既然在GPU段无法做到改变物体位置,那么是否可以在CPU端将各个物体的位置计算好后再传入GPU呢?答案是可以,这就是动态合批的思路.

动态合批的原理如下:

  1. 场景绘制前,将所有同一材质物体的顶点信息变化到世界空间中(这样就实现了物体位置的变化)
  2. DrawCall

当然问题也十分明显:

  1. CPU承担了GPU的计算任务,导致CPU压力增加.
  2. shader中不能直接使用Vertex position等属性.

因此动态合批只适合对一些小模型进行合批.

Unity也对动态合批做出了限制:

  1. 进行Dynamic batching的模型最高能有900个顶点属性
  2. 如果使用了Vertex position等属性,能够进行Dynamic batching的模型最多只能够有300个顶点

而使用动态合批本身也有一些条件:

  1. GameObject之间如果有镜像变换不能进行合批(感兴趣读者可以结合原理自行推导)
  2. 使用Multi-pass Shader的物体会禁用Dynamic batching,因为Multi-pass Shader通常会导致一个物体要连续绘制多次,并切换渲染状态。这会打破其跟其他物体进行Dynamic batching的机会
  3. 材质相同
  4. 物体的lightMap指向位置相同

使用方法

在Unity中,开启动态合批方法如下:

  • built-in Render Pipeline:
    • 打开Project Setting -> Graphics -> Batching -> Dynamic Batching
  • URP:
    • UniversalRenderPipelineAsset -> Advanced -> Dynamic Batcheing

问题

在Unity中,动态合批功能是默认关闭的,这是为什么呢?

原因是打开动态合批,CPU端的压力会增加,由于CPU不擅长并行操作,所以当CPU增加的消耗大于降低DrawCall的收益时,开启动态合批反而会降低性能.

优缺点

优点:

  • 减少DrawCall的数量,提升渲染效率.
  • 物体可以移动,旋转等.
  • 内存占用少于静态合批

缺点:

  • 增加CPU负担

GPU Instancing

看下面这张截图:

荒野大镖客2

场景中有大量的草.读者马上想到可以使用静态合批的方法来增加性能,但问题在于:

  1. 草数量太大了,消耗的内存也十分恐怖.
  2. 草往往需要随风而动,不然会十分呆板,静态合批做不到,对动态合批量又太大了

因此对于这种物体,我们需要一种更好的方式来进行绘制.

GPU Instancing的思路是: 我只获取一次数据,然后渲染时渲染多个物体.

那如何解决每个个体的个性化呢?

答案是通过一个InstanceID来区分每个个体.每个个体通过InstanceID来获取自己的属性,比如位置,颜色,UV等.这样就可以做到一个DrawCall渲染多个物体.

opengl提供了三个函数来实现GPU Instancing:

void glDrawArraysInanced(GLenum mode, GLint first, GLsizei count, Glsizei primCount);//无索引的顶点网格集多实例渲染

void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei primCount);//索引网格的多实例渲染

void glDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instanceCount, GLuint baseVertex);//索引基于偏移的网格多实例渲染
  • mode: 绘制模式,一般为GL_TRIANGLES
  • first: 第一个顶点的索引
  • count: 顶点数量
  • primCount: 实例数量

使用条件

在Unity中,使用GPU Instancing的条件如下:

  1. 材质shader支持Instancing(如何编写支持Instancing的Shader,请参考相关文档)

使用方法

在Unity中,针对单个材质开启 Enable Instancing 选项即可.

优缺点

优点:

  • 减少DrawCall的数量,提升渲染效率.
  • 物体有个性化

缺点:

  • 不支持skinned mesh renderer
  • 不支持缩放为负值
  • 代码动态改变材质变量后不算同一个材质,但可以通过将颜色变化等变量加入常量缓冲器中实现
  • 受限于常量缓冲区在不同设备上的大小的上限,同批的个数可能不同
  • 只支持一盏实时光,要要在多个光源的情况下使用实例化,只能切换到延迟渲染路径

SRP Batcher

SRP Batcher是Unity的新功能,可以合批相同着色器变体的多种材质在场景中的CPU渲染速度.

使用条件

在unity中,使用SRP Batcher的条件如下:

  1. SRP管线
  2. unity版本5.6及以上

使用方法

在Unity中,开启SRP Batcher的方法如下:

  1. UniversalRenderPipelineAsset -> Advanced -> SRP Batcher

优先级顺序

SRP Batcher > Static Batching > GPU Instancing > Dynamic Batching

总结

静态 动态 GPU Instancing SRP Batcher
优点 限制少 自动 性能优秀 多材质加速
缺点 增加包体大小,增加运行时内存消耗 增加CPU消耗,限制多 限制多 只能用于SRP
适用情景 静态场景,不适合大量重复物体 小物体,特效 大量重复物体 较为广泛