Unity中的静态合批,动态合批,SRPBatcher,GpuInstance

1. 从图形API分析为什么合批和合批的原理

简单学习过OpenGL或者DX的小伙伴肯定了解,如果初学Opengl的时候想要渲染出1个正方形,1个plane,1个圆形,那么就要声明3个顶点数组,创建3个顶点数组对象(VAO),3个顶点缓存(VBO),3个索引缓存(VEO),3个shader(代码及其需要的数据);然后开始渲染每一个物体,首先设置第一个物体的渲染状态(shader、shader数据(空间变换矩阵,光源属性等uniform变量)、激活texture),然后绑定VAO,最后调用glDrawArrays()去绘制第一个物体;然后用相同的方式依次绘制出剩下的物体。

好了,我们绘制出了3个物体,设置了3次渲染状态,绑定了3次VAO,调用了3次DrawCall函数,很明显在一个大场景复杂场景中如果都是这样一个一个绘制那么将会有N多次的渲染状态设置、几何数据的收集绑定提交和DrawCall调用,性能毫无疑问会有问题。那么怎么样减少这些呢。

假设我们绘制的3个物体使用的是同一个shader,shader数据也相同(运行前物体的顶点数组中坐标直接使用手动计算出的世界空间坐标,那么shader数据中的模型->世界矩阵就可以都设置为单位矩阵),那么这个时候就可以设置一次shader,直接绑定3次VAO调用3次DrawCall,就绘制出了3个物体,好了渲染状态的设置减少了,绑定VAO和DrawCall调用该怎么办,翻了下自己乱糟糟的代码......忽然想到如果我把3个物体的顶点数组放到一个数组里面,VAO,VBO,VEO就可以都只创建一个,然后只绑定一次VAO调用了一次DrawCall,就绘制出了3个物体了(当然比起3个顶点数组,1个数组还需要运行前计算一下,3个物体各种数据在整个顶点数组的起始索引等等)。

经过一翻折腾,设置渲染状态、收集提交几何数据和DrawCall调用都从3次变成了1次,那么现在的性能毫无疑问比之前好了。在不知不觉中其实你已经简单实现了广义上的合批,那么合批就可以广义的理解为减少渲染状态的设置、减少几何数据的收集绑定、减少DrawCall的调用。当然上面的操作都是在使用了同一个shader且shader数据相同的情况下实现的,所以,Unity里的静态合批和动态合批都有一个前提条件那就是网格使用的材质相同。那么在大的复杂的场景里,其实是有很多使用了相同材质的物体的,这个时候如果使用合批那么性能肯定比仍然一个一个渲染要好的多。所以,在大的复杂的场景里如果有很多物体使用了相同的材质,那么这个时候不使用合批去渲染这些物体,就会造成性能浪费,所以要使用合批。

成熟的商业引擎合批机制比我们在学习渲染API中了解的合批要复杂,下面就简单分析一下Unity里的合批。

2. Unity中的几种合批渲染

2.1 Unity中的静态合批

首先要强调一点就是,Unity的静态合批不会降低DrawCall调用,下面会分析具体为什么不降低

如何使用静态合批:场景中选中需要静态合批的物体,在该物体的Inspector窗口中将右上角的Static勾选上。

静态合批的条件:正如上面所说的①使用相同的材质球②标明为Static的静态物体

静态合批的适用范围:像生活中的静态物体一样,建筑,树木等,因为计算合并后的数据在场景内就是固定的不会再变动

静态合批的过程:

扫描二维码关注公众号,回复: 14771906 查看本文章

① 游戏软件运行前,将场景内这些使用同一材质勾选static物体的顶点、索引数据提取出来计算,直接变换到世界空间下然后再存入一个大的顶点和索引缓冲中,并记录每个物体的起始索引

② 游戏运行后渲染场景时,一次性提交整个合并后的顶点数据,然后设置一次渲染状态,Unity的场景管理系统会判断每个物体的可见性,然后分别调用DrawCall绘制每一个模型

这里为什么不调用一次DrawCall直接渲染呢,我的理解是,渲染消耗主要还是设置渲染状态(SetPassCall)和收集提交几何数据(Opengl里绑定VAO),DrawCall其实只是一个函数的调用,并不耗性能,如果不判断可见性,直接调用一次DrawCall那么那些看不见的物体的顶点数据也会进行一次顶点着色函数然后再参与视锥体剔除才能被剔除掉,这样更会造成性能浪费。

