聊聊Batch的前世今生 您所在的位置:网站首页 opengl底层用了xlib 聊聊Batch的前世今生

聊聊Batch的前世今生

2023-04-19 17:13| 来源: 网络整理| 查看: 265

本文已收录于Game+,Game+ 表示游戏或游戏相关以及游戏以外的事情,他以游戏为起点,是⼀个游戏研发⾏业 元⽼家、学者 的聚集地,他为这个⾏业的不同领域不同阶层提供了开放的沟通交流平台,希望⼤家公益性的平台互相交流,⼀起学习,共同进步!Game+ 会定期免费的推送⼀些有价值的内容,⼀些线下交流分享活动,欢迎有想法感兴趣的主动发起与参与。 关于Game+ 的活动以及内容投放的进⼀步咨询合作,请联系 [email protected]

Batch翻译成中文一般我们称之为“批次”。我们经常用引擎每帧提交的批次数量来作为衡量渲染压力的指标。关于Batch和DrawCall的介绍文章已经很多了,而且这个话题也是烂大街了。但是对于刚刚玩完opengl的我,只是想站在opengl底层api的角度来讲一讲drawcall和batch。

图2 opengl绘制函数 1个drawcall

对于Batch其实就是DrawCall的另一种称呼。Batch里装的就是刚才上述图1贴出的绘制一个正方体需要的顶点资料数据。而这一个正方体的一组数据也可以描述成一个Mesh。将这一组数据提交给GPU的过程就是1个批次。也就是说1个Mesh的数据我们称之为1批。我们看到每一个批次数据的提交都对应着glDrawArrays/glDrawElements的一次调用,所以DrawCall和Batch从API调用角度来看是等价的。但是在引擎中Batch实际的意义一般指的是经过打包之后的DrawCall。再来句官方对于Batch的定义(装逼时刻到hh):调用一次渲染API的绘制接口(如OpenGL的glDrawArrays/glDrawElements)来向GPU提交使用相同渲染状态的一定数量的三角形的行为为一个渲染批次。

那么合批和哪些因素有关呢,我们来看下面这张图:

从这张图我们可以看出,每秒钟可以处理的Batch数量和CPU的性能好坏有着直接的联系。而和GPU的性能好坏以及Batch的大小关系影响不大。所以我们应该保证在没有达到GPU的性能极限的情况下,尽可能的去增大Batch的大小。这就好比吃苹果,假定有100个苹果,如果1小时吃1个,需要100个小时,而如果1小时可以吃2个,那么50小时就可以吃完。到了真正的游戏里也是一样,假定我们游戏中每帧绘制的三角形数目是固定的,每个Batch的尺寸很小,而每次提交Batch CPU的性能i消耗是一定的(本质),那么只有花费更多的时间才能绘制完所有的三角形。这样一来帧率自然就降低了。

接下来我们再谈一谈DrawCall是干什么的,以及如何影响游戏的性能。

DrawCall就是上面图2中的glDrawArraysh/glDrawElements函数的调用。1次调用就是1个DrawCall。那么DrawCall又是怎样影响性能的呢。看下面这张图:

先看图的上半部分:当每次调用API的时候,背后其实都需要经历这么几个阶段。application->runtime->driver->gpu。这每一步都存在性能消耗。前三步是在CPU中进行的。而最后一步的gpu消耗主要是在vertex shader,fragment shader,framebuffer中的各种操作(blend,test)。

看图的下半部分:每调用1次渲染api并不是直接经过以上的所有组件通知gpu执行我们的调用。runtime会将api调用转换为设备无关的"命令"(这样才能保证在任何设备硬件上兼容,其实也就是做到对不同的硬件架构做到透明),然后将命令缓存到commandbuffer中去。而命令从runtime到driver这个过程中,cpu会发生从用户模式到内核模式的转换。这个操作是一件非常耗时的工作。所以说如果每次api调用都直接发送命令到driver,那将是非常巨大的性能消耗。所以在不是必须要马上提交给GPU绘制的命令,我们可以先缓存在commandbuffer中,等到需要时一次性提交,优化效率。当然这里边还涉及到渲染状态(texture,shader,material各种参数)的影响,如果渲染状态改变了,那么要使用之前渲染状态进行渲染的所有drawcall命令必须全部被执行了。也就是说之前缓冲在commandbuffer中的所有drawcall命令必须刷新。这其实会发生一次从用户模式到内核模式的切换。

从以上两点来看每次渲染状态的改变导致cpu消耗增加其实包含着两个方面的性能消耗:1,runtime需要将api转换成设备无关的"命令"的时间消耗。2,状态切换导致的从用户模式到内核模式的模式切换的时间消耗。同样的对于优化batch,减少drawcall也有两条路可走:1,对driver进行优化,使其在转换上降低开销。但是目前的最新api接口,比如vulkan,metal其实在这方面已经做了很大的改进,大大降低了模式转换上的开销。2,通过合批降低drawcall。这也是我们唯一能做的手段哈哈。所以接下来就讲一讲unity中的四大batch利器(四大天王)来优化我们的游戏性能。

