上回 游戏图形批量渲染及优化:Unity动态合批技术 简单总结了一下动态合批,这次我们继续说说Unity实例化渲染。
| 实例化渲染
当我们想要呈现这样的场景:一片茂密的森林、广阔的草原或崎岖的山路时,会发现在这些场景中存在大量重复性元素:树木、草和岩石。
仙境怕是也不过如此吧
它们都使用了相同的模型,或者模型的种类很少,比如:树可能只有几种;但为了做出差异化,它们的颜色略有不同,高低参差不齐,当然位置也各不相同。
使用静态合批来处理它们(假设它们都没有动画),是不合适的。因为数量太多(林子大了,多少树都有),所以合并后的网格体积可能非常大,这会引起内存的增加;而且,这个合并后的网格还是由大量重复网格组成的,不划算。
使用动态合批来处理他们,虽然不会“合并”网格,但是仍然需要在渲染前遍历所有顶点,进行空间变换的操作;虽然单颗树、石头的顶点数量可能不多,但由于数量很多,所以也会在一定程度上增加CPU性能的开销,没必要。
那么,对于场景中这些模型重复、数量多的渲染需求,有没有适合的批处理策略呢?有吧,实例化渲染就是为了解决这样的问题。
| 简述工作原理
实例化渲染,是通过调用“特殊”的渲染接口,由GPU完成的“批处理”。
它与传统的渲染方式相比,最大的差别在于:调用渲染命令时需要告知GPU这次渲染的次数(绘制N个)。当GPU接到这个命令时,就会连续绘制N个物体到我们的屏幕上,其效率远高于连续调用N次传统渲染命令的和(一次绘制一个)。
举个例子,假设希望在屏幕上绘制出两个颜色、位置均不同的箱子。如果使用传统的渲染,则需要调用两次渲染命令(DrawCall = 2),分别为:画一个红箱子 和 画一个绿箱子。
两个颜色、位置各异的箱子
如果使用实例化渲染,则只需要调用一次渲染命令(DrawCall = 1),并且附带一个参数2(表示绘制两个)即可。
当然,如果只是这样,那GPU就会把两个箱子画在相同的位置上。所以我们还需要告诉GPU两个箱子各自的位置(其实是转换矩阵)以及颜色。
这个位置和颜色我们会按照数组的方式传递给GPU,大概这个样子吧:
分别传递保存位置和颜色的数组
那接下来GPU在进行渲染时,就会在渲染每一个箱子的时候,根据当前箱子的索引(第几个),拿到正确的属性(位置、颜色)来进行绘制了。
一个简单的实例化渲染流程
| Unity是如何处理实例化的
我们通过一个简单的场景,来看一下Unity为实例化渲染做了什么。
实例化渲染两个彩色箱子
颜色属性通过MaterialPropertyBlock传入
通过GPA观察Unity做了什么。
GPA中的VertexBuffer和IndexBuffer中的信息
注:Unity默认Cube网格,包含24个顶点和36个索引。
顶点缓冲区Size = (Position(float3)
+ Normal(float3)
+ Tangent(float4)
+ TexCoord(float2)
+ TexCoord1(float2)) x 24 = 1344Byte
索引缓冲区Size = Index(ushort) x 36 = 72Byte
可见,顶点、索引缓冲区内,确实只有一个网格的数据。
那么GPU如何判断每个Cube的绘制位置,及其颜色呢?
结合引擎为Dx平台生成的shader(我的测试环境使用的是Pc),可以很容易找到对应的数据。
转换矩阵及颜色被分别填入Constant Buffer中
Constant Buffer中的矩阵(Dx为行向量)
Constant Buffer中的属性(颜色)
可见,渲染时GPU可以通过当前实例化单位的索引,从Buffer中获取到对应的属性,完成正确的绘制。
| Unity中启用实例化渲染
当然,相比于上述无用的知识点,如何在Unity中使用实例化渲染可能更为重要。
在Unity中可以通过自动或手动的方式,启用实例化渲染。
自动启用实例化渲染
使用支持实例化渲染的Shader,并勾选材质球上的启用开关,Unity便会对满足条件的物体,自动开启实例化渲染。
有这个选项即表示该Shader支持实例化渲染
自定义Shader
如果你希望自己的Shader也支持实例化渲染,应重点注意以下内容:
#pragma multi_compile_instancing
启用实例化渲染(材质球上将出现启用实例化的勾选框);
UNITY_VERTEX_INPUT_INSTANCE_ID
在a2v及v2f的结构中定义实例化索引下标(SV_InstanceID ),也就是当前渲染单位的索引,用于从Constant Buffer中提取正确的属性(做显示差异化用);
UNITY_INSTANCING_BUFFER_START ~ END
在这个起止区域内定义属性,才能在着色器中正确的根据索引提取出当前渲染单位所对应的属性;
UNITY_SETUP_INSTANCE_ID
定义在着色器的起始位置,使顶点着色器(或片段着色器)可以正确的访问到实例化单位的索引;
UNITY_ACCESS_INSTANCED_PROP
根据索引访问到这个单位对应的属性,如上面例子中每个箱子的颜色属性。
这里只是简述一些相对重要的内容(凑些字数),官方文档中有更详细内容,建议优先了解。
手动实例化渲染
使用 Graphics.DrawMeshInstanced 和 Graphics.DrawMeshInstancedIndirect 来手动执行 GPU 实例化,详见官方文档中的解释,这里就不再赘述了。
| 实例化渲染的使用要求
并非所有设备都可以使用实例化渲染。
在Unity官方文档中,列举了各平台支持实例化渲染的最低要求。
官方文档中对支持实例化渲染的最低API要求
当然,我们也可以通过引擎中SystemInfo.supportsInstancing属性来判断环境是否支持实例化渲染。
那支持实例化渲染的机器占比大概是多少呢?由于国内大多数游戏公司都是以手游项目糊口。所以开发者可能会更多关注其在安卓平台上的情况。
根据Android开发者的官方数据显示,截至2020年8月30日,约88%的活跃安卓设备,都已经支持实例化渲染,所以基本上可以放心使用。
android开发者官网发布的活跃设备OpenGL ES版本占比信息
| 与静、动态合批的差异
静、动态合批实质上是将可以合批的对象真正的合并成一个大物体后,再通知GPU进行渲染,也就是其顶点索引缓冲区中必须包含全部参与合批对象的顶点信息;因此,可以认为是CPU完成的批处理。
实例化渲染是对网格信息的重复利用,无论最终要渲染出几个单位,其顶点和索引缓冲区内都只有一份数据,可以认为是GPU完成的批处理。
其实这么总结也有点问题,本质上讲:动、静态合批解决的是合批问题,也就是先有大量存在的单位,再通过一些手段合并成为批次;而实例化渲染其实是个复制的事儿,是从少量复制为大量,只是利用了它“可以通过传入属性实现差异化”的特点,在某些条件下达到了与合批相同的效果。
| 简单总结静、动态合批及实例化渲染
无论是静态合批、动态合批或实例化渲染,本质上并无孰优孰劣,它们都只是提高渲染效率的解决方案,也都有自己适合的场景或擅长解决的问题。
个人以为:
如果你的场景中存在多数静止的、使用了不同网格、相同材质的物体,特别是当你的相机通常只能照到一部分物体时(如第一视角),可以优先尝试下静态合批,通过牺牲一些内存来提升渲染效率;
针对那些运动的、网格顶点数很少、材质相同的物体,比如飞行的各种箭矢、炮弹等,使用动态合批,通过增加一些CPU处理顶点的性能开销,来提升渲染效率,也许是不错的选择;
如果有大量模型相同、材质相同、或尽管表现上有一些不同,但仍然可以通过属性来实现这些差异化的物体时,启用实例化渲染通常可以在很大程度上提升渲染效率。
| 写在最后
按计划下次更新的内容应该是“优化骨骼蒙皮动画,以及两种常用的批量渲染方式”,但觉得内容有点多,所以将其分为两个部分;因此,下次更新的内容变为“优化骨骼蒙皮动画”,而“两种常用的骨骼蒙皮动画单位的批量渲染方式”,将作为本系列的最后一次更新内容。
下回见喽。
作者:枸杞忧天
来源:偶尔学学Unity公众号
热门课程
专业讲师指导 快速摆脱技能困惑相关文章
多种教程 总有一个适合自己专业问题咨询
你担心的问题,火星帮你解答想学习UI设计,却不知道哪里的培训机构比较好?本文推荐一家专业的UI设计培训机构,助你快速掌握技能。
想学习平面设计却不知道该去哪里?本文为您介绍学习平面设计的好去处。
想学习VR软件开发但不知道选择哪家?本文为您介绍如何选择学VR软件开发的最佳选择。
想学习后期动画制作技巧?本文详细介绍火星影视后期动画培训,助您成为后期动画专家!
专业的影视美术提升培训机构为学员提供全方位的影视美术技能培训,助力学员在影视美术领域取得成功。
想要学习交互设计却不知道从哪里入手?本文为您提供选择最佳学习路径的指南,助您顺利成为交互设计师。
1. 打开微信扫一扫,扫描左侧二维码
2. 添加老师微信,马上领取免费课程资源
同学您好!