由浅到浅入门批量渲染(三)

发表于2020-11-16
评论0 7.3k浏览

好久不见。

 

这是第34篇与游戏开发有关的文章。

 

上回简单总结了一下动态合批,这次我们继续说说实例化渲染

 

| 实例化渲染

 

当我们想要呈现这样的场景:一片茂密的森林、广阔的草原或崎岖的山路时,会发现在这些场景中存在大量重复性元素:树木、草和岩石。

 

FGU1FfjagdzPM4V9FeDu.png

仙境怕是也不过如此吧

 

它们都使用了相同的模型,或者模型的种类很少,比如:树可能只有几种;但为了做出差异化,它们的颜色略有不同,高低参差不齐,当然位置也各不相同。

 

使用静态合批来处理它们(假设它们都没有动画),是不合适的。因为数量太多(林子大了,多少树都有),所以合并后的网格体积可能非常大,这会引起内存的增加;而且,这个合并后的网格还是由大量重复网格组成的,不划算。

 

使用动态合批来处理他们,虽然不会“合并”网格,但是仍然需要在渲染前遍历所有顶点,进行空间变换的操作;虽然单颗树、石头的顶点数量可能不多,但由于数量很多,所以也会在一定程度上增加CPU性能的开销,没必要。

 

那么,对于场景中这些模型重复数量多的渲染需求,有没有适合的批处理策略呢?有吧,实例化渲染就是为了解决这样的问题。

 

| 简述工作原理

 

实例化渲染,是通过调用“特殊”的渲染接口,由GPU完成的“批处理”。

 

它与传统的渲染方式相比,最大的差别在于:调用渲染命令时需要告知GPU这次渲染的次数(绘制N个)。当GPU接到这个命令时,就会连续绘制N个物体到我们的屏幕上,其效率远高于连续调用N次传统渲染命令的和(一次绘制一个)。

 

举个例子,假设希望在屏幕上绘制出两个颜色、位置均不同的箱子。如果使用传统的渲染,则需要调用两次渲染命令(DrawCall = 2),分别为:画一个红箱子 和 画一个绿箱子。

lGmtZmHPeyijQPn6yPA2.png

两个颜色、位置各异的箱子

 

如果使用实例化渲染,则只需要调用一次渲染命令(DrawCall = 1),并且附带一个参数2(表示绘制两个)即可。

 

当然,如果只是这样,那GPU就会把两个箱子画在相同的位置上。所以我们还需要告诉GPU两个箱子各自的位置(其实是转换矩阵)以及颜色

 

这个位置和颜色我们会按照数组的方式传递给GPU,大概这个样子吧:

P6yd81vlYgdJJCCJOdpJ.png

分别传递保存位置和颜色的数组

 

那接下来GPU在进行渲染时,就会在渲染每一个箱子的时候,根据当前箱子的索引(第几个),拿到正确的属性(位置、颜色)来进行绘制了。

vV9VdFlEPlm7MefnUh6n.png

一个简单的实例化渲染流程


 

| Unity是如何处理实例化的

 

我们通过一个简单的场景,来看一下Unity为实例化渲染做了什么。

RxsTHLIhyOiZUTT7XoZt.png

实例化渲染两个彩色箱子

8GMoajrfA056jYa4AYEX.png

颜色属性通过MaterialPropertyBlock传入

 

通过GPA观察Unity做了什么。

5rsZiheTgouZ0dFwqBhe.png

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),可以很容易找到对应的数据。

 

NlH9t2SyHb3URWHQrT4j.png

转换矩阵及颜色被分别填入Constant Buffer中

CpspAzkX32bY5XBWzWTz.png

Constant Buffer中的矩阵(Dx为行向量)

4xJXnXxTZNAD47J6xsX8.png

Constant Buffer中的属性(颜色)

 

可见,渲染时GPU可以通过当前实例化单位的索引,从Buffer中获取到对应的属性,完成正确的绘制。

 

