unity中的渲染优化技术——(减少draw call数目方法二:静态批处理、共享材质、批处理的选择)

一、静态批处理

相比于动态批处理来说,静态批处理适用于任何大小的几何模型。它的实现原理是,只在运行开始阶段,把需要进行静态批处理的模型合并到一个新的网格结构中,这意味着这些模型不可以在运行时刻被移动。但由于它只需要进行一次合并操作,因此比动态批处理更加高效。静态批处理的缺点在于,它往往需要占用更多的内存来存储合并后的几何结构,这是因为,如果在静态批处理前一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发送给GPU。如果这类使用同一网格的对象很多,那么这就会成为一个性能瓶颈了。例如一个使用了1000个相同树模型的森林中使用静态批处理,那么就会多使用1000倍的内存,这会造成严重的内存影响。这种时候,解决方法要么忍受这种牺牲内存换取性能的方法,要么不要使用静态批处理,而使用动态批处理技术(但要小心控制模型的顶点属性数目),或者自己编写批处理的方法。

现在做个实验,我采用了静态批处理:


在playerSetting->Other Settings中勾选Static Batching,然后运行项目查看profiler:


找到Memory->Scene Memory->Mesh可以看到有几十M的合并网格。然后我们取消勾选,不采用静态批处理:


Mesh的内存就减少了非常多,合并网格都不见了。 

下面我们在做一个简单实验,我们给出一个测试静态批处理的场景。场景中包含了3个水壶模型,他们使用的同一个材质,同时还包含了一个使用不同材质的立方体。场景中还包含了平行光,关闭了阴影效果,以避免阴影计算对批处理数目的影响。在运行前,渲染统计数据如下图:


从上图看出,尽管3个水壶模型使用了相同的材质,但它们仍然没有被动态批处理,因为模型包含的顶点数目是393:


而它们使用的shader需要使用4个顶点属性(顶点位置、法线方向、切线方向和纹理坐标),超过了动态批处理中限定的900限制,此时想要减少draw call就需要使用静态批处理。

	struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			};

静态批处理的实现非常简单。只需要确保playerSetting->Other Settings中勾选Static Batching,然后把物体面板上的Static复选框勾选上即可(实际上只需要勾选Batching Static即可),如下图:


下面运行程序后,如图:


我们发现批处理数目变成了2,而Save by batching数目也显示为2。此时如果我们在运行时查看每个模型使用的网格,会发现他们都变成了一个名为Combine Mesh(roo:scene)的东西:


这个网格是unity合并了所有被标识为“Static”的物体的结果,在这个例子中就是场景中的4个模型,但是4个对象明明不是都使用了一个材质,为什么可以合并成一个?我们发现右下方标明了“4 submeshes”,也就是说这个合并后的网格其实包含了4个子网格,即场景中4个对象,对于合并后的网格,unity会判断其中使用同一个材质的子网格,然后对它们进行批处理。


在内部实现上,unity首先把这些静态物体变换到世界空间下,然后为他们构建一个更大的顶点和索引缓存。对于使用了同一材质的物体,unity只需要调用一个draw call就可以绘制全部物体。而对于使用了不同材质的物体,静态批处理同样可以提升渲染性能。尽管这些物体仍然需要调用多个draw call,但静态批处理可以减少这些draw call之间的状态切换,而这些切换往往是费时的操作。从合并后的网格结构中发现,尽管3个水壶模型使用了同一个网格,但合并后却变成了3个独立网格。而且我们可以从unity的profiler观察到在应用静态批处理前后VBO total(Vertex Buffer Object,顶点缓冲对象)的变化,数目会变大,这正是因为静态批处理会占用更多内存的缘故。

如果场景中包含了除平行光以外的其他光源,并且在shader中定义了额外的Pass来处理它们,这些额外的Pass部分是不会被批处理的。但是处理平行光的Base Pass部分仍然会被静态批处理,因此仍然可以节省两个draw call:


二、共享材质

无论动态批处理来说静态批处理,都要求模型之间需要共享同一个材质。但不同模型之间需要由不同的渲染属性,例如使用不同的纹理、颜色等。这时我们需要一些策略来尽可能地合并材质。

如果两个材质之间只有使用的纹理不同,我们可以把这些纹理合并到一张更大的纹理中,这张更大的纹理被称为是一张图集。一旦使用了同一张纹理,我们就可以使用同一个材质,再使用不同的采样坐标对纹理采样即可。

但有时除了纹理不同外,不同的物体在材质上还有一些微小的参数变化,例如颜色不同、某些浮点属性不同。但是不管是动态批处理还是静态批处理,它们的前提都是要使用同一个材质,也就是说它们指向的材质必须是同一个实体。这意味着只要调整了参数,就会影响到所有使用这个材质的对象。那么想要微小的调整怎么办?一种常用的方法就是使用网格的顶点数据(最常见的就是顶点颜色数据)来存储这些参数。

前面说过,经过批处理后的物体会被处理成更大的VBO发送给GPU,VBO中的数据可以作为输入传递给顶点着色器,因此我们可以巧妙地对VBO中的数据进行控制,从而达到不同效果的目的。一个例子是,森林场景中所有的树都使用了同一种材质,我们希望它们可以通过批处理来减少draw call,但不同的树的颜色可能不同,这时我们可以利用网格的顶点的颜色数据来调整。

需要注意,如果我们需要在脚本中访问共享材质,应该使用Renderer.sharedMaterial来保证修改的是和其他物体共享的材质,但这意味着修改会应用到所有使用该材质的物体上。另一个类似的API是Renderer.material,如果使用Renderer.material来修改材质,unity会创建一个该材质的复制品,从而破坏批处理在该物体上的应用,这可能并不是我们希望看到的。

三、批处理的注意事项

在选择使用动态批处理还是静态批处理时,有一些小建议:

1.尽可能选择静态批处理,但得时刻小心对内存的消耗,并且记住经过静态批处理的物体不可以再被移动。

2.如果无法进行静态批处理,而要使用动态批处理的话,小心上面提到的各种条件限制。例如尽可能让这样的物体少并且尽可能让这些物体包含少量的顶点属性和顶点数目。

3.对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。

4.对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“static”。

还有一些需要注意的地方。由于批处理需要把多个模型变换到世界空间下再合并它们,因此如果shader中存在一些基于模型空间下的坐标运算,那么往往会得到错误的结果。一种解决方法是,在shader中使用DisableBatching标签来强制使用该shader的材质不会被批处理。另一个注意事项是使用半透明材质的物体通常需要使用严格的从后往前的绘制顺序来保证透明混合的正确性。对于这些物体,unity会首先保证它们的绘制顺序,再尝试对它们进行批处理。这意味着,当绘制顺序无法满足时,批处理无法在这些物体上被成功应用。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/102950175

最新文章