蛮牛教育
本系列文章主要向大家讲解如何优化VR游戏的性能并提高帧率。
 
第一部分:Gear VR环境以及高效VR游戏的特点
 
这不是关于Gear VR性能优化的实操指南——它更像是一篇快速入门。在本文中我们将讨论Gear VR硬件和设计良好的移动VR应用程序的特点。接下来的文章将讲解如何提升已有App的性能。我选择基于unity的行为(及特性)来介绍,因为Gear VR开发者大多都用Unity。不过,这里提出来的概念应该可以适用于几乎所有的游戏引擎
 
认识硬件
 
在拆解项目寻找低效原因之前,有必要稍微考虑一下手机的性能特性。一般来说,移动图形管道依赖于一个相当快速的CPU,它通过一条比较慢速的总线和/或内存控制器连接到一个相当快速的GPU,以及一个开销较大的OpenGL ES驱动。Gear VR运行在三星Note 4和三星Galaxy S6上。这两种产品线实际上代表了大多数不同的硬件配置:
  • Note4包含两种芯片组。北美和欧洲销售的设备基于高通骁龙芯片组(具体地说,是Snapdragon805),而南韩以及亚洲其他地区销售的设备使用三星Exynos芯片组(Exynos 5433)。骁龙是四核CPU配置,而Exynos有八核。这些设备分别使用两种不同的GPU:Adreno 420和Mali-T760。
  • Note4设备可以进一步以操作系统来区分。大部分运行android 4.4.4(KitKat),但Android 5 (Lollipop)现在也适用,在全球许多载体上可以更新。基于Exynos的Note 4设备全部运行Android5。
  • GalaxyS6设备全部基于同一种芯片组:Exynos7420 (带一块Mali-T760M8 GPU)。其实还有另一种版本的S6,Galaxy S6 Edge,但内部来说它和S6是一样的。
  • 所有的Galaxy S6都搭载Android 5。
     

 
如果这点看起来很费事,别担心:虽然各种设备硬件都不同,但所有这些设备性能的基本轮廓非常相似(也有一个特例——看下面的“陷阱”章节)。如果您能让它在一种设备上快速运行,它应该在其他的设备上也能运行得很好。
 
对于大部分的移动芯片组来说,说到3D图形性能,这些设备有一个相当可靠的特性。下面是一些通常会让Gear VR项目变慢的事情(按严重性排序):
  • 需要依赖渲染的场景(比方说,阴影和反射)(消耗CPU/GPU)
  • 绑定VBO发起绘制调用(draw call)(消耗CPU/驱动)
  • 透明,多通道(multi-pass)着色器,逐像素照明,以及其它填充大量像素的效果(消耗GPU/IO)
  • 大纹理加载,blits和其它形式的memcpy(消耗IO/内存控制)
  • 带蒙皮的动画(消耗CPU)
  • Unity垃圾回收消耗(消耗CPU)

     

 
另一方面,这些设备有相对大量的RAM,可以推送相当多的多边形。注意Note 4和S6都是2560×1440的显示屏,虽然缺省情况下我们会渲染为两个1024×1024贴图来节省填充帧率。
 
认识VR环境
 
VR渲染引发了硬件性能特性的强烈反差,因为每一帧都需要渲染两次,每只眼睛一次。在Unity 4.6.4p3和5.0.1p1(本文写作时最新的版本),那意味着每个draw call都要发起两次,每个网格物体要绘制两次,每张纹理要绑定两次。还有少量开销涉及将最终输出的帧与失真和时间偏差(TimeWarp,预算为2毫秒)组合起来。可以合理预期将来会有提升性能的优化,但是眼下,我们不得不为将整个帧绘制两次而奋斗。这就意味着图形管道中有些消耗最大的部分在VR中的消耗会是平面游戏中的两倍。
 
记住上面这些,以下是Gear VR应用中需要达到的一些合理的目标:
           一帧大约有30000个多边形及40次DrawCall
  • 每帧50-100个draw调用
  • 每帧50k-100k多边形
  • 尽可能少的纹理(但纹理大一些没关系)
  • 脚本执行花费的时间在1-3毫秒(Unity Update())
     

 
记住这些不是硬性限制,您可以把它当成是经验规则。
 
同时请注意,Oculus Mobile SDK引入了一个API用来限制CPU和GPU,以控制发热和电池消耗(示例用法请参见OVRModeParams.cs)。这些方法允许您选择对于特定场景,CPU和GPU哪个更重要。比方说,在绑定draw call提交时,提高CPU(降低GPU)可能会提高整个帧率。如果疏于设置这些值,您的应用将被显著限制,所以花点时间来试验它们吧。
 