综上所述,静态合批其实就像是我们在学习Opengl里做的,在游戏软件运行前就计算出一些数据直接存放,然后运行时直接加载,预先计算数据这个过程Unity什么时候完成的呢?就是在导出安装包的时候,计算出场景内静态合批物体的数据存放在了.scene文件里,所以,有静态合批的时候安装包会比较大一些。需要注意的是在编辑器下就算你勾选了静态合批,在FrameDebug里看到了静态合批,其实也不是真正的静态合批,文件夹里的.scene文件也不会变大,而且FrameDebug里还会看到DrawCall减少(其实没减少),因为这都是在打包的时候进行的,打包后才会有效果。

静态合批的缺点:包体增大;将场景加载后,由于场景内有静态合批数据,会使内存变大。

总结:静态合批实际上是用内存空间来降低渲染的性能消耗(减少设置渲染状态和提交几何数据)

2.2 动态合批

动态合批,听名字的意思就像是动态的计算合并后的数据,其实实际上也确实是这样,如果物体不是静态不动的,但是这些物体仍然产生了多个并且使用了同一材质球,那么这时候就可以使用动态合批

如何使用动态合批:在自定义管线的工程下,会有渲染管线资源,在上面勾选静态合批就,引擎就会在可以动态合批的时候自动进行,需要注意的是,在Unity的自定义管线项目里当物体可以同时进行多种合批的时候,合批的优先使用顺序SRPBatcher>静态合批>GpuInstance>动态合批

动态合批的条件:①使用相同的材质球②在视野范围内的物体

动态合批的适用范围: 移动的,没有进行前几种合批的物体

动态合批的过程:

既然不能预先计算,那么可以在运行时,在cpu里直接把可以合批物体的顶点等数据变换到世界空间下

②渲染时,提交合并后的顶点数据,设置一次渲染状态,调用一次DrawCall绘制多个模型(不同于静态合批的多次调用DrawCall,因为可以进行动态合批的物体已经判断过可见性)

很明显,从过程中就可以看出,动态合批是利用CPU的计算性能消耗来换取渲染状态设置,几何处理和绘制调用的消耗

动态合批的缺点:消耗CPU的计算性能,当计算消耗大于设置渲染状态等消耗时得不偿失。

总结:动态合批其实就是用消耗CPU的计算性能消耗来换取绘制性能,在现代图形API里绘制调用的消耗已经减小了,当cpu的计算消耗小于绘制调用消耗时动态合批才有效果。

2.3 GpuInstance

这里只粗略讲一下,具体的理解还是希望去学一下底层API,OpenGL和VULKAN都有一个小行星案例,讲的就是实例化渲染,而且这种合批也确实是底层API给的函数调用。

还是从Opengl的API说起,在opengl里,我想绘制100个一模一样的正方形,初学的时候就会想着设置完shader和shader数据,提交绑定了VAO后,直接for循环100个DrawCall,想完就觉得不可能是这样做的,怎么可能底层API绘制N个顶点和shader数据都相同的东西要耗费N次DrawCall调用,一定有其他API可以针对这种情况,继续学下去后,确实是有的glDrawArraysInstancedglDrawElementsInstanced,

这里讲一下glDrawArraysInstanced,这个函数的最后一个参数,可以传入要实例化的数量,那么在设置完shader、数据、VAO后直接调用一次这个函数,传入想要实例化的数量,那么GPU就会执行对应次数的顶点着色器,并且在每次执行顶点着色函数时都传入一个实例化ID(该实例的索引),这样不仅可以一次性渲染N个网格和着色器相同的东西,还可以将每个实例的差异数据(如变换矩阵)存入数组通过uniform数组传递给着色器,然后在顶点函数中用实例化ID去索引存储差异数据的数组,这样就可以实现每个实例有不同的位置旋转和缩放等效果。

如何使用gpuinstance合批:Unity中使用动态合批需要自己的着色器写法支持,如果支持那么创建的材质Inspector面板上就会有选项,勾选即可,但是需要注意URP下的合批顺序。

gpuinstance的条件:①使用相同材质②使用相同的mesh

gpuinstance的适用范围:大规模的同mesh同材质小物件,需要表现位置渲染缩放等异样,如花、草等,草地渲染最为常见

gpuinstance的过程:判断视野内满足条件的物体,抽取一个对象作为实例,将实例的顶点数据送往GPU,然后设置着色器和着色器数据(差异数据存入uniform数组中(OpenGL的API下)),最后调用绘制函数,那么顶点着色器就会循环渲染每个实例,并且每次都会用实例ID去索引差异数组来渲染出每一个实例。

gpuinstance的限制:在OpenGL里会消耗uniform变量,着色器中的uniform变量其实是存储在GPU的常量缓存区上,显存并不是无限的资源,所以对着色器的unifrom变量是有限制的

