20
【编者按】作者王锐,VR行业资深从业者。
每一位游戏玩家,无论是传统的PC和主机游戏,还是时新的手游以至VR游戏,都会对这样一个名词有着特殊的理解和关注,那就是FPS(帧速率,Frames Per Second)。FPS太低的话,画面就一卡一卡的,玩起来不舒服;如果是戴上VR头盔的话,因为晕动症的影响,玩家甚至会迅速感到头晕和恶心,一天都无法心情舒畅。
FPS顾名思义就是每秒钟渲染的帧数,也就是说,显卡和显示器每秒钟都会更新数十张不同的图像(帧),进而形成一种动画的效果。
从传统动画的角度上来说,每秒10帧以上的画面就可以形成连续的动画效果(例如中国古代的走马灯),而不需要任何交互的电影放映,则采用每秒25帧或者接近30帧的播放速度,让观看者自在地欣赏大片。
但是加入了交互元素之后,例如可以快速转动视角的鼠标,手柄,以及VR头盔,人们对于帧速率的需求就直线上升了——如果是面对平面液晶显示屏的话,受限于IPS屏幕本身的刷新率特性,内容本身通常需要达到60FPS的渲染速度;而戴上VR头盔之后,采用OLED屏幕的内容则可以达到75FPS的更新,事实上,出于尽量避免晕眩的考虑,游戏内容的画面刷新也必须达到这个数值。
然而这谈何容易:交互游戏并不是预先拍摄好的电影和动画片。它需要根据玩家的操作来触发不同的逻辑,并渲染出不同的画面内容。而游戏场景可能是复杂的,细碎的,并且有很高的真实感和特效方面的要求——而这一切都必须在短短的1/75秒内完成!就算显示硬件的水准逐年提升,这依然是一个极具挑战性的话题:如何优化我们的场景和渲染策略,在如此有限的时间要求内,实现高效与细节并存的游戏内容呢?
本文将针对这一话题做适当的讨论,然而话题本身的技术含量已是深不见底,所以文章也只能浅尝辄止,只期望为读者和同行们小启一扇门扉。
在描述一些晦涩难懂的名词之前,我们不妨先来看一个例子:
假设我们有一个果园,每天它都要派出一辆卡车,走固定的路线运送水果到市里去。如果运送的水果总量为10吨,而这辆卡车一次能够承载200千克水果的话,那么显而易见它需要跑上50趟才能完成这一任务,也许这会花费整整一天的时间才能够完成。
显而易见,我们并不希望这项工作花掉那么多的宝贵时间,那么一种直截了当的解决方案是:给这辆卡车换上更为给力的发动机,比如NVIDIA的战术核显卡,让它跑全程的速度减半再减半,同样50趟的任务,这回只要一个上午就可以搞定了。
当然生活也许并不总是那么如意的,也许果园的工作人员早就心怀怨气,每次给卡车只装了50千克水果,于是可怜的司机就需要走上足足200个来回,直到别人吃上早饭了还垂死地奔波在路上……
幸好,拯救他的方法也不止一种,比如,装上战术核显卡之后,他至少能够和别人一样每天拉完50趟再按时回家吃饭了。
但是一个智商正常的果园老板应该不会把这当成是最合理的决策吧……难道不应该先开除那个装货的?让爷的小卡车别这么傻兮兮地跑上200个批次吗?
结束我们的遐想,而现实却也许傻得有些可爱:当作为内容开发者的我们同样遇到了“卡车花在路上的时间太长”这种问题的时候,第一选择往往是换用更逆天的显卡和系统,而不是好好琢磨一下该死的工作人员藏哪儿了。
没错,送了200趟水果的卡车,就好比跑了200个渲染批次(Draw Call)的显卡,而每个批次的执行都是会消耗固定时间的。而为了缩短这一时间,进而提升帧速率的开发者们,与其直接买入更新的显示设备,倒不如先坐下来仔细想一想,渲染批次过多的瓶颈是什么(那个作孽的装货工?),我又能把它优化到什么地步(至少恢复到50个批次的正常水准?)。而这成百上千吨的水果就好比是复杂和精致的VR场景内容,同一时间内能够送达的越多,它能够表达的内容真实感与细节程度也就越详实。
渲染批次(Draw Call)在以往并不是一个被十分重视的概念,制作游戏场景的美术人员更愿意用“三角面数”(Triangle Face)来描述内容的复杂程度或者制作水准,譬如:这是一个足有1000万面的卡通少女(她的每一根毛发也许都清晰可辨),又或者这是一个只有100万面的故宫实景模型(然而我的高超技巧让这一艺术瑰宝依然栩栩如生)。
殊不知,对于实时渲染而言,三角面数并不足以决定渲染的效率以及帧速率结果。仅用了一个批次就完成渲染的1000万面模型,和用了1万个批次才渲染完成的100万面模型,其执行效率恐怕是天壤之别。后者在实际执行当中的表现,恐怕一定会让那些自信于低面数模型的人们大跌眼镜。
然而这就引起了另一个有趣的论题:为什么会有大量的Draw Call呢?既然每一位开发者都能明白这样的道理,为什么不一开始就设计成一次Draw Call执行全部场景物体的渲染操作呢?就算是因为承载力的问题不得不分成多个Draw Call,这种简单粗暴的设计依然应当是最优的解法无疑吧?
然而这样的愿望往往无力成为现实,因为现代图形渲染底层接口对于Draw Call的实际执行,是在绘制实际几何体图元(Draw Primitive)的阶段;而每次绘制图元的操作之前,我们只能为这组图元(可能是200个三角面,也可能是10万个三角面)设置一张纹理图像(Texture),以及一组着色器(Shader)——而这两者正是虚拟现实和游戏应用当中用于表达物体材质和真实感的最核心组件。一张图像能够容纳和清晰地表达一个精致美女的肌肤,秀发,慧眼,红唇,丝衣,高跟鞋,LV包,以及其他种种细致入微的内容吗?当然不能。所以我们也没有办法在一个渲染批次里搞定美女模型的一切。
而与此相关的另一个问题也会困扰着更深层次的图形开发者:场景内容的管理。例如一座数字化的虚拟城市,每一栋楼,每一个房间,每一辆车,每一位居民,都应当是独立存在的个体,对这些模型资源的管理也显然应当按照相似的分类方法。然而从渲染的角度来说,这样产生的Draw Call恐怕远远不是最优的选择,甚至轻而易举就会让前文中送水果的卡车司机崩溃掉。
如果按照材质把模型重新分组和整合,倒是可以进一步提升渲染的效率,不过资源的管理工作却无疑会让人崩溃。试想一下,在公安局的户口本上,你不再属于某个家庭,而你身体的各部分则分别隶属于短发组,麻脸组,格子衫组,灰裤子组……这种“按材质分组”的行为看起来多少有点“汉尼拔”似的惊悚气氛了。
然而,场景优化面临的麻烦还远没有尽头。
试想一位画家,挥毫泼墨,将现实中的山水人物跃然纸上,描绘得栩栩如生。这些山水人物原本是自然存在的,绘制到纸上,就成了油彩,凑近去看,就会有浓重的颗粒感——虽然大多数情况下这并不妨碍我们观瞻就是了。
现代计算机的图形渲染过程与此类同。把复杂的场景模型跃然于屏幕之上,这一过程称作光栅化(Rasterization),而屏幕上的像素点,近看起来同样存在颗粒感,低分辨率的屏幕则更为明显。这种颗粒感对于VR类的内容来说更为显著(因为VR眼镜相当于放大了屏幕分辨率对画面质量的影响),而它也是破坏场景真实感和效果的主因之一。
那好办啊,有人可能会说,那就拼命增加屏幕分辨率不就好了。从现在的1080p,到2K,到4K,到8K……总会有彻底解决这个问题的一天吧?
然而事情并没有这么简单,还是回过头来谈谈我们的画家:在一张A4纸上画简笔画,也许他只需要1-2分钟而已;如果是10米长卷,那么也许要一天的时间;如果是在万里长城上……那么画家可能直接就跳下去了,搞这么一辈子的工程,生不如死啊。
没错,这里的画卷可以类比为我们所说的屏幕分辨率,而画家求死的原因,只因为要画的东西太多,而他对画卷内容的填充率(Fill Rate)太低了,因而渲染效率也变得惨不忍睹。
这个问题的解决方案,无非三种,弊端也是一目了然:
一,改用小点的画卷(降低屏幕分辨率和用户体验);
二,换个疯狂的画家(升级硬件,提升填充率);
三,少画点花里胡哨的东西(降低渲染内容的质量)。
听起来都不是什么一劳永逸的选择,并且大多数开发者一定会选用最直接的那个方案,没错,换更疯狂的画家,搞硬件的军备竞赛。
不过从场景优化的角度来说,方案三反而成了最靠谱的一条道路,并且这给各路算法豪侠和数学家们也提出了一个有趣的命题:如何在渲染质量还看得过去的前提下,尽量少画点“对用户没用”的内容呢?
对这句话的详细解释就是:用户看不到的场景不要画出来,把它提前裁减掉(Culling);而用户可能本来也看不清的场景,就用更低的细节程度(Level of Details)把它画出来。
这里必须要解释一句。显卡并不是多么聪明的一种硬件产品,它并不能主动分辨出当前提交的渲染指令中,包含的信息是否真的能够被显示到屏幕之上;而是选择了另一种更为简单的策略:不管有多少东西都先画上去,如果不幸没画到纸上的话……反正你也不在乎对不对?毕竟最终用户关注的只有纸面上的内容而已。
然而古今中外,那些为了优化场景而苦思冥想的开发者们,却仅为了这一个目标前赴后继,伤痕累累。
当然,这里所说到的“填充率”一词,只是相关图形系统运行机制的冰山一角。它还可能被进一步细分为像素填充率(光栅化操作和屏幕缓存绘制的速率)和纹理填充率(纹理在模型表面映射和采样操作的速率)。而实际执行过程中,还可能受到显存带宽(显卡在单位时间能够传输数据的总量)参数的影响,而这些信息往往都会标识在具体显卡品牌的性能说明文档中,作为发烧友比较和购买的依据(虽然它们实际上并无统一标准可言)。
不过这并非本文所要继续深入阐述的命题了,我们关心的,还是那些图形学和VR领域的先行者们,为了哪怕一点点的优化效果做出过的努力。
之前的长篇大论,列出了各种看起来棘手和不可逾越的问题,在这里归结起来,无非有以下几点:
一,渲染批次(Draw Call)的合并与优化问题;
二,裁减看不到的物体,降低渲染批次和填充率;
三,对于看不清的物体(比如距离玩家位置较远的物体),改变细节程度并降低填充率。
单纯合并几何体数据也许只是一个数学和几何拓扑学上的问题而已,然而每个渲染批次只能使用一组材质,这就大大提高了工作的难度和策略性。
一个听起来还不错的方案就是,将不同的材质合并到一起,这样对应的渲染批次也就合并在一起了——这一过程通常被称为Atlas。
没错,就是《云图(Cloud Atlas)》那部电影中所体现的,把不同的碎片(纹理)拼合在一起。
这一工作并不像想象中那么简单,因为你要合理地选择被拼合的纹理图像,以及考虑拼合的结果是否真的就提升了你的渲染效率?因为纹理Atlas的结果无非是一张更为庞大的纹理图像,而这实质上增加了纹理填充的时间。过度的Atlas只能让系统变得更慢,甚至远低于拼合之前的结果。只有将那些本来就零散细碎的小块纹理拼合起来,并且合并对应的绘制过程到同一个Draw Call当中,才有实际的价值和意义。
然后是数据的裁减(Culling),同样是一个简单而又复杂的命题:如何定义“玩家看不到的物体”?
最简单的一种情形是,他视野之外的物体。也就是说,如果把人的视野当作是一个空间的锥体的话,那么暂时丢掉这个视锥体之外的所有物体,让它们不要被渲染出来即可,即视锥体裁减(Frustum Culling)。
另一种麻烦但是很有意义的情形是,被其它什么东西挡住的物体。比如床底下的皮球,穿在外套下的衬衣等等。他们虽然在观察者的视野范围内,在当前时刻却不可能被看到。此时我们也可以选择直接剔除掉这样的物体,因而降低渲染批次和填充率——不过前提是,我们能尽快先找出那些确实被遮挡住的物体来,也就是遮挡查询(Occlusion Query)和遮挡裁减(Occlusion Culling)的概念。
不幸的是,在空间中进行复杂形体之间的遮挡判断,绝非易事。就算有合理的数学方法可以最终遍历和找到所有被遮挡的物体,这一运算过程耗费的时间恐怕也早已超过了直接渲染它们所花费的时间。因此,更为简化和高效的裁减算法研究也成为了一个仍在持续进行的话题,近年来更有了不起的开发者群体实现了自己的软件光栅化过程,仅用作快速的遮挡判断。不过相关技术的普适性验证和推广,还有很长的一段路要走。
另一种同样重要的优化方式就是场景物体的细节层次(Level of Details,LOD)划分。
当物体距离观察者较远的时候,采用粗糙层次的物体模型;靠近观察者之后,采用精细层次的物体模型。这种策略对于大规模场景的渲染(尤其是那些动辄以数十数百GB来算的地球级场景数据),往往有着不可替代的价值和意义。
当然,单纯的离散模型LOD也有种种的弊端,譬如切换层次时的突兀感,以及资源管理的复杂性等等。人们也在不断研究更新的分级,自动处理,分页调度等策略,只为了让更多更好的内容跃然于那不断增幅的画纸之上。
而高质量的渲染画面,对于场景优化的负面影响,往往也是不可忽略的。比如游戏中最为常见的阴影图技法(ShadowMap),从光源的角度重新渲染一次非黑即白的场景,再映射到原始的场景画面当中,形成逼真的阴影效果。这个看起来必不可少的需求却让我们苦心降下来的Draw Call直接翻倍。又比如多重采样抗锯齿(MSAA),实时反射(Reflecting)等种种效果需求,都是快速吃掉有限渲染效率的大杀器……为此,已是熬成谢顶与白发的开发者们也只能重整旗鼓,在场景优化的狭小空间里再耕耘,再奋战;而这一切,也许从未被屏幕前的玩家所知吧。
VR这个领域的火热,也直接带来了一个现实的问题——内容由谁来完成,高质量的内容如何去呈现?
VR眼镜本身具有左右眼分别渲染的特点,因此这也意味着实际场景也要根据左右眼的位置和视角分别渲染一次。这就意味着渲染批次的直接翻倍,填充率指标也大受影响。一些原本在PC显示器上流畅运行的逼真游戏,一下子跌入“卡顿”的谷底。
而这还远远没有尽头,VR行业的研究者们已经证明,60Hz的传统显示器刷新率,远不能满足VR游戏内容的需求,75Hz也只是刚好可以避免过快的晕动症而已,90Hz乃至更高的刷新率和渲染帧速率才是人们的期望。没错,翻倍的渲染压力,以及近乎翻倍的帧速率要求……这就好比一个“二模”阶段才勉强够上一本线的高中生,突然被告知“必须考上清华北大,不然就回去拾荒”时候的心情吧。
但是,心情归心情,现在这些低劣的VR游戏场景骗不了最终的消费者,各种幼稚和简单的游戏逻辑也丝毫无助于VR元年的期望。
AAA级别的大作在哪里?黑客帝国一般的幻境在何处?要为此付出巨大努力的绝不只有硬件工程师们,也绝不可以简单归罪于美术人员的无能。要知道,纵使是Unity和Unreal这样的成熟商业引擎,面对VR的苛刻需求和毫无优化可言的套用的传统游戏场景,其渲染性能也会捉襟见肘,狼狈不堪——而这一切的始作俑者,正是缺乏图形底层知识和深入实战经验的开发者自己;能够想到方法,脱出困局的,也只有靠这群战士发奋后百倍千倍的努力。
是的,属于VR的战斗,才刚刚开始。
雷峰网原创文章,未经授权禁止转载。详情见转载须知。