最后,Gear VR也提供了Oculus的异步时间偏差(Asynchronous TimeWarp)技术。当您的游戏速度开始慢下来的时候,TimeWarp基于最近的头部姿势信息提供中间帧。它通过扭曲前一帧来匹配最近的头部姿势,不过虽然它能不时地帮助您平滑一些丢失的帧,它也不应该成为运行时帧率一直低于60的借口。如果头部移动时在视野边缘看到黑色闪烁条,那就说明您的游戏运行速度过慢以至于TimeWarp没有足够接近的帧来填补那些空白。
 
为性能而设计
 
要制作出一款高性能应用,最好的办法是预先设计。对于Gear VR应用来说,那通常意味着围绕移动GPU的特性来设计您的美术资源。
 
设置
 
在开始之前,确保Unity project settings已设置为性能最优。特别是确保设置了以下值:
  •         静态批处理
  •         动态批处理
  •         GPU蒙皮
  •         多线程渲染
  •         缺省方向为LandscapeLeft

     

 
批处理
 
我们知道,draw call是Gear VR应用中开销最大的部分,第一步最好先设计美术资源,让它尽可能需要最少的drawcall。draw call是给GPU的一条指令,让它绘制一个网格物体或者网格物体的一部分。这个操作的开销正是对网格物体的选择。每次当游戏决定要绘制一个新的网格物体,这个网格物体在提交给GPU之前必须先被驱动处理。必须绑定着色器并完成格式转换等等;每次一个新的网格物体被选择,驱动就有CPU的工作要做。发起一次draw call最主要的开销就是处理选择。
然而,那也意味着每次当一个网格物体(或者,更具体点,一个顶点缓存物体vertexbuffer object,简称VBO)被选择的时候,我们可以只需一次选择的开销就可以多次绘制它。在没有新的网格物体(或者shader,或者纹理)被选择之前,这个状态会在驱动中缓存,后续发起的draw call就会快得多。为了加大这种行为以提升性能,我们可以把多个网格物体打包到一个包含大量顶点的数组中,然后分别绘制出相同的顶点缓存物体。我们只需要对整个网格物体花费一次选择开销,然后可以针对那个对象中包含的网格物体发布尽可能多的draw call。这种技巧叫做batching,比给每个网格物体生成一个单独的VBO要快得多,也是几乎所有draw call优化的基础。
要想让batching正常工作,一个单独的VBO中包含的所有网格物体必须拥有同样的材质设定:同样的贴图,同样的shader,同样的shader参数。在Unity中为了加大batching的效果,您需要更进一步:对象只有在有同样的材质物体指针的时候才会被正确batch。为了这个目的,以下是一些经验规则:
                   一个纹理图集
  •         大纹理/纹理图集:通过用一小批大纹理贴给尽可能多的模型的方法,尽可能减少纹理数量。
  •         静态标志:在UnityInspector中,将所有永不移动的物体标记为Static。
  •         材质访问:访问Renderer.material时要小心。这将复制材质,并返回副本,而这会将物体从batching中清除(因为它的材质指针现在是独立的了)。使用Renderer.sharedMaterial可以避免这种情况。
  •         确保batching是打开的:在Player Settings(见下图)中,确保Static BatchingDynamic Batching都是启用的。
     

Unity提供两种不同的方法来批处理网格物体:静态批处理(static batching)和动态批处理(dynamic batching)。
 
静态批处理
 
如果您将一个网格物体标记为static,您实际上是告诉Unity这个物体将不会移动、动画或者缩放。在打包(build)时,Unity 使用这个信息自动地将这些网格物体批处理为一个共享材质的单独的大网格物体。在有些情况下,这将是很显著的优化;除了将网格物体组织起来以减少draw call之外,Unity还将变换信息固定到每个网格的顶点位置,这样它们在运行时就不需要进行变换。场景中可以标记为static的部分越多,效果越好。记住:这个操作需要网格物体有同样的材质,这样才能被批处理。
注意,由于静态批处理会在运行时产生新的组合网格物体,它将增加应用的二进制版本的最终体积。对于Gear VR开发者来说,这通常不是问题,但如果您的游戏有大量的单独场景,每个场景有大量的静态网格物体,开销就相当大了。另一个选择是运行时使用StaticBatchingUtility.Combine来生成批处理后的网格物体而不会导致应用体积膨胀(代价是一次性的CPU开销和一些内存)。
 
最后,请小心确保您正在使用的的Unity版本支持静态批处理(参阅下面的“陷阱”)。
 
动态批处理
 
Unity也可以批处理没有标记为static的网格物体,只要它们符合共享材质的需求。如果您将DynamicBatching选项打开,这个处理就基本上是自动的。每帧去计算这些网格物体将产生一些额外的开销,但相比提升的性能通常是值得这样做的。
 
其它的批处理问题
 