总结:gpuinstance其实就是利用底层API的绘制调用函数和unifrom数组,可以让顶点着色器多次执行并且每次可以传入实例化ID(索引)去索引存放了差异数据的uniform数组。gpuinstance不需要浪费静态合并mesh后的内存,内存运算消耗也没有动态合批那么高,但是要受限于着色器uniform变量的个数。

2.4 SRPBatcher

讲SRP之前还要说一个OpengGL里重要的东西,就是uniform块(unifrom缓存对象),没什么复杂的,形式上只是把多个的uniform变量做成了结构体之类的东西。比如将观察和透视矩阵放到一个uniform块中:

layout (std140) uniform Matrices

{

    mat4 projection;

    mat4 view;

};

具体怎么绑定和传入数据和(std140)的布局形式以及和uniform单一常量的具体区别,请去查阅OpenGL资料,想要学好渲染,掌握多个底层API是必须的。这里只需要知道uniform块也存在常量缓存区中,而且是全局常量,可以不同着色器共享。

注意!!!想要使用SRP合批,shader的写法就必须满足SRP合批的要求,当写法满足要求的时候,unity向shader设置常量数据就是使用uniform缓存对象,不再用uniform单一变量了!!!

在同一shader程序的前提下(unity中shader的变体相同则它们编译后的shader程序相同,而不是像其他合批一样必须材质相同),消耗时间性能的大头,无非就是向shader设置uniform数据更新GPU的常量缓存了,SRP合批针对的也正是这个。

SRP合批核心就是两个uniform缓存对象:UnityPerDraw(大块缓存对象a large GPU buffe)和UnityPerMaterial(材质持久化数据缓存区),具体在过程中解释

srp合批如何使用:在管线中开启合批,并且shader代码写法支持

srp合批的条件:网格所用材质的shader变体相同

srp合批的过程:①收集视野内满足条件的物体,渲染第一个物体时,将这一个SRP合批批次中的所有物体,它们的物体相关属性数据收集起来存入UnityPerDraw缓存区中,然后绑定缓存区时调用glBindBufferRange而不是glBindBufferBase(问就是去学OpenGL......一篇文章讲不了太多),而它们所用的所有材质的材质数据则存入一些缓存区中,作为持久化数据(这个批次中如果使用了这个变体创建的n个材质,则存入n个uniform缓存区)

②渲染第二个物体时,绑定了顶点索引缓存后(网格没变则不需要再次绑定),如果这个物体和上一个材质相同,则不用再次glBufferSubData填充数据只需要调用glBindBufferRange去UnityPerDraw常量缓存区中做一个偏移拿到该物体的物体相关属性数据(这应该就是大缓存对象的大字所在吧,存储了本批次所有物体的物体相关属性数据),然后直接调用DrawCall渲染;就算和上一个的材质不同,那么也只需要多一个绑定UnityPerMaterial缓冲区的操作,并且也不需要用glBufferSubData去填充就能调用DrawCall渲染,因为一开始就都填充好了。

接着继续绘制下一个物体过程同②,这里应该就能理解,SRP合批提速的原因就是这一批次开始渲染时就会将所有物体的物体相关属性数据填充好,所用到材质的材质相关属性数据也填充好,之后绘制其他物体只做绑定不再填充更新常量缓存。

(这里提一下物体相关属性和材质相关属性,比如每个物体的模型到世界和世界到模型的转换矩阵,就是根据每个物体变化的,每次绑定设置偏移;而材质相关属性,比如材质的光滑度金属度则是材质的属性,所以这一批次用了几个材质就需要填充几个UnityPerMaterial缓存,渲染时只要所用的材质没变则材质属性就不变,那么也就不需要去切换绑定)

总结:SRP合批就是基于uniform缓存对象的优化,一次性填充,后续渲染只做绑定切换。

整篇文章可以说是基于OpenGL讲的,有错误或者不懂的地方可以评论私聊讨论,但还是建议去好好学一下OpenGL或者DX,游戏引擎只是封装过的工具,学习底层才能更好理解。

大佬的地址:Unity中的静态合批,动态合批,SRPBatcher,GpuInstance - 知乎1. 从图形API分析为什么合批和合批的原理 简单学习过OpenGL或者DX的小伙伴肯定了解,如果初学Opengl的时候想要 渲染出1个正方形,1个plane,1个圆形,那么就要声明3个顶点数组,创建3个顶点数组对象(VAO),3个顶点…https://zhuanlan.zhihu.com/p/603406270

猜你喜欢

转载自blog.csdn.net/Ling_SevoL_Y/article/details/129842988