这篇文章根据我们目前在游戏中使用 NVIDIA RTX 光线跟踪的经验收集了最佳实践。实用技巧被组织成简短的、可操作的项目,供今天从事光线跟踪工作的开发人员使用。他们旨在深入了解什么样的解决方案在大多数情况下会带来良好的性能。为了找到特定案例的最佳解决方案,我总是建议进行分析和实验。
本文中使用的常见缩写和短词:
AABB: 轴对齐边界框
AS :加速度结构
BLAS: 底层加速结构
Geometry: 和 BLAS 中的几何体
Instance: TLAS 中 BLAS 的实例
TLAS: 顶层加速结构
加速结构
本节重点介绍光线跟踪加速结构的构建和管理,这是将光线跟踪用于任何目的的起点。主题包括:
一般提示
建筑时最大化 GPU 利用率
内存分配
将几何图形组织为 BLAS
生成首选项标志
动态 BLASE
非不透明几何形状
粒子
一般提示
考虑异步计算作为构建。 特别是在混合渲染中, G 缓冲区或阴影贴图被光栅化,在异步计算的基础上执行可能是有益的。
考虑将工作线程生成为构建命令列表。 生成生成生成命令可能包括大量 CPU 端工作。它可以直接在编译调用中,也可以直接在一些相关任务中,如对象的剔除。将 CPU 工作转移到一个或多个工作线程可能是有益的。
TLA 的剔除实例。 通常,在 TLAS 中包括整个场景不是最佳的。相反,根据情况挑选实例。例如,考虑基于扩展的相机截锥体进行消隐。在光栅化中,最大距离通常可以小于远平面距离。在剔除时,还可以考虑实例大小,以便在较短的距离内剔除较小的实例。
对实例使用适当的详细程度( LOD )。 与光栅化一样,对所有内容使用最详细的几何 LOD 通常是次优的。用于远距离对象的 LOD 可以更简单。在混合渲染中,可以考虑使用相同的 LOD 进行光栅化和光线跟踪。这是避免自相交伪影(如曲面阴影本身)的有效方法。
还应考虑在光线跟踪中使用较低细节 LOD ,特别是为了降低动态 BLAS 的更新成本。如果光栅化和光线跟踪之间的 LOD 不匹配,则在光线跟踪中通常需要启用背面消隐,以防止自相交。有关光线跟踪中 LOD 的更多信息,以及如何实现随机 LOD ,请参见 使用 Microsoft DirectX 光线跟踪实现随机细节级别 。
尽可能将几何体或实例标记为不透明。 将实例或几何体标记为不透明允许不间断的硬件交叉点搜索,并防止调用任何命中着色器。尽可能做到这一点。只允许对需要的几何体使用任何命中着色器;例如,进行 alpha 测试。
尽可能使用三角形几何形状。 硬件擅长执行光线三角形相交。光线盒交点也会加速,但在跟踪三角形几何体时,您可以充分利用硬件。
建筑时最大化 GPU 利用率
批处理顶点变形和 BLAS 构建。 连续执行所有顶点变形调用,生成用作 BLAS 构建和所有 BLAS 构建调用输入的三角形。不要在连续通话之间设置资源障碍。这允许驱动程序在一定程度上并行调用。所有 BLAS 构建调用都需要唯一的暂存内存,以允许无障碍执行。
无需为每个资源持有区设置单独的无人机屏障。相反,在 TLAS 构建之前,您可以有一个单一的全球 UAV 屏障,以确保所有 BLAS 构建都已完成,无论它们位于何处。
考虑合并小顶点变形调用。 通常,为一个几何体或实例输出变形顶点的调用是轻量级的,即使在连续调用之间没有障碍的情况下执行,也不会填充整个 GPU 。将多个几何图形或实例的处理合并到一个调用中可以提高 GPU 利用率并产生更好的性能。
内存分配
汇集小额拨款: BLASE 可以很小,有时只有几千字节。使用单独的提交资源来存储每个这样的小 BLA 并不是最优的。相反,用更大的资源集中他们。池可以节省内存,通常可以提高性能。一种选择是在大型资源堆中使用放置的资源。
或者,通过手动从缓冲区中分配部分,可以将许多 BLAE 存储在单个缓冲区中。由于子分配只需遵循 256 字节对齐,因此这允许更紧密地将 BLASE 备份到内存中。无论采用何种池机制,都要避免内存碎片,以保持池带来的好处。
考虑压缩静态 BLAS: 压缩 BLASE 可以节省内存并提高性能。内存消耗的减少取决于几何形状,但可能高达 50% 左右。由于在 GPU 上完成 BLAS 构建后,需要将压缩大小读回 CPU ,这对于只构建一次的 BLAS 最为实用。请记住,要集中小的分配并避免内存碎片,以从压缩中获得最大的好处。
将几何图形组织为 BLAS
当实例的世界空间 AABB 中有很多空白空间时,考虑拆分 BLA 。 世界空间 AABB 用于测试光线是否可能击中实例并遍历其相关 BLA 。大量的空白会导致通过 BLAS 进行不必要的遍历。
独立移动的几何体通常应该在自己的 BLAS 中。将它们合并到单个 BLAS 中可能会导致一个具有大量空白空间的 AABB ,以及不必要的 BLAS 重建,而不是简单地更改独立实例的转换。
当实例世界空间 AABB 显著重叠时,考虑合并 BLASE 。 当实例的世界空间 AABBs 重叠时,穿过该区域的每条光线必须分别处理所有重叠的 BLAS 实例,以找到潜在的交点。遍历一个合并的 BLA 将更有效。
针对 BLAS 的跟踪性能不取决于其中几何体的数量。合并到单个 BLAS 中的几何体仍然可以具有独特的材质。
尽可能实例化 BLAS 。 实例化 BLASE 可以节省内存。它还可以提高光线跟踪性能。实例可以具有唯一的材质和变换。在实例的 AABB 重叠很多的情况下,尽管内存消耗增加,但将它们复制并合并为单个 BLA 作为多个几何体仍然是更好的选择。
避免几何形状中的细长三角形。 长而薄的三角形具有非最佳边界体积,具有大量的空白空间。它们很容易与许多其他边界体积重叠。当根据几何体跟踪光线时,这会导致非最佳性能。
驾驶员可以根据几何形状在一定程度上缓解问题。第一个三角形不太可能引起问题,但太多的三角形确实会引起问题,因此我建议尽可能避免它们;例如,将它们分割成较小的三角形。
不要在 TLA 中包含天空几何体。 天盒或天球将有一个 AABB ,与其他所有物体重叠,所有光线都必须进行测试。对于表示天空的几何体,在未命中着色器中处理天空着色比在命中着色器中更有效。
生成首选项标志
对于 TLA ,考虑PREFER_FAST_TRACE标志并仅执行重建。通常,这会产生最佳的整体性能。其基本原理是,无论场景中发生何种移动,尽可能高质量地制作 TLA 都很重要,而且成本不高。
对于静态 BLASE ,使用PREFER_FAST_TRACE标志。对于所有只构建一次的 BLASE ,优化最佳光线跟踪性能是一个简单的选择。
对于动态 BLASE ,请选择使用PREFER_FAST_TRACE或PREFER_FAST_BUILD标志,或者两者都不使用。对于偶尔重建或更新的 BLAE ,最佳构建首选项标志取决于许多因素。建造了多少?射线痕迹有多贵?可以通过在异步计算上执行构建来隐藏构建成本吗?为了找到特定情况下的最佳解决方案,我建议尝试不同的选项。
动态 BLASE
尽可能重复使用旧 BLA 。 如果您知道 BLAS 的顶点在上次更新后没有移动,请继续使用旧 BLAS 。
仅为可见对象更新 BLAS 。 当从 TLA 中剔除实例时,也将其剔除的 BLAS 从 BLAS 更新过程中排除。
考虑根据距离和大小跳过更新。 有时不需要在每一帧上更新 BLA ,这取决于它在屏幕上的大小。可以跳过某些更新,而不会引起明显的视觉错误。
在大变形后重建 BLASE 。 有限变形后的 BLAS 更新是一个不错的选择,因为它们比重建便宜得多。然而,先前重建后的大变形可能会导致非最佳光线跟踪性能。细长的三角形加剧了这个问题。
考虑定期重建更新的 BLASE 。 当几何体变形过大并且需要重建以恢复最佳光线跟踪性能时,可以进行检测。简单地定期重建所有 BLASE 是一种合理的方法,可以避免显著的性能影响,而不考虑变形。
在框架上分布重建。 由于重建比更新慢得多,因此在单个帧上进行的许多重建可能会导致口吃。为了避免这种情况,最好将重建分布在框架上。
考虑仅使用具有不可预测变形的重建。 在某些情况下,当几何体变形足够大和快速时,在构建 BLAS 时省略ALLOW_UPDATE标志并始终重建它是有益的。如果需要,可以考虑使用PREFER_FAST_BUILD标志来降低重建成本。在极端情况下,使用PREFER_FAST_BUILD标志会比使用PREFER_FAST_TRACE标志和更新产生更好的整体光线跟踪性能。
避免在 BLAS 更新中更改三角形拓扑。 更新中的拓扑变化意味着三角形退化或复活。如果退化三角形的位置不代表恢复三角形的位置,则可能导致非最佳光线跟踪性能。“弯曲”变形中偶尔发生的拓扑变化通常不会造成问题,但“断裂”变形中较大的拓扑变化可能会造成问题。
在可能的情况下,最好使用单独的 BLAS 版本,或对“破坏”变形引起的不同拓扑使用非活动三角形。当三角形的位置为 N A N 时,三角形处于非活动状态。如果这些替代方案不可行,我建议重建 BLAS ,而不是在拓扑更改后更新。更新中不允许通过索引缓冲区修改来更改拓扑。
非不透明几何形状
尽可能减少非不透明区域。 调用任何命中着色器(通常用于对非不透明三角形执行 alpha 测试)会中断硬件交点搜索。尽可能减少未标记为不透明的区域是提高性能的简单方法。使用更多的三角形更准确地定义非不透明区域可能是一个很好的权衡。
考虑拆分为不透明和非不透明几何体。 当定义良好的几何三角形部分可以被视为完全不透明时,可以考虑将其拆分为单独的几何体并将其标记为不透明。不同的几何形状仍然可以驻留在同一 BLAS 中。
粒子
考虑将公告牌粒子表示为三角形几何体。 在 BLASE 中表示广告牌粒子的一个选项是将广告牌输出为三角形,将广告牌的一部分沿垂直轴旋转 90 度到不同的方向。这允许利用三角形相交硬件,同时为粒子的视觉边界提供合理的近似。
考虑 alpha 测试而不是混合。 根据粒子类型,在二次光线中对渲染主可见性时混合的粒子进行 alpha 测试可能会提供合理的视觉质量。这种方法最适用于边界清晰的粒子。对于表示烟或雾等物体的粒子,这可能不适用。有关更多信息,请参阅 光线跟踪 《沃尔芬斯坦:年轻的血液》中的思考 。
避免对死粒子使用退化三角形。 更新 BLASE 中的退化三角形可能会使结构对于光线跟踪而言不是最优的。对于具有动态活粒子数的粒子系统,我建议考虑其他解决方案,例如使用正确的粒子数在每个帧上重建 BLAS 。
考虑将网格粒子表示为 TLA 中的实例。 对于渲染为三角形网格的粒子,每个粒子都有一个唯一的实例是一个合理的解决方案。当粒子在场景周围分布时,这是真实的,因此单个光线通常不会击中许多实例。实例应共享基础网格 BLAS 。此外,考虑压缩 BLAS 。
点击着色
本节重点介绍光线命中的着色。即使是经验丰富的图形开发人员在开始开发光线跟踪着色器时也可能会受益于新想法,因为最佳解决方案可能与光栅化中的不同。主题包括:
一般提示
最小化分歧
任何命中着色器
着色器资源绑定
内联光线跟踪
管道状态
一般提示
保持光线有效载荷较小。 寄存器用于保存有效负载值,它们减少了命中着色器可用的寄存器数量。我建议避免草率使用有效负载,尽管向包值中添加复杂代码很少有好处。
使用有效负载访问限定符。 此功能在 HLSL 着色器模型 6.6 中可用。它允许指定哪些着色器阶段写入或读取有效负载中的每个字段,并使编译器能够更好地优化寄存器使用,从而提高占用率和性能。为了获得最大的潜在效益,请尽可能准确地定义每个字段的限定符。有关更多信息,请参阅 GitHub 上的 DirectX-Specs 。
考虑将安全默认值写入未使用的有效负载字段。 当某些着色器不使用负载中其他着色器所需的所有字段时,仍然可以将安全默认值写入未使用的字段。这允许编译器在写入之前丢弃未使用的输入值,并将有效负载寄存器用于其他目的。
尽可能在第一次击中时终止射线。 当不需要解析正确的最近命中时(对于阴影光线),使用RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH或gl_RayFlagsTerminateOnFirstHitEXT标记光线是一种简单有效的优化。
仅当需要正确性时才使用面剔除。 与光栅化不同,启用背面或正面消隐不会提高性能。相反,它稍微减慢了光线遍历。仅当需要获得正确的渲染结果时才使用它们。
最小化光线跟踪调用的活动状态。 在TraceRay或traceRayExt调用之前初始化并在调用后使用的变量是活动状态,在调用命中和未命中着色器时必须在调用过程中保持这些状态。司机有几种不同的选择,但都有成本。
我建议尽量减少活动状态的数量。识别这样的变量并不总是微不足道的。 NVIDIA 和微软正在合作开发一种编译器功能,用于自动检测活动状态。
避免深度递归。 深度、非均匀光线递归可能代价高昂。
最小化分歧
对每个材质模型使用单独的命中着色器。 减少命中着色器中的代码和数据发散是有帮助的,尤其是在非相干光线的情况下。特别是,避免在材质模型之间手动切换的 U bershader 。在单独的命中着色器中实现每个所需的材质模型,为系统提供了管理发散命中着色的最佳可能性。
当材质模型允许使用统一的着色器而没有太多分歧时,可以考虑对具有各种材质的几何体使用公共命中着色器。
考虑简化着色。 通常,不需要复制用于渲染主要可见性的所有功能,以进行着色镜面反射或间接漫反射照明。忽略特征并不总是导致显著的视觉差异。或者,视觉效果的改善并不能证明渲染成本是合理的。光线越不相干,通常需要的主要可见性特征的复制越不准确。此外,随着命中距离的增加,着色有时可以进一步简化。
避免直接从顶点和像素着色器转换。 在命中着色中获得最佳性能的方法不同于光栅化的最佳方法。在光栅化中,即使代码差异很小,也可以使用单独的着色器置换。在命中着色中,减少单个命中着色器内的发散度和单独命中着色器的数量都很有帮助。通常,我不建议直接将顶点和像素着色器转换为命中着色器。
考虑将公共代码移到命中和未命中着色器之外。 当所有命中着色器都有一个公共部分时,我建议将该代码从命中着色器中移除;例如,到光线生成着色器。有时,命中着色器和未命中着色器中也可能存在常见代码,例如,当命中着色器中下一次反弹的近似值与未命中着色器中第一次反弹的近似值相同时。同样,我建议将该常见代码移到命中和未命中着色器之外。
任何命中着色器
更喜欢统一和简化的任何命中着色器。 在光线遍历期间,可能会大量执行任意命中着色器,并且它会中断硬件交点搜索。任何命中着色器的成本都会对整体性能产生显著影响。我建议在光线跟踪过程中使用统一且简化的任意命中着色器。此外, GPU 的完整寄存器容量不适用于任何命中着色器,因为它的一部分被驱动程序用于存储光线状态。
优化对材料数据的访问。 在任何命中着色器中,对材质数据的最佳访问通常至关重要。一系列相关内存访问是一种常见模式。加载顶点索引、顶点数据和采样纹理。在可能的情况下,从该路径中删除间接操作是有益的。
混合时,请记住未定义的点击顺序。 沿光线的点击被发现,并且相应的任何点击着色器调用以未定义的顺序发生。这意味着混合技术必须与顺序无关。这还意味着,为了排除最近的不透明命中之外的命中,必须适当限制光线距离。此外,可能需要使用NO_DUPLICATE_ANYHIT_INVOCATION标记混合几何体,以确保结果正确。有关更多信息,请参阅 光线跟踪宝石 中的第 9 章。
着色器资源绑定
如果可能,首选全局根表( DXR )或直接描述符访问( Vulkan )。 通常,光线生成和未命中着色器使用的资源可以像计算着色器一样方便地绑定,而不是通过着色器记录绑定。此外,不管命中了什么,通常也可以这样绑定使用的命中着色器资源。在所有命中记录中具有相同的资源限制不是最优的。
考虑 hit 着色器的无绑定资源。 无界描述符表( DXR )或无大小描述符数组( Vulkan )中的资源,由命中特定的系统值(如 InstanceIndex 或 gl _ InstanceID )或直接存储在命中记录中的值( DXR 中的根常数)索引,可以是向命中着色器提供资源的有效方法。
考虑索引和顶点缓冲区的根描述符。 ( DXR )作为无界描述符表的替代方法,可以高效地将索引和顶点缓冲区地址直接存储在命中记录中作为根描述符。当通过根描述符访问资源时,不会隐式执行越界检查。根描述符地址必须遵循四字节对齐。预计算到基址的 16 位索引的偏移量可能会破坏对齐。
尽可能使用根签名版本 1.1 和静态描述符。 ( DXR )根签名 1.1 允许驱动程序预期描述符是静态的;也就是说,在记录命令列表后,应用程序不会修改它们。这可以在驱动程序中实现一些潜在的有益优化,尤其是当根描述符不用于访问缓冲区时。与根描述符一样,越界检查不是用静态描述符隐式执行的。此外,静态描述符和根描述符都不能为 null 。
考虑在 GPU 上构建着色器表。 当有许多几何体和许多光线跟踪过程时,命中表可能会变大,上载它们可能会耗费大量时间。与其上载在 CPU 上构建的整个命中表,不如只上载每个帧上所需的新信息,例如当前可见实例的材质索引,然后在 GPU 上执行命中表构建过程以提高效率。
表构造中所需的大部分信息可以永久驻留在 GPU 内存中,例如命中组标识符、顶点缓冲区地址和几何体的偏移量。
内联光线跟踪
考虑螺纹组尺寸为 8 × 8 或更大。 作为计算着色器进行内联光线跟踪的经验法则,可以使用大小为 8 × 8 的线程组。通常,一组中的线程数是 GPU 波大小的倍数是有效的。 NVIDIA GPU 中的波形大小为 32 个线程。
然而,由于同时执行的组数量有限,使用只有一个波形的线程组限制了线程占用。一组中有两个波浪会使潜在占用率翻倍。着色器寄存器和组共享内存消耗也可以设置占用限制。当其他因素允许时,可以从三个波组开始达到最大线程占用率。
组大小的实际选择可以是 16 × 8 螺纹。将尺寸增加到远远超出此范围通常是没有好处的。通过不同尺寸的实验,可以发现针对特定情况的最佳尺寸。不同硬件代的最佳尺寸可能不同。
使用内联光线跟踪避免发散着色。 由于未基于命中调用命中着色器,因此所有着色都在投射光线的着色器中内联发生。在根据点击数选择的着色器中具有发散的代码路径或数据访问可能会减慢着色速度,尤其是在光线不相干的情况下。当需要多个不同的着色模型时,使用DispatchRays或vkCmdTraceRaysKHR是更好的选择。
使用 hit 特定的系统值进行内联光线跟踪的无绑定资源访问。由于命中记录中的绑定不可用,因此必须通过其他方式提供特定于几何体的绑定。基于特定于 hit 的系统值(如InstanceContributionToHitGroupIndex和GeometryIndex)访问无界描述符表中的资源是一种很好的做法。
我建议尽可能避免间接访问索引、顶点和材质数据。例如,基于系统值(如InstanceID)从缓冲区读取资源索引以选择索引缓冲区可能会导致难以隐藏的延迟。
首选编译时光线标志。 编译时和运行时光线标志都可以用于内联光线跟踪。我建议尽可能使用编译时标志,因为它们可以实现有益的编译时优化。
监视查询对象的寄存器消耗。 初始化后,当着色器执行可能继续遍历的代码时,查询对象必须保持光线遍历的状态。这会消耗寄存器,复杂的用户代码可能会比通常更快地限制占用。这种情况类似于在DispatchRays或vkCmdTraceRaysKHR过程中执行任何命中着色器。在使用查询对象之前初始化并在之后使用的变量可能会消耗额外的寄存器。
考虑线程组重新排序以提高一致性。 当使用来自计算着色器的内联光线跟踪时,调度线程组的默认行主分配到 GPU 执行通常不会产生最佳性能。通过手动重新排序线程组,可以提高在 GPU 上执行时线程组同时进行的内存访问的一致性。有关更多信息,请参阅 光线跟踪管道状态的并行着色器编译 。
管道状态
考虑每个光线生成着色器一个状态对象。 我建议为每个DispatchRays或vkCmdTraceRaysKHR调用使用该过程中所需的着色器编译一个单独的状态对象。它可以帮助优化寄存器消耗,并允许优化本文后面描述的管道配置值设置。
将MaxTraceRecursionDepth、MaxRecursionDepth、MaxPayloadSizeInBytes , 和MaxAttributeSizeInBytes设置得尽可能小。将这些值设置为高于必要值可能会对性能产生不必要的负面影响。在DispatchRays或vkCmdTraceRaysKHR调用中使用内联光线跟踪时,这些光线跟踪调用不计入最大递归深度。
尽可能使用SKIP_PROCEDURAL_PRIMITIVES, SKIP_AABBS , 和SKIP_TRIANGLES。这些管道状态标志允许在状态编译中进行简单但潜在有效的优化。
考虑使用着色器集合进行并行编译和共享。 ( DXR )当您管理多个着色器时,着色器集合可能允许多线程编译状态对象,并在状态对象之间共享编译代码。有关更多信息,请参阅 光线跟踪管道状态的并行着色器编译 。
当需要自动绑定点分配时,请考虑编译器选项。 ( DXR )默认情况下,编译着色器库时不使用着色器资源的自动绑定点指定。如果需要,有几个有用的编译器选项。首先,/auto-binding-space在给定寄存器空间中启用自动绑定点分配。此外,默认情况下,所有未标记关键字 static 的函数都被视为库导出。
使用/auto-binding-space时,任何导出函数访问的资源都会消耗绑定点,而不管它们是否在最终状态对象中使用。为了将绑定点消耗限制为真正需要的函数,可以使用/exports来限制库导出。
考虑AddToStateObject的增量构建。它允许基于现有对象增量构建状态对象,这在使用多个着色器管理动态内容时非常有用。
M 如果适用,每年管理堆栈。 使用 API 的查询函数来确定每个着色器所需的堆栈大小,并应用有关调用图的应用程序端知识来减少内存消耗并提高性能。
一个很好的例子是拍摄二次阴影光线的昂贵反射着色器,应用程序知道,二次阴影光线仅使用具有低堆栈要求的普通命中着色器。驱动程序无法提前知道此调用图,因此默认的保守堆栈大小计算会过度分配内存。
关于作者
Juha 是一名软件工程师,在实时图形方面有 15 年的经验。他曾在各种游戏、引擎和硬件基准测试中使用过尖端渲染技术。最近,他专注于 RTX 光线追踪在游戏中的应用。
审核编辑:郭婷
-
寄存器
+关注
关注
31文章
5290浏览量
119792 -
NVIDIA
+关注
关注
14文章
4847浏览量
102702
发布评论请先 登录
相关推荐
评论