提示和技巧:光线跟踪最佳实践
Tips and Tricks: Ray Tracing Best Practices
本文介绍了在游戏和其他实时图形应用程序中实现光线跟踪的最佳实践。我们尽可能简短地介绍这些,以帮助您快速找到关键的想法。这是基于英伟达工程师在2019年GDC上所做的陈述。
通过修剪和选择性更新,优化加速结构(BLAS/TLAS)构建/更新最多需要2ms。
去噪RT效果至关重要。我们已经用NVIDIA RTX Deniser SDK打包了同类产品中最好的Deniser)。
使用异步计算队列将加速结构(BLAS/TLAS)的构建/更新和去噪,与其他机制(G缓冲区、阴影缓冲区、物理模拟)重叠。
尽可能利用硬件加速进行遍历。
最少的光线投射应该被称为“RT开启”,并且应该提供比光栅化明显更好的图像质量。提高质量水平应能以公平的速度提高图像质量和性能。见下表:
Performance Best Practices
1.0 Acceleration Structure Management
1.1 General Practices
作为管理(生成/更新)移动到异步计算队列。在图形工作负载中很好地使用异步计算队列对,并且在许多情况下几乎完全隐藏了成本。类似地,任何AS依赖项(例如蒙皮)也可以移动到异步计算并很好地隐藏。
构建顶级加速结构(TLAS),而不是更新。在大多数情况下,它更容易管理,而且改装的成本节约可能不值得牺牲TLA的质量。
确保GetRaytracingAccelerationStructurePrebuildInfo和BuildRaytracingAccelerationStructure的描述符匹配。否则,分配的缓冲区可能太小,无法容纳AS或scratch内存,可能会产生一个微妙的错误!
不要在TLA中包含skybox/skysphere。在场景中使用天空几何体只会增加光线跟踪时间。改为在Miss着色器中实现天空明暗处理。
在BLAS和TLAS构建之间实现单个屏障。一般来说,正确性不需要更多的要求。BLAS构建之间的重叠可以在硬件上自然发生,但是添加不必要的屏障可以序列化该工作的执行。
1.2 Bottom-Level Acceleration Structures (BLAS)
在AABBs上使用三角形。RTX图形处理器在加速由三角形几何创建的AS的遍历方面表现突出。
尽可能将几何体标记为不透明。如果几何体不需要执行任何命中着色器代码(例如,对于alpha测试),则始终确保将其标记为不透明,以便尽可能有效地使用HW。不透明标志是来自几何体描述符(D3D12_RAYTRACING_geometry_flag_OPAQUE/VK_geometry_OPAQUE_BIT)、实例描述符
(D3D12_RAYTRACING_instance_flag_FORCE_OPAQUE/VK_geometry_instance_FORCE_OPAQUE_BIT)还是通过光线标志(ray_flag_FORCE_OPAQUE/gl_RayFlagsOpaqueNV)并不重要。
批处理/合并生成/更新调用和几何图形非常重要。最终,在对小批量原语执行AS操作的情况下,GPU将被占用。利用一个构建可以接受多个几何描述符的事实,并在构建时转换几何体。这通常会导致最有效的数据结构,特别是当对象的aabb相互重叠时。将事物分组到BLAS/实例应该遵循空间局部性。不要“把所有有相同材料的东西扔进同一个BLAS中,不管它们最终在太空中的什么地方”。
知道何时更新,而不是(重新)构建。持续更新BLAS会降低其作为空间数据结构的效率,使遍历/交叉查询相对于新构建的查询要慢得多。作为一般规则,应该只考虑动态对象进行更新。如果部分网格相对于其局部邻域的位置发生剧烈变化,则更新后的遍历质量将迅速下降。如果事情只是“弯曲而不是断裂”,那么更新将非常有效。示例:树在风中摇摆:update=good;mesh exploding:update=bad。决定更新或重建蒙皮角色:这取决于。假设最初的构建是以t-pose的形式完成的,那么每次更新都会假设脚很近。在行走/跑步动画中,这可能会影响跟踪效率。这里的一个解决方案是建立几个关键姿势的加速度结构,然后使用最接近的匹配作为重新装配的来源。建议采用实验指导的流程/工艺。
对所有静态几何图形使用压缩。压缩速度很快,通常可以回收大量内存。当对压缩加速度结构跟踪光线时,性能没有下降。
Use the right build flags.
从下表中选择一个组合开始…
然后考虑添加这些标志:
ALLOW_COMPACTION。对所有静态几何体执行此操作通常是一个好主意,以回收(潜在的)大量内存。
对于可更新的几何体,压缩那些具有较长生存期的blas是有意义的,因此额外的步骤是值得的(压缩和更新不是互斥的!)。
对于每帧都重建(而不是更新)的完全动态几何体,使用压缩通常没有好处。
不使用压缩的一个潜在原因是利用BLAS存储需求随原始计数单调增加的保证—这在压缩上下文中不成立。
MINIMIZE_MEMORY (DXR) / LOW_MEMORY_BIT (VK)。仅当应用程序在如此大的内存压力下,如果不尽可能优化内存消耗,光线跟踪路径将不可行时才使用。此标志通常会牺牲构建和跟踪性能。并非所有情况下都是这样,但要注意,未来的驱动程序版本可能会有不同的行为,因此不要依赖实验数据“确认”标志不会降低性能。
2.0 – Ray-Tracing
2.1 – Pipeline Management
避免在关键路径上创建状态对象。集合和管道编译可能需要几十到几百毫秒。因此,应用程序应该预先创建所有pso(例如,在level load),或者在后台线程上异步创建状态对象,并在准备就绪时进行热交换。
考虑使用多个光线跟踪管道(状态对象)。当跟踪几种类型的光线(例如阴影和反射),其中一种类型(阴影)具有几个简单的着色器、小的有效载荷和/或较低的寄存器压力,而另一种类型(反射)涉及许多复杂的着色器和/或较大的有效载荷时,这尤其适用。将这些情况分为不同的管道有助于驱动程序更高效地调度着色器执行,并在更高的占用率下运行工作负载。
将负载和属性大小设置为可能的最小值。为MaxPayloadSizeInBytes和MaxAttributeSizeInBytes配置的值直接影响寄存器压力,因此不要将它们设置得高于应用程序/管道绝对需要的值。
将最大跟踪递归深度设置为可能的最小值。跟踪递归深度影响为DispatchRays启动分配的堆栈内存量。这会对内存消耗和总体性能产生很大影响。
2.2 – Shaders
2.2.1 – General
保持射线有效载荷小。有效负载大小转换为寄存器计数,因此直接影响占用率。像打包gbuffer那样打包有效负载通常需要一些数学知识。大量的有效载荷会溢出到记忆中。
保持低属性计数。与有效负载数据类似,自定义交集着色器的属性转换为寄存器计数,因此应保持最小值。固定函数三角形相交使用两个属性,这是一个很好的准则。
尽可能使用RAY_FLAG_ACCEPT_FIRST_HIT_和_END_SEARCH/gl_RayFlagsTerminateOnFirstHitNV。例如,这通常适用于阴影光线和环境光遮挡光线。请注意,使用此标志比显式调用AcceptHitAndEndSearch()/terminateRayNV()的any hit着色器更有效。
避免跨跟踪调用的实时状态。通常,在TraceRay调用之前计算并在TraceRay之后使用的变量必须溢出到堆栈中。编译器可以在某些情况下避免这种情况,例如使用重新物质化,但通常溢出是必要的。因此,材质球一开始越能避开它们,效果越好。在某些情况下,当阴影复杂度非常低并且没有递归TraceRay调用时,将一些活动状态放入有效负载中以避免溢出是有意义的。然而,这与保持有效载荷小的愿望相冲突,所以要非常明智地使用这个技巧。
避免着色器中的跟踪调用过多。着色器中的许多TraceRay调用可能会导致次优性能和着色器编译时间。尝试构造代码,使多个跟踪调用合并为一个。
明智地使用循环展开。如果所述循环包含跟踪调用(前一点的必然结果),则尤其如此。复杂的材质球可能会受到展开循环的影响,而不是从中受益。尝试HLSL中的[loop]属性或GLSL中的显式展开。
尝试无条件执行TraceRay调用。保持TraceRay调用不使用“if”语句可以帮助编译器简化生成的代码并提高性能。不要使用条件,尝试将光线的tmin和tmax值设置为0以触发未命中,并且(如果需要正确的行为)使用无操作未命中着色器以避免意外的副作用。
Use RAY_FLAG_CULL_BACK_FACING_TRIANGLES / gl_RayFlagsCullBackFacingTrianglesNV judiciously。与光栅化不同,光线跟踪中的背面消隐通常不是性能优化,它可以导致执行更多而不是更少的工作。
2.2.2 – Ray-Generation Shaders
确保每个光线生成线程生成一个光线。在最终不会产生任何光线的光线生成着色器中调度/分配线程可能会损害调度。这里可能需要人工压实。
2.2.3 – Any Hit Shaders
保持任何击中阴影极简。任何命中着色器在每一个TraceRay中执行多次(与最近的命中或未命中着色器相比,例如,后者只执行一次),这使得它们非常昂贵。此外,任何命中都在调用图中寄存器压力最高的点执行。所以为了获得最佳性能,尽可能让它们变得微不足道。
2.2.4 – Shading Execution Divergence
从一个简单的着色实现开始。当实现需要大量材质着色(例如反射或GI)的技术时,性能可能会受到着色散度的限制。这通常有许多原因,但不限于:指令缓存抖动和/或发散内存访问。采用以下策略来解决这些问题:
使用简化的着色器优化指令发散:
使用较低质量或简化的材质球(相对于光栅化)进行光线跟踪。
在某些极端情况下(例如:漫反射GI或粗糙镜面反射),可以从视觉上接受完全回落到顶点级别的着色(这也有减少噪声的附加好处)。
通过以下方法优化发散内存访问:
降低纹理访问的分辨率-或偏移mip贴图级别
将光线跟踪着色器中的照明计算推迟到帧中的稍后点
在极端情况下,可能需要手动安排(分类/装箱)阴影。当上述优化策略不足时,应用程序可以手动安排着色。但是,这会阻止基于driver/HW的调度生效。英伟达不断改进我们的日程安排。
2.3 – Resources
对场景全局资源使用全局根签名(DXR)或全局资源绑定(VK)。这将避免在本地每几何体根表中进行复制,并应导致更好的缓存行为。
避免资源临时性。这通常会导致非原语代码重复。例如,将一个纹理保留在一个临时的位置,并根据某些条件对其进行指定,将导致每个可能的纹理指定的所有采样操作重复。可能的解决方法:使用资源数组并动态索引到其中。
同时访问64位或128位对齐的本地根表数据可以实现矢量化加载。
对于对齐的原始数据,首选StructuredBuffer而不是ByteAddressBuffer。
3.0 – Denoisers
使用RTX去噪SDK实现高质量、快速的光线跟踪效果去噪。您可以在GameWorks光线跟踪页面找到更多详细信息。
4.0 – Memory Management
对于DXR,将QueryVideoMemory API报告的预算视为软提示。实际节段大小大约大20%。
将命令分配器隔离到不同类型的命令列表。如果可以避免,不要将非DXR-CAs与DXR-CAs混合和匹配。
命令分配器重置将不会释放关联的内存。可以使用destroy/create释放这些分配,但必须在关键路径之外执行此操作,以避免长时间暂停
注意管道的堆栈大小。堆栈大小随着TraceRay调用中保持的活动状态的数量和TraceRay调用周围的控制流复杂性而增加。最大跟踪深度实际上是堆栈大小的一个直接乘法器——尽可能保持低的深度。
手动管理堆栈(如果适用)。使用API的查询函数来确定每个着色器所需的堆栈大小,并应用关于调用图的应用程序端知识来减少内存消耗和提高性能。一个很好的例子是在trace depth 1上使用昂贵的反射着色器来拍摄阴影光线(trace depth 2),应用程序知道这些阴影光线只会命中堆栈要求较低的小命中着色器。驱动程序无法预先知道此调用图,因此默认的保守堆栈大小计算将过度分配内存。
重用临时资源。例如,为BVH构建重用scratch内存资源以用于其他目的(可能是非光线跟踪)。在DXR上,使用放置的资源和第2层资源堆。
5.0 – Profiling and Debugging
请注意以下工具,其中包括对DirectX光线跟踪和NVIDIA的VKRay的支持。它们发展很快,所以请确保使用最新版本。
NVIDIA Nsight图形。为光线跟踪开发人员提供了优秀的调试和分析工具(着色器表和资源检查器、加速结构查看器、范围分析、扭曲占用和GPU度量、通过NVIEW后的崩溃调试、C++帧捕获)。
NVIDIA Nsight系统。提供系统范围的分析功能和口吃分析功能。
微软PIX
FAQ
Q、 基本体的数量和加速结构构建/更新的成本(时间)之间的关系是什么?
A、 基本上是线性关系。好吧,它开始变得线性超过某个原始计数,在此之前,它被常数开销所限制。这里的确切数字在不断变化,不可靠。
Q、 假设最大占用率,加速结构构建/更新的GPU吞吐量SOL是多少?
A、 一个数量级准则是:对于完整构建,O(1亿)原语/秒;对于更新,O(10亿)原语/秒。
Q、 RT-PSOs的唯一着色器数量与编译成本(时间)之间的关系是什么?
A、 它大致是线性的。
Q、 现在游戏中RT-PSO编译的典型成本是多少?
A、 任何地方,20 ms→300 ms,每条管道。
Q、 有没有关于应该使用多少alpha/透明度的指导?任何命中和最近命中的代价是什么?
A、 任何打击都是昂贵的,应该使用最少。最好将几何体(或实例)标记为不透明,这将允许在固定功能硬件中进行光线遍历。当需要AH时(如评估透明度等),尽可能简单。不要仅仅为了执行alpha-tex查找和if语句而计算巨大的着色网络。
Q、 开发人员应该如何管理阴影差异?
A、 首先在最近的命中着色器中着色,在一个简单的实现中。然后分析perf并决定问题分歧的程度以及如何解决。解决方案可能包括也可能不包括“手动调度”。
Q、 开发人员如何查询堆栈内存分配?
A、 API具有在管道/着色器上查询每个线程堆栈需求的功能。这对于跟踪和分析非常有用,应用程序应该尽可能少地使用着色器堆栈(一个建议是在开发期间转储堆栈大小直方图和标记异常值)。堆栈需求最直接地受到跟踪调用的实时状态的影响,这应该最小化(请参阅最佳实践)。。
Q、 一个典型的光线跟踪实现需要额外消耗多少VRAM?
A、 今天,实现光线跟踪的游戏通常使用1到2 GB的额外内存。主要影响因素是加速结构资源、光线跟踪特定屏幕大小的缓冲区(扩展g缓冲区数据)和驱动程序内部分配(主要是着色器堆栈)。