这篇文章根据我们迄今为止在游戏中使用 NVIDIA RTX 光线追踪的经验,收集了最佳实践。我将这些技巧组织成简短的、可操作的项目,为今天从事光线跟踪工作的开发人员提供实用的技巧。他们的目标是提供一个在大多数情况下什么样的解决方案可以带来良好的性能。为了找到针对特定情况的最佳解决方案,我总是建议进行分析和试验。
以下是简短的缩写:
- 作为 — 加速结构
- TLAS 公司 – 顶层加速结构
- 布拉斯 ——底层加速结构
- AABB 。 – 轴对齐边框
- 实例 -TLAS 中 BLAS 的实例
- 几何学 —一个几何体和一个 BLAS
加速度结构
这是帖子中两个主要部分的第一部分。它侧重于光线跟踪加速结构的构建和管理,这自然是将光线跟踪用于任何目的的起点。
- 一般提示
- 构建时最大化 GPU 利用率
- 内存分配
- 将几何图形组织成 BLASE
- 生成首选项标志
- 动态布拉斯
- 非不透明几何体
- 粒子
一般提示
考虑 AS building 的异步计算。 特别是在混合渲染中, G-buffer 或阴影贴图被栅格化,在异步计算的基础上执行是非常有益的。
考虑生成命令列表时使用的工作线程。 生成为建筑命令可能包括大量的 CPU 边工作,比如对象的剔除。将其移动到一个或多个工作线程可能是有益的。
通常,包括 VZX54 在内的整个场景不是最佳的。相反,根据情况挑选实例。例如,考虑基于展开的摄影机视锥体进行剔除。在光栅化中,最大距离通常小于远平面距离。也可以在剔除时考虑实例大小,以便在较短距离内剔除较小的实例。
对实例使用适当的 LOD 。 与光栅化一样,使用最详细的几何体 LOD 通常是次优的。用于远距离对象的 LOD 可以更简单。在混合渲染中,可以考虑使用相同的 LOD 进行光栅化和光线跟踪。这是避免自相交伪影(如曲面阴影本身)的有效方法。在光线追踪中也可以考虑使用较低细节细节的细节细节细节,特别是为了降低动态 BLAS 的更新成本。如果光栅化和光线跟踪之间的细节层次不匹配,在光线跟踪中通常需要启用背面消隐以防止自相交。有关光线跟踪中 LOD 的更多讨论,以及如何实现随机 LOD 的解释,请参见 使用 Microsoft DirectX 光线跟踪实现随机细节级别 。
尽可能将几何图形或实例标记为不透明。 将实例或几何体标记为不透明,允许不间断的硬件交叉点搜索,并防止调用任何命中着色器。尽可能这样做。仅对需要的几何体启用任何命中着色器;例如,执行 alpha 测试。
尽可能使用三角形几何图形。 硬件在执行光线三角形相交方面表现出色。光线盒相交也会加速,但在跟踪三角形几何体时,可以最大限度地利用硬件。
构建时最大化 GPU 利用率
批处理顶点变形和 BLAS 生成。 连续执行生成三角形的所有顶点变形调用,这些三角形用作 BLAS building 的输入,以及所有 BLAS build 调用。不要在连续呼叫之间设置资源屏障。这允许驱动程序在一定程度上并行化调用。所有的 BLAS 构建调用都需要唯一的暂存内存,以允许无障碍地执行。此外,不需要为每个资源持有 BLAS 设置单独的 UAV 屏障。相反,您可以在 TLAS 构建之前拥有一个全球 UAV 屏障,以确保所有 BLAS 构建完成,而不管它们驻留在何处。
考虑合并小顶点变形调用。 通常,为一个几何体或实例输出变形顶点的调用是轻量级的,即使在连续调用之间没有屏障的情况下也不会填充整个 GPU 。在一个调用中合并多个几何体或实例的处理可以提高 GPU 的利用率并产生更好的性能。
内存分配
集中小额拨款。 blas 可以很小,有时只有几 KB 。使用单独的提交资源来存储每个这样小的 bla 不是最佳的。相反,将它们与更大的资源集中在一起。池可以节省内存,而且通常还可以提高性能。一种选择是在大型资源堆中使用放置的资源。或者,只需从缓冲区手动分配部分,就可以将许多 blas 存储在单个缓冲区中。这使得 BLASE 更紧密地备份到内存中,因为子分配只需要遵循 256 字节的对齐方式。不管采用何种池机制,都要避免内存碎片化,以保持池的好处。
释放或不分配关键路径上的资源。 资源分配 API 调用的成本可能变化很大,有时会非常高,导致明显的结巴。避免结巴的可靠方法是将调用从关键路径(即呈现线程)移到工作线程。这适用于分配和取消分配,即对创建的对象执行 CreateCommittedResource
和 CreateHeap
、 vkAllocateMemory
调用以及 Release
和 vkReleaseMemory
调用。呈现线程不应该等待线程进行分配调用。
考虑压缩静态 blas 。 压缩 blas 可以节省内存并提高性能。内存消耗的减少取决于几何结构,但最多可达 50% 。由于在 GPU 上完成 BLAS 构建后,需要将压缩后的大小读回 CPU ,因此对于只构建一次的 BLAS 来说,这是最实际的做法。记住,要集中小的分配并避免内存碎片,以便从压缩中获得最大的好处。
将几何图形组织成 BLASE
当实例的世界空间 AABB 中有很多空白空间时,请考虑拆分 BLAS 。 World space AABBs 用于测试光线是否可能命中实例并遍历其关联的 BLAS 。大量的空白空间会导致不必要的穿越 BLAS 。通常是他们自己的几何体。将它们合并到一个单独的 BLAS 中很容易导致 AABB 具有大量的空白空间,并且可能导致不必要的 BLAS 重建,而不是简单地改变独立实例的转换。
当实例世界空间 AABBs 明显重叠时,考虑合并 blas 。 当实例的世界空间 AABBs 重叠时, TLAS 变得不最优。然后,光线可以在空间中的卷中命中多个实例。然后需要遍历所有这些实例的 BLASes 以解决最近的命中。通过一个合并的 BLA 将更有效。针对 BLA 跟踪性能不取决于其中的几何图形数。合并到单个 BLA 中的几何图形仍然可以拥有独特的材质。
尽可能实例化 blase 。 实例化 BLASes 可以节省内存。它还可以提高光线跟踪性能。实例可以具有独特的材质和变换。在实例的 AABBs 重叠很多的情况下,尽管增加了内存消耗,但是作为多个几何体复制并合并到单个 BLAS 中仍然是一个更好的选择。
避免在几何图形中拉长三角形。 长 , 薄三角形具有非最优边界体积,且有大量的空空间。它们很容易与许多其他边界卷重叠。这导致在跟踪光线时,与几何体相对应的性能不最佳。驱动程序可以根据几何图形在一定程度上缓解问题。第一个这样的三角形不可能引起问题,但是太多的三角形确实会导致问题,所以我建议尽可能避免它们,例如将它们分割成更小的三角形。
不要在 TLA 中包含天空几何体。 一个 skybox 或 skysphere 将有一个与其他所有东西重叠的 AABB ,所有光线都必须根据它进行测试。对于表示天空的几何体,在 miss 着色器中而不是在 hit 着色器中处理天空着色更为有效。
生成首选项标志
对于 TLA ,请考虑 PREFER_FAST_TRACE
标志并仅执行重建。 通常,这会产生最佳的整体性能。其基本原理是,使 TLA 尽可能高质量,而不考虑场景中发生的运动,这一点很重要,而且成本不会太高。
对于静态 blas ,请使用 PREFER_FAST_TRACE
标志。 对于只构建一次的所有 blas ,优化最佳光线跟踪性能是一个简单的选择。
对于动态 blas ,请选择使用 PREFER_FAST_TRACE
或 PREFER_FAST_BUILD
标志,或两者都不使用。 对于偶尔重建或更新的 blase ,最佳构建首选标志取决于许多因素。建造了多少?射线痕迹有多贵?是否可以通过在异步计算上执行构建来隐藏构建成本?为了找到特定情况下的最佳解决方案,我建议尝试不同的选择。
动态布拉斯
尽可能重复使用旧的 BLAS 。 每当您知道 BLAS 的顶点在上一次更新后没有移动,请继续使用旧的 BLAS 。
仅更新可见对象的 BLAS 。 当从 tla 中剔除实例时,也要将它们剔除的 BLAS 从 BLAS 更新过程中排除。
根据大小考虑跳转。 有时不需要在每一帧上更新 BLAS ,这取决于它在屏幕上的大小。可以跳过一些更新而不引起明显的视觉错误。
在大变形后重建 blas 。 BLAS 更新是有限变形后的一个不错的选择,因为它们比重建要便宜得多。但是,上一次重建后的大变形可能导致光线跟踪性能不理想。拉长的三角形放大了这个问题。
考虑定期重建更新的 blas 。 当几何体变形过大,需要重建以恢复最佳光线跟踪性能时,可以很容易地进行检测。简单地周期性地重建所有 blas 是一种合理的方法,可以避免显著的性能影响,而不考虑变形。
在框架上分布重建。 由于重建比更新慢得多,因此在单个帧上进行多次重建会导致结巴。为了避免这种情况,一个好的做法是在框架上分布重建。
考虑仅使用具有不可预测变形的重建。 在某些情况下,当几何体变形足够大且足够快时,在构建 BLAS 时省略 ALLOW_UPDATE
标志并始终重建它是有益的。如果需要,可以考虑使用 PREFER_FAST_BUILD
标志来降低重建成本。在极端情况下,与使用 PREFER_FAST_TRACE
标志和更新相比,使用 PREFER_FAST_BUILD
标志可以获得更好的总体光线跟踪性能。
vz8 中的拓扑更新意味着三角形的更新。如果退化三角形的位置不代表恢复的三角形的位置,则可能导致非最佳光线跟踪性能。“弯曲”变形中偶尔发生的拓扑变化通常不会有问题,但“破坏”变形中的较大拓扑变化可能会出现问题。如果可能的话,最好使用单独的 BLAS 版本,或者对“破坏性”变形导致的不同拓扑使用非活动三角形。当三角形的位置为 NaN 时,该三角形处于非活动状态。如果这些替代方案不可行,我建议重新构建 BLAS ,而不是在拓扑更改后进行更新。在更新中不允许通过索引缓冲区修改进行拓扑更改。
非不透明几何体
尽可能减少非不透明区域。 对非不透明三角形调用任何命中着色器(通常用于执行 alpha 测试)会中断硬件交集搜索。如果可能,最小化未标记为不透明的区域是提高性能的简单方法。使用更多的三角形来更精确地定义非不透明区域可能是一个很好的折衷方案。
考虑拆分为不透明和非不透明几何体。 当一个定义良好的几何体三角形部分可以被视为完全不透明时,可以考虑将其拆分为单独的几何体并将其标记为不透明。不同的几何图形仍然可以驻留在同一个 BLA 中。
粒子
考虑将公告牌粒子表示为三角形几何体。 在 BLASes 中表示广告牌粒子的一个选项是将广告牌输出为三角形,将广告牌的一部分沿垂直轴旋转 90 度,使其朝向不同的方向。这允许使用三角形相交硬件,同时为粒子的视觉边界提供合理的近似值。有关更多信息,请参阅 “它就是能操作”:光线跟踪反射在“战场五号” , 2019 年游戏开发者大会。
考虑阿尔法测试而不是混合。 根据粒子类型,在二次光线中对渲染主可见性时混合的粒子进行 alpha 测试可以提供合理的视觉质量。这种方法最适用于边界清晰的粒子。对于代表烟或雾的粒子,这可能不适用。有关详细信息,请参阅 “沃尔芬斯坦:扬布拉德”中的光线追踪反射 。
避免对死粒子使用退化三角形。 更新 BLASes 中的退化三角形会使结构不适合光线跟踪。对于具有动态数量活动粒子的粒子系统,我建议考虑其他解决方案,例如用正确的粒子数在每个帧上重建 BLAS 。
考虑将网格粒子表示为 TLA 中的实例。 对于渲染为三角形网格的粒子,为每个粒子创建一个唯一的实例可能是一个合理的解决方案。当粒子分布在场景周围,因此单个光线通常不会命中多个实例时,这是正确的。实例应共享基础网格 BLAS 。另外,考虑压缩 BLAS 。
命中底纹
这篇文章的这一部分主要讨论光线命中的阴影。即使是经验丰富的图形开发人员在开始开发光线跟踪着色器时也可能从新的想法中受益,因为最佳解决方案可能与光栅化中的不同。
- 一般提示
- 最小化发散
- 任何命中着色器
- 着色器资源绑定
- 内联光线跟踪( DXR 1 . 1 )
- 管道状态
一般提示
保持射线有效载荷小。 寄存器用于保存有效负载值,它们会减少命中着色器可用的寄存器数量。我建议避免粗心地使用有效负载,尽管向打包值添加复杂的代码很少有益。
考虑向未使用的有效负载字段写入一个安全的默认值。 当某些着色器不使用负载中的所有字段(其他着色器需要这些字段)时,仍然向未使用的字段写入一个安全的默认值是有益的。这允许编译器丢弃未使用的输入值,并在写入有效负载寄存器之前将其用于其他用途。
如果可能,在第一次命中时终止射线。 当不需要解析正确的最近命中时,例如对于阴影光线,使用 RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH
或 gl_RayFlagsTerminateOnFirstHitNV
标记光线是一种简单而有效的优化。
只有在需要正确性时才使用面剔除。 与栅格化不同,启用背面或正面剔除不会提高性能。相反,它会稍微减慢光线的遍历速度。仅当需要获得正确的渲染结果时才使用它们。
最小化光线跟踪调用的活动状态。 变量在 TraceRay
或 traceNV call
之前初始化并在其处于活动状态之后使用,在调用命中着色器时需要在调用期间保持该状态。司机有几个不同的选择,但他们都有成本。我建议尽量减少活动状态的数量。识别这些变量并不总是小事。 NVIDIA 和微软正在合作开发一种编译器功能,用于自动检测活动状态。
避免深度递归。 深度的、非均匀的光线递归会很昂贵。
最小化发散
考虑统一的命中着色,但避免使用übershaders 。 当材质模型允许时,考虑统一各种几何体的着色,以允许使用常见的命中着色器。通常,减少命中着色器中的代码和数据分歧是有帮助的。尤其要避免手动在材质模型之间切换的übershaders 。当需要不同的材质模型时,我建议在单独的 hit 着色器中实现每个模型。这给了系统最好的可能性来管理不同的命中阴影。
考虑简化着色。 通常,不需要复制渲染主可见性中用于着色镜面反射或间接漫反射照明的所有功能。忽略特征并不总是会导致显著的视觉差异,或者视觉改进并不能证明渲染成本的合理性。光线越不相干,通常需要的主要可见性特征的复制就越不精确。此外,随着命中距离的增加,阴影有时可以更简化。
避免从顶点和像素着色器直接转换。 在命中着色中获得最佳性能的方法与光栅化的最佳方法不同。在光栅化中,即使是很小的代码差异,也可以使用单独的着色器置换。在命中着色中,减少单个命中着色器内的散度和单独命中着色器的数量都很有帮助。通常,我不建议直接将顶点和像素着色器转换为命中着色器。
考虑将通用代码移到命中和未命中着色器之外。 当所有命中着色器都有一个公共部分时,我建议将该代码从命中着色器移到光线生成着色器,例如。有时,在命中着色器和未命中着色器中也可能存在公用代码,例如,命中着色器中下一次反弹的近似值与未命中着色器中第一次反弹的近似值相同。同样,我建议将该通用代码移到命中和未命中着色器之外。
任何命中着色器
喜欢统一和简化任何命中着色器。 在光线遍历过程中,任何命中着色器都可能执行大量操作,它会中断硬件交叉点搜索。任何命中着色器的成本都会对整体性能产生显著影响。我建议在光线跟踪过程中使用统一和简化的任何命中着色器。另外, GPU 的全部寄存器容量对于任何命中着色器都不可用,因为驱动程序会消耗部分寄存器容量来存储光线状态。
优化对材料数据的访问。 在任何命中着色器中,对材质数据的最佳访问通常至关重要。一系列依赖内存访问是一种常见的模式。加载顶点索引、顶点数据和采样纹理。如果可能,从该路径中删除间接寻址是有益的。
混合时,记住未定义的命中顺序。 沿着光线的命中被发现,相应的任何命中着色器调用都以未定义的顺序发生。这意味着混合技术必须与顺序无关。这也意味着,要排除最近不透明命中之外的命中,必须适当限制光线距离。此外,您可能需要用 NO_DUPLICATE_ANYHIT_INVOCATION
标记混合几何体,以确保结果正确。有关详细信息,请参阅 光线追踪宝石 中的第 9 章。
着色器资源绑定
如果可能,首选全局根表( DXR )或直接描述符访问( Vulkan )。 通常,光线生成和未命中着色器使用的资源可以像计算着色器那样方便地绑定,而不是通过着色器记录进行绑定。此外,不管命中了什么,都可以像这样绑定命中着色器资源。在所有命中记录中使用相同的资源不是最佳的。
请考虑命中着色器的无绑定资源。 无限描述符表( DXR )或无大小描述符数组( Vulkan )中的资源,由特定于命中的系统值(如 InstanceIndex
或 gl_InstanceID
或直接存储在命中记录中的值( DXR 中的根常量)编制索引,可以有效地为命中着色器提供资源。
考虑索引和顶点缓冲区的根描述符。 ( DXR ) 作为无界描述符表的替代方法,直接将索引和顶点缓冲区地址作为根描述符存储在命中记录中是非常有效的。当通过根描述符访问资源时,不会隐式执行越界检查。根描述符地址必须遵循 4 字节对齐方式。预先计算到基地址的 16 位索引的偏移量可能会中断对齐。
尽可能使用根签名版本 1 . 1 和静态描述符。 ( DXR ) 根签名 1 . 1 允许驱动程序期望描述符是静态的;也就是说,在命令列表被记录之后,它们不会被应用程序修改。这将在驱动程序中启用一些潜在的有益优化,特别是当根描述符不用于访问缓冲区时。与根描述符一样,不隐式地使用静态描述符执行越界检查。此外,静态描述符和根描述符都不能为 null 。
不要使用无人机进行只读访问。 当着色器仅对给定资源执行读取操作时,将其绑定为 UAV 不会提供最佳性能。
考虑在 GPU 上构造着色器。 当有许多几何体和许多光线跟踪过程时,命中表可能会变大,上载它们会消耗大量时间。不是上传在 CPU 上构建的整个命中表,而是只在每个帧上上载所需的新信息,例如当前可见实例的材质索引,然后在 GPU 上执行命中表构造过程以提高效率。表构造所需的大部分信息可以永久地驻留在 GPU 内存中,例如命中组标识符、顶点缓冲区地址和几何图形的偏移量。
内联光线跟踪( DXR 1 . 1 )
使用统一的命中着色与内联光线跟踪。 由于命中着色器不是基于命中调用的,因此所有着色都在投射光线的着色器中内联进行。这意味着应用经典着色器优化实践。我强烈建议使用统一的 hit shading ,它允许使用一个公共代码路径处理不同的几何图形,并避免使用大量发散代码的übershader 。当需要多个不同的着色模型时,我建议使用 DispatchRays
。
使用特定于命中的系统值来访问具有内联光线跟踪的无绑定资源。 由于命中记录中的绑定不可用,必须通过其他方式提供特定于几何体的绑定。基于特定于命中的系统值(如 InstanceContributionToHitGroupIndex
和 GeometryIndex
)访问无界描述符表中的资源是一个很好的实践。我建议尽可能避免间接访问索引、顶点和材质数据。例如,根据系统值(如 InstanceID
从缓冲区读取资源索引以选择索引缓冲区可能会导致难以隐藏的延迟。
首选编译时光线标志。 编译时和运行时光线标志都可以与内联光线跟踪一起使用。我建议尽可能使用编译时标志,因为它们可以实现有益的编译时优化。
监视查询对象的寄存器使用情况。 初始化后,当着色器执行可能继续遍历的代码时,查询对象必须保持光线遍历的状态。这会消耗寄存器,复杂的用户代码可能会比通常更快地限制占用。这种情况类似于在 DispatchRays
过程中执行任何命中着色器。在使用查询对象之前初始化并在之后使用的变量可能会消耗额外的寄存器。
考虑重新排序线程组以提高一致性。 从计算着色器使用内联光线跟踪时,将调度线程组的默认行主分配给 GPU 执行通常不会产生最佳性能。通过手动重新排序线程组,可以提高线程组在 GPU 上同时执行的内存访问的一致性。有关重新排序的详细信息,请参见 使用线程组 ID 切换优化 L2 位置的计算着色器 。
管道状态
考虑每个光线生成着色器一个状态对象。 我建议为每个 DispatchRays
或 vkCmdTraceRaysNV
调用使用该过程中所需的着色器编译一个单独的 state 对象。它有助于优化寄存器消耗,并允许对本文后面描述的管道配置值进行优化设置。
将 MaxTraceRecursionDepth
、 MaxRecursionDepth
、 MaxPayloadSizeInBytes
和 MaxAttributeSizeInBytes
设置为尽可能小。 将这些值设置为高于所需的值可能会对性能产生不必要的负面影响。在 DispatchRays
或 vkCmdTraceRaysNV
调用中使用内联光线跟踪时,这些光线跟踪调用不计入最大递归深度。
尽可能使用 SKIP_PROCEDURAL_PRIMITIVES
和 SKIP_TRIANGLES
。 ( DXR 1 . 1 )这些管道状态标志允许在状态编译中进行简单但可能有效的优化。
避免在关键路径上创建状态对象。 状态对象编译可能很慢。因此,请预先创建状态对象;例如,在级别加载期间或在工作线程上异步创建状态对象。
考虑使用着色器集合进行并行编译和共享。 ( DXR ) 管理多个着色器时,着色器集合可能允许多线程编译状态对象并在状态对象之间共享已编译代码。有关详细信息,请参见 光线跟踪管道状态的并行着色器编译 。
当需要自动指定绑定点时,请考虑编译器选项。 ( DXR )默认情况下,编译着色器库时不使用着色器资源的自动绑定点分配。如果需要的话,有几个有用的编译器选项。首先, /auto-binding-space
在给定的寄存器空间中启用自动绑定点分配。此外,默认情况下,所有没有用关键字 static
标记的函数都被视为库导出。使用 /auto-binding-space
选项时,所有导出都可以使用绑定点,而不管它们是否在最终状态对象中使用。要将绑定点消耗限制为真正需要的函数,可以使用/ exports 选项来限制库导出。
考虑使用 AddToStateObject
进行增量构建。 ( dxr1 . 1 ) dxr1 . 1 为状态对象编译引入了一个新选项。它允许基于现有对象增量构建状态对象,这在使用许多着色器管理动态内容时非常有用。
手动管理堆栈(如果适用)。 使用 API 的查询函数确定每个着色器所需的堆栈大小,并应用有关调用图的应用程序端知识来减少内存消耗并提高性能。一个很好的例子是昂贵的反射着色器来拍摄次阴影光线,应用程序知道这些着色器只使用堆栈要求较低的普通命中着色器。驱动程序不能预先知道这个调用图,所以默认的保守堆栈大小计算过度分配内存。
工具
考虑实施热图。 为了发现与特定的 BLASE 或特定几何图形的阴影相关的性能问题, NVIDIA 提供了一个方便的 API 来实现热图,以便可视化每个像素的处理成本。光线跟踪在提高过程性能方面非常有用。有关详细信息,请参见 使用计时器检测分析 DXR 着色器 。
使用 NVIDIA Nsight 图形进行分析和调试。 有关检查加速结构、着色器表和分析光线跟踪过程的详细信息,请参阅 Nsight 图形 详细信息页。
有关如何最有效地使用 Nsight 图形的深刻建议,请参阅以下文章:
考虑更新到 Microsoft Shader 编译器的最新版本。 ( DXR )偶尔会提供带有新功能和优化的 Microsoft 着色器编译器的更新版本。更新到 DirectXShaderCompiles GitHub repo 中的最新版本通常是值得的。