好的,所以你决定用 Unity 来做一个 VR 游戏,并选定了三星 Gear vr 为你的目标平台。做好之后,打开应用,在设备上执行文件再容易不过 – 但有个问题,帧率实在太低。视野边上有闪烁的黑边出现,感觉好像有谁往摄像机操作员的肚子上踢了几脚。你听说过保持稳定的帧率有多重要,现在你明白为什么了 – 在虚拟现实中,任何低于每秒60帧的东西不仅看起来不好,让人难受才是最糟糕的。你的高端上档台式机能把这个游戏运行到每秒1000帧,但机箱发出的声音像飞机引擎一样,而且……风扇发动时机箱好像真的升起来一点了!现在你要做的就是把你的大作针对移动芯片好好地优化一下。这个系列的文章针对的就是这个问题。
第一部分: 运行环境以及高效率VR应用的一些特征
这并不是关于 Gear vr 的一个优化大全,更像是一个快速入门吧。在这第一贴中我们将先讨论下 Gear vr的硬件以及设计良好的移动 VR 应用的一些特点。之后将会着重讲述如何针对你的应用来提升表现。这篇文章主要针对 Unity,因为这是目前 Gear VR 开发中的主流引擎。但是,这些概念对于其它引擎来说也是可以适用的。
了解你的硬件
在把你的项目大卸八块寻找其低效之处之前,最好先花点时间思考下当下移动手机的一些表现特征。总的来说,移动图形管线基于一个很快的 CPU ,通过一个很慢的总线和/或内容控制器来和一个很快的 GPU 连接,还需要一个造成很多开销的 OpenGL ES 驱动。Gear VR 在三星 Note4 和 S6 上运行。这两个产品线代表了几种不同的硬件规格:
- Note 4 的核心有两种。在北美和欧洲售卖的版本基于高通的骁龙芯片(SnapDragon 805),而在韩国和亚洲一些其它地区的基本都是三星自己的猎户座芯片(Exynos 5433)。骁龙芯片是四核 CPU 规格,而猎户座有八核,这两个核心分别对应两种 GPU: Adreno 420 以及 Mali-T760。
- Note 4 又被最近的谷歌分成两种系统。分别是安卓 4.4.4 (KitKat) 和 Android 5 (Lollipop)。目前基本上世界上的猎户座的 Note4 都运行安卓5.0了(看来我给世界拖后腿了)。
- S6 则都只有一种核心:Exynos 7420 (图形芯片是 Mali-T760M8)。还有另一个版本的 S6,S6Edge,但它们除了外形不一样以外里面都是一样的。
- 所有的 S6 都是安卓5.0棒棒糖系统。
看起来好复杂是吧!没事,它们所有的性能和表现其实都很接近(除了一种状况,最下面的注意事项里会说),如果你能在某个设备上跑得不错,在其它上面应该也不会有生命问题。
和多数移动芯片一样,它们的 3D 图形表现方面的属性都比较稳定可靠。因此,这里有一些普遍的可能让你的项目跑得不好的原因(按严重顺序排列):
- 场景需要独立的渲染器(比如阴影和反射)(CPU/GPU开销)
- 绑定 VBO 来进行绘制调用(CPU/驱动开销)
- 透明,多通道渲染,每像素照明以及其它的像素效果(GPU/IO 开销)
- 大型纹理加载,blits,以及各种类型的内存开销(IO/内存控制开销)
- 蒙皮动画(CPU开销)
- Unity 垃圾回收开销(CPU开销)
另一方面来说,这些设备有着比较大的内存,可以描画比较多的多边形。Note4 和 S6 都有着 2560×1440 的分辨率,但默认情况下我们一般只渲染 1024×1024 的纹理分辨率来节约填充率。
了解你的VR环境
VR 渲染让硬件的表现受到最严苛的考验,因为每一帧都必须给双眼绘制共两次。在 Unity 4.6.4p3 和 5.0.1p1 里,意味着每个绘制调用都被执行了两次,每个网格被绘制了两次,每个纹理被装订了两次。此外还有少量的开销被分配给了最终的畸变和时间穿越(2ms的预算)上。虽然我们期待未来硬件的表现以及程序的流程会越来越好,但目前就是得每帧画两次。这意味着相较于普通的游戏,VR 游戏的开销要多几乎一倍。
而基于这些特性,以下是一些比较靠谱的 Gear VR 应用渲染目标。
这一帧大概有30000个多边形和40个绘制调用
- 每帧 50 – 100 个绘制调用
- 每帧 50k – 100k 个多边形
- 纹理贴图越少越好(但每个可以很大)
- 脚本执行时间在 1 ~ 3 毫秒以内 (Unity Update())
注意这些不是硬性规范,只是我们的经验之谈。
另外注意 Oculus Mobile SDK 还引进了一个给 CPU 和 GPU 节流降频的 API 来控制热量和电池消耗(查阅使用样例 OVRModeParams.cs)这些方式让你可以选择对于在某个场景下,控制 CPU 和 GPU 的开销。比方说,如果在绘制调用的提交上出了问题,你让 CPU 升频(同时让 GPU 降频)可能能提升整体的帧率。如果你忽视这些做法,你的应用可能会被迫运行在降频的环境下,因此你最好多花点时间在这上面。
最后,Gear VR 也拥有 Oculus 的异步时间穿越(Asynchronous TimeWarp)技术。TimeWarp 能在程序变慢时基于最近的头部姿态信息来得到接下来的帧画面。它通过头部信息来扭曲上一帧的画面,能帮助你即便在偶尔丢失几帧时也能有流畅的体验,但绝对不是一个让你把应用随意运行在60帧每秒以下的借口。如果你左右摇摆脑袋时能看到眼角黑色的色块,就说明你的游戏已经慢到 TimeWarp 没有足够的帧画面来填补这些黑色空白了。
为性能表现而设计
做出表现良好的应用就是为了表现良好而去设计,而这意味着围绕移动 GPU 的一些特性来设计你的美术资源。
准备事项
在开始之前,先确保你的 Unity 项目的设定都已经为最高表现设定好了。特别的,是确定如下值的设定:
- 静态批处理(Static batching)
- 动态批处理(Dynamic batching)
- GPU 蒙皮(GPU skinning)
- 多线程渲染(Multithreaded Rendering)
- 默认方向到地平左边(Default Orientation to Landscape Left)
批处理
现在我们知道的是,一般来说绘制调用数量是 Gear VR 应用中最占用资源的方面,那么优化的第一步就是在艺术层面上做出一些设计,让程序在最终实现时调用越少的绘制命令越好。一个绘制调用就是对 GPU 的一个命令,让它绘制一个网格或者网格的一部分。而这个命令最占用资源的部分其实是网格的选择本身。每一次当程序决定绘制一个新网格时,网格在被提交给 GPU 之前必须先被驱动进行处理。着色器必须被弹回,可能会发生一些格式的转化等等;而这些过程在每次一个新的网格被选中后都会发生,并占用了最大的开销。
但这也同样意味着,每次一个网格(或者更具体一点,一个顶点缓冲区对象VBO)被选中后,只需要占用这一次开销,就能使用多次。只要没有新的网格(或者纹理、着色器)被选中,当前状态会一直存在于驱动缓存中并可以进行反复使用。为了利用这个特性,我们可以将多个网格整合进一个大的顶点阵列中,并通过 VBO 进行单独绘制。我们付出一次选择的代价后,就可以从这个对象内包含的多个网格进行调用而不会提高系统开销。这个方式被称作为批处理(Batching),比起为每一个不同的网格创建 VBO 要快得多,也是针对绘制调用进行优化的基础。
单个 VBO 中的所有网格必须享有同样的材质,才能进行批处理的整合:同样的纹理、着色器以及着色器参数。为了更高效地在 Unity 中利用批处理,我们还得更进一步:对象必须有同样的材质对象指针。为此,这里有一些参考规则:
纹理集合/图谱
- 纹理集合(Macrotexture / Texture Atlases):通过将尽可能多的模型映射到少数几个大的纹理集合中来达到尽可能少的纹理数量。
- 静态旗帜(Static Flag): 将所有不会移动的对象在 Unity 的 Inspector 中标记为静态。
- 材质访问:小心访问 Renderer.material。这个操作会复制材质并返回给你复件,导致对象被排除在批处理之外(因为材质指针变成独特的了)。请使用 Renderer.sharedMaterial。
- 确保批处理是开启的:在玩家设定(看下面)中确保静态批处理和动态批处理都被开启了。
Unity 提供两种将网格批处理的方式:静态批处理和动态批处理。
静态批处理
一旦你把某个网格标记成静态,这意味着你告诉 Unity 这个对象永远不会移动、变形或者缩放等。Unity 会基于这一点来自动将所有享用同样材质的网格整合成一个大的。在某些情况下,这将是很好的优化;不仅减少了绘制调用数量,Unity 也把变形操作变成了顶点的位置选择,这样在运行时就不需要变形了。在一个场景内,越多的部分能被标记成静态越好。要记得只有同样材质的才能被整合在一起哦!
但需要注意的是,静态批处理生成了新的一个大网格,有可能导致最终的应用容量变得很大。一般来说对于 Gear VR 开发者问题不大,但如果你的应用里有很多不同场景,每个场景都有很多静态网格,那么最终占用可能就会比较大。所以另一个选项就是在运行时使用StaticBatchingUtility.Combine来生成批处理网格,而不会让你的应用变得太大(但你得付出一个一次性的 CPU 大量开销以及一定的内存占用的代价)。
最终,小心你的 Unity 版本,确认是否支持静态批处理(看最后的注意事项)。
动态批处理
只要共享同样的材料,Unity 也能把那些并没有标记为静态的网格进行批处理。只要你把动态批处理选项打开,剩下的基本就不太用管了。对每帧都进行批处理会对计算性能造成一些开销,但一般来说对于整体性能表现总是会有提升。
批处理的其它问题
当然,还有其它一些情况也要小心。比如给物体绘制阴影啦,其它需要给物体转换状态的多通道着色器(Multi-pass shader)啦,都会让批处理出现问题。多通道着色会让网格被提交多次,针对 Gear VR 时务必要小心处理。逐像素光照(Per-pixel lighting)也有着类似的效果:使用 Unity4 里默认的扩散着色器(Diffuse Shader),网格会在每次被光线接触时重新提交一次,很快会把你的绘制调用数和多边形数耗光。如果你需要逐像素光照,可以试着在质量设置窗口中的并发光照总数设置为一。最近的光线会被逐像素渲染,而周遭的光线会通过球面调和函数(Spherical Harmonics)来计算得出。更好的方式是放弃逐像素光照,采用光照探针。另外要注意的是批处理不支持蒙皮网格。透明对象必须要按某种顺序来进行绘制所以很难被进行恰当的批处理。
不过,你还是可以在编辑器中测试、调校批处理的。无论是在 Unity Profiler (Unity Pro 才有) 还是游戏窗口的统计栏中,都能显示当前有多少绘制调用被下达了,多少被通过批处理节省了。如果你围绕着很小数量的纹理来组织你的几何图形,那么确保不要实例化你的材质,并把静态物体都标记上静态的旗帜,这样整个场景一般来说就比较节能环保绿色高效了。
透明,Alpha Test 以及重复绘制(Overdraw)
如上所提及,移动芯片一般都是“填充率瓶颈”,意味着像素填充可能是一帧中占用开销最大的地方。减少填充开销的关键就在于尽量让每一个像素只被绘制一次。多通道着色器,逐像素光照效果(比如 Unity 的默认高光着色器)还有透明对象等都需要对像素进行多次渲染。太多的话,就会影响总线。
作为最佳实践的标准之一,你可以在质量设置(Quality Settings)中试着限制像素光照数(Pixel Light Count)为一。如果超过了一,你也要确保自己知道是哪部分的问题以及其造成的开销。同样,尽量让透明的物体小。这里的开销由碰到的像素决定,因此你接触到的像素越少,这帧渲染的速度就越快。小心那些透明的粒子效果,比如烟雾等,其涉及到的像素数量可能会超过你的预期。
此外还要注意你不应该在移动设备上去使用 alpha test 着色器,比如 Unity 的镂空着色器(Cutout Shader)。那些 Alpha Test 的操作(以及 clip(),或者片段着色器里的显式丢弃), 会强制目前多数移动 GPU 撤销那些硬件优化,让其运行变得极慢。在管线中丢弃片段也经常会导致各种丑陋的反锯齿,因此还是请用不透明几何或者 Alpha to coverage 来做镂空。
性能节流
在你能可靠测试你的场景之前,你需要确保 CPU 和 GPU 的节流设置已经设定好了。因为 VR 游戏已经把手机的性能压榨到了极致,因此你需要好好掌握 CPU 和 GPU 之间的平衡。如果你的游戏瓶颈在 CPU,那么你可以为 GPU 降频来让 CPU 全速运转。如果你的应用瓶颈在于 GPU 那你就反过来。如果你的应用效率非常高,那么你可以把 CPU 和 GPU 都降频来降低耗电和温度。可以在 CPU 和 GPU 移动 SDK 文档讲述电源管理的章节“Power Management” 来查看更多关于 CPU 和 GPU 的节流设置方面的信息。
比较重要的一个地方是在做这类性能测试前必须先选定一个 CPU 和 GPU 的节流配置。如果这些具体的数值没有被成功初始化,你的应用会被默认运行在降频的环境下。由于多数 Gear VR 的应用都会被 CPU 那边的驱动开销(比如绘制调用提交数目)所限制,因此一般来说对于频率的设定会更有利 CPU 一些。你可以在 OVRModeParams.cs 中找到一个关于如何初始化节流目标的例子,也可以直接复制粘贴到一个在游戏开始时执行的脚本里来检验效果。
注意事项
这几天是你在考量自己应用性能表现时应当注意的:
- 运行在 Lolipop 下骁龙805核心的 Note4 比起其它所有版本的 Note4 都要慢;貌似图形驱动关于提交绘制调用这一块有了退步。如果某些游戏在绘制调用这一块已经到底了那么有可能会发现新的瓶颈(最多可能达增加20%的响应时间),足够让常规的管线停顿并导致帧率下降。我们正努力和三星以及高通共通解决这个问题。运行安卓 4.4 的骁龙805 Note4 以及猎户座 Note4 和 S6 设备皆不受此影响。
- 尽管为 CPU 和 GPU 进行降频节流能极大降低手机的热量,那些开销大的应用在长时间的使用过程中依然可能让手机过热。在这种情况发生时,手机会发出警告,然后动态降低处理器的频率,一般都会导致应用帧率低到不可用的地步。如果你在做性能测试时碰到了这种情况,请关闭应用并让手机休息个至少五分钟再继续测试。
- Unity 4 免费版不支持静态批处理以及 Unity Profiler。但是 Unity 5 个人版支持这些。
- S6 不支持各向异性纹理过滤 。