关于VR游戏的性能优化
VR游戏与传统游戏相比,主要在玩法设计、输入方式和性能压力这三个方面存在差异。本文将着重探讨VR游戏的性能优化问题。
为什么VR游戏的性能压力很大?
VR游戏性能压力大主要受三个因素影响,影响权重由高到低依次为:高帧率、高分辨率和画两遍。
高帧率
不同设备的帧率要求有所不同,如DK2为75帧/秒,最新的CV1是90帧/秒,HTC Vive为90帧/秒,PS4 VR为120帧/秒。相比之下,PC游戏通常为60帧/秒,主机游戏为30帧/秒,由此可见VR游戏在帧率方面的压力巨大。鉴于帧率如此之高,每一帧即便有2ms的提升也意义重大。以75帧/秒为例,每帧时间为13.33ms,2ms占比达15%。
高分辨率
不同设备的分辨率也各有不同,DK2为1920 1080,最新的CV1为2160 1200,HTC Vive为2160 1200,PS4 VR为1920 1080。除了账面分辨率,实际渲染时为抵消透镜畸变带来的分辨率损失,需要进行超采样。具体而言,DK2为135%,CV1和HTC Vive都为140%。以DK2的数据1920 * 1080 @75Hz来说,每秒的像素处理量为283 millions,这个数据是一般主机游戏的4倍。而最新硬件的像素处理量更是提升至457 millions。
画两遍
在VR游戏中,有两种常见的绘制方式:
- 方法一:依次画两遍场景:SetTexture、SetTransforms、SetViewport、切换RenderState、DrawCall等操作均需执行两次。
- 方法二:依次画两遍物体:相比方法一有所节省,但DrawCall依旧翻倍。
关于像素处理部分
从上述数据可以看出,VR游戏的性能压力主要集中在像素处理方面。因此,与像素处理相关的部分需要特别关注:
- 光影计算方案的选择:采用空间换时间的策略尤为重要。应尽可能使用light map、静态AO、环境反射贴图等方案,而dynamic shadow在任何时候都应尽量节省使用。
- 后期处理:去除不必要的效果。如DOF、Motion Blur、Lens Flare等效果本身就不适合VR游戏;SSR、SSAO等尽量用前面提到的静态方案替代;由于已经有Super Sampling,AA也可以不使用。
- 特别注意OverDraw的问题:例如,范围巨大的透明面片特效应谨慎使用,避免叠加过多层数。
- Shader复杂度问题:在UE4的viewmode中,有一个专门用于查看shader复杂度的模式。一般来说,当出现粉色和白色的情况时,说明shader过于复杂,需要进行修正。
- early z culling:虽然延迟渲染已成为各大引擎的标配,很多人认为对于延迟渲染而言,early z culling没有必要,因为生成GBuffer后相当于已经进行了像素级别的culling,而且多一个提前写深度的pass往往得不偿失。但实际上,early z culling针对延迟渲染的受益部分主要在GBuffer的生成阶段。在传统游戏中,这部分相对于lighting计算阶段的开销不大,因此常被忽略。但在VR游戏中,由于像素处理量巨大,这部分的优化提升效果经过测试相当明显。不过,实际情况需根据游戏场景进行详尽测试。
关于画两遍
由于批次翻倍和面数翻倍,VR游戏中优化批次和面数的意义比传统游戏更大。
- 静态场景的批次优化:针对UE4,我们专门开发了扩展工具来合并场景中相同物体的批次,避免美术人员对已完成的场景进行返工。在大多数情况下,这是程序开发效率对美术制作效率的一种妥协。
- 动态批次优化:采用instance的思想合并数量众多但体积较小的物体,例如FPS游戏中的子弹。在优化过程中,很多看似不起眼的因素也可能对性能和内存造成巨大压力,不过现代成熟引擎通常已经对底层优化做好了处理。
- 面数:在UE4中,面数的消耗主要体现在生成GBuffer的Base pass阶段。应善用统计工具定期、定性地分析游戏场景。除了美术提供的静态场景和角色,还需关注自动生成的内容,如tessellation,有些工具可能无法统计到这些内容。例如,UE4中Ribbon特效的tessellation默认步长为15uu,而在我们的游戏中,Ribbon特效可达30000uu。如果不改变默认值,一条拖尾可生成4000面,同屏50条拖尾会使绝大部分GPU不堪重负。因此,在特定游戏中,应善用不同工具从多个角度进行分析。
其他
前面介绍的是针对VR游戏特点重点强调的优化方法,其他优化方法同样适用。根据以往经验总结如下:
- 对表现效果妥协:例如,很多手机平台的游戏角色甚至没有normal贴图,同时还可以降低贴图精度和模型精度。
- 对制作流程和制作效率妥协:如在开发无尽之剑XboxOne版时,发现UI直接调用d3d API进行绘制。
- 开发效率的妥协:注意shader中的数据类型和顶点的数据格式,能用16位浮点就不用32位浮点。
- 根据游戏类型具体分析:如果确定场景中所有物件都必须渲染,则可以关闭Ocullusion Culling,因为此时不需要预计算遮挡剔除关系。
- 注意同步点:特别关注CPU、GPU的同步点以及线程之间的同步点(多发生在竞争统一资源时,如主线程和第三方库的线程使用同一个内存分配器)。
- 善用第三方库:如果小内存分配频繁且自己不想编写内存库,可以使用tcmalloc、nedmalloc等第三方库。
- 多用LOD:不仅包括贴图mipmap、模型LOD等,还可以考虑逻辑层面的LOD,如特效分层LOD。
- 设置不同的更新频率:为不同的Actor、Component和系统设置不同的更新频率。
- 加速优化:采用多线程加速和SIMD加速。
- 避免使用低效函数:避免使用基于win32 API的高级函数,如memeset,因为它是单字节填充,可用汇编进行优化,以提高效率(成熟引擎通常已处理此类问题)。
其他方案
除了上述方法,业界还有一些全新的优化方案:
- 多/双显卡渲染:DX12支持显卡混搭,可将render task绑定到任意GPU上。Nvidia的SLI和ATI的CrossFire可用于非DX12的情况,原理是一块显卡渲染左眼,另一块显卡渲染右眼。要求两块显卡型号必须一致,实测效果良好。
- StencilMesh的思想:同样是一种culling方法,在UE4中的实现叫做HMD Distortion Mask,可节省周围四角区域的像素计算。
- Instanced stereo Rendering:核心思想是一次提交绘制双份几何体,draw call无需翻倍。UE4的4.11 preview版本已经推出了第一个版本的实现。
- Multi-Resolution:由于人眼对中心区域像素更为敏感,因此可以保持中心区域分辨率,降低边缘区域分辨率。这种方法可以节省25% - 50%的像素处理量。
补充和总结
在进行优化之前,有两点非常重要:
- 稳定测试环境:关闭PC上的其他3D程序,关闭垂直同步,确保每次采样点和采样上下文完全一致,不要以编辑器模式启动游戏。
- 量化观测数据:在完全稳定的测试环境下,同一游戏前后两次测试的性能观测数据可能会有轻微浮动,因此不能仅凭直觉判断。应捕获精确的数据进行分析。此外,优化是一个长期迭代的过程,需要做好记录。在与美术人员产生分歧时,尽量用数据说话。