你已经决定使用Unity开发一款VR游戏,并且以Samsung GearVR作为你的目标平台。想要在这个设备上运行起来其实很容易,但是问题是,帧率实在是太低了。你的十字准星像粘住了一样,在视野的边上还会闪烁黑边,相机的移动看起来好像摄影师被踢了一样糟糕。你已经知道维持一个稳定的帧率有多重要,现在终于知道为什么了——在移动VR应用中,低于60FPS的时候不仅仅是看起来糟糕,更重要的是体验超烂。你的高端PC动辄跑到1000FPS,听起来好像搭载了个喷气机引擎,但实际上但风扇转起来的时候没有明显提升(不太理解原作者这句话的意思)。你所需要的是一个优化方法,以便让你的作品跑在移动芯片上。
GearVR环境与高效VR游戏的特点这并不是关于GearVR的一个包罗万象的性能优化介绍,这仅仅是个快速开始。在这一篇博客中,我们将讨论GearVR硬件,以及一个设计良好的移动VR程序的特点。接下来的一篇博客将介绍你已经构建好的程序怎样做性能优化。这篇博客基于Unity做优化,因为它看起来在GearVR开发者中间比较流行。但是,这里提到的概念可以应用于任何游戏引擎。
了解你的硬件在你动手找到性能瓶颈以前,应该先思考一下手机的性能特点。一般来说,移动图形管线基于一个非常快的CPU,一个非常快的GPU,它们通过一个非常慢的总线或者内存控制器连接,以及具有诸多限制的OpenGL ES驱动。GearVR运行在Samsung Note 4和Samsung Galaxy S6(估计这篇博客写的时候是这样的)。这两个产品先实际上代表了许多不同的硬件配置:
Note4有两种不同的芯片组。在南美和欧洲出售的设备基于高通骁龙Snapdragon处理器(骁龙805),而在韩国和亚洲其他地方出售的则是三星Exynos猎户座处理器(猎户座5433)。骁龙处理器有四个核心,而猎户座有八个核心。这些设备分别搭载了两种不同的GPU:高通Adreno420图形处理器和Mali-T760。
Note4可以进一步按操作系统划分。大多数设备多运行这Android 4.4.4 (KitKat),但是Android 5 (Lollipop) 已经可以获取更新了。使用Exynos猎户座处理器的Note4设备全都运行在Android 5上面。
Galaxy S6设备都基于同样的芯片组:三星猎户座处理器Exynos 7420 (搭载Mali-T760M8 GPU)。S6还存在另一个版本,Galaxy S6 Edge,但是从内部来说,它和S6是一样的。
所有的Galaxy S6都是Android 5系统。
如果这看起来比较乱的话,不要着急:尽管设备之间的硬件配置各部相同,这些设备的性能测试方法非常类似(只有一个例外,参见下面“陷阱”一节)。如果你能够使他们在一台设备上运行加快,那么在其他设备上也会加快。
对大多数移动芯片组来说,3D图形性能在这些设备有相当可靠的特点。下面列举一些使得GearVR工程变慢的原因(按严重程度排序):
场景必须依赖渲染器(例如阴影和反射)(消耗CPU/GPU)
绑定VBOs来发射draw call(消耗CPU/驱动)
透明,多通道shader,逐像素光照,其他填充大量像素的效果(消耗GPU/IO)
大型纹理加载,传送以及其他形式的内存拷贝(消耗IO/内存控制)
蒙皮动画(消耗CPU)
Unity垃圾回收限制(消耗CPU)
另一方面,这些设备具有相对较大的内存,可以塞进去大量的多边形。注意,Note4和S6都是2560x1440的显示屏,默认情况下,为了节省填充率我们渲染两个1024x1024的纹理。
了解VR环境由于VR渲染每帧都要渲染两次,每次渲染一只眼睛的图像,非常消耗硬件性能。在Unity 4.6.4p3和5.0.1p1版本中,这意味着每个draw call都要发送两次,每个mesh绘制两次,每个纹理绑定两次。还有一个比较小的开销,就是把最终的帧图像输出,包括扭曲和TimeWarp(大约2毫秒)。将来肯定还会有进一步优化的,但是目前我们卡在整个帧渲染两次。这意味着,渲染管线中一些开销最大部分的消耗是一般的游戏的两倍。
考虑到这些,下面是GearVR应用的一些合理目标:
每帧50-100次draw call
每帧50k-10k个多边形
纹理越少越好(可以使用大纹理)
脚本执行时间1-3毫秒(Unity Update())
请记住,这些并不是死限制,而是一些经验法则。
这帧画面中有3万个多边形和40个draw call
还要注意,Oclus Mobile SDK引入了一个API,可以调节CPU和GPU的使用来控制手机发热和电池消耗(示例用法查看OVRModeParams.cs)。这些方法允许你选择CPU或者GPU哪个对你的场景更重要。举例来说,如果你需要更多的draw call提交,把CPU调高(GPU调低)可能改进整体帧率。如果你忽略了设置这些值,你的程序会被严重的限制住,所以花点时间来实验是值得的。
最后,GearVR具有Oculus的异步TimeWarp技术。当你的游戏开始减慢的时候,TimeWarp基于最近的头部位置信息,提供了中间帧。它通过扭曲前一帧来匹配最近的头部位置,在掉帧的时候可以帮助你平稳过度,所以没有借口跑不到60帧。当你摇头的时候,如果你在视场边缘看到了黑色闪烁的条块,这意味着你的游戏运行的太慢了,就算TimeWarp也没有足够的帧图像来填充空白了。
面向性能的设计开发一个高性能程序的最佳方法就是预先设计好。对于GearVR应用来说,这通常意味着围绕移动设备GPU的特点来设计。
设置在开始之前,确保你的Unity工程设置为最佳性能。特别的,一定要确保设置下面的选项:
Static batching
Dynamic batching
GPU skinning
Multithreaded Rendering
Default Orientation to Landscape Left
Batching既然知道了draw call通常是GearVR程序最耗费性能的部分,那么第一步就是要把场景所需draw call尽可能少。一个draw call是一条发送给GPU使其绘制一个mesh或者一部分mesh的指令。这个操作代价最高的部分实际上是mesh本身的选择。每次游戏决定绘制一个新mesh时,这个mesh在被发送到GPU之前必须被驱动处理过。Shader必须要绑定,可能发生格式转换等等;每次一个新mesh选定之后,驱动都要使用CPU进行工作。所以当draw call发送时,最严重的开销是这个选择的过程。
然而,这也意味着,一旦一个mesh(或者更加具体的,顶点缓冲对象VBO)选择之后,我们可以花费一次选择的成本,然后对它渲染多次。只要没有新mesh(或者shader,或者纹理)被选择,这个状态会被驱动缓存下来,然后draw call的发送就快多了。为了利用这一点来提升性能,我们实际上可以把多个mesh打包成一个大的顶点数组,然后使用同一个VBO渲染它们。我们花费了一次整体mesh的选择成本,然后从包含在这个对象内部的mesh中,发送尽可能多的draw call。这个技巧叫做batching,比每个mesh都创建一个VBO的方法快的多,这也是我们进行draw call优化的基础。
包含在一个VBO中的所有mesh,为了能正确的batching,必须有相同的材质设置:相同的纹理、相同的shader,相同的shader参数。为了利用unity中的batching,实际上你需要更进一步:只有对象具有相同的材质对象指针,他们才能给正确batching。最后,下面是一些经验法则:
大纹理/纹理图集:尽可能少的使用纹理,尽可能多的把模型映射到仅有的几张大纹理上。
纹理图集
Static标志:在Unity Inspector中把从不移动物体标记成Static。
材质访问:访问Renderer.material的时候要小心。这将会复制一份材质并返回,这会把该对象排除在batching之外(因为它的材质指针不再相同了)。请使用Renderer.sharedMaterial。
确保batching打开了:在Player Settings中,确保Static Batching以及Dynamic Batching都打开了。
Unity 提供了两种方法来把mesh打包在一起: Static Batching以及Dynamic Batching。
Static Batching当你把一个mesh标记为static,你就是在告诉Unity这个对象不会移动,动画,或者缩放。Unity使用这个信息,在构建的时候自动的把共享材质的mesh打包成一个大型的mesh。有些情况下,这是一个非常重要的优化;出来打包mesh减少draw call,Unity还把变换信息记录在每个mesh的顶点位置中,所以它们就不需要在运行时变换。场景中标记为static的部分越多越好。请记住为了进行批处理该过程需要mesh具有同样的材质。
注意,因为static batching在构建时产生新的大型mesh,这会增加程序最终二进制文件的大小。这通常对GearVR开发者来说不是问题,但是如果你的游戏有好多个独立场景,并且每个场景有很多的静态mesh,加起来就会很大。另一个选项是使用StaticBatchingUtility.Combine在运行时来生成打包的纹理,而不会使你的程序体积膨胀(需要花费一次性的CPU使用和一些内存)。最后,请确保你正在使用的Unity版本支持static batching。
Dynamic BatchingUnity同样可以打包没有标记为静态的mesh,只要他们符合共享材质的需求。如果你打开了Dynamic Batching选项,这个过程几乎可以自动生成。每帧计算要打包的mesh的时候会有一些开销,但是从性能的角度来看,通常会有一个明显的提升。
其他batching问题要注意的是,有一些方法会破坏打包。渲染阴影和其他多通道shader需要一个状态转换,使得对象不能正确的打包。多通道shader也会使得mesh被提交多次,在GearVR上应该引起注意。逐像素的光照有同样的效果:在Unity4中使用默认的Diffuse着色器,投射到mesh上的每个光源都会使得mesh提交一次。这会使得draw call很快增长并超过多边形数量限制。如果你需要逐像素的光照,可以尝试把Quality Setting窗口中同时光源的数量设置成1。最接近的光源会以逐像素方式进行渲染,附近的光照将会使用球谐方法进行计算。要更好的话,删除所有像素光照,使用光照探测器。还要注意的是,batching通常对蒙皮的mesh不起作用。透明物体必须以一定的顺序进行渲染,因此batch效果不太好。
好消息是,你可以在编辑器中测试和调试batching。Unity Profiler和Game窗口中的Stats面板都可以告诉你发送了多少次draw call以及batching节省了多少资源。如果你的几何体使用很少数量的纹理,那你就不用实例化材质、标记static对象,你的场景也会很高效。
Transparency, Alpha Test, and Overdraw如上所述,移动设备的芯片通常是“fill-bound”,意味着填充像素的花销是一帧中最昂贵的部分。降低填充花销的关键是每个屏幕上的像素都只渲染一次。多通道的shader,逐像素光照效果(例如Unity默认的specular着色器),还有透明物体都需要把它们涉及到的像素渲染多次。类似这些像素太多的话,将会充满总线。
做为一个最佳实践,尝试把Quality Settings中的Pixel Light Count限制到1.如果你使用了多余一个的逐像素光源,确保你知道他们要作用于那个几何体,以及渲染该几何体多次的代价。类似的,让透明物体尽量小点。因为主要的开销是渲染像素,所以物体占据的像素越小,帧渲染完成的越快。注意像烟雾这样的透明特效,可能占据比你想象中更多的像素。
还要注意的是,在移动设备上绝对不要使用alpha测试着色器,例如Unity的cutout着色器。Alpha测试的操作(还要clip(),或者像素着色器中显式的discard操作)强制一些普通的移动设备GPU取消了硬件填充优化,使得渲染非常慢。Discard像元的操作会导致大量难看的走样(aliasing),尽量使用不透明物体。
Performance Throttling在你测试场景性能之前,你需要确保你设置了CPU和GPU的限制。因为VR游戏的性能达到了移动设备的界限,你必须在CPU和GPU之间设置一个权重。如果你的游戏偏重CPU,为了让CPU满速运行,你可以调低GPU。如果偏重GPU可以做相反的操作。如果你的游戏已经很流畅了,可以把两者都调低,这样能够节省电量,延长用户在线时间。更多的信息请查看Mobile SDK documentation中的“Power Management”部分。
重要的是,在你开始任何类型的性能测试之前都要设置CPU和GPU限制。如果这些值初始化失败的话,程序将会在默认设置非常低的环境中运行。因为大多数GearVR应用一般都会偏重CPU,所以通常会把CPU调整的高于GPU。OVRModeParams.cs中有一个关于怎样初始化节流设置的例子,你可以复制粘贴到游戏启动脚本中。
Gotchas陷阱在考虑性能优化的时候,你还应该记住下面这些技巧:
有一个特殊的设备比其他都要慢,具体来说就是基于骁龙的Note 4运行Android 5的系统,图形驱动在draw call方面似乎有一个倒退。对于draw call已经达到限制的游戏,会发现这个问题非常严重(draw call时间增加了20%),会导致正常的渲染管线停滞并且整体帧率下降。我们正和三星和高通努力解决这个问题。运行Android4.4系统的骁龙处理器的Note 4设备,还有Exynos处理器的Note 4和S6设备,都不受此影响。
尽管对CPU和GPU进行节流很大程度上减轻了手机的发热,对重量级的应用来说,在长时间运行的时候导致设备过热仍然是可能的。这种情况发生时,手机会警告用户,然后降低处理器的频率,会导致VR应用无法使用。如果你正在做性能测试和解决发热问题,请让手机先休息五分钟再继续。
Unity 4免费版不支持static batching和Unity Profiler。Unity 5的个人版已经有了。
S6不支持各向异性纹理过滤。
到此结束。下一篇,我们将讨论怎样调试真实的性能问题。
联系方式:0755-81699111
课程网址: http://www.vrkuo.com/course/vr.html