目前Unity引擎里边支持4种Batch操作,Static Batching(静态合批),Dynamic Batching(动态合批),GPU Instance,以及最新推出的 SRP Batch。那么这几种Batch在底层的原理都是怎样的,以及怎么合理的使用,使用起来又有哪些优缺点呢,我们继续展开。

Static Batching

如果游戏场景中有很多模型共享同一材质,而且这些模型自始至终都不会移动、旋转和缩放。我们可以将这些模型设置为Static:

在我们打包Build的时候Unity会自动地提取这些共享材质的静态模型的Vertex buffer和Index buffer。根据其摆放在场景中的位置等信息,将这些模型的顶点数据统一变换到世界空间下,存储在新构建的大Vertex buffer和Index buffer中。并且记录每一个子模型的Index buffer数据在构建的大Index buffer中的起始及结束位置。相当于在opengl中将多个Mesh的顶点数组和顶点索引数组都集成到1个顶点数组和1个索引数组当中去。

1个mesh,两个三角形的顶点数据和索引数据

在后面的绘制过程中,引擎会一次性提交整个合并模型的顶点数据,然后引擎根据自己的场景管理系统判断各个子模型的可见性。然后设置一次渲染状态,调用多次Draw call分别绘制每一个子模型。Static Batching并不会减少DrawCall的数量,但是我们预先将所有模型变换到世界空间下,而且这些模型共享同一材质,所以它们之间不会有状态切换,这样CommandBuffer就可以缓存绘制命令。等到所有子模型的绘制命令都准备好后,一次性提交到GPU,起到了合批优化的目的。现在在制作游戏的过程中,很多美术喜欢在3dmax或者maya中去手动把模型attach在一起,所谓的手动合并,这样其实是非常不赞成的。原因存在两点:1,不利于及时的在场景中摆放调整。2,由于是手动合并,所以即便是相机只能照到一大块儿模型的一两个三角形,那么整个大块儿的模型数据还是会全部提交给GPU的,虽然在渲染管线中会被裁剪剔除,但是提交的时候还是消耗了性能的。而如果是Unity自己的Static Batch,它会判断相机照射范围的模型,超出范围的模型直接不提交。所以要求我们尽量的把模型做的零散一些,便于unity自己内部处理。

StaticBatching还存在一些负面影响,就是会增大内存。如果场景中有很多个重复的GameObject的话,它将会复制N份同样的Mesh去扔进大的VBO和EBO中。这样带来的影响还是挺大的。所以如果有很多个重复的GameObject可以考虑不进行static处理,这样在渲染的时候,它们是可以共享同一份Mesh数据的。

Dynamic Batching

Dynamic batching的原理本质和Static Batching没什么太大差异,只不过是针对动态物体的。在场景绘制之前将所有共享同一材质的模型顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型来达到合批。由于该操作(模型顶点变换)是在每一帧中进行的。所以每一帧都有CPU的消耗,所以呢计算的模型顶点数量不宜太多,否则CPU串行计算耗费的时间太长会造成渲染卡顿。目前Unity限制Dynamic batching的模型最高能有900个顶点属性。这里注意不是900个顶点,而是900个定点属性。如果我们在Shader中使用了Vertex Position,Normal and single UV,那么能够进行Dynamic batching的模型最多只能够有300个顶点。如果我们在Shader中使用了Vertex Position、Normal、UV0、UV1 and Tangent那么顶点的数量就减少到180个。这个顶点属性数量的要求是Unity自己设计的,未来随着硬件性能的提升,这个值也会调整。除此之外还存在很多限制因素:1,两个物体的scale刚好是呈镜像关系。2,multi-pass。3,多光照。4,不支持Lightmap 5,材质实例不同也不会合批。

Dynamic Batching相比于Static Batching不需要预先复制顶点数据,所以在内存占用和发布程序包体大小方面存在一些优势。尽管Dynamic Batching减少了DrawCall,但同时也增加了CPU性能的开销。而现在好多新型渲染接口(Vulkan,Metal)在批次方面也降低了不少,所以在使用DynamicBatching的时候应确保合批操作的性能消耗低于不合批。

其实还有一种两者思路结合灵活取巧的合批方法,那就是在运行时进行StaticBating。当游戏运行的时候,通过调用publicstaticvoidCombine(GameObject[] gos, GameObjectstaticBatchRoot)将多个共享材质的i小物件合批。这种方法可以避免打包时的包体过大。因为运行时合批需要CPU变换顶点位置到世界空间,所以会到来一次性的内存和CPU开销。

关于上述两种合批,也是有排序规则的。Unite2019大会上也给出了讲解(简单贴两张hh)。

GPU Instancing