| Unity中启用实例化渲染

 

当然,相比于上述无用的知识点,如何在Unity中使用实例化渲染可能更为重要。

 

在Unity中可以通过自动或手动的方式,启用实例化渲染。

 

自动启用实例化渲染

使用支持实例化渲染的Shader,并勾选材质球上的启用开关,Unity便会对满足条件的物体,自动开启实例化渲染。

OR9XQDOvFTHoLurPDbDp.png

有这个选项即表示该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官方文档中,列举了各平台支持实例化渲染的最低要求。

pXCSTe0V1tGgTVmJN1nP.png

官方文档中对实例化渲染的最低API支持要求

 

当然,我们也可以通过引擎中SystemInfo.supportsInstancing属性来判断环境是否支持实例化渲染。

那支持实例化渲染的机器占比大概是多少呢?由于国内大多数游戏公司都是以手游项目糊口。所以开发者可能会更多关注其在安卓平台上的情况。

 

根据Android开发者的官方数据显示,截至2020年8月30日,约88%的活跃安卓设备,都已经支持实例化渲染,所以基本上可以放心使用。

wtxR0Fw4KAuzR1EW2yVA.png

android开发者官网发布的活跃设备OpenGL ES版本占比信息

 

| 与静、动态合批的差异

静、动态合批实质上是将可以合批的对象真正的合并成一个大物体后,再通知GPU进行渲染,也就是其顶点索引缓冲区中必须包含全部参与合批对象的顶点信息;因此,可以认为是CPU完成的批处理。
 

实例化渲染是对网格信息的重复利用,无论最终要渲染出几个单位,其顶点和索引缓冲区内都只有一份数据,可以认为是GPU完成的批处理。


其实这么总结也有点问题,本质上讲:动、静态合批解决的是合批问题,也就是先有大量存在的单位,再通过一些手段合并成为批次;而实例化渲染其实是个复制的事儿,是从少量复制为大量,只是利用了它“可以通过传入属性实现差异化”的特点,在某些条件下达到了与合批相同的效果。

 

| 简单总结静、动态合批及实例化渲染

无论是静态合批、动态合批或实例化渲染,本质上并无孰优孰劣,它们都只是提高渲染效率的解决方案,也都有自己适合的场景或擅长解决的问题。

 

个人以为:

如果你的场景中存在多数静止的、使用了不同网格、相同材质的物体,特别是当你的相机通常只能照到一部分物体时(如第一视角),可以优先尝试下静态合批,通过牺牲一些内存来提升渲染效率;

 

针对那些运动的、网格顶点数很少、材质相同的物体,比如飞行的各种箭矢、炮弹等,使用动态合批,通过增加一些CPU处理顶点的性能开销,来提升渲染效率,也许是不错的选择;

 

如果有大量模型相同、材质相同、或尽管表现上有一些不同,但仍然可以通过属性来实现这些差异化的物体时,启用实例化渲染通常可以在很大程度上提升渲染效率。

 

| 写在最后

 

按计划下次更新的内容应该是“优化骨骼蒙皮动画,以及两种常用的批量渲染方式”,但觉得内容有点多,所以将其分为两个部分;因此,下次更新的内容变为“优化骨骼蒙皮动画”,而“两种常用的骨骼蒙皮动画单位的批量渲染方式”,将作为本系列的最后一次更新内容。

 

下回见喽。

 

我的公众号 偶尔学学Unity 会特别不定期更新与游戏开发可能有关的文章,欢迎关注,谢谢。

g2EbL7TiMdpcoYGguMao.png


 

  • 允许他人重新传播作品,但他人重新传播时必须在所使用作品的正文开头的显著位置,注明用户的姓名、来源及其采用的知识共享协议,并与该作品在磨坊上的原发地址建立链接
  • 可对作品重新编排、修改、节选或者以作品为基础进行创作和发布
  • 可将作品进行商业性使用

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引