移动平台的特点
为了尽可能移除那些隐藏的表面,减少overdraw (即一个像素被绘制多次), PowerVR芯片(通常用于iOS 设备和某些Android 设备〉使用了基于瓦片的延迟渲染(Tiled-based Deferred Rendering, TBDR)架构, 把所有的渲染图像装入一个个瓦片(tile),再由硬件找可见的片元。只有这些可见片元才会执行片元着色器。
另外一些基于瓦片的GPU 架构,如Adreno (高通的芯片)和Mali ( ARM 的芯片〉则会使用Early-Z 或相似的技术进行一个低精度的的深度检测,来剔除那些不需要渲染的片元。
还有一些GPU,如Tegra (英伟达的芯片〉,则使用了传统的架构设计, 因此在这些设备上,overdraw 更可能造成性能的瓶颈。
由于这些芯片架构造成的不同, 一些游戏往往需要针对不同的芯片发布不同的版本,以便对每个芯片进行更有针对性的优化。尤其是在Android 平台上,不同设备使用的硬件,如图形芯片、屏幕分辨率等,大相径庭,这对图形优化提出了更高的挑战。相比与Android 平台, iOS 平台的硬件条件则相对统一。读者可以在Unity 手册的iOS 硬件指南( http://docs.unity3d.corn/Manual/iphone-Hardware.html )
影响性能的因素
需要使用两种方式计算资源: CPU 和GPU。它们会互相合作,来让我们的游戏可以在预期的帧率和分辨率下工作。其中, CPU 主要负责保证帧率, GPU 主要负责分辨率相关的一些处理。
(1 ) CPU。
●过多的draw call 。
●复杂的脚本或者物理模拟。
(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 方面的相关技术,因此,这些内容不在本书的讨论范围内。
而对于GPU 来说,它负责整个渲染流水线。它从处理CPU 传递过来的模型数据开始,进行顶点着色器、片元着色器等一系列工作,最后输出屏幕上的每个像素。因此, GPU 的性能瓶颈和需要处理的顶点数目、屏幕分辨率、显存等因素有关。而相关的优化策略可以从减少处理的数据规模(包括顶点数目和片元数目〉、减少运算复杂度等方面入手。
在了解了上面基本的内容后,本章后续章节会涉及的优化技术有。
(1) CPU 优化。
●使用批处理技术减少draw call 数目。
(2) GPU 优化。
●减少需要处理的顶点数目。
▶ 优化几何体。
▶使用模型的LOD (Level ofDetail )技术。
▶使用遮挡剔除( Occlusion Culling )技术。
●减少需要处理的片元数目。
▶控制绘制顺序。
▶警惕透明物体。
▶减少实时光照。
●减少计算复杂度。
▶使用Shader 的LOD (Level of Detail) 技术。
▶代码方面的优化。
(3)节省内存带宽。
●减少纹理大小。
●利用分辨率缩放。
在开始优化之前,我们首先需要知道是哪个步骤造成了性能瓶颈。而这可以利用Unity 提供的一些渲染分析工具来实现。
Unity 中的渲染分析工具
Unity 内置了一些工具,来帮助我们方便地查看和渲染相关的各个统计数据。这些数据可以帮助我们分析游戏渲染性能,从而更有针对性地进行优化。在Unity 5 中,这些工具包括了渲染统计窗口( Rendering Statistics Window )、性能分析器( Profiler ) ,以及帧调试器( Frame Debugger )。
需要注意的是,在不同的目标平台上,这些工具中显示的数据也会发生变化。
Unity 5 提供了一个全新的窗口,即渲染统计窗口(Rendering Statistics Window )来显示当前游戏的各个渲染统计变量,我们可以通过在Game 视图右上方的菜单中单击Stats 按钮来打开它,如图16.1 所示。从图16.1 中可以看出, 渲染统计窗口主要包含了3 个方面的信息: 音频(Audio )、图像( Graphics )和网络(Network)。我们这里只关注第二个方面,即图像相关的渲染统计结果。
各个信息的含义
性能分析器的渲染区域 profiler
性能分析器显示了绝大部分在渲染统计窗口中提供的信息,例如,绿线显示了批处理数目、蓝线显示了Pass 数目等,同时还给出了许多其他非常有用的信息,例如, draw call 数目、动态批处理/静态批处理的数目、渲染纹理的数目和内存占用等。
结合渲染统计窗口和性能分析器,我们可以查看与渲染相关的绝大多数重要的数据。一个值得注意的现象是,性能分析器给出的draw call 数目和批处理数目、Pass 数目并不相等,并且看起来好像要大于我们估算的数目,这是因为Unity 在背后需要进行很多工作,例如,初始化各个缓存、为阴影更新深度纹理和阴影映射纹理等,因此需要花费比“预期”更多的draw call。一个好消息是,Unity 5 引入了一个新的工具来帮助我们查看每一个draw call 的工作,这个工具就是帧调试器。
帧调试器
之前的章节中多次看到帧调试器(Frame Debugger) 的应用,例如5.5.3 节中解释了如何使用帧调试器来对Shader 进行调试。我们可以通过Window -> Frame Debugger 来打开它。在这个窗口中,我们可以清楚地看到每一个draw call 的工作和结果,如图16.3 所示。
帧调试器的调试面板上显示了渲染这一帧所需要的所有的渲染事件,在本例中,事件数目为14,而其中包含了10 个draw call 事件〈其他渲染事件多为清空缓存等〉。通过单击面板上的每个事件,我们可以在Game 视图查看该事件的绘制结果,同时渲染统计面板上的数据也会显示成截止到当前事件为止的各个渲染统计数据。以本例为例〈场景如图16.1 所示〉,要渲染一帧共需要花费10 个draw call,其中4 个draw call 用于更新深度纹理(对应UpdateDepthTexture), 4 个draw call 用于渲染平行光的阴影映射纹理,1 个draw call 用于绘制动态批处理后的3 个立方体模型, 1 个draw call 用于绘制球体。
在Unity 的渲染统计窗口、分析器和帧调试器这3 个利器的帮助下,我们可以获得很多有用的优化信息。但是,很多诸如渲染时间这样的数据是基于当前的开发平台得到的,而非真机上的结果。事实上, Unity 正在和硬件生产商合作,来首先让使用英伟达图睿
( Tegra)的设备可以出现在Unity 的性能分析器中。我们有理由相信,在后续的Unity 版本中,直接在Unity 中对移动设备进行性能分析不再是梦想。然而,在这个梦想实现之前,我们仍然需要一些外部的性能分析工具的帮助。
其他性能分析工具
对于移动平台上的游戏来说,我们更希望得到在真机上运行游戏时的性能数据。这时,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 时间是几乎不可能的。这时,一些宏观上的统计数据可能更有参考价值。
一些其他的性能分析工具可以在Unity 的官方手册( http://docs.unity3d.com/Manual/MobileProfiling.html )中找到。当找到了性能瓶颈后,我们就可以针对这些方面进行特定的优化。
减少DrawCall数目
读者最常看到的优化技术大概就是批处理( batching )了。批处理的实现原理就是为了减少每一帧需要的draw call 数目。为了把一个对象渲染到屏幕上, CPU 需要检查哪些光源影响了该物体,绑定shader 并设置它的参数,再把渲染命令发送给GPU。当场景中包含了大量对象时,这些操作就会非常耗时。一个极端的例子是,如果我们需要渲染一千个三角形,把它们按一千个单独的网格进行渲染所花费的时间要远远大于渲染一个包含了一千个三角形的网格。在这两种情况下,GPU 的性能消耗其实并没有多大的区别,但CPU 的draw call 数目就会成为性能瓶颈。因此,批处理的思想很简单,就是在每次面对draw call 时尽可能多地处理多个物体。我们已经在2.2 节和 2.4.3 节中详细地讲述了draw call 和批处理之间的联系,本节旨在介绍如何在Unity 中利用批处理技术来优化渲染。
那么,什么样的物体可以一起处理呢?答案就是使用同一个材质的物体。这是因为,对于使用同一个材质的物体,它们之间的不同仅仅在于顶点数据的差别。我们可以把这些顶点数据合并在一起,再一起发送给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 来为模型添加更多的光照效果,但这样一来模型就不会被动态批处理了。
静态批处理
Unity 提供了另一种批处理方式, 即静态批处理。相比于动态批处理来说,静态批处理适用任何大小的几何模型。它的实现原理是, 只在运行开始阶段, 把需要进行静态批处理的模型合并到一个新的网格结构中, 注意味着这些模型不可以在运行时刻被移动。但由于它只需要进行一次合并操作, 因此, 比动态批处理更加高效。静态批处理的另一个缺点在于,它往往需要占用更多的内存来存储合并后的几何结构。这是因为,如果在静态批处理前一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发送给GPU 。如果这类使用同一网格的对象很多, 那么这就会成为一个性能瓶颈了。例如,如果在一个使用了1 000 个相同树模型的森林中使用静态批处理,那么,就会多使用1 000 倍的内存, 这会造成严重的内存影响。这种时候, 解决方法要么忍受这种牺牲内存换取性能的方法,要么不要使用静态批处理,而使用动态批处理技术(但要小心控制模型的顶点属性数目),或者自己编写批处理的方法。
对于合并后的网格, Unity 会判断其中使用同一个材质的子网格,然后对它们进行批处理。
在内部实现上,Unity 首先把这些静态物体变换到世界空间下,然后为它们构建一个更大的顶点和索引缓存。对于使用了同一材质的物体,Unity 只需要调用一个draw call 就可以绘制全部物体。而对于使用了不同材质的物体,静态批处理同样可以提升渲染性能。尽管这些物体仍然需要调用多个draw call ,但静态批处理可以减少这些draw call 之间的状态切换,而这些切换往往是费时的操作。从合并后的网格结构中我们还可以发现,尽管3 个Teapot 对象使用了同一个网格,但合并后却变成了3 个独立网格。
而且,我们可以从Unity 的分析器中观察到在应用静态批处理前后VBO total的变化,从图16.10 所示中可以看出,VBO ( Vertex Buffer Object,顶点缓冲对象〉的数目变大了。这正是因为静态批处理会占用更多内存的缘故,正如本节一开头所讲,静态批处理需要占用更多的内存来存储合并后的几何结构,如果一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品。
共享材质
从之前的内容可以看出,无论是动态批处理还是静态批处理,都要求模型之间需要共享同一个材质。但不同的模型之间总会需要有不同的渲染属性,例如,使用不同的纹理、颜色等。这时,我们需要一些策略来尽可能地合并材质。
如果两个材质之间只有使用的纹理不同,我们可以把这些纹理合并到一张更大的纹理中,这张更大的纹理被称为是一张图集( atlas )。一旦使用了同一张纹理,我们就可以使用同一个材质,再使用不同的采样坐标对纹理采样即可。
但有时,除了纹理不同外,不同的物体在材质上还有一些微小的参数变化,例如,颜色不同、某些浮点属性不同。但是,不管是动态批处理还是静态批处理,它们的前提都是要使用同一个材质。是同一个,而不是使用了同一种Shader 的材质,也就是说它们指向的材质必须是同一个实体。这意味着,只要我们调整了参数,就会影响到所有使用这个材质的对象。那么想要微小的调整怎么办呢?一种常用的方法就是使用网格的顶点数据(最常见的就是顶点颜色数据〉来存储这些参数。
前面说过,经过批处理后的物体会被处理成更大的VBO 发送给GPU, VBO 中的数据可以作为输入传递给顶点着色器,因此,我们可以巧妙地对VBO 中的数据进行控制,从而达到不同效果的目的。一个例子是,森林场景中所有的树使用了同一种材质,我们希望它们可以通过批处理来减少draw call ,但不同树的颜色可能不同。这时,我们可以利用网格的顶点的颜色数据来调整。
需要注意的是,如果我们需要在脚本中访问共享材质,应该使用Renderer.sharedMaterial 来保证修改的是和其他物体共享的材质,但这意味着修改会应用到所有使用该材质的物体上。另一个类似的API 是Renderer.material ,如果使用Renderer.material 来修改材质, Unity 会创建一个该材质的复制品,从而破坏批处理在该物体上的应用,这可能并不是我们希望看到的。
批处理的注意事项
-尽可能选择静态批处理,但得时刻小心对内存的消耗,并且记住经过静态批处理的物体不可以再被移动。
- 如果无法进行静态批处理,而要使用动态批处理的话,那么请小心上面提到的各种条件限制。例如,尽可能让这样的物体少并且尽可能让这些物体包含少量的顶点属性和顶点数目。
- 对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。
- 对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”
如果shader 中存在一些基于模型空间下的坐标的运算,那么往往会得到错误的结果。一个解决方法是,在shader 中使用DisableBatching 标签来强制使用该Shader 的材质不会被批处理。另一个注意事项是,使用半透明材质的物体通常需要使用严格的从后往前的绘制顺序来保证透明混合的正确性。对于这些物体,Unity 会首先保证它们的绘制顺序,再尝试对它们进行批处理。这意味着,当绘制顺序无法满足时,批处理无法在这些物体上被成功应用。
在Unity 5.2 中,只实现了对一些渲染部分的批处理。而诸如渲染摄像机的深度纹理等部分,还没有实现批处理。
减少需要处理的顶点数量
优化几何体
3D 游戏制作通常都是由模型制作开始的。而在建模时,有一条规则我们需要记住:尽可能减少模型中三角面片的数目, 一些对于模型没有影响、或是肉眼非常难察觉到区别的顶点都要尽可能去掉。为了尽可能减少模型中的顶点数目,美工人员往往需要优化网格结构。在很多三维建模软件中,都有相应的优化选项,可以自动优化网格结构。
Unity 是站在GPU 的角度上去计算顶点数的。在GPU 看来,有时需要把一个顶点拆分成两个或更多的顶点。这种将顶点一分为多的原因主要有两个:一个是为了分离纹理坐标(uv splits), 另一个是为了产生平滑的边界(smoothing splits).它们的本质,其实都是因为对于GPU 来说,顶点的每一个属性和顶点之间必须是一对一的关系。而分离纹理坐标,是因为建模时一个顶点的纹理坐标有多个。例如,对于一个立方体,它的6 个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。对于GPU 来说,这是不可理解的,因此,它必须把这个顶点拆分成多个具有不同纹理坐标的顶点。而平滑边界也是类似的,不同的是,此时一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条硬边
( hard edge )还是一条平滑边(smooth edge )。
LOD技术
在Unity 中,我们可以使用LOD Group 组件来为一个物体构建一个LOD。我们需要为同一个对象准备多个包含不同细节程序的模型,然后把它们赋给LOD Group 组件中的不同等级, Unity就会自动判断当前位置上需要使用哪个等级的模型
遮挡剔除技术
顶点优化策略就是遮挡剔除( Occlusion culling)技术。遮挡剔除可以用来消除那些在其他物件后面看不到的物件,这意味着资源不会浪费在计算那些看不到的顶点上,进而提升性能。
我们需要把遮挡剔除和摄像机的视锥体剔除( Frustum Culling )区分开来。视锥体剔除只会剔除掉那些不在摄像机的视野范围内的对象,但不会判断视野中是否有物体被其他物体挡住。而遮挡剔除会使用一个虚拟的摄像机来遍历场景,从而构建一个潜在可见的对象集合的层级结构。
要在Unity 中使用遮挡剔除技术,我们需要进行一系列额外的处理工作。具体步骤可以参见Unity 手册的相关内容(http://docs.unity3d.com/Manual/OcclusionCulling.html ),本书不再赘述。
模型的LOD 技术和遮挡剔除技术可以同时减少CPU 和GPU 的负荷。CPU 可以提交更少的draw call ,而GPU 需要处理的顶点和片元数目也减少了。
减少片元数目
另一个造成GPU 瓶颈的是需要处理过多的片元。这部分优化的重点在于减少overdraw 。简单来说, overdraw 指的就是同一个像素被绘制了多次。
。实际上,这里的视图只是提供了查看物体相互遮挡的层数,并不是真正的最终屏幕绘制的overdraw 。也就是说,可以理解为它显示的是,如果没有使用任何深度测试和其他优化策略时的overdraw 。这种视图是通过把所有对象都渲染成一个透明的轮廓,通过查看透明颜色的累计程度,来判断物体之间的遮挡。当然,我们可以使用一些措施来防止这种最坏情况的出现。
控制绘制顺序
为了最大限度地避免overdraw, 一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少overdraw。这是因为,在后面绘制的物体由于无法通过深度测试,因此,就不会再进行后面的渲染处理。
在Unity 中,那些渲染队列数目小于2 500 (如“Background" "Geometry ”和“Alpha Test")的对象都被认为是不透明( opaque )的物体,这些物体总体上是从前往后绘制的,而使用其他的队列(如“ Transparent "“ Overlay"等)的物体,则是从后往前绘制的。这意味着,我们可以尽可能地把物体的队列设置为不透明物体的渲染队列,而尽量避免使用半透明队列。
而且,我们还可以充分利用Unity 的渲染队列来控制绘制顺序。例如,在第一人称射击游戏中,对于游戏中的主要人物角色来说,他们使用的shader 往往比较复杂,但是,由于他们通常会挡住屏幕的很大一部分区域,因此我们可以先绘制它们(使用更小的渲染队列〉。而对于一些敌方角色,它们通常会出现在各种掩体后面,因此,我们可以在所有常规的不透明物体后面渲染它们(使用更大的渲染队列〉。而对于天空盒子来说, 它几乎覆盖了所有的像素,而且我们知道它本远会出现在所有物体的后面,因此, 它的队列可以设置为“ Geometry+ 1 ”。这样,就可以保证不会因为它而造成overdraw 。
这些排序的思想往往可以节省掉很多渲染时间。
透明物体
半透明物体几乎一定会造成overdraw。
在移动平台上, 透明度测试也会影响游戏性能。虽然透明度测试没有关闭深度测试, 但由于它的实现使用了discard 或clip 操作, 而这些操作会导致一些硬件的优化策略失效。例如, 我们之前讲过PowerVR 使用的基于瓦片的延迟渲染技术, 为了减少overdraw 它会在调用片元着色器前就判断哪些瓦片被真正渲染的。但是,由于透明度测试在片元着色器中使用了discard 函数改变了片元是否会被渲染的结果,因此, GPU 就无法使用上述的优化策略了。也就是说,只要在执行了所有的片元着色器后, GPU 才知道哪些片元会被真正渲染到屏幕上, 这样, 原先那些可以减少overdraw 的优化就都无效了。这种时候,** 使用透明度混合的性能往往比使用透明度测试更好**。
实时光照和阴影
实时光照对于移动平台是一种非常昂贵的操作。如果场景中包含了过多的点光源,并且使用了多个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 也需要进行更多的处理。因此,我们应该尽量减少实时阴影,例如,使用烘焙把静态物体的阴影信息存储到光照纹理中,而只对场景中的动态物体使用适当的实时阴影。
节省宽带
主要是纹理的压缩问题
mipmapping
当勾选了Generate Mip Maps选项后,会生成多张不同大小纹理,类似LOD 根据项目实际需求选择。UI不需要。定视角也不需要。
纹理选择 合适的压缩格式。不同的GPU 架构有它自己的纹理压缩格式,例如, PowerVRAM 的PVRTC 格式、Tegra 的DXT 格式、Adreno 的ATC 格式。所幸的是, Unity 可以根据不同的设备选择不同的压缩格式,而我们只需要把纹理压缩格式设置为自动压缩即可。但是, GUI 类型的纹理同样是个例外,一些时候由于对画质的要求,我们不希望对这些纹理进行压缩。
利用分辨率 缩放
过高的屏幕分辨率也是造成性能下降的原因之一,尤其是对于很多低端手机,除了分辨率高其他硬件条件并不尽如人意,而这恰恰是游戏性能的两个瓶颈: 过大的屏幕分辨率和糟糕的GPU。因此,我们可能需要对于特定机器进行分辨率的放缩。当然,这样可能会造成游戏效果的下降,但性能和画面之间永远是个需要权衡的话题。
在Unity 中设置屏幕分辨率可以直接调用Screen.SetResolution。实际使用中可能会遇到一些情况,雨松MOMO 有一篇文章
( http://www.xuanyusong.com/archives/3205 )详细讲解了如何使用这种技术,读者可参考。
减少计算复杂度
shader的LOD技术
模型的 LOD 技术类似, Shader 的 LOD 技术可以控制使用的Shader 等级。它的原理是,只有Shader 的 LOD 值小于某个设定的值,这个Shader 才会被使用,而使用了那些超过设定值的Shader 的物体将不会被渲染。
我们通常会在SubShader 中使用类似下面的语句来指明该shader 的LOD 值:
SubShader {
Tags {”RenderType”=”Opaque”}
LOD 200
我们也可以在Unity Shader 的导入面板上看到该Shader 使用的LOD 值。在默认情况下,允许的LOD 等级是无限大的。这意味着,任何被当前显卡支持的Shader 都可以被使用。但是,在某些情况下我们可能需要去掉一些使用了复杂计算的Shader 渲染。这时,我们可以使用** Shader.maximumLOD 或 Shader.globalMaximumLOD 来设置允许的最大LOD 值**。
Unity 内置的Shader 使用了不同的LOD 值,例如,Diffuse 的LOD 为200 ,而Bumped Specular 的 LOD 为400 。
然后,就可以在脚本代码中指定一次全局Shader.globalMaximumLOD值,该值是LOD所执行的最大值,也就是说所有shader中的subshader的LOD值小于该全局值,该subshader才被执行。当然,也可以单独设置Shader.maximumLOD控制个别shader.
就是可以单独控制 某个shader的渲染的subshader 也可以全局控制 所有的subshader 到底调用哪个。
代码的优化
游戏需要计算的对象、顶点和像素的数目排序是
对象数<顶点数<像素数。
我们应该尽可能地把计算放在每个对象或逐顶点上。在第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 操作,因为这会影响硬件的某些优化。
根据硬件条件进行缩放
诸如iOS 和 Android 这样的移动平台,不同设备之间的性能千差万别。我们很容易可以找到一台手机的渲染性能是另一台手机的10 倍。那么,如何确保游戏可以同时流畅地运行在不同性能的移动设备上呢? 一个非常简单且实用的方式是使用所谓的放缩(scaling)思想。我们首先保证游戏最基本的配置可以在所有的平台上运行良好,而对于一些具有更高表现能力的设备,我们可以开启一些更“养眼”的效果,比如使用更高的分辨率,开启屏幕后处理特效,开启粒子效果等。
16.9 扩展阅读
Unity 官方手册的移动平台优化实践指南 (http://docs.unity3d.com/Manual/MobileOptimizationPracticalGuide.html )一文给出了一些针对移动平台的优化技术,包括渲染和图形方面的优化,以及脚本优化等。手册中另一个针对图像性能优化的文档是优化图像性(http://docs.unity3d.com/Manual/OptimizingGraphicsPerformance.html)一文,在这个文档中,Unity 给出了常见的性能瓶颈以及一些相应的优化技术。除此之外, 文档列出了一个清单,包含了优化游戏性能的常见做法和约束。
在SIGGRAPH 2011 上, Unity 进行了一个关于移动平台上 Shader 优化的演讲
(http://blogs.unity3d.com/2011/08/18/fast-mobile-shaders-talk-at-siggraph/)。在这个演讲中,作者给出了各个主流移动GPU 的架构特点,并给出了相应的shader 优化细节, 还结合了真实的Unity 游戏项目来进行实例学习。在Unite 2013 会议上, Unity 呈现了一个名为针对移动平台优化Unity 游戏的演讲,在这个简短的演讲中,作者对造成性能瓶颈的原因进行了分类,并给出了一些常见的优化技术。在GDC 2014 上, Unity 展示了如何使用内置的分析器分析移动平台的游戏性能,读者可以在Youtube上找到相应的视频。在最近的SIGGRAPH 2015 会议上, Unity 进行了一系列演讲和课程。在Unity和来自高通、ARM 等公司的开发人员共同呈现的名为Moving Mobile Graphics 的课程中,来自Unity 的Renaldas Zioma 讲解了移动平台上PBR 的优化技术。更多Unity 在SIGGRAPH 2015 上的演讲,读者可以参见Unity 的博客。
除了手册和演讲资料外,成功的移动平台中的游戏同样是非常好的学习资料。《ShadowGun》是由MadFinger 在2011 年发布的一款移动平台的第三人称射击游戏, 使用的开发工具正是Unity 。在Unite 2011 上,该游戏的开发者给出了《ShadowGun》中使用的渲染和优化技术,读者可以在Youtube 上面找到这个视频。更难能可贵的是,在2012 年, 《ShadowGun》的开发者放出了示例
场景,来让更多的开发者学习如何优化移动平台上的shader。另一个非常好的游戏优化实例是Unity 自带的项目《Angry Bots》, 读者可以直接在Unity 资源商店下载到完整的项目源代码。