使用 RenderDoc Meta Fork 优化您的应用 - 第 1 部分
本指南将向您介绍 RenderDoc Meta Fork 的几个关键使用场景,以便您使用该工具来优化您的应用。这些场景有助于确保您以最有效的方式进行渲染,适应 Meta Quest 设备所用基于图块的渲染技术,并通过一些场景引导您验证自己的 CPU 和 GPU 渲染方法是否尽可能高效。
由于 VR 渲染的性质,确保绘制调用设置的最优化至关重要。在后台,引擎分别渲染两只眼睛。每只眼睛都有各自的模型-视图-投影矩阵,该矩阵由镜头设置限定的 IAD(轴间距离)所定义。许多人都知道 IPD(瞳孔间距离)设置会影响轴间距离。商业引擎中通常会为您处理这个问题,但您必须确保设置正确。以下是对每种渲染选项的简要概述:
单通道渲染 - 对于基础通道中的每个网格:
多通道渲染 - 对于基础通道中的每个网格:
- 绘制左眼(一次绘制调用) - 对于基础通道中的每个网格(再一次):
- 绘制右眼(一次绘制调用)
实例化立体渲染/多视图 - 对于基础通道中的每个网格:
如您所见,实例化立体渲染/多视图会将基础通道中的总绘制调用次数减半,因而是最高效且最佳的选项。
如果您想了解关于多视图的更多信息,可以查看以下部分相关资源:
使用纹理查看器检查输出/渲染纹理,确保资源名称标记为 RTTextureArray,而非 TempBuffer、ColorBuffer 或其他名称。要应用 MSAA 和/或节省固定注视点渲染量 (FFR),您必须直接写入交换链纹理。这同时将确保您不会意外地解析中间渲染目标/临时缓冲区,从而节省 1 到 1.5 毫秒的固定 GPU 解析成本。
当您写入正确的交换链纹理时,输出纹理名称为 XR Texture [#]。在本例中,您会看到选定的绘制调用位于名为“位块传输最终通道”的注释下。这意味着您要使用先前生成的渲染目标并将其用作临时缓冲区,然后再把它复制到交换链纹理。GPU 需要付出昂贵的解析成本,而仅将缓冲区数据映射到交换链纹理又要付出每像素成本。
这张截图展示了先前所示同一捕获帧中突出显示的基础通道绘制调用。相应输出渲染目标显示的标签为 _CameraColorTexture。这意味着引擎生成了一个中间渲染纹理,并在之后用作位块传输/复制的输入资源,这意味着解析成本,没有固定注视点渲染技术节省像素着色器,也错失了使用 MSAA 提升图像质量的机会。这表明您应该始终关注渲染目标,确保仅在单通道中直接渲染到交换链纹理。
在这张截图中,您可以看到 Unity 中的调试标记通过“解析阴影”标记显式注释了解析阴影贴图的过程。您还可以看到“位块传输最终通道”注释,看到将临时颜色缓冲区复制到交换链纹理的过程。
所有投射和/或接收阴影的网格都将需要从光源方向写入深度缓冲区方向的额外绘制调用。尽管没有针对这种绘制运行的片段着色器(这意味着它们的 GPU 开销要低于一般的着色绘制),但 CPU(绘制调用次数增加)和 GPU(顶点着色器投影所有网格几何图形以及深度阴影贴图解析)的开销依然会产生。
这张截图展示了 RenderDoc 中阴影贴图的外观。您会发现阴影通道中有一个用于稍后采样场景阴影的临时深度缓冲区。重要的是要验证总体阴影贴图分辨率,以及与每个级联相关的绘制调用次数。在几乎所有情况下,随着相机距离的增加和每增加一个级联,较接近的级联将具有较少的绘图调用。此外请注意橙色的解析阴影部分。当您将临时缓冲区存储在外部存储中时,这会是固定成本。如果必须使用阴影,请确保将分辨率和其他绘制调用保持在尽可能低的水平。
GPU实例化是图形 API/驱动程序级别的功能,可允许您调度单个绘制调用来渲染多个网格。对此必须遵守若干规则,包括共享完全相同的管线状态和网格输入。如果一个网格每帧使用了多次(子弹、装饰、颗粒和树叶等),则可以通过实例化网格来大幅减少绘制调用。请注意,这不会降低 GPU 的成本,因为您依然必须投影每个网格。
每当您调度一个实例化绘制,每个网格都会收到可以在着色器中使用的实例 ID。这样,您就可以将每个网格实例的 ID 与属性信息(如纹理索引、颜色或其他)相关联。举例来说,在赛车游戏中渲染大量人群时,您可以通过实例化并使用不同颜色的衬衫来绘制每个人,从而确保一种观众满满的感觉,同时不必为每个网格分别调度单独的绘制调用而产生过多的 CPU 开销。在这个基本示例中,您不会用到蒙皮网格,但您可以在 GPU 进行蒙皮/动画操作来实例化蒙皮网格。这是一种更先进的技术,但同样有着优点和缺点。背后的想法是,您会实例绘制共享的 T 形网格,然后使用实例 ID 查找并计算时间戳和姿势信息。这样可以完成混合形状查找,并在顶点着色器中进行蒙皮。或者,您可以提前在计算着色器中批量处理所有混合形状和蒙皮操作,然后使用实例 ID 查找当前绘制的网格的相关数据的位置。
旁注:GPU 实例化是实例立体/多视图渲染的工作方式。使用此方法的每个绘制调用为每个网格调度两个实例。两个网格映射中的每一个实例 ID 均绘制到相关的视觉缓冲区,而顶点着色器将用来索引到模型视图投影矩阵的一个阵列,从而正确地将网格投影到相关的眼部位置。
在上方截图中,您看到的不是十二个单独的 glDrawElements(36) 调用,而是 4x glDrawInstanced。glDrawElementsInstanced(36, 5) 不能一次绘制所有多维数据集的原因是,某些多维数据集违反了 Unity 中的批处理规则。要判断未对它们进行批处理的原因,您可以通过 Unity 中的帧调试工具来确定是否调用了 glDrawElementsInstanced(36, 12),从而将多维数据集的总绘制计数缩减为一次绘制调用。
这张 Unity 帧调试工具截图中选择了第二次实例绘制调用。Unity 在为什么本次绘制调用不能与上一次进行批处理部分中表示,多维数据集受不同的反射影响。要确保它们全部在一次绘制调用中完成,就应该对所有多维数据集使用单个反射探针。帧调试工具免除了方程式中的猜测,而您同时可以通过 RenderDoc 的“管线状态”选项卡中使用的调查方法来得出这个结论。只需检查并比较两个实例绘制调用之间的输入资源即可。
对于大多数 GPU,有一种优化方法可以让硬件拒绝遮挡的不透明像素(从前到后绘制)。例如在 VR 中,在大多数场景下,如果手不透明,则应首先渲染手。如果在绘制手(可能是最近接近相机的对象之一)时写入深度缓冲区,并分配 Z-检验以拒绝其后的像素,则硬件将足够智能,选择不为位于手后面的投影几何图形像素执行像素着色器。如果您使用了合理的几何图形计数,并且顶点着色器中没有执行任何复杂的操作,则像素着色器通常是渲染管线中开销最多的一环。
需要注意的重要一点是,由于 Meta Quest 窗口采用了高分辨率面板,并且您必须为双眼进行绘制,所以这里节省量的价值将会翻倍。在特定情况下,使用成本昂贵的像素着色器来为叶子提供一个深度预通道,亦即为 alpha !=0 像素设置深度缓冲区,然后再执行一次彩色通道,将深度测试设置为相等。使用这两种方法进行试验可能会对您的应用有所帮助。
下面是一个图片方面的示例:
这张截图展示了一个问题场景中的最终渲染目标。第一眼看上去一切都没问题,也许建筑物内部存在对象渲染,它们不仅使用了不必要的绘制调用而且在 CPU 端扰乱管线状态,并且通过投影所有内部对象的几何图形及遮盖所有不必要的投影来占用 GPU 资源。请查看下方截图,详细了解这种低效场景的具体情况。
带注释的绘制只是分配给 GPU 的所有不必要绘制调用的一小部分。这些绘制无缘无故地对所有几何图形进行处理和着色。如果 CPU 采取若干预防措施来遮挡剔除它们,则 GPU 将不必再对其处理。我们会在后面介绍更多关于遮挡剔除的细节。
我们可以这样做个类比,假设您聘请了一位油漆工来给您的房子刷漆,按时薪计费。如果这名工人决定先将所有框梁都涂成红色,接着给整个房子涂上一种纯色。尽管您已经告诉他要把房屋涂成一种颜色,但工人却把框梁都涂成了红色,而如果您依然要为他的这部分工作付费,相信您会感到非常不解和沮丧。在游戏和图形世界中,GPU 处理是您本可以花费在其他地方的资源,比如花在使用更细致的着色器/材质。为了方便比较,下一张截图展示了同一帧,但选择了在调度顺序中更靠后的绘制调用。
如前所述,当您在 RenderDoc 的事件日志中选择绘制调用时,帧会按时间顺序在您眼前构建。在选择这个绘制调用时,在上一张截图中的绘制后只进行了几次调用,您可以看到一堆墙体模型遮挡了先前绘制的所有内容。这意味着您先前调度的所有绘制调用均执行了其像素着色器并被覆盖(就像房屋上的红色框梁)。如果您在布景墙体和天花板之前绘制了这些对象,您本会将深度缓冲区设置为墙体网格的深度,节省每个像素被遮挡的片段着色器的成本,因为硬件可以判断较近的像素在遮挡它。如果您的游戏负担不起遮挡剔除,您依然可以通过为游戏量身定制优化的排序算法来节省 GPU 时间。Unity 和 Unreal 均允许覆盖渲染队列中的特定绘制和材质。
利用 RenderDoc,您可以验证输入和输出的纹理分辨率是否太高,是否提供了多级渐远纹理,压缩格式是否符合预期,每次绘制采样的纹理数量等等。您在引擎编辑器中很难跟踪所有纹理的纹理信息,所以这是一个节省内存、维持效率的优秀验证方法。
为了感受具体的差别,下面我们来看一个输入纹理:
在这张截图中,我们在“输入”选项卡中选择了这个 Ogre 绘制调用,然后选择 albedo 输入纹理时,可以看到每个输入纹理的分辨率、压缩格式和多级渐远级别数量。我们可以看到输入纹理使用的是 ASTC 纹理压缩格式,而这是获得最佳质量和大小的推荐格式。
大多数情况下,不应在 Meta Quest 上使用高动态范围 (HDR) 纹理格式。HDR 需要一个临时缓冲区,其格式与交换链纹理的格式有所不同,通常采用的是 R11G11B10_FLOAT 而非普通的 R8G8B8A8_SRGB 格式。问题不在于每像素位数,因为它们都是 32 位,而在于当帧准备就绪时将临时缓冲区从 HDR 复制并转换为标准格式的位块传输成本。由于需要更高的浮点精度,所以 HDR 效果和计算的成本通常也会昂贵得多。同样,任何时候使用临时缓冲区都不会获得采用固定注视点渲染或 MSAA 的好处,所以您可能会遭受额外的性能和/或质量损失。要确定渲染目标是否符合 HDR 规范,您可以确保自己正在写入采用 R8G8B8A8_SRGB 格式的 XR Texture [#] 交换链纹理,如以下对比截图所示。