一一Michael A. Jackson
在进行程序优化的时候,人们经常会引用英国的计算机科学家Michael A. Jackson 在1988 年的优化准则。Jackson 是想借此强调,对问题认识不清以及过度优化往往会让事情变得更加复杂,产生更多的程序错误。
然而, 如果我们在游戏开发过程中从来都没有考虑优化,那么结果往往是惨不忍睹的。一个正确的做法是, 从一开始就把优化当成是游戏设计中的一部分。正在阅读本书的读者,有可能是移动游戏的开发者。和PC 相比,移动设备上的GPU 有着完全不同的架构设计,它能使用的带宽、功能和其他资源都非常有限。这要求我们需要时刻把优化谨记在心,才可以避免等到项目完成时才发现游戏根本无法在移动设备上流畅运行的结果。
在本章,我们将会阐述一些Unity 中常见的优化技术。这些优化技术都是和渲染相关的,例如, 使用批处理、LOD (Level of Detail )技术等。在本章最后的扩展阅读部分, 我们给出一些非常有价值的参考资料, 在那里读者可以学习到更多真实项目中的优化技术。
在开始学习之前, 我们希望读者能够理解, 游戏优化不仅是程序员的工作,更需要美工人员在游戏的美术上进行一定的权衡,例如, 避免使用全屏的屏幕特效, 避免使用计算复杂的shader, 减少透明混合造成的overdraw 等。也就是说,这是由程序员和美工人员等各个部分人员共同参与的工作。
16.1 移动平台的特点
例如,为了尽可能移除那些隐藏的表面,减少overdraw (即一个像素被绘制多次), PowerVR芯片(通常用于iOS 设备和某些Android 设备〉使用了基于瓦片的延迟渲染(Tiled-based Deferred Rendering, TBDR)架构, 把所有的渲染图像装入一个个瓦片(tile )中,再由硬件找到可见的片元,而只有这些可见片元才会执行片元着色器。另一些基于瓦片的GPU 架构,如Adreno (高通的芯片)和Mali ( ARM 的芯片〉则会使用Early-Z 或相似的技术进行一个低精度的的深度检测,来剔除那些不需要渲染的片元。还有一些GPU,如Tegra (英伟达的芯片〉,则使用了传统的架构设计, 因此在这些设备上,overdraw 更可能造成性能的瓶颈。
由于这些芯片架构造成的不同, 一些游戏往往需要针对不同的芯片发布不同的版本,以便对每个芯片进行更有针对性的优化。尤其是在Android 平台上,不同设备使用的硬件,如图形芯片、屏幕分辨率等,大相径庭,这对图形优化提出了更高的挑战。相比与Android 平台, iOS 平台的硬件条件则相对统一。读者可以在Unity 手册的iOS 硬件指南
16.2 影响性能的因素
据此,我们可以把造成游戏性能瓶颈的主要原因分成以下几个方面。
(1 ) CPU。
(2) GPU。
● 顶点处理。
▶ 过多的顶点。
▶ 过多的逐顶点计算。
●片元处理。
▶ 过多的片元〈既可能是由于分辨率造成的,也可能是由于overdraw 造成的〉。
▶ 过多的逐片元计算。
(3)带宽。
● 使用了尺寸很大且未压缩的纹理。
●分辨率过高的帧缓存。
对于CPU 来说,限制它的主要是每一帧中draw call 的数目。我们曾在2.2 节和2.4.3 节中介绍过draw call 的相关概念和原理。简单来说,就是CPU 在每次通知GPU 进行渲染之前,都需要提前准备好顶点数据(如位置、法线、颜色、纹理坐标等〉,然后调用一系列API 把它们放到GPU 可以访问到的指定位置,最后,调用一个绘制命令,来告诉GPU ,“嘿,我把东西都准备好了,你赶紧出来干活(渲染〉吧!”。而调用绘制命令的时候,就会产生一个 draw call。过多的draw call 会造成CPU 的性能瓶颈,这是因为每次调用draw call 时, CPU 往往都需要改变很多渲染状态的设置,而这些操作是非常耗时的。如果一帧中需要的draw call 数目过多的话,就会导致CPU 把大部分时间都花费在提交draw call 的工作上面了。当然,其他原因也可能造成CPU 瓶颈,例如物理、布料模拟、蒙皮、粒子模拟等,这些都是计算量很大的操作,但由于本书主要讨论Shader 方面的相关技术,因此,这些内容不在本书的讨论范围内。
在了解了上面基本的内容后,本章后续章节会涉及的优化技术有。
(1) CPU 优化。
●使用批处理技术减少draw call 数目。
(2) GPU 优化。
●减少需要处理的顶点数目。
▶ 优化几何体。
▶使用模型的LOD (Level ofDetail )技术。
▶控制绘制顺序。
▶警惕透明物体。
▶减少实时光照。
▶使用Shader 的LOD (Level of Detail) 技术。
▶代码方面的优化。
(3)节省内存带宽。
●减少纹理大小。
●利用分辨率缩放。
在开始优化之前,我们首先需要知道是哪个步骤造成了性能瓶颈。而这可以利用Unity 提供的一些渲染分析工具来实现。
16.3 Unity 中的渲染分析工具
需要注意的是,在不同的目标平台上,这些工具中显示的数据也会发生变化。
16.3.1 认识Unity 5 的渲染统计窗口
Unity 5 提供了一个全新的窗口,即渲染统计窗口(Rendering Statistics Window )来显示当前游戏的各个渲染统计变量,我们可以通过在Game 视图右上方的菜单中单击Stats 按钮来打开它,如图16.1 所示。从图16.1 中可以看出, 渲染统计窗口主要包含了3 个方面的信息: 音频(Audio )、图像( Graphics )和网络(Network)。我们这里只关注第二个方面,即图像相关的渲染统计结果。16.3.2 性能分析器的渲染区域
我们可以通过单击Window -> Profiler 来打开Unity 的性能分析器(Profiler) 。 性能分析器中的渲染区域(Rendering Area )提供了更多关于渲染的统计信息,图16.2 给出了对图16.1 中场景的渲染分析结果。结合渲染统计窗口和性能分析器,我们可以查看与渲染相关的绝大多数重要的数据。一个值得注意的现象是,性能分析器给出的draw call 数目和批处理数目、Pass 数目并不相等,并且看起来好像要大于我们估算的数目,这是因为Unity 在背后需要进行很多工作,例如,初始化各个缓存、为阴影更新深度纹理和阴影映射纹理等,因此需要花费比“预期”更多的draw call。一个好消息是,Unity 5 引入了一个新的工具来帮助我们查看每一个draw call 的工作,这个工具就是帧调试器。
16.3.3 再谈帧调试器
在Unity 的渲染统计窗口、分析器和帧调试器这3 个利器的帮助下,我们可以获得很多有用的优化信息。但是,很多诸如渲染时间这样的数据是基于当前的开发平台得到的,而非真机上的结果。事实上, Unity 正在和硬件生产商合作,来首先让使用英伟达图睿
16.3.4 其他性能分析工具
对于移动平台上的游戏来说,我们更希望得到在真机上运行游戏时的性能数据。这时,Unity 目前提供的各个工具可能就不再能满足我们的需求了。对于Android 平台来说,高通的Adreno 分析工具可以对不同的测试机进行详细的性能分析。英伟达提供了NVPerfHUD 工具来帮助我们得到几乎所有需要的性能分析数据,例如,每个draw call 的GPU 时间,每个shader 花费的cycle 数目等。
一些其他的性能分析工具可以在Unity 的官方手册( http://docs.unity3d.com/Manual/MobileProfiling.html )中找到。当找到了性能瓶颈后,我们就可以针对这些方面进行特定的优化。
16.4 减少draw call 数目
Unity 中支持两种批处理方式:一种是动态批处理,另一种是静态批处理。对于动态批处理来说,优点是一切处理都是Unity 自动完成的,不需要我们自己做任何操作,而且物体是可以移动的,但缺点是,限制很多,可能一不小心就会破坏了这种机制,导致Unity 无法动态批处理一些使用了相同材质的物体。而对于静态批处理来说,它的优点是自由度很高,限制很少;但缺点是可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了(即便在脚本中尝试改变物体的位置也是无效的〉。
16.4.1 动态批处理
如果场景中有一些模型共享了同一个材质并满足一些条件, Unity 就可以自动把它们进行批处理,从而只需要花费一个draw call 就可以渲染所有的模型。动态批处理的基本原理是,每一帧把可以进行批处理的模型网格进行合并,再把合并后模型数据传递给GPU然后使用同一个材质对其渲染。除了实现方便,动态批处理的另一个好处是,经过批处理的物体仍然可以移动,这是由于在处理每帧时Unity 都会重新合并一次网格。虽然Unity 的动态批处理不需要我们进行任何额外工作,但只有满足条件的模型和材质才可以被动态批处理。需要注意的是,随着Unity 版本的变化,这些条件也有一些改变。在本节中,我们给出一些主要的条件限制。
- 能够进行动态批处理的网格的顶点属性规模要小于900 。例如,如果shader 中需要使用顶点位置、法线和纹理坐标这3 个顶点属性,那么要想让模型能够被动态批处理,它的顶点数目不能超过300。需要注意的是,这个数字在未来有可能会发生变化,因此不要依赖这个数据。
- 一般来说,所有对象都需要使用同一个缩放尺度(可以是(1, 1, 1 )、( 1, 2, 3)、(1.5, 1.4,1.3)等,但必须都一样〉。一个例外情况是,如果所有的物体都使用了不同的非统一缩放,那么它们也是可以被动态批处理的。但在Unity 5 中,这种对模型缩放的限制已经不存在了。
- 使用光照纹理(lightmap )的物体需要小心处理。这些物体需要额外的渲染参数,例如,在光照纹理上的索引、偏移量和缩放信息等。因此,为了让这些物体可以被动态批处理,我们需要保证它们指向光照纹理中的同一个位置。
- 多Pass 的shader 会中断批处理。在前向渲染中,我们有时需要使用额外的Pass 来为模型添加更多的光照效果,但这样一来模型就不会被动态批处理了。
现在,我们再向场景中添加一个点光源,并调整它的位置使它可以照亮场景中的4 个物体。由于场景中的物体都使用了多个Pass 的shader,因此,点光源会对它们产生光照影响。图16.5 给出了添加点光源后的渲染统计数据。
动态批处理的限制条件比较多,例如很多时候,我们的模型数据往往会超过900 的顶点属性限制。这种时候依赖动态批处理来减少draw call 显然已经不能够满足我们的需求了。这时,我们可以使用Unity 的静态批处理技术。
16.4.2 静态批处理
在本书资源的Scene_16_3_2 场景中,我们给出了一个测试静态批处理的场景。场景中包含了3 个Teapot 模型, 它们使用同一个材质,同时还包含了一个使用不同材质的立方体。场景中还包含了一个平行光,但我们关闭了它的阴影效果,以避免阴影计算对批处理数目的影响。在运行前,这样一个场景的渲染统计数据如图16.6 所示。
静态批处理的实现非常简单, 只需要把物体面板上的Static 复选框句选上即可(实际上我们只需要勾选Batching Static 即可),如图16.7 所示。
对于合并后的网格, Unity 会判断其中使用同一个材质的子网格,然后对它们进行批处理。
16.4.3 共享材质
从之前的内容可以看出,无论是动态批处理还是静态批处理,都要求模型之间需要共享同一个材质。但不同的模型之间总会需要有不同的渲染属性,例如,使用不同的纹理、颜色等。这时,我们需要一些策略来尽可能地合并材质。如果两个材质之间只有使用的纹理不同,我们可以把这些纹理合并到一张更大的纹理中,这张更大的纹理被称为是一张图集( atlas )。一旦使用了同一张纹理,我们就可以使用同一个材质,再使用不同的采样坐标对纹理采样即可。
但有时,除了纹理不同外,不同的物体在材质上还有一些微小的参数变化,例如,颜色不同、某些浮点属性不同。但是,不管是动态批处理还是静态批处理,它们的前提都是要使用同一个材质。是同一个,而不是使用了同一种Shader 的材质,也就是说它们指向的材质必须是同一个实体。这意味着,只要我们调整了参数,就会影响到所有使用这个材质的对象。那么想要微小的调整怎么办呢?一种常用的方法就是使用网格的顶点数据(最常见的就是顶点颜色数据〉来存储这些参数。
前面说过,经过批处理后的物体会被处理成更大的VBO 发送给GPU, VBO 中的数据可以作为输入传递给顶点着色器,因此,我们可以巧妙地对VBO 中的数据进行控制,从而达到不同效果的目的。一个例子是,森林场景中所有的树使用了同一种材质,我们希望它们可以通过批处理来减少draw call ,但不同树的颜色可能不同。这时,我们可以利用网格的顶点的颜色数据来调整。
需要注意的是,如果我们需要在脚本中访问共享材质,应该使用Renderer.sharedMaterial 来保证修改的是和其他物体共享的材质,但这意味着修改会应用到所有使用该材质的物体上。另一个类似的API 是Renderer.material ,如果使用Renderer.material 来修改材质, Unity 会创建一个该材质的复制品,从而破坏批处理在该物体上的应用,这可能并不是我们希望看到的。
16.4.4 批处理的注意事项
- 尽可能选择静态批处理,但得时刻小心对内存的消耗,并且记住经过静态批处理的物体不可以再被移动。
- 如果无法进行静态批处理,而要使用动态批处理的话,那么请小心上面提到的各种条件限制。例如,尽可能让这样的物体少并且尽可能让这些物体包含少量的顶点属性和顶点数目。
- 对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。
- 对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”。
尽管在Unity 5.2 中,只实现了对一些渲染部分的批处理。而诸如渲染摄像机的深度纹理等部分,还没有实现批处理。但我们相信,在后续的Unity 版本中,批处理会应用到越来越多的渲染部分中。
16.5 减少需要处理的顶点数目
16.5.1 优化几何体
3D 游戏制作通常都是由模型制作开始的。而在建模时,有一条规则我们需要记住:尽可能减少模型中三角面片的数目, 一些对于模型没有影响、或是肉眼非常难察觉到区别的顶点都要尽可能去掉。为了尽可能减少模型中的顶点数目,美工人员往往需要优化网格结构。在很多三维建模软件中,都有相应的优化选项,可以自动优化网格结构。在Unity 的渲染统计窗口中,我们可以查看到渲染当前帧需要的三角面片数目和顶点数目。需要注意的是, Unity 中显示的数目往往要多于建模软件里显示的顶点数,通常Unity 中显示的数目要大很多。谁才是对的呢?其实,这是因为在不同的角度上计算的,都有各自的道理,但我们真正应该关心的是Unity 里显示的数目。
我们在这里简单解释一下造成这种不同的原因。三维软件更多地是站在我们人类的角度理解顶点的,即组成几何体的每一个点就是一个单独的点。而Unity 是站在GPU 的角度上去计算顶点数的。在GPU 看来,有时需要把一个顶点拆分成两个或更多的顶点。这种将顶点一分为多的原因主要有两个:一个是为了分离纹理坐标(uv splits), 另一个是为了产生平滑的边界(smoothing splits).它们的本质,其实都是因为对于GPU 来说,顶点的每一个属性和顶点之间必须是一对一的关系。而分离纹理坐标,是因为建模时一个顶点的纹理坐标有多个。例如,对于一个立方体,它的6 个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。对于GPU 来说,这是不可理解的,因此,它必须把这个顶点拆分成多个具有不同纹理坐标的顶点。而平滑边界也是类似的,不同的是,此时一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条硬边
对于GPU 来说,它本质上只关心有多少个顶点。因此,尽可能减少顶点的数目其实才是我们真正需要关心的事情。因此,最后一条几何体优化建议就是:移除不必要的硬边以及纹理衔接,避免边界平滑和纹理分离。
16.5.2 模型的LOO 技术
另一个减少顶点数目的方法是使用LOD (Level of Detail) 技术。这种技术的原理是,当一个物体离摄像机很远时,模型上的很多细节是无法被察觉到的。因此,LOD 允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能。在Unity 中,我们可以使用LOD Group 组件来为一个物体构建一个LOD。我们需要为同一个对象准备多个包含不同细节程序的模型,然后把它们赋给LOD Group 组件中的不同等级, Unity就会自动判断当前位置上需要使用哪个等级的模型。
16.5.3 遮挡剔除技术
我们最后要介绍的顶点优化策略就是遮挡剔除( Occlusion culling)技术。遮挡剔除可以用来消除那些在其他物件后面看不到的物件,这意味着资源不会浪费在计算那些看不到的顶点上,进而提升性能。我们需要把遮挡剔除和摄像机的视锥体剔除( Frustum Culling )区分开来。视锥体剔除只会剔除掉那些不在摄像机的视野范围内的对象,但不会判断视野中是否有物体被其他物体挡住。而遮挡剔除会使用一个虚拟的摄像机来遍历场景,从而构建一个潜在可见的对象集合的层级结构。
在运行时刻,每个摄像机将会使用这个数据来识别哪些物体是可见的,而哪些被其他物体挡住不可见。使用遮挡剔除技术,不仅可以减少处理的顶点数目,还可以减少overdraw,提高游戏性能。
要在Unity 中使用遮挡剔除技术,我们需要进行一系列额外的处理工作。具体步骤可以参见Unity 手册的相关内容
模型的LOD 技术和遮挡剔除技术可以同时减少CPU 和GPU 的负荷。CPU 可以提交更少的draw call ,而GPU 需要处理的顶点和片元数目也减少了。
16.6 减少需要处理的片元数目
Unity 还提供了查看overdraw 的视图,我们可以在Scene 视图左上方的下拉菜单中选中Overdraw 即可。实际上,这里的视图只是提供了查看物体相互遮挡的层数,并不是真正的最终屏幕绘制的overdraw 。也就是说,可以理解为它显示的是,如果没有使用任何深度测试和其他优化策略时的overdraw 。这种视图是通过把所有对象都渲染成一个透明的轮廓,通过查看透明颜色的累计程度,来判断物体之间的遮挡。当然,我们可以使用一些措施来防止这种最坏情况的出现。
16.6.1 控制绘制顺序
为了最大限度地避免overdraw, 一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少overdraw。这是因为,在后面绘制的物体由于无法通过深度测试,因此,就不会再进行后面的渲染处理。在Unity 中,那些渲染队列数目小于2 500 (如“Background" "Geometry ”和“Alpha Test")的对象都被认为是不透明( opaque )的物体,这些物体总体上是从前往后绘制的,而使用其他的队列(如“ Transparent "“ Overlay"等)的物体,则是从后往前绘制的。这意味着,我们可以尽可能地把物体的队列设置为不透明物体的渲染队列,而尽量避免使用半透明队列。
而且,我们还可以充分利用Unity 的渲染队列来控制绘制顺序。例如,在第一人称射击游戏中,对于游戏中的主要人物角色来说,他们使用的shader 往往比较复杂,但是,由于他们通常会挡住屏幕的很大一部分区域,因此我们可以先绘制它们(使用更小的渲染队列〉。而对于一些敌方角色,它们通常会出现在各种掩体后面,因此,我们可以在所有常规的不透明物体后面渲染它们(使用更大的渲染队列〉。而对于天空盒子来说, 它几乎覆盖了所有的像素,而且我们知道它本远会出现在所有物体的后面,因此, 它的队列可以设置为“ Geometry+ 1 ”。这样,就可以保证不会因为它而造成overdraw 。
这些排序的思想往往可以节省掉很多渲染时间。
16.6.2 时刻警惕透明物体
对于半透明对象来说,由于它们没有开启深度写入,因此,如果要得到正确的渲染效果,就必须从后往前渲染。这意味着,半透明物体几乎一定会造成overdraw。如果我们不注意这一点,在一些机器上可能会造成严重的性能下降。例如,对于GUI 对象来说,它们大多被设置成了半透明,如果屏幕中GUI 占据的比例太多,而主摄像机又没有进行调整而是投影整个屏幕,那么GUI就会造成大量overdraw 。因此,如果场景中包含了大面积的半透明对象,或者有很多层相互覆盖的半透明对象(即使它们每个的面积可能都不大〉,或者是透明的粒子效果, 在移动设备上也会造成大量的overdraw 。这是应该尽量避免的。
对于上述GUI 的这种情况,我们可以尽量减少窗口中GUI 所占的面积。如果实在无能为力,我们可以把GUI的绘制和三维场景的绘制交给不同的摄像机, 而其中负责三维场景的摄像机的视角范围尽量不要和GUI的相互重叠。当然,这样会对游戏的美观度产生一定影响,因此,我们可以在代码中对机器的性能进行判断,例如,首先关闭一些耗费性能的功能,如果发现这个机器表现非常良好,再尝试开启一些特效功能。
在移动平台上, 透明度测试也会影响游戏性能。虽然透明度测试没有关闭深度测试, 但由于它的实现使用了discard 或clip 操作, 而这些操作会导致一些硬件的优化策略失效。例如, 我们之前讲过PowerVR 使用的基于瓦片的延迟渲染技术, 为了减少overdraw 它会在调用片元着色器前就判断哪些瓦片被真正渲染的。但是,由于透明度测试在片元着色器中使用了discard 函数改变了片元是否会被渲染的结果,因此, GPU 就无法使用上述的优化策略了。也就是说,只要在执行了所有的片元着色器后, GPU 才知道哪些片元会被真正渲染到屏幕上, 这样, 原先那些可以减少overdraw 的优化就都无效了。这种时候, 使用透明度混合的性能往往比使用透明度测试更好。
16.6.3 减少实时光照和阴影
实时光照对于移动平台是一种非常昂贵的操作。如果场景中包含了过多的点光源,并且使用了多个Pass 的Shader,那么很有可能会造成性能下降。例如,一个场景里如果包含了3 个逐像素的点光源,而且使用了逐像素的Shader,那么很有可能将draw call 数目( CPU 的瓶颈〉提高3倍,同时也会增加overdraw ( GPU 的瓶颈)。这是因为, 对于逐像素的光源来说, 被这些光源照亮的物体需要被再渲染一次。更糟糕的是,无论是静态批处理还是动态批处理,对于这种额外的处理逐像素光源的Pass 都无法进行批处理,也就是说,它们会中断批处理。当然,游戏场景还是需要光照才能得到出色的画面效果。我们看到很多成功的移动平台的游戏,它们的画面效果看起来好像包含了很多光源,但其实这都是骗人的。这些游戏往往使用了烘焙技术,把光照提前烘焙到一张光照纹理(lightmap )中, 然后在运行时刻只需要根据纹理采样得到光照结果即可。另一个模拟光源的方法是使用God Ray 。场景中很多小型光源的效果都是靠这种方法模拟的。它们一般并不是真的光源, 很多情况是通过透明纹理模拟得到的。更多信息可以参见本章的扩展阅读部分。在移动平台上, 一个物体使用的逐像素光源数目应该小于1(不包括平行光) 。如果一定要使用更多的实时光,可以选择用逐顶点光照来代替。
在游戏《ShadowGun》中,游戏角色看起来使用了非常复杂高级的光照计算, 但这实际上是优化后的结果。开发者们把复杂的光照计算存储到一张查找纹理(lookup texture,也被称为查找表, lookup table, LUT )中。然后在运行时刻,我们只需要使用光源方向、视角方向、法线方向等参数,对LUT 采样得到光照结果即可。使用这样的查找纹理,不仅可以让我们使用更出色的光照模型,例如,更加复杂的BRDF 模型,还可以利用查找纹理的大小来进一步优化性能,例如,主要角色可以使用更大分辨率的LUT,而一些NPC 就使用较小的LUT 。《ShadowGun》的开发者开发了一个LUT 烘倍工具,来帮助美工人员快速调整光照模型,并把结果存储到LUT 中。
实时阴影同样是一个非常消耗性能的效果。不仅是CPU 需要提交更多的draw call, GPU 也需要进行更多的处理。因此,我们应该尽量减少实时阴影,例如,使用烘焙把静态物体的阴影信息存储到光照纹理中,而只对场景中的动态物体使用适当的实时阴影。
16.7 节省带宽
16.7.1 减少纹理大小
之前提到过,使用纹理图集可以帮助我们减少draw call 的数目,而这些纹理的大小同样是一个需要考虑的问题。需要注意的是,所有纹理的长宽比最好是正方形,而且长宽值最好是2 的整数幕。这是因为有很多优化策略只有在这种时候才可以发挥最大效用。除此之外,我们还应该尽可能使用多级渐远纹理技术( mipmapping )和纹理压缩。在Unity 中,我们可以通过纹理导入面板来查看纹理的各个导入属性。通过把纹理类型设置为Advanced,就可以自定义许多选项,例如,是否生成多级渐远纹理( mipmaps ),如图16.12 所示。
16.7.2 利用分辨率缩放
过高的屏幕分辨率也是造成性能下降的原因之一,尤其是对于很多低端手机,除了分辨率高其他硬件条件并不尽如人意,而这恰恰是游戏性能的两个瓶颈: 过大的屏幕分辨率和糟糕的GPU。因此,我们可能需要对于特定机器进行分辨率的放缩。当然,这样可能会造成游戏效果的下降,但性能和画面之间永远是个需要权衡的话题。在Unity 中设置屏幕分辨率可以直接调用Screen.SetResolution。实际使用中可能会遇到一些情况,雨松MOMO 有一篇文章
16.8 减少计算复杂度
16.8.1 Shader 的LOD 技术
和16.5.2 节提到的模型的 LOD 技术类似, Shader 的 LOD 技术可以控制使用的Shader 等级。它的原理是,只有Shader 的 LOD 值小于某个设定的值,这个Shader 才会被使用,而使用了那些超过设定值的Shader 的物体将不会被渲染。我们通常会在SubShader 中使用类似下面的语句来指明该shader 的LOD 值:
SubShader {
Tags {”RenderType”=”Opaque”}
LOO 200
我们也可以在Unity Shader 的导入面板上看到该Shader 使用的LOD 值。在默认情况下,允许的LOD 等级是无限大的。这意味着,任何被当前显卡支持的Shader 都可以被使用。但是,在某些情况下我们可能需要去掉一些使用了复杂计算的Shader 渲染。这时,我们可以使用 Shader.maximumLOD 或 Shader.globalMaximumLOD 来设置允许的最大LOD 值。
16.8.2 代码方面的优化
在实现游戏效果时,我们可以选择在哪里进行某些特定的运算。通常来讲, 游戏需要计算的对象、顶点和像素的数目排序是
对象数 < 顶点数<像素数。因此, 我们应该尽可能地把计算放在每个对象或逐顶点上。例如,在第13 章实现高斯模糊和边缘检测时,我们把采样坐标的计算放在了顶点着色器中,这样的做法远好于把它们放在片元着色器中。
而在具体的代码编写上,不同的硬件甚至需要不同的处理。因此,一些普遍的规则在某些硬件上可能并不成立。更不幸的是,通常Shader 代码的优化并不那么直观,尤其是一些平台上缺少相关的分析器, 例如iOS 平台。尽管如此,在本节我们还是会给出一些被认为是普遍成立的优化策略,但读者如果发现在某些设备上性能反而有所下降的话,这并不奇怪。
首先第一点是,尽可能使用低精度的浮点值进行运算。最高精度的float/highp 适用于存储诸如顶点坐标等变量, 但它的计算速度是最慢的,我们应该尽量避免在片元着色器中使用这种精度进行计算。而half/mediump 适用于一些标量、纹理坐标等变量,它的计算速度大约是float 的两倍。而 fixed/lowp 适用于绝大多数颜色变量和归一化后的方向矢量,在进行一些对精度要求不高的计算时,我们应该尽量使用这种精度的变量。它的计算速度大约是float 的4 倍,但要避免对这些低精度变量进行频繁的swizzle 操作(如color.xwxw )。还需要注意的是,我们应当尽量避免在不同精度之间的转换,这有可能会造成一定的性能下降。
对于绝大多数GPU 来说,在使用插值寄存器把数据从顶点着色器传递给下一个阶段时,我们应该使用尽可能少的插值变量。例如,如果需要对两个纹理坐标进行插值,我们通常会把它们打包在同一个float4 类型的变量中,两个纹理坐标分别对应了xy 分量和 zw 分量。然而,对于PowerVR平台来说,这种插值变量是非常廉价的,直接把不同的纹理坐标存储在不同的插值变量中,有时反而性能更好。尤其是, 如果在PowerVR 上使用类似 tex2D(_MainTex, uv.zw)这样的语句来进行纹理采样, GPU 就无法进行一些纹理的预读取, 因为它会认为这些纹理采样是需要依赖其他数据的。因此,如果我们特别关心游戏在PowerVR 上的性能, 就不应该把两个纹理坐标打包在同一个四维变量中。
尽可能不要使用全屏的屏幕后处理效果。如果美术风格实在是需要使用类似Bloom、热扰动这样的屏幕特效,我们应该尽量使用 fixed/lowp 进行低精度运算(纹理坐标除外,可以使用 half/mediump )。那些高精度的运算可以使用查找表(LUT)或者转移到顶点着色器中进行处理。除此之外, 尽量把多个特效合并到一个Shader 中。例如,我们可以把颜色校正和添加噪声等屏幕特效在Bloom 特效的最后一个Pass 中进行合成。还有一个方法就是使用16.8.3 节中介绍的缩放思想, 来选择性地开启特效。
还有一些读者经常会听到的代码优化规则。
- 尽可能不要使用分支语句和循环语句。
- 尽可能避免使用类似sin 、tan、pow、log 等较为复杂的数学运算。我们可以使用查找表来作为替代。
- 尽可能不要使用discard 操作,因为这会影响硬件的某些优化。
16.8.3 根据硬件条件进行缩放
以开启一些更“养眼”的效果,比如使用更高的分辨率,开启屏幕后处理特效,开启粒子效果等。
16.9 扩展阅读
在SIGGRAPH 2011 上, Unity 进行了一个关于移动平台上 Shader 优化的演讲
除了手册和演讲资料外,成功的移动平台中的游戏同样是非常好的学习资料。《ShadowGun》是由MadFinger 在2011 年发布的一款移动平台的第三人称射击游戏, 使用的开发工具正是Unity 。在Unite 2011 上,该游戏的开发者给出了《ShadowGun》中使用的渲染和优化技术,读者可以在Youtube 上面找到这个视频。更难能可贵的是,在2012 年, 《ShadowGun》的开发者放出了示例
场景,来让更多的开发者学习如何优化移动平台上的shader。另一个非常好的游戏优化实例是Unity 自带的项目《Angry Bots》, 读者可以直接在Unity 资源商店下载到完整的项目源代码。