gpu硬件支持的一种特性,使用少量的渲染调用(DrawCall)渲染同一网格的多个副本.也就是说在渲染时,他只需要提交一个网格副本,一个材质球,然后在把这些模型对象中不同的属性(比如:位置,大小,旋转,颜色等)提取出来放到一个数组中.
2.底层逻辑,为什么能够加速渲染,提高性能?- 针对同网格同材质的模型多个对象进行渲染时,有一下几种渲染方式:
- 原始模式,每个材质和模型都调用一次渲染,这样会导致的后果是,每次渲染一个对象CPU就会向GPU提交一次渲染相关的所有资源.很明显这里面有很多重复资源的提交造成了很大的浪费,也让渲染速度大大降低.
- 动态合批模式,这个很好,把最近相同的模型进行合并成一个模型,然后一次提交一次渲染,爽快利落,典型的空间换速度,也很划算,最大化的提高渲染效率.但是,当网格数量和网格的顶点数超过一定数量时,动态合批就不能在进行了因为这个时候就会拉低CPU的执行效率,以及内存空间的暴涨.所以动态合批有较多的限制.
- 静态合批模式,这个也很好,离线把模型全部给合并到一个网格(合并后的网格顶点数量有限制:65535),这样能够解决动态合批的问题,但是也有问题,会导致一下几个问题:
- 重复网格的副本会被复制很多份,导致资源包体和运行时内存都会有很大的增长.
- 一些不需要渲染的顶点也被提交给GPU,增大了GPU的顶点处理器的压力,.
- 在逻辑处理上,静态合批也有一个问题,就是合并完后的模型对象不能再被位移,旋转,缩放等处理了.
- 然后就是GPUInstance,他也是针对相同模型的网格和材质只提交一次,然后他组合这些模型对象的不同之处打成一个数组提交给GPU,这样既能够减少提交的次数,有不会让内存暴涨.CPU端也不会因为合并网格而各种计算,增加消耗.这样他的特点就出来了:
- 在CPU端进行模型裁剪,只合并看见的模型对象.这样提交给GPU渲染的实例会更少更有效.在某些场景下,这个相对于静态批处理来说优势很足.
- 只组织和提交模型对象之间的不同之处,内存的消耗也能够控制.这个相对于动静两种合批来说都是优势巨大.
- 不需要做网格的合并处理,这方面也CPU没有消耗.这个相对于动态合批来说优势巨大.
- 在CPU端的实例不同点的合并处理,需要一定的消耗,这个对于静态批处理来说没有优势.
- 在GPU端要增加一个索引和缓存的读取处理,这个也算是额外的消耗,这个对于静态批处理和动态批处理都没有优势.
- 因为一个批量渲染,只提交一个网格副本,所以对于网格的大小就没有了限制.适应范围更广泛,这个对于动态合批来说优势巨大.
- 综上几个渲染方式的对比之后,在针对这种大量的同网格,.同材质的模型时,数量越大性能提高程度就越明显.
二.适用场景 1.不能被动态合批
模型顶点数据(位置,法线,切线,UV等)总和不能超过2700个float.
2.静态合批不划算或者不能静态核批模型为一簇簇的出现或者散布广泛
模型对象位置,大小,旋转等都在变化.
比如:草丛,树木丛
建筑装饰物件
陨石群
三.在U3D里实现
Shader的支持
基础使用
1).使用U3D内置的Shader
- 使用Unity自带的能够开启GpuInstance 的Shader(几乎都有,但是也有例外,比如Partcles的材质好像都没有.),创建一个材质球.
- 在材质球的Inspector中把Enable GPU Instancing的选项给勾选上.
3.把使用这个材质球的模型,复制N多个,然后运行你就会通过一下两个地方来判断是否进行GPUInstance合批.
1).Profiler
2). FrameDebug ,当出现DrawMesh(instanced),就说明模型被GpuInstance合并.右边窗口点击Preview按钮,切换到模型展示窗口,在这个窗口的右下方你能看到xxx Instances字样,就表示有中一个批次包含多少个网格副本.
2).自定义Shader使用
第一步:预编译中增加instancing的宏编译.
#pragma multi_compile_instancing
只要加上这一条,那么在对应的材质Inspector上就能看到多一个Enable GPU Instancing的选项.
但是只是这样还不行,还需要进行下面步骤的操作,
第二步:在顶点数据结构的最后,增加UNITY_VERTEX_INPUT_INSTANCE_ID定义
这里需要说明的是,如果你使用Unity内置的顶点数据结构,比如:appdata_base,appdata_full等结构的时候,就忽略这一步吧,因为在内置的数据结构中,都默认添加了上这个宏.
第三步:在顶点程序的最前面增加UNITY_SETUP_INSTANCE_ID ()的宏方法.
第四步:在材质球的Inspector中把EnableGpuInstance的选项给勾选上.
到此,这个自定义的shader就是一个合格的支持GpuInstance的Shader了.材质球也开启了GpuInstance.然后替换上运行通过FrameDebug检验是不是合格.
看下图中左边的DrawMesh(Instancing)表示启用了GpuInstancing合批,右边面板中的Shader信息栏中展示的是,咱们自定义的Shader.这样就表明我们的Shader开启Instancing没有问题了.
3).自定义属性使用
承接上面的,完成在自定义shader中增加GpuInstancing的支持之后.
在上面的材质球中,如果我想改变某个属性怎么办?比如想让更改材质球的Color属性.让每个模型对象都展示不一样的颜色?那么这就需要下面的步骤了.
1.在顶点到片段的结构体中增加宏: UNITY_VERTEX_INPUT_INSTANCE_ID
注意这个增加的宏,与顶点结构体中增加的宏名字是一样的.
2.在顶点程序的最后,return 之前,增加一个宏方法: UNITY_TRANSFER_INSTANCE_ID (v,o) 的调用.
3.在片元程序中增加一个UNITY_SETUP_INSTANCE_ID (i);
定义InstancingBuffer的结构体,并声明一个数组.
- 结构体的位置最好放在v2f结构体的下面.所有程序代码的上面.
- 它有固定的定义格式:
UNITY_INSTANCING_BUFFER_START(bufTypeName)
UNITY_DEFINE_INSTANCED_PROP(type1,pname1)
UNITY_DEFINE_INSTANCED_PROP(type2,pname2)
UNITY_DEFINE_INSTANCED_PROP(type3,pname3)
UNITY_INSTANCING_BUFFER_END(arrName)
我们这里的定义示例如下图:
注意:这里的定义都不需要最后的分号”;”.
在顶点或者片元程序中调用InstancingBuffer的内容.
- 需要使用宏方法UNITY_ACCESS_INSTANCED_PROP(arrName,pname)
到此,自定义材质球属性这一块Shader端全部搞定,现在就要考虑如何为这个属性赋值呢?这就要走到下一步了,
C#脚本端, 针对Instancing的材质,修改InstancingBuffer中定义的属性值.
- 需要定义一个MaterialPropertyBlock的对象.
-
- 定义一个颜色数组,类型使用Vertor4.然后根据逻辑填充颜色.
- 然后调用MaterialPropertyBlock的SetVertor4Array来复制.(因为MaterialPropertyBlock的SetXXXArray方法群中没有SetColorArray,我们用SetVertor4Array来代替).
- 然后获取模型对象的Renderer组件,执行组件的方法SetPropertyBlock
UnityInstancing.cginc中定义宏的简单分析
UNITY_VERTEX_INPUT_INSTANCE_ID
根据代码分析,这个宏其实就是定义了一个整型变量instanceID.
UNITY_SETUP_INSTANCE_ID(input)
根据代码分析,这个宏方法的作用,其实就是把input数据中的instanceID变量经过变换之后赋值给了变量:unity_InstanceID了.之后从InstancingBuffer中获取内容都是通过这个变量来索引读取的.
UNITY_TRANSFER_INSTANCE_ID(input,output)
我们看到,这个宏的作用其实就是把顶点数据中的instanceID赋值给片段输入数据结构的instanceID.
两对宏定义:
UNITY_INSTANCING_BUFFER_START 和UNITY_INSTANCING_BUFFER_END
以及
UNITY_DEFINE_INSTANCED_PROP和UNITY_ACCESS_INSTANCED_PROP
通过上面代码的分析,我们知道,在上一节中定义的结构,转换成GLES3平台下的结构:
UNITY_INSTANCED_ARRAY_SIZE
这个宏使用来定义默认的合批最大数量在移动端是250. 其他端是500.
unity预定义的InsancingBuffer的情况.
我们通过这个截图可以看到,有3个PerDraw, 其实Unity针对坐标转换矩阵,Lightmap,SH,等都做了预处理,并对一些Buffer的访问做了封装,.所以很多地方我们调用的才那么方便.
脚本端支持
默认情况
系统会自动对场景中符合规范的,被开启GpuInstance的对象进行Instance合批处理.
绘制实例化网格的API函数
DrawInstanceMesh:
- 一次调用Mesh的数量有限,最大为1023个模型
- 材质球必须开启GpuInstance,如果不开启就会出发异常
- 属性赋值使用MaterialBlockProperties的Array方法群..
DrawInstanceMeshIndirect:这个函数是放在这里就是看的,他的处理就比较复杂需要单独另开一章进行讲解.
- 一次调用没有限制副本数量,
- 材质球不需要开启Instance
- 执行效率更高
DrawMesh:
没想到吧,使用这个API, 只要材质球使用同一个,并且材质球开启了GPUInstance也会被自动Instancing合批的.
意外情况- 材质不同或者材质没有被开启GpuInstancing都不会被合批.
- UNITY_SETUP_INSTANCE_ID宏调用的位置(不是程序的最前面第一行).可能会导致显示不正确.
- 带有光照贴图的对象使用了没有处理光照贴图的材质,这个不会被合批.
- 对象以及所有父对象中,只要Scale的分量中包含负值 ,这个不会被合批.
- 对象位置深度顺序中包含了其他模型或者材质的对象,这个会产生多个合批.
- 带有骨骼蒙皮动画的对象.这个不会被合批.(如果这个使用GPU蒙皮的话,就会被合批处理,针对GPU蒙皮+Instance之后我们再聊.)
- 由于Unity的封装,只要掌握前面的几个宏,就能够非常方面的使用.
- Unity只是针对视野之内的模型进行Instance合批处理,对于那些遍布场景的同模型同材质的对象优化效果更加友好.
四.软硬件支持
Unity的官方文档描述
1.文档地址:https://docs.unity3d.com/cn/2019.4/Manual/GPUInstancing.html
2.摘自文档:
平台的支持太乱了,如果自己去判断的话基本上会累死的.所以Unity中对于这些平台是否支持提供了一个api.
一句代码搞定: SystemInfo.supportsInstancing1.让材质球开启GpuInstance
2.使用材质球切换DrawInstanceMesh和DrawMesh
五.运行和调试环境
Unity 2018.4.12f1
Microsoft Visual Studio Professional 2019
六.代码地址
链接:https://pan.baidu.com/s/1aE3AjUGwide-Mk-f3vihmA 提取码:ngvy
代码运行截图如下:



