为 Gear VR 设计游戏和体验是一件非常有挑战的事情。由于移动渲染力的限制,三角形数量和着色器都需要经过小心的优化。虚拟现实渲染中的一个比较浪费渲染力的地方就是你需要为双眼在略微不同的角度针对同一场景进行重复渲染。这种“双目视差”能给用户带来景深立体的感觉 - 左眼和右眼都能从稍微不同的位置看到同一个物体,然后大脑就能够把这种区别解析为物体到自己面前的距离。
这种左右眼之间看到的差异,对距离自己较近的物体,差别还是比较大的;但随着物体越来越远这种差别也会越来越小。双眼之间的距离 - 以及生成的像素 - 也在物体离自己越来越远时差别相对越来越小。为了利用这一点,我们制作了一个 Unity 的样品和一个改进后的 UE4 移动前置渲染器,来为近处物品进行立体渲染(为两眼分别渲染一次),同时为远处物体进行单目渲染(为两眼同时渲染一次)。在这里我们会解释一些我们在制作这个单目渲染样品过程中的一些选择,以及一些折衷的妥协。
我们在代表左右眼的立体摄像机位中间放置了第三个摄像机位。这三个机位在一条线上,这样它们的视角才能匹配上。对于 Gear VR,两只眼睛的摄像机的视椎体都是左右对称的,因此中间这个摄像机位的投射矩阵和双眼立体摄像机的投射矩阵也是一样的。在 Rift 上,两眼的摄像机位视椎体则是非对称的(靠内朝鼻子这个角度,比靠外的角度要稍小一些),因此中间这个平面摄像机位的视椎体实际上是左右眼视椎体的集合体。这包括了两眼看到的所有物体,但是也导致了它渲染出来的目标会比两只眼分别渲染的目标要稍微大些。
我们增加了一个分裂平面,来选择哪些内容会被用双眼立体摄像机来渲染,而哪些内容会用中间的平面摄像机来渲染。我们发现用10米来作为默认值效果还不错(十米内立体渲染,十米外平面渲染)。这个距离值可以在游戏中任意帧被随意修改,也可以在编辑器中的 World Settings / VR / Mono Culling Distance 下进行配置。
我们还修改了 UE4 针对移动设备的前向渲染器来达到以下效果:
-
用立体摄像机来渲染非透明内容。
-
通过改变和结合输出来制作一个平面的遮罩。这个遮罩也会预先设定平面的深度缓存。
-
用平面摄像机来渲染非透明内容。
-
把平面摄像机的结果整合进立体的缓存里。
-
立体化渲染所有透明内容并进行所有的后期处理。
第一步的结果,纯立体缓存加上近景深度剔除
第三步的结果,远景是平面的,此截图特意做成左右两眼
第四步的结果,把第三步结果整合进第一步,加上近景深度剔除
为了把立体和平面渲染的内容分开,我们采用了一种用到深度缓存的方式:所有超过离摄像机位预定距离的像素的立体效果,会通过清除那个距离以外的深度缓存被放弃掉。同样的,在近处的平面投射也会从那个预订距离开始,来放弃任何已经被渲染为立体的碎片。
这个方法让我们能够在立体像素和平面像素之间有一个清晰的深度顺序:我们知道所有的立体像素离摄像机位的距离都比平面像素的距离要小。这避免了在某些方式下进行最终的场景整合阶段时,需要用到深度比较而导致的昂贵性能开销,最小化了像素着色器调用的数量。
但比起基于物体的方式,这种基于像素的分裂平面的方式,最大缺陷就是绘制调用数量不那么可控。任何穿过此分裂平面上的物体,都必须同时进行立体和平面的渲染,尽管最终没有任何一个像素会被绘制两次。哪怕对很近的远平面的视椎体剔除,用普通的视椎体剔除技巧来最小化递交到立体缓存的绘制调用数量,也会比较麻烦:一个远处的物体有着很大的包围盒,比如说环境的立方体贴图永远会通过视椎体剔除绘制;就算由于远平面距离较短,它的任何像素都没有被被显示出来,但是它的包围盒和摄像机位的视椎体产生了交叉。
为了避免这种情况,我们还增加了一种方式来让你在平面渲染引擎中可以手动标记一些物体,让它们永远不会被立体缓存进行渲染。为了知道哪个物体需要标记,我们还增加了两种渲染模式:一种只显示立体缓存,而不显示包含平面远景部分的整合层;而另一种显示立体缓存的同时,加上了用来进行视锥体剔除的区分平面和立体部分的远景平面,但没有深度测试。简要来说:任何被渲染到第二张图像上而没有渲染到第一张图像上的物体,都需要一个绘制调用,但由于远平面距离较短而不会显示出来。
纯立体缓存
递交到纯立体缓存的绘制调用
在这个例子里,我们可以注意到,远景的地形由于其很大的包围球,经过了视锥体剔除的测试,但由于其距离已超过了三十英尺的深度分裂平面,而不应该被渲染出来:我们需要为它加上标记来强制只进行平面的渲染,节省宝贵的绘制调用资源。这个标记在 UE4 编辑器的物体细节/渲染区域下,名字叫“Force Mono”。
远景平面渲染的主要问题在于,它最小化像素着色器开销带来的性能开销节约,可能会被立体层和平面层之间缺乏互相遮挡给浪费掉。由于我们并不会为那些离摄像机比较近的物体进行平面渲染,那些在进行最终场景整合之后并不会被看到的远景部分,依然会在平面缓存中被着色,因为没有什么东西来遮挡它。为了避免这个问题,我们选择先渲染立体部分,再读取它们的深度缓存,并计算它们的交叉。所有在左眼和右眼摄像机被同时渲染过的像素,都会被写入远景平面的深度缓存中,来避免它们在平面摄像机中进行渲染。最终结果如下图:前面立体视觉下的大柱子,在平面缓存中被遮挡掉了,确保了后面的像素不被渲染。
纯立体缓存
经过场景整合后的平面缓存
为了避免立体缓存和平面缓存在分裂平面上产生的割裂,我们计算了分裂平面的 3D 点在平面摄像机和左右眼立体摄像机的投射之间的差别。结果是区别很小:在1024像素分辨率 90° 可视角度和30英尺距离下,差别只有两个像素。但我们依然考虑了这一点,并在场景整合时,对平面缓存进行了位置上的补偿。下图是扩大后的截屏,显示了立体到平面的转场,以及包含和没有包含这两个像素的补偿的效果。
无补偿的立体-平面转场
包含了补偿
当我们有了清晰的深度顺序,所有的平面像素都在立体像素之后了,我们就只需要一个全屏的 Compositing Pass 来在没有写入立体缓存的位置通过使用(1, 1-dest_alpha)作为混合功能把平面像素整合进来。
平面渲染带来的性能节约和所在场景有很大关系,但在前置渲染管线下,会有很大的提升。在 Epic 的日落寺庙样品里,我们的帧渲染时间从45毫秒稳定提升到了34毫秒 — 足足25%的提升。就算在寺庙内远景物体和近景物体都比较多的情况下,也没有任何明显的画面质量或深度感上的下降。同样的,在采用了经过修改后的 Rift 前置渲染器后,Dreamdeck 中的恐高城市场景 Vertigo 里也得到了类似的结果。
但是那些采用了昂贵近景内容的场景,性能可能不升反降。因为当你采用平面渲染时,你有一个固定的第三个摄像机位的开销,以及平面深度遮挡的开销,以及最后场景整合的开销。但是由于实时激活或者关闭平面渲染模式并不会带来卡顿或帧率的降低,你可以只在合适的场景下打开它。
有几个原因,让我们看到这个平面渲染模式对于 UE4 的延迟渲染器并没有那么大的帮助。第一点在于,延迟光照渲染使用了很多必须在场景整合后的屏幕空间效果,而且必须立体渲染。很多延迟渲染的应用,瓶颈在于带宽,而非像素着色器,所以增加一个新的渲染目标以及 Composition Pass 只会让问题麻烦。因此,我们在 UE4 上只为前置渲染器增加了平面渲染模式,目前也只针对移动设备。
这个 UE 4.12 的修改版本还没有直接被 Oculus 和 Epic 官方支持(在11月16日发布的 UE 4.14 中已正式支持),但我们鼓励你来试一下。我们也正在和 Epic 合作,希望日后能成为主版本的一部分(成功!)。
Unity 的混合平面渲染采用了和 UE4 版本基本一致的思路,但是完全通过 C# 和着色器来实现的。
用到了两个摄像机 — 一个进行近景的立体渲染,一个进行远景的平面渲染。远近景的分裂平面被用来限制渲染到合适的深度范围,我们依赖 Unity 的视锥体剔除来避免为视锥体外的物体进行绘制调用。
基本的步骤如下:
-
把远景部分渲染到纹理(平面摄像机)。
-
把近景部分用立体渲染 — 每只眼一次(左右眼立体摄像机)。
-
针对每只眼,通过 Unity 图像效果来把立体单眼图像放在平面图像之上。
这个平面渲染器是通过把摄像机的“Target Eye”设置为“Left”而非“Both”来实现的。摄像机物体位置经过调整,因此实际上是通过中间眼来进行渲染的。
这都已经打包成了一个可以取代 Oculus 摄像机的预制包。
此 Unity 的实现包并没有采用任何类似 UE4 下的过着色优化,毕竟我们的主要兴趣在于节省绘制调用。当然,那些东西之后都可以被整合进来。
下图显示了采用了混合立体/平面渲染后的包含了很多球体的合成测试场景的结果。
未开启平面渲染 — 10846 绘制调用,48.7 FPS
混合立体/平面渲染(平面区域被显示为红色) — 8583 绘制调用,97.7 FPS
当然显然这个场景不是那么常用的,主要展示了最好情况下的效果。当你把平面远景距离设为十米时,基本上无法区分出全立体和混合式场景的区别。
我们打算把这个作为 Unity 的一项内置功能,让所有开发者都能轻易使用,并会继续为所有引擎的立体渲染进行优化。