Unity_Shader高级篇_16_Unity Shader入门精要_减少draw call 数目

在Unity手册的iOS硬件指南(https://docs.unity3d.com/Manual/iphone-Hardware.html)中我们可以了解到iOS平台的硬件条件则相对统一。

影响性能的因素
(1)CPU:过多的draw cal。过多的Draw call。复杂的脚本或者模拟的任务。
(2)GPU:顶点处理:多的顶点,过多逐顶点计算·。
片元处理:过多片元(即可能是由于分辨率造成的,也可能是由于overdraw造成的),过多的片元计算。
(3)宽带:使用了尺寸很大且未压缩的纹理。分辨率锅盖的计算的帧缓存。

(1)CPU优化。
使用批处理技术减少draw call 数目。
(2)GPU优化。
减少需要处理的顶点数目:优化几何体。使用模型的LOD(Level of Detail)技术。使用遮挡剔除(Occlusion Culling)技术。
减少需要处理的片元数目:控制绘制顺序。警惕透明物体。减少实时光照。
减少计算复杂度:使用Shader的LOD(Level of Detail)技术。代码方面的优化。
(3)节省内存带宽。
减少纹理大小。
利用分辨率缩放。

其他性能分析工具
对于移动平台上的游戏来说,我们更希望得到在真机上运行游戏时的性能数据。这时,Unity目前提供的各个工具可能就不再能满足我们的需求了。
对于Android平台来说,高通的Adreno分析工具可以对不用的测试机进行详细的性能分析。英伟达提供了NVPerfHUD工具来帮助我们得到几乎所有需要的性能分析数据,例如,每个draw call的GPU时间,每个shader花费的cycle数目等。
对于iOS平台来说,Unity内置的分析器可以得到整个场景花费的GPU时间。PowerVRAM的PVRUniSCo shader 分析器也可以给给出一个大致的性能评估。Xcode中的OpenGL ES Driver Instruments 可以给出一些宏观上的性能信息,例如,设备利用率、渲染器利用率等。但相对于Android平台,对iOS的性能分析更加困难(工具较少)。而且PowerVR芯片采用了基于瓦片的延迟渲染器,因此,想要得到每个draw call 花费的GPU时间是几乎不可能的。这时,一些宏观上的统计数据可能更有参考价值。
http://docs.unity3d.com/Manual/MobileProfiling.html)

减少draw call 数目
最常见的优化技术大概就是**批处理(batching)**了。批处理的实现原理就是为了减少每一帧需要的draw call 数目。为了把一个对象渲染到屏幕上,CPU需要检查哪些光源影响了该物体,绑定shader并设置它的参数,再把渲染命令发送给GPU。当场景中包含了大量对象时,这些操作就会非常耗时。一个极端的例子是,如果我们需要渲染一个三角形的网格。在这两种情况下,GPU的性能消耗其实本并没有多大的区别,但CPU的draw call数目就会成为性能瓶颈。因此,批处理的思想很简单,就是在每次面对draw call时尽可能多地处理多个物体。
使用同一个材质的物体,才可以进行一起处理。这是因为,对于使用同一个材质的物体,他们之间的不同仅仅在于顶点数据的差别。我们可以把这些顶点数据合并在一起,再一起发送给GPU,就可以完成一次批处理。
Unity中支持两种批处理方式:一种是动态批处理,另一种是静态批处理。对于动态批处理来说,优点是一切处理都是Unity自动完成的,不需要我们自己做任何操作,而且物体是可以移动的,但缺点是,限制很多,可能一不小心就会破坏了这种机制,导致Unity无法动态批处理一些使用了相同材质的物体。而对于静态批处理来说,它的优点是自由度很高,限制很少;但缺点是可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了(即便在脚本中尝试改变物体的位置也是无效的)。

16.4.1 动态批处理
如果场景中有一些模型共享了同一个材质并满足一些条件,Unity就可以自动把他们进行批处理,从而只需要花费一个draw call就可以渲染所有的模型。动态批处理的基本原理是,每一帧把可以进行批处理的模型网格进行合并,再把合并后模型数据传递给GPU,然后使用同一个材质对其渲染。除了实现方便,动态批处理的另一个好处是,经过批处理的物体仍然可以移动,这是由于在处理每帧时Unity都会重新合并一次网格。
限制条件:
·能够进行动态批处理的网格的顶点属性规模要小于900。例如,如果shader中需要使用顶点位置、法线和纹理坐标这三个顶点属性,那么要想让模型能够被动态批处理,它的顶点数目不超过300。
·一般来说,所有对象都需要使用同一个缩放尺度。一个例外情况是,如果所有的物体都使用了不同的非统一缩放,那么他们也是可以被动态批处理的。但在Unity5中,这中对模型缩放的限制已经不存在了。
·使用光照纹理(lightmap)的物体需要小心处理。但在Uinty5中,这些物体需要额外的渲染参数,例如,在光照纹理上的索引、偏移量和缩放信息等。因此,为了让这些物体可以被动态批处理,我们需要保证它的指向关照纹理中的同一位置。
·多Pass和shader会中断批处理。在前向渲染中,我们有时需要使用额外的Pass来为模型添加更多的光照效果,但这样一来模型就不会被动态批处理了。
在Scene_16_3_1场景中,我们给出了这样一个场景。场景中包含了三个立方体,他们使用同一个材质,同时还包含了一个使用其他材质的是球体。场景中还包含了一个平行光,但我们关闭了它的阴影效果,以避免阴影计算对批处理数目的影响。这样一个场景的渲染统计数据如下:
从图中看出,共需要两个批处理。一个批处理用于绘制经过动态批处理合并后的三个立方体网格;另一批处理用于绘制球体。我们可以从Save by batching看出批处理帮我们节省了两个draw call。
现在,我们再向场景中添加一个点光源,并调整它的位置使它可以照亮场景中的四个物体。由于场景中的物体都使用了多个Pass的shader,因此,点光源会对他们产生光照影响。下图给出了添加电光源后的渲染统计数据。
可以看出,渲染一帧所需的批处理数目增大了,而Save by batching的数目变为了0。这是因为,使用了多个Pass的shader在需要应用多个光照的情况下,破坏了动态批处理的机制,导致Unity不能对这些物体进行动态批处理。而由于平行光和点光源需要对四个物体分别产生影响,因此,需要2×4个批处理操作。需要注意的是,只有物体在点光源的影响范围内,Unity才会调用额外的Pass来处理它。因此,如果场景中点光源距离物体很远,那么它们仍然会被动态批处理的。
动态批处理的限制条件比较多,例如很多时候,我们的模型数据往往会超过900的顶点属性性质。这种时候依赖动态批处理来减少draw call显然已经不能满足我们的需求了。这时,我们可以使用Unity的静态批处理技术。

16.4.2 静态批处理
Unity提供了另一种批处理方式,即静态批处理。相比于动态批处理来说,静态批处理适用于任何大小的几何模型。它的实现原理是,只在运行开始阶段,把需要进行静态批处理的模型合并到一个新的网格结构中,这意味着这些模型不可以在运行时刻被移动。但由于他只需要进行一次合并操作,因此,比动态批处理更加高效。静态批处理的另一个缺点在于,它往往需要占用更多的内存来存储合并后的几何结构。这是因为,如果在静态批处理前一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发送个GPU。如果这类使用使用同一网格的对象很多,那么这就会成为一个性能瓶颈。例如,如果在一个使用了1000个相同树模型的森林中使用静态批处理,那么,就会多使用1000倍的内存,这会造成严重的内存影响。这种时候,解决方案要么忍受这种牺牲内存换取性能的方法,要么不要使用静态批处理,而是用动态批处理技术(但要小心控制模型的顶点属性数目),或者自己编写批处理的方法。
在场景Scene_16_3_2中,我们给出来了一个测试静态批处理的场景。渲染统计数据如下:
可以看出,尽管3个Teapot模型使用了相同的材质,但他们仍然没有被动态批处理。这还是因为,Teapo模型包含的顶点数目是393,而它们使用你的shsder中需要使用四个顶点属相(顶点位置、法线位置、切线方向和纹理坐标 225左右),超过了动态批处理中限定的900限制。此时,要想减少draw call就需要使用静态批处理。
静态批处理的实现非成长简单,只需要把物体面板上的Static复选框勾选上即可(实际上我们只需要候选Batching Static即可)。
这时,我们再观察渲染统计窗口中的批处理数目,还是没有变化。但是让程序运行起来,变化就出现了。
现在的批处理数目变成了2,而Save by batching数目也显示为2。此时,如果在运行时查看每个模型使用的网格,会发现他们都变成为了一个名为Combined Mesh(root:scene)的东西,如下图所示。这个网格是Unity合并了所有被标识为“Static”的物体的结果,在我们的例子里,就是三个Teapot和一个立方体。
我们会发现“4 submeshes”的字样,也就是说明,这个合并后的网格其实包含了4个子网格,及场景中的4个对象。对于合并后的网格,Unity会判读其中使用同一个材质的子网格,然后对它们进行批处理。
在内部实现上,Unity首先把这些静态物体变换到世界空间下,然后为它们构建一个更大的顶点和索引缓存。对于使用了同一材质的物体,Unity只需要调用一个draw call就可以绘制全部物体。而对于使用了不同材质的物体,静态批处理同样可以提升渲染性能。尽管这些物体仍然需要调用多个draw call,但静态批处理可以减少这些draw call之间的状态切换,而这些切换往往是费时的操作。而合并后的网格结构中我们还可以发现,尽管三个Teapot对象使用了同一网格,但合并后却变成了三个独立网格。而且,我们可以从Unity的分析器中观察到在应用静态批处理前后VBO total(Vertex Buffer Object,顶点缓冲对象)的变化,这次的数目变大了。这正是因为静态批处理会占用更多内存的缘故,如果一些物体分享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品。
如果场景中包含了除了平行光以外的其他光源,并且在shader中定义了额外的Pass来处理他们,这些额外的Pass部分是不会被批处理的。但是,处理平行光的Base Pass部分仍然会被静态批处理,因此,我们仍然可以节省两个draw call。

16.4.3 共享材质
总而言之,批处理都要求模型之间需要共享同一个材质。但不同的模型之间总会需要有不同的渲染属性,例如,使用不同的纹理、颜色等。这时,我们需要一些策略来尽可能的合并材质。
如果两个材质之间只有使用的纹理不同,我们可以把这些纹理合并到一张更大的纹理中,这张更大的纹理被称为是一张图集(atlas)。一旦使用了同一张纹理,我们就可以使用同一个材质,再使用不同的坐标对纹理采样即可。
但有时,除了纹理不同外,不同的物体在材质上还有一些微小的参数变化,例如,颜色不同、某些浮点属性不同。但是,不管是动态批处理还是静态批处理,它们的前提都是要使用同一个材质。是同一个,而不是同一种Shader的材质,也就是说他们指向的材质必须似乎同一个实体。这意味着,只要我们调整了参数,就会影响到所有使用这个材质的对象。想要微小的调整时我们可以使用网格的顶点数据(最常见的就是顶点颜色数据)来存储这些参数。
前面说过,经过批处理后的物体会被处理成更大的VBO发送给GPU,VBO中的数据可以作为输入传递给顶点着色器,因此,我们可以巧妙地对VBO中的数据进行控制,从而达到不同效果的目的。一个例子是,森林场景中所有的树使用了同一种材质,我们希望他们可以通过批处理来减少draw call,但不同树的颜色可能不同。这时,我们可以利用网格的顶点的颜色数据来调整。
需要注意的是,如果我们需要在脚本中访问共享材质,应给使用Renderer.sharedMaterial来保证修改的是和其他物体共享的材质,但这意味着修改会应用到所有使用该材质的物体上。另一个类似的API是Renderer.material。如果使用Renderer.material来修改材质,Unity会创建一个该材质的复制品,从而破坏批处理在该物体上的应用,这可能不是我们希望看到的。
16.4.4 批处理的注意事项
·尽可能选择静态批处理,但得时刻小心对内存的消耗,并且记住经过静态批处理的物件不可以再被移动。
·如果无法进行静态批处理,而要使用动态批处理的话,那么请小心上面提到的各种条件限制。例如,尽可能让这样的物体少并且尽可能让这些物体包含少量的顶点属性和顶点数目。
·对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。
·对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”。
除了上述提示外,在使用批处理时还有一些需要注意的地方。由于批处理需要把多个模型变换到世界空间下在合并它们,因此,如果shader中存在一些基于模型空间下的坐标的运算,那么往往会得到错误的结果。一个解决方法是,在shader中使用DisableBatching标签来强制使用该Shader的材质不会被批处理。另一个注意事项是,使用半透明材质的物体通常需要使用严格的从后往前的绘制顺序来保证透明混合的正确性。对于这些物体,Unity会首先保证它们的绘制顺序,再尝试对它们进行批处理。这意味着,当绘制顺序无法满足时,批处理无法在这些物体上被成功应用。
以上是在Unity5.2中实现的对一些渲染部分的批处理。

代码交流 2021