然而上述两种Batching方式在某些方面还是存在弊端的。比如一个大的场景中,存在大量相同的植被等物件,静态批处理后,对内存的增加是非常大的,动则就是几十兆的内存。而动态批处理,对于合批要求苛刻,同时可能存在动态合批消耗过大,得不偿失。如果我们自己在游戏运行时StaticBatching,对于mesh的readwrite属性是要求开启的,这无疑也增大了内存的占用,复杂的合批处理可能会消耗更多的cpu时间。所以呢,Unityy在5.4版本以后,增加了一项强大的功能GPU Instancing。这样的话我们就可以将这些静态的物件如植被等全部从场景中剔除,而保存其位置、缩放、uv偏移、lightmapindex等相关信息,在需要渲染的时候,根据其保存的信息,通过Instance来渲染,这能够减少那些因为内存原因而不能合批的大批量相同物件的渲染时间。下面这两张官网图片展示了同个场景下渲染多个gameobject的batch数量,图1开启了GPU Instancing,而图2没有

图1图2

由于这个特性是从OpenGL ES 3.0开始支持的,所以手机端使用的话必须支持OpenGL ES3.0。从OpenGL的角度看,就是调用glDrawArray和glDrawElements的绘制实例版本即glDrawArraysInstanced和glDrawElementsInstanced。该函数的最后一个参数需要指定实例的个数。而在Vertex Shader中使用一个Uniform数组,来保存实例的偏移量。vector3[] offsets[100],这样每次在绘制的时候,就不需要传递所有的顶点数据到GPU了,GPU会根据实例的偏移量,挨个的画出来,对于性能的i提升还是很大的。

关于GPU Instancing的使用网上已经有很多案例了,在这里我再说下它和StaticBatching以及DynamicBatching的优先级问题。1.Static batching的优先级要比Instancing的优先级高,如果一个GameObject被标记为static物体并且在Build阶段成功地执行了静态合批,那么如果这个物体还要使用Instancing Shader渲染的话,Instancing会失效。2.Dynamic batching的优先级要低于Instancing。如果一个GameObject使用Instancing渲染的话,那么对于它的Dynamic batching会失效。

SRP Batch

Unity 2018引入了可编程渲染管线SRP,其中包含新的底层渲染循环SRP Batcher批处理器,它可以大幅提高CPU在渲染时的处理速度,根据场景内容的不同,提升效果为原来的1.2~4倍不等。先来看下官方Demo(需要翻墙)。

以上视频展示了Unity的最坏情况:每个对象都是动态的,并使用不同的材质(颜色,纹理);场景显示了许多相似的Mesh,但每个对象使用一个不同的Mesh(因此不能使用GPU Instance技术); 结果:在PS4上性能提升4倍.

传统的内置管线

Unity编辑器拥有非常灵活的渲染引擎。我们可以在一帧中随时修改任意材质属性。此外Unity一直面向非常量缓冲区开发,支持DirectX9等图形API,但这些不错的功能有一些缺点,例如:当Draw Calls使用新材质时,需要进行很多处理。场景内的材质越多,设置GPU数据所需的CPU资源就越多。

在内部渲染循环期间,当检测到新材质时,CPU会收集所有属性,并在GPU内存中设置不同的常量缓冲区,GPU缓冲区的数量取决于着色器如何声明其CBUFFER。

SRP Batcher的工作流程

在开发SRP技术时,我们必须重写部分底层引擎。我们发现了在本地集成新范例的机会,例如:GPU数据持久化。我们的目标是提高常见用例的速度,在常见用例中,场景会使用大量不同材质和少量着色器变体。现在,底层渲染循环可以使材质数据在GPU内存中具有持久性。如果材质内容没有改变,则不需要设置并上传缓冲区到GPU。此外我们还使用了专用代码路径,从而快速更新大型GPU缓冲区的内置引擎属性。 新的渲染工作流程如下图所示

在这个工作流程中,CPU只处理内置引擎属性,它们被标记为对象矩阵变换。所有材质都有位于GPU内存的持久性CBUFFER,可以随时使用。提速效果源于二个方面:1,每个材质内容现在都会一直保留在GPU内存中。 2,专用代码会管理大型“per object” GPU CBUFFER;

支持的设备接口

从图中也可以看出,Unity让越来越多的设备开始支持SRP Batcher了,说明这也是Unityi以后的主推方向,毕竟它可以做到不论材质属性是否相同,同一个shader只消耗1个批次,这个太重要了。具体的SRP Batch使用请参考下方官方链接

在真正的游戏开发中还是需要根据具体的业务逻辑需求,去选择合适的合批手段并搭配使用。前三个在以前的传统内置管线中都可以支持。而最后的SRP Batcher需要Unity2018 2019新推出的LWRP管线或者HDRP管线才可以支持。所以对于老项目而言,可以从前三个手段下手去优化Batch,而新立项的新项目则可以考虑SRP Batcher去优化Batch,毕竟SRP Batcher使用了GPU CBuffer来缓存数据,可以给我们在使用不同的材质的时候带来极大的性能提升和帮助。

就先写到这里吧,主要也是想告诉更多开发者Batch和DrawCall背后的原理。文章写的仓促,难免有不对或不合理的地方,欢迎指正。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有