注意,一些方式可能会中断批处理。绘制阴影和其它的多通道shader需要状态切换并阻止物体正确批处理。多通道shader还会引起网格物体被多次提交,在Gear VR中应该格外小心。逐像素光照也可以有同样的效果:在Unity 4中使用缺省的Diffuse shader,网格物体每次在被灯光接触时会重新提交。这将导致draw call和多边形数量快速突破限制。如果您需要逐像素光照,试着在QualitySettings中将同时作用的灯光数量设置为1。这样,最近的灯光会被逐像素渲染,周围的灯光将会使用球形调和函数来计算。最好的办法是放弃所有的像素光,而依靠灯光探针(Light Probe)。同时请注意,批处理通常不会处理蒙皮的网格物体。透明物体必须按照特定的顺序来绘制,因此很少能被正确批处理。
 
好消息是您可以在编辑器中测试和微调批处理。UnityProfiler(Unity Pro才有)和Game窗口中的Stats面板都会显示draw call数量以及其中通过批处理被省略的数量。如果将几何体组织到极少的纹理,请确保不会实例化材质,并将静态物体标记Static标志,场景效率就会慢慢改善。
 
透明、Alpha测试以及过度绘制
 
就像我们上面讨论的,移动芯片组经常被填充绑定“fill-bound”,意思是说填充像素的开销可能是每帧开销最大的部分。减少填充开销的关键是尝试让每个像素只在屏幕上绘制一次。多通道shader,逐像素灯光效果(比方说Unity的缺省specular shader)以及透明物体都需要多次通过它们所作用的像素。如果这种像素过多,会挤爆总线。
 
最佳做法是,试着在Quality Settings中将Pixel Light Count限制为1。如果使用多个逐像素灯光,确保您知道它会被应用到哪一个几何体,以及多次绘制那个几何体的开销。同样地,尽量使透明物体很小。这里的开销是修饰像素,您修饰的像素越少,帧绘制的越快。注意透明粒子效果比方说烟,它们可能会修饰比预想中多得多的像素。
 
同时请注意,您绝不应该在移动设备上使用alpha测试shader,比如Unity的cutoutshader。Alpha测试操作(还有clip(),或者在片段着色器中显示忽略)强制某些普通的移动GPU清除掉某些硬件填充优化,使它异常地慢。忽视fragment mid-pipe 也可能会引起大量丑陋的变形,所以坚持使用不透明几何体结合alpha遮罩来进行裁剪。
 
性能调节
 
在进行可靠的场景性能测试之前,需要确保您的CPU和GPU限流已经设置好了。因为VR游戏将手机性能推向极限,您需要在CPU和GPU之间做个衡量。如果您的游戏是CPU密集型,您可以为GPU降频以全速运行CPU。如果应用是GPU密集型,您可以相反操作。如果应用效率够高,您可以给两者降频,这样可以为用户节省电量从而使游戏时间更长。查阅Mobile SDK documentation上的“Power Management”了解更多关于CPU和GPU限流的信息。
 
此处的重点是您在做任何性能测试之前必须选择对CPU和GPU限流。如果您没有初始化这些值,您的app默认会运行在一个严重降频的环境中。因为大部分的Gear VR应用趋向于绑定CPU驱动消耗(比方说draw call提交),通常将时钟设置为偏向CPU而不是GPU。关于如何初始化限流目标的例子可以在OVRModeParams.cs找到,您可以将它复制粘贴到游戏启动的一个脚本中。
 
陷阱
 
以下是一些您在考虑性能概况时应该铭记在心的一些小技巧:
  •         一些特定的设备,尤其是基于骁龙并且运行Android 5的Note 4,要慢于其他所有的设备;图形驱动似乎包含一个关于draw call提交的回退。已经绑定了draw call的游戏可能有新的额外开销(它将会增加20%的draw call时间)严重到足够引起正常的管道延迟,并降低整个帧率。基于骁龙运行Android4.4的Note4设备以及基于Exynos的Note4和S6不受影响。
  •         虽然限制CPU和GPU能显著地降低手机发热,重量级的应用长时间运行还是会导致设备过热的。当这种情况发生的时候,手机会警告用户,然后动态地降低处理器的时钟频率,这通常会使VR应用无法使用。如果您正在做性能测试,并导致设备过热了,让它停止运行游戏五分钟,然后再做测试。
  •         Unity4免费版不支持静态批处理或者Unity Profiler。而Unity 5个人版支持。
  •         S6不支持各向异性纹理过滤。

     

 
本文到此结束。在下一篇文章中,我们将讨论如何调试实际项目的性能。
 
原文作者:chrispruett
感谢蛮牛译员“轩辕之子”对本文翻译所做的贡献~~~


本文由蛮牛译馆倾情奉献,除 合作社区 及 合作媒体 外,禁止转载。
 

关注蛮牛教育,更新中...