科技魔方

Oculus研发分享:开发移动VR内容时应避免的PC渲染技术

AR/VR

2019年11月25日

  (映维网 2019年11月25日)有不少开发者都是以与PC相同的方式来开发Quest游戏,但这可能会导致优化性能方面出现大量困难。Oculus软件工程师特雷弗·达什(Trevor Dasch)的工作是帮助开发者构建高质量的游戏,并确保它们能够以稳定的72FPS速度运行。日前,达什撰文分享了在为移动VR开发内容时应该避免的PC渲染技术。下面是映维网的具体整理:

  尽管移动芯片组可以支持下面概述的大多数技术,但我们强烈建议你不要这样做。不过,这并不总是一成不变的规则,因为我有看到开发者有实现下述技术但依然达到帧速率的要求。但通过避免下文提及的PC渲染技术,你会为自己省下大量的麻烦。

  1. 延迟渲染

  延迟渲染(或延迟着色)这种技术是将光照/渲染计算推迟到第二步进行计算。这样做的目的是为了避免多次渲染同一个像素。延迟渲染主要分为两步:在第一步中,渲染场景,但只是简单地将几何信息(位置坐标,法线向量,纹理坐标和反射系数等等)存储在中间缓冲区中;在第二步中,从中间缓冲区读取信息,应用反射模型,计算出每个像素的最终颜色。延迟渲染对PC开发非常有效,因为它可以将几何图形与照明分离,你只需更新每个照明所触达的像素,即可在更少的GPU周期内渲染更多照明。

  为何不适合移动开发?

  原因有很多,但主要是因为解析成本。什么是解析费用?在我告诉你什么是解析成本之前,你首先需要理解基于图块渲染的工作原理。

  为了以更低功耗实现更高的吞吐量,移动GPU(如Oculus Quest中的骁龙835)通常采用基于图块的架构,其中每个渲染目标都分解为块状网格或“图块”(从16×16像素到256×256像素之间,具体取决于硬件和像素格式)。接下来,你的几何会“绑定”到图块,然后再提交给异步处理器,异步处理器执行渲染工作并计算每个“图块”的图像结果。计算完每个图块图像后,GPU必须从片上内存中将图块复制回通用内存。这实际上非常慢,因为它需要通过总线传输数据。我们将这个转移过程称为“解析”,所以花费的时间称为“解析费用”。

  因为要渲染的每个纹理都需要解析,并且延迟渲染需要在计算照明之前渲染大量纹理,所以你的解析成本将从正向渲染的大约1ms增加至3ms以上。如你所见,你可能没有这样的时间。

  除了解析成本问题之外,延迟渲染仅在几何形状复杂且有多个光源的情况下才能显露出优势。无论如何,移动设备都真正实现这两者,因为GPU用来处理大量顶点和计算计像素填充的能力有限。

  目前的答案是坚持采用正向渲染。

  2. Depth Pre-Pass

  Depth/Z Pre-Pass是一种常见的技术,其中所有场景几何图形都作为第一步渲染,无需填充帧缓冲区,仅生成深度缓冲区值。然后,你渲染第二通道,检查每个像素的计算深度是否等于深度缓冲区中的值。如果不是,则可以跳过对这一片段的着色处理。由于处理顶点的速度通常比像素着色快得多,所以可以节省大量时间。

  为何不适合移动开发?

  首先,如果在提交绘制调用之前对几何进行排序,通过进行Depth Pre-Pass而节省的片段填充时间应该最少。来回绘制会导致常规深度测试拒绝你的像素,所以你只能避免对未正确排序或两个对象在不同点彼此重叠的几何图形进行像素填充。

  其次,这需要绘制调用加倍,因为你必须先提交所有内容以进行Depth Pre-Pass,然后再才是Forward Pass。由于CPU的绘制调用已经相当沉重,所以你需要避免这种情况。

  第三,所有顶点都需要处理两次,与通过避免填充少数像素两次而节省的时间相比,你通常增加的GPU时间会更多。这是因为移动设备的顶点处理会在PC耗费更多的时间,并且处理片段的时间同样相对较少。

  3. HDR纹理

  解析成本与图像中的字节数直接相关,而不与像素数直接相关,所以,尽管我们通常以32位RGBA像素来思考量度,但如今大多数开发者都在使用HDR纹理,即每像素64位。这会将你的解析成本增加一倍,并且由于显示器仅支持每通道8位,所以你在解析HDR纹理时会浪费大量时间。更不用说移动GPU是针对32位帧缓冲区进行优化。

  4. 后处理

  后处理是一种经常用于为游戏施加多种效果的技术,如Color Grading,Bloom照明和运动模糊。具体的实现方式是获取游戏渲染的输出,然后对图像运行全屏通道以产生新图像,然后再将其呈现给玩家。一些后期处理效果是作为一个额外的通道执行(如颜色分级),另一些则需要多个通道。

  对于移动设备,后处理的主要问题同样是解析成本。生成第二张图像将引起另一次解析,并会立即消耗大约1毫秒的时间。更不用说计算后处理效果所花费的时间,取决于效果,这可能会占用大量资源。所以,最好避免进行后处理。

  以下是替代常见后处理效果的方案:

  Color grading:与其在后处理中执行Color Grading,不如在每个片段着色器的末尾添加一个函数调用以执行相同的数学运算。这将产生相同的视觉结果,但无需额外的解析。

  Bloom:真正的Bloom效果非常耗时。最好的选择是“伪造”。采用包含blob纹理的billboarding sprite可以产生非常接近的效果。

  5. 实时阴影

  我认为这是最有争议的一项技术。一系列具有完整实时阴影的应用已成功支持移动设备。但是,这样做存在大量的折衷,而我认为值得避免使用。

  实时阴影的一种常用技术是级联阴影贴图,这意味着场景会以各种视口大小进行多次渲染。对于必须由GPU处理的几何,这会令次数增加1到4倍,这从根本上限制了场景可以支持的顶点数量。它同时增加了阴影贴图纹理的解析成本(与纹理大小有关)。在GPU管道的另一端,在对阴影贴图进行采样时有两个选项:硬阴影和软阴影。硬阴影可以更快地渲染,但具有不可避免的锯齿问题。

  由于阴影贴图的工作方式,这个测试只能得出二进制结果。你无法对阴影贴图进行双线性采样,因为它表示深度值而非颜色值。应该避免使用软阴影,因为它们需要将多个采样放到阴影贴图中,而这当然很慢。最好的选择是烘烤所有可能的阴影,而如果需要实时阴影,请寻找另一种方法。如果照明大部分都是漫反射,则通常可以接受blob阴影。如果需要强光照明并且阴影表面是平面,则几何阴影的效果同样相当出色。

  6. 深度(及帧缓冲区)采样

  对于PC,你可以在着色器中采样当前的深度纹理(Unity将其显示为_CameraDepthTexture)。之所以可行,是因为深度纹理只是PC上的另一种纹理,并且由于每个绘制调用都接连发生,所以深度纹理的状态将是上一次绘制调用之后的状态。但对于基于图块渲染,当前深度不在纹理之中,而是仅存储在你的图块内存中,所以无法将其作为普通纹理进行采样。

  考虑到上述情况,有一个GLES扩展可允许你查询深度缓冲区(和帧缓冲区)的当前状态。问题是它们非常慢,只能支持你对相同像素的值进行采样(无法查询附近的像素),并且在启用MSAA时它们会产生一系列的问题。

  启用MSAA时,图块实际上具有一个足够大,能够容纳所有采样的缓冲区(即2×MSAA的像素为2倍,4×MSAA的像素为4倍)。这意味着默认情况下,如果对深度缓冲区进行采样,则必须按每个采样执行片段着色器,这意味着时间密集度将比预期高2倍或4倍。存在一种“解决方案”,即调用glDisable(FETCH_PER_SAMPLE_ARM)。但这样做的问题是,它将仅检索第一个采样的值,而不是混合采样的结果,所以在启用所述功能后,MSAA将被禁用。

  除非绝对必要,否则你应避免它们对帧时间产生的影响。

  7. 几何着色器

  几何着色器允许你在运行时生成额外的顶点,这对于诸如动态细分等功能十分有用。但是,对于基于图块渲染的GPU而言,几何着色器会产生问题。生成额外顶点的步骤阻止了合并过程的进行,这意味着GPU不能这样做,所以它会切换为“立即”模式(完全跳过分块过程)。可以猜到,这非常缓慢。所以,最好避免使用几何着色器,并且如果有必要,选择CPU生成顶点。

  8. Mirrors/Portals

  如果你用天真的方式实现它们……对于“天真的方式”,我是指分配两个眼睛缓冲区大小的纹理,计算反射矩阵,然后将场景渲染到两个纹理中。然后,你的mirror几何将进行屏幕空间纹理采样,从而显示反射。这种方法存在众多明显的缺陷:

  绘制调用增加了两倍。

  填充的像素比屏幕可见的像素要多。

  必须解析另外两个纹理。

  我发现的最低提升是限制了mirror camera的视口,并更改了相应的投影矩阵,只能在视锥中渲染camera平面边界框。这对上面的第二点问题有所帮助。理想情况下,你同时可以使用多视图,通过一组绘制调用来渲染左右眼,但Unity目前不支持这项功能,它不能解决上面的第三点问题 ,并且会令第二点问题更加恶化,因为你只能为两只眼睛使用单个视口,所以你必须使用两个mirror边界框的重叠。所以,理想的解决方案将首先解决第三点问题,这意味着一次绘制mirror场景和非mirror场景。

  有一种解决方案可以利用修改后的着色器和模板缓冲区。场景中的每种材质都将具有两种版本的着色器,一种仅在模板缓冲区中的特定位为0时绘制,而另一种仅在1时绘制。然后,你将使用材质绘制mirror网格。它会在模具缓冲区中设置所述位,使用第一组着色器绘制场景,使用反射矩阵设置camera,并且在最后使用第二组着色器绘制场景。这将产生你想要的反射,同时不会填充超出所需像素的像素,并且避免了不必要的解析。然而,它无法避免绘制一堆对象两次(任何解决方案都无法避免)。

  尽管这听起来很容易,但如果是使用Unity,你将会遇到很多问题(我在Unreal中没有遇到过,但你可能会遇到类似的挑战)。首先,在启用Single Pass Stereo(多视图)后,Unity将不允许你修改camera的投影矩阵,所以你不能使用反射camera(如果关心CPU性能,你绝对应该使用这个camera)。其次,这没有考虑Late-Latching(在渲染线程启动时更新camera矩阵,从而尽可能减少延迟)。通常来说,这是一次纯粹的胜利,但如果你使用mirror camera,则反射camera的变形将不再与头部变形匹配,所以你会得到奇怪的伪影,镜面中的元素不会按照预期方式排列。

  最简单的解决方案是“伪造”。如果你的镜面是静态,则只需创建所有世界几何的反射副本,然后将其放在场景中即可。你需要使用脚本来移动任何动态对象的“反射”副本,从而模仿包括玩家在内的“真实”版本位置,但这将是最快、最简单的渲染解决方案,无需复杂的矩阵数学。如果可以看到镜子后面,你将不得不使用两组具有不同模板蒙版的着色器,但如果玩家由于墙壁等原因而无法看到后面,则可以只保留一组着色器。

  9. 总结

  无论你是从零开始一个新项目,还是要从PC移植到移动设备,明确哪里可以沿用原有的知识经验,哪里又需要采取创新的解决方案是获得最佳游戏效果的关键。 但是,你不必遵循本文的建议。请自由探索,并寻找最适合自己的方案。

+1

来源:映维网

推荐文章