使用LOD(Level of detail)技术
[size=11.8181819915771px][size=13.63636302948px]LOD技术有点类似于Mipmap技术,不同的是,LOD是对模型建立了一个模型金字塔,根据摄像机距离对象的远近,选择使用不同精度的模型。它的好处是可以在适当的时候大量减少需要绘制的顶点数目。它的缺点同样是需要占用更多的内存,而且如果没有调整好距离的话,可能会造成模拟的突变。
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px][size=13.63636302948px]在Unity中,可以通过LOD Group来实现LOD技术:
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px][size=13.63636302948px]通过上面的LOD Group面板,我们可以选择需要控制的模型以及距离设置。下面展示了油桶从一个完整网格到简化网格,最后完全被剔除的例子:
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px][size=13.63636302948px]
[size=11.8181819915771px]
使用遮挡剔除(Occlusion culling)技术
[size=11.8181819915771px][size=13.63636302948px]遮挡剔除是用来消除躲在其他物件后面看不到的物件,这代表资源不会浪费在计算那些看不到的顶点上,进而提升性能。关于遮挡剔除,Unity Taiwan有一个系列文章大家可以看看(需翻墙):
[size=11.8181819915771px]Unity 4.3 关于Occlusion Culling : 基本篇
[size=11.8181819915771px]Unity 4.3 关于Occlusion Culling : 最佳做法
[size=11.8181819915771px]Unity 4.3 关于Occlusion Culling : 错误诊断
[size=11.8181819915771px]
[size=11.8181819915771px]具体的内容大家可以自行查找。
现在我们来谈[size=13.63636302948px]像素优化。
[size=13.63636302948px]
[size=13.63636302948px]
像素优化
[size=11.8181819915771px]
[size=11.8181819915771px]
[size=11.8181819915771px]像素优化的重点在于减少overdraw。之前提过,overdraw指的就是一个像素被绘制了多次。关键在于控制绘制顺序。
[size=11.8181819915771px]
[size=11.8181819915771px]Unity还提供了查看overdraw的视图,在Scene视图的Render Mode->Overdraw。当然这里的视图只是提供了查看物体遮挡的层数关系,并不是真正的最终屏幕绘制的overdraw。也就是说,可以理解为它显示的是如果没有使用任何深度检验时的overdraw。这种视图是通过把所有对象都渲染成一个透明的轮廓,通过查看透明颜色的累计程度,来判断物体的遮挡。
[size=11.8181819915771px]
[size=11.8181819915771px] ![]()
[size=11.8181819915771px]
[size=13.63636302948px]上图图,红色越是浓重的地方表示overdraw越严重,而且这里涉及的都是透明物体,这意味着性能将会受到很大影响。
[size=11.8181819915771px]
控制绘制顺序[size=11.8181819915771px]
[size=11.8181819915771px]需要控制绘制顺序,主要原因是为了最大限度的避免overdraws,也就是同一个位置的像素可以需要被绘制多变。在PC上,资源无限,为了得到最准确的渲染结果,绘制顺序可能是从后往前绘制不透明物体,然后再绘制透明物体进行混合。但在移动平台上,这种会造成大量overdraw的方式显然是不适合的,我们应该尽量从前往后绘制。从前往后绘制之所以可以减少overdraw,都是因为深度检验的功劳。
[size=11.8181819915771px]
[size=11.8181819915771px]在Unity中,那些Shader中被设置为“Geometry” 队列的对象总是从前往后绘制的,而其他固定队列(如“Transparent”“Overla”等)的物体,则都是从后往前绘制的。这意味这,我们可以尽量把物体的队列设置为“Geometry” 。
[size=11.8181819915771px]
[size=11.8181819915771px]而且,我们还可以充分利用Unity的队列来控制绘制顺序。例如,对于天空盒子来说,它几乎覆盖了所有的像素,而且我们知道它永远会在所有物体的后面,因此它的队列可以设置为“Geometry+1”。这样,就可以保证不会因为它而造成overdraws。
[size=11.8181819915771px]
[size=11.8181819915771px]
时刻警惕透明物体
[size=11.8181819915771px]而对于透明对象,由于它本身的特性(可以看之前关于Alpha Test和Alpha Blending的一篇文章)决定如果要得到正确的渲染效果,就必须从后往前渲染(这里不讨论使用深度的方法),而且抛弃了深度检验。这意味着,透明物体几乎一定会造成overdraws。如果我们不注意这一点,在一些机器上可能会造成严重的性能下面。例如,对于GUI对象来说,它们大多被设置成了半透明,如果屏幕中GUI占据的比例太多,而主摄像机又没有进行调整而是投影整个屏幕,那么GUI就会造成屏幕的大量overdraws。
[size=11.8181819915771px]
[size=11.8181819915771px]
[size=11.8181819915771px]因此,如果场景中大面积的透明对象,或者有很多层覆盖的多层透明对象(即便它们每个的面积可以都不大),或者是透明的粒子效果,在移动设备上也会造成大量的overdraws。这是应该尽量避免的。
[size=11.8181819915771px]
[size=11.8181819915771px][size=13.63636302948px]对于上述GUI的这种情况,我们可以尽量减少窗口中GUI所占的面积。如果实在无能为力,我们可以把GUI绘制和三维场景的绘制交给不同的摄像机,而其中负责三维场景的摄像机的视角范围尽量不要和GUI重叠。对于其他情况,只能说,尽可能少用。当然这样会对游戏的美观度产生一定影响,因此我们可以在代码中对机器的性能进行判断,例如首先关闭所有的耗费性能的功能,如果发现这个机器表现非常良好,再尝试开启一些特效功能。
[size=11.8181819915771px][size=13.63636302948px]
减少实时光照[size=11.8181819915771px]
[size=11.8181819915771px]实时光照对于移动平台是个非常昂贵的操作。如果只有一个平行光还好,但如果场景中包含了太多光源并且使用了很多多Passes的shader,那么很有可能会造成性能下降。而且在有些机器上,还要面临shader失效的风险。例如,一个场景里如果包含了三个逐像素的点光源,而且使用了逐像素的shader,那么很有可能将Draw Calls提高了三倍,同时也会增加overdraws。这是因为,对于逐像素的光源来说,被这些光源照亮的物体要被再渲染一次。更糟糕的是,无论是动态批处理还是动态批处理(其实文档中只提到了对动态批处理的影响,但不知道为什么实验结果对静态批处理也没有用),对于这种逐像素的pass都无法进行批处理,也就是说,它们会中断批处理。
[size=11.8181819915771px]
[size=11.8181819915771px]例如,下面的场景中,四个物体都被标识成了“Static”,它们使用的shader都是自带的Bumped Diffuse。而所有的点光源都被标识成了“Important”,即是逐像素光。可以看到,运行后的Draw Calls是23,而非3。这是因为,只有“Forward Base”的Pass时发生了静态批处理(这里的动态批处理由于多Pass已经完全失效了),节省了一个Draw Calls,而后面的“Forward Add” Pass,每一次渲染都是一个单独的Draw Call(而且可以看到Tris和Verts数目也增加了):
[size=11.8181819915771px]
[size=11.8181819915771px] ![]()
[size=11.8181819915771px]
[size=11.8181819915771px]
[size=11.8181819915771px]这点正如文档中说的:The draw calls for “additional per-pixel lights” will not be batched。原因我不是很清楚,这里有一个讨论,但里面的意思说是对静态批处理没有影响,和我这里的结果不一样,知道原因的麻烦给我留言,非常感谢。我也在Unity论坛里提问里。
[size=11.8181819915771px]
[size=11.8181819915771px]我们看到很多成功的移动游戏,它们的画面效果看起来好像包含了很多光源,但其实这都是骗人的。
[size=11.8181819915771px]
[size=11.8181819915771px]
使用Lightmaps[size=11.8181819915771px]
[size=11.8181819915771px]Lightmaps的很常见的一种优化策略。它主要用于场景中整体的光照效果。这种技术主要是提前把场景中的光照信息存储在一张光照纹理中,然后在运行时刻只需要根据纹理采样得到光照信息即可。
[size=11.8181819915771px]
[size=11.8181819915771px]当然与之配合的还有Light Probes技术。风宇冲有一个系列文章讲过,但是时间比较久远,但教程我相信网上有很多。
[size=11.8181819915771px]
使用God Rays[size=11.8181819915771px]
[size=11.8181819915771px]场景中很多小型光源效果都是靠这种方法模拟的。它们一般并不是真的光源产生的,很多情况是通过透明纹理进行模拟。具体可以参见之前的文章。
CPU优化
减少Draw Calls
批处理(Batching)
这方面的优化教程想必是最多的了。最常见的就是通过批处理(Batching)了。从名字上来理解,就是一块处理多个物体的意思。那么什么样的物体可以一起处理呢?答案就是使用同一个材质的物体。这是因此,对于使用同一个材质的物体,它们之间的不同仅仅在于顶点数据的差别,即使用的网格不同而已。我们可以把这些顶点数据合并在一起,再一起发送给GPU,就可以完成一次批处理。
Unity中有两种批处理方式:一种是动态批处理,一种是静态批处理。对于动态批处理来说,好消息是一切处理都是自动的,不需要我们自己做任何操作,而且物体是可以移动的,但坏消息是,限制很多,可能一不小心我们就会破坏了这种机制,导致Unity无法批处理一些使用了相同材质的物体。对于静态批处理来说,好消息是自由度很高,限制很少,坏消息是可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了。
首先来说动态批处理。Unity进行动态批处理的条件是,物体使用同一个材质并且满足一些特定条件。Unity总是在不知不觉中就为我们做了动态批处理。例如下面的场景:
这个场景共包含了4个物体,其中两个箱子使用了同一个材质。可以看到,它的Draw Calls现在是3,并且显示Save by batching是1,也就是说,Unity靠Batching为我们节省了1个Draw Call。下面,我们来把其中一个箱子的大小随便改动一下,看看会发生什么:
可以发现,Draw Calls变成了4,Save by batching的数目也变成了0。这是为什么呢?它们明明还是只使用了一个材质啊。原因就是前面提到的那些需要满足的其他条件。动态批处理虽然自动得令人感动,但它对模型的要求很多:
- 顶点属性的最大限制为900,而且未来有可能会变。不要依赖这个数据。
- 一般来说,那么所有对象都必须需要使用同一个缩放尺度(可以是(1, 1, 1)、(1, 2, 3)、(1.5, 1.4, 1.3)等等,但必须都一样)。但如果是非统一缩放(即每个维度的缩放尺度不一样,例如(1, 2, 1)),那么如果所有的物体都使用不同的非统一缩放也是可以批处理的。这个要求很怪异,为什么批处理会和缩放有关呢?这和Unity背后的技术有关系,有兴趣的可以自行谷歌,比如这里。
- 使用lightmap的物体不会批处理。多passes的shader会中断批处理。接受实时阴影的物体也不会批处理。
上述除了最常见的由于缩放导致破坏批处理的情况,还有就是顶点属性的限制。例如,在上面的场景中我们添加之前未优化后的箱子模型:
可以看到Draw Calls一下子变成了5。这是因为新添加的箱子模型中,包含了474个顶点,而它使用的顶点属性有位置、UV坐标、法线等信息,使用的总和超过了900。
动态批处理的条件这么多,一不小心它就不干了,因此Unity提供了另一个方法,静态批处理。接着上面的例子,我们保持修改后的缩放,但把四个物体的“Static Flag”勾选上:
点击Static后面的三角下拉框,我们会看到其实这一步设置了很多东西,这里我们想要的只是“Batching static”一项。这时我们再看Draw Calls,恩,还是没有变化。但是不要急,我们点击运行,变化出现了:
Draw Calls又回到了3,并且显示Save by batching是1。这就是得利于静态批处理。而且,如果我们在运行时刻查看模型的网格,会发现它们都变成了一个名为Combined Mesh (roo: scene)的东西。这个网格是Unity合并了所有标识为“Static”的物体的结果,在我们的例子里,就是四个物体:
你可以要问了,这四个对象明明不是都使用了一个材质,为什么可以合并成一个呢?如果你仔细观察上图的话,会发现里面标明了“4 submeshes”,也就是说,这个合并后的网格其实包含了4个子网格,也就是我们的四个对象。对于合并后后的网格,Unity会判断其中使用同一个材质的子网格,然后对它们进行批处理。
但是,我们再细心点可以发现,我们的箱子使用的其实是同一个网格,但合并后却变成了两个。而且,我们观察运行前后Stats窗口中的“VBO total”,它的大小由241.6KB变成了286.2KB,变大了!还记得静态批处理的缺点吗?就是可能会占用更多的内存。文档中是这样写的:
“Using static batching will require additional memory for storing the combined geometry. If several objects shared the same geometry before static batching, then a copy of geometry will be created for each object, either in the Editor or at runtime. This might not always be a good idea - sometimes you will have to sacrifice rendering performance by avoiding static batching for some objects to keep a smaller memory footprint. For example, marking trees as static in a dense forest level can have serious memory impact.”
也就是说,如果在静态批处理前有一些物体共享了相同的网格(例如这里的两个箱子),那么每一个物体都会有一个该网格的复制品,即一个网格会变成多个网格被发送给GPU。在上面的例子看来,就是VBO的大小明显增大了。如果这类使用同一网格的对象很多,那么这就是一个问题了,这种时候我们可能需要避免使用静态批处理,这意味着牺牲一定的渲染性能。例如,如果在一个使用了1000个重复树模型的森林中使用静态批处理,那么结果就会产生1000倍的内存,这会造成严重的内存影响。这种时候,解决方法要么我们可以忍受这种牺牲内存换取性能的方法,要么不要使用静态批处理,而使用动态批处理(前提是大家使用相同的缩放大小,或者大家都使用不同的非统一缩放大小),或者自己编写批处理的方法。当然,我认为最好的还是使用动态批处理来解决。
有一些小提示可以使用:
- [size=13.63636302948px]尽可能选择静态批处理,但得时刻小心对内存的消耗。
[size=13.63636302948px]如果无法进行静态批处理,而要使用动态批处理的话,那么请小心上面提到的各种注意事项。例如:
- [size=13.63636302948px]尽可能让这样的物体少并且尽可能让这些物体包含少量的顶点属性。
- [size=13.63636302948px]不要使用统一缩放,或者都使用不同的非统一缩放。
[size=13.63636302948px]对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。
[size=13.63636302948px]对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”。
一些讨论:
How static batching works
Static batching use a ton of memory?
unity3d draw call optimization
合并纹理(Atlas)[size=13.63636302948px]
[size=13.63636302948px]虽然批处理是个很好的方式,但很容易就打破它的规定。例如,场景中的物体都使用Diffuse材质,但它们可能会使用不同的纹理。因此,尽可能把多张小纹理合并到一张大纹理(Atlas)中是一个好主意。
利用网格的顶点数据[size=13.63636302948px]
[size=13.63636302948px]但有时,除了纹理不同外,还有对于不同的物体,它们在材质上还有一些微小的参数变化,例如颜色不同、某些浮点参数不同。但铁定律是,不管是动态批处理还是静态批处理,它们的前提都是要使用同一个材质。是同一个,而不是同一种,也就是说它们指向的材质必须是同一个实体。这意味着,只要我们调整了参数,就会影响到所有使用这个材质的对象。那么想要微小的调整怎么办呢?由于Unity中的规定非常死,那么我们只好想些“歪门邪道”,其中一种就是使用网格的顶点数据(最常见的就是顶点颜色数据)。
[size=13.63636302948px]
[size=13.63636302948px]前面说过,经过批处理后的物体会被处理成一个VBO发送给GPU,VBO中的数据可以作为输入传递给Vertex Shader,因此我们可以巧妙地对VBO中的数据进行控制,从而达到不同效果的目的。一个例子是,还是之前的森林,所有的树使用了同一种材质,我们希望它们可以通过动态批处理来实现,但不同树的颜色可能不同。这时我么可以利用网格的顶点数据来调整。具体方法,可以参见后面会写的一篇文章。
[size=13.63636302948px]
[size=13.63636302948px]但这种方法的缺点就是会需要更多的内存来存储这些用于调整参数用的顶点数据。没办法,永远没有绝对完美的方法。
[size=13.63636302948px]
|