新闻中心

NVIDIA OptiX 着色器绑定表优化实现高效光线追踪 NEWS DETAIL

当前位置:首页 > 新闻中心
资讯分类 · 新闻中心 作者 · 超级管理员 审核人 · 中科新远技术内容组 发布时间 · 2025-07-11 更新时间 · 2026-07-23 来源 · 本站

NVIDIA OptiX 着色器绑定表优化实现高效光线追踪 NEWS DETAIL

NVIDIA OptiX 是通过 CUDA 实现 GPU 加速光线追踪的 API,通常用于渲染包含各种物体和材质的场景。在 OptiX 启动期间,当光线与几何基元相交时,系统会执行命中着色器。着色器绑定表 (Shader Binding Table, SBT) 回答了为给定的交集执行哪个着色器的问题。SBT 还可用于将输入数据映射到着色运算。

本文介绍了在应用中布局 Shader Binding Table(SBT)的几种不同方法,以及着色器访问其数据的不同方法。通过尽可能减少 SBT 和着色数据,您可以节省内存、提高性能并简化 SBT 本身的管理。

着色器绑定表设计模式

光线追踪应用通常会为每个网格对象存储两种主要类型的数据:几何信息 (例如着色法线) 和材质参数 (例如漫反射或粗糙度参数)。材质着色器会访问这些数据,以执行计算,例如当前交叉点的光照。

以下各节将介绍使用 SBT 和实现数据查找的两种方法。第一种方法可直接利用 SBT 来存储着色器程序和查找数据。它易于实施,但显存使用效率低下。第二种方法对这种基本方法进行迭代,以实现更高效的布局,减少 SBT 记录和着色数据中的冗余,从而提高效率。

这两种设计针对的是场景层次结构的常见情况,其中许多实例共享一组材质。出于说明目的,我们假设所有几何图形都使用 OptiX 内置三角形相交,并且应用程序使用单一光线类型。这些约束易于放松,我们将在下文中详细讨论。

朴素方法:每个实例的 Geometry 和 material properties

将着色器和数据映射到实例的最简单方法是在每个实例 (每种光线类型) 的单独 SBT 记录中明确存储对所有着色器和数据的引用。着色器本身在 SBT 记录的标题部分中引用,而对几何数据和着色参数的引用存储在用户定义的数据块中。可以直接存储小参数。

例如,路径追踪渲染器支持简单的漫反射和光泽材质,并通过每个顶点法线执行平滑着色。请注意,诸如顶点属性之类的重量级数据存储在单独的全局内存分配中,并且只有对数据的引用直接存储在数据块中。图 1 显示了每个组件的关联数据。

Diagram showing three fundamental data blocks. From left to right: Per-Vertex, Per-Glossy Material, and Per-Diffuse Material.

图 1、示例设置中的基本数据块


以下结构将在 SBT 记录中容纳此内容:

struct ShadingParams {
    Float3* normals
    Float3 reflectance
    Float roughness
}

请注意,反射率参数可以在漫反射和光泽材质之间共享,但即使不需要,也必须存储粗糙度参数。这是这种方法的缺点。data 部分的大小必须至少等同于所有材质中设置的最大参数的占用空间。

我们来看看应用于假设场景的布局:

  • 100000 个实例

  • 50000 个唯一三角形网格 (GAS)

  • 两个材质着色器 (如前所述,光泽着色器和漫反射着色器)

  • 10000 个唯一材质参数集

由于某些对象可能共享相同的材质描述,因此材质的数量通常小于实例的数量。

Diagram showing simple data layout with inlined SBT data segments.


图 2、在 SBT 数据片段中内联数据的简单布局


此方案所需的存储空间为实例数量乘以 SBT 记录的大小。在本例中,SBT 数据部分大小为 24 字节;但是,16 字节对齐会导致四舍五入高达 32 亿。根据 API 的要求,报文头部分始终为 32 字节。这样一来,着色数据总开销约为 6 MB,相当于 SBT 命中组列表的总大小。

这种方法占用大量内存,会多次存储相同的 SBT 标头和数据部分。这种内存肿现象不仅占用宝贵的 GPU 存储空间,而且可能会由于内存访问不一致而导致 GPU 端性能下降,并导致填充和维护超大 SBT 阵列的主机端用度。但是,对于简单的应用和场景设置,这种技术仍然是一个合理的选择。

另外,请注意,通过将所有着色和几何数据放入全局分配中,并只需在记录中存储数据指针,数据部分始终可以保持 16 字节的最小非零大小。但是,这会产生额外指针和额外内存间接的存储成本。

优化方法:通过远离每个实例存储来减少冗余

在本节中,我们将通过优化 SBT 和数据布局来缓解这些问题。减少 SBT 和所有着色数据的内存占用的关键是利用冗余。

首先,完全删除 SBT 记录的 data 部分。而是将着色数据存储在全局内存中,并使用 OptiX 层次结构的相关知识进行访问。在每个 GAS 的单个 SBT 记录的情况下,这种映射是微不足道的。着色参数移动到全局内存中的数组中,场景中存在的材质参数和几何数据的每个独特组合都对应一个条目。

此数组中的 ShadingParams 条目数量每个实例最多一个,但实际上可能要小得多,因为给定网格的多个实例通常具有相同的材质参数。然后,场景中的每个实例都将其实例 ID 设置为着色参数的数组索引。

图 3 显示了数据的当前外观。所有着色数据均移动到一个单独的全局数组中。阵列的大小由几何图形和材质参数的独特组合数量决定。

Diagram of global memory array with ShadingParams elements.

图 3、用于着色参数的全局数组,与 SBT 分离


您可以在设备代码中访问当前的着色数据:

ShadingParams& params = shading_data_array[optixGetInstanceId()]

请注意,实例 ID 不需要是唯一的。如果多个实例共享相同的着色和几何数据,它们可以共享用于索引到参数列表的相同实例 ID。

接下来,解决几何参数的冗余存储问题。当给定的 GAS 被实例化多次时,其着色法线数组的引用会被存储多次——朴素布局中的每个实例和当前方法中设置的唯一几何/着色参数分别存储一次。理想情况下,每个 GAS 只应存储一次法线,因为它们不会因实例而异。

可以使用 OptiX 8.1 中引入的新 optixGetGASPointerFromHandle 函数 轻松 实现对每个 GAS 数据的存储。此函数检索与给定 GAS 关联的二进制加速结构数据的地址。因此,在 GAS 构建时分配内存时,应用程序可以在此加速结构数据前加上任意用户数据块。设备函数可以通过调用 optixGetGASPointerFromHandle 并减去用户数据段的大小来检索用户数据。

现在,数据的组织方式如下:

MaterialParams {
    float3 reflectance
    float roughness
}
GeometryParams {
    float3* normals
}

图 4 显示了此新方案的内存布局。现在,几何着色参数与其各自的几何网格相关联。全局着色数据阵列的大小现在仅取决于唯一材质参数的数量。

Diagram of geometry shading parameters associated with respective geometry meshes.

图 4、现在,几何着色参数与其各自的几何网格相关联


这不仅可以消除每个唯一 GeometryParams 的多个存储,还可以减少材质参数中的冗余,因为参数的唯一性不再需要一组唯一的材质和几何参数。

最终优化消除了 SBT 标头中材质程序的冗余存储。只有两组独特的材质程序,一组用于光泽,另一组用于漫反射,但这些程序会多次存储,每个实例一次。我们无需为每个实例存储单个 SBT 条目,而是可以为每种材质类型存储单个 SBT 记录 – 在本例中,为两种。现在,每个实例的 SBT 偏移量都设置为 0 或 1,具体取决于其是否为漫反射或光泽。

图 5 显示最终数据布局。请注意,几何图形和材质参数布局与图 4 相同。

Reduced SBT showing Header (32 bytes) and Opaque Shader Handle.

图 5、SBT 仅取决于场景中唯一着色器的数量

着色数据的最终数据用法如下:

num-GASs*sizeof(GeometryParams) + num-unique-material-instances*sizeof(MaterialParams) + num material-shaders*OPTIX_SBT_RECORD_HEADER_SIZE

这远低于 1 兆字节。请注意,存储大小与场景中实例的数量完全无关。对于包含大量实例和着色器且每个都有数十或数百个参数的复杂真实场景而言,这可能是一场巨大的胜利。

扩展替代着色设置 

OptiX 内联函数提供对场景状态的访问,与受限示例相比,可实现更复杂的数据查找。这些状态查询函数包括:

  • optixGetSBTDataPointer:当前基元的 SBT 记录数据块的地址。这就是使用[Equation 1]通过不透明记录报文头的大小偏移量计算出的地址。

  • optixGetInstanceId:应用在创建当前实例 (OptixInstance::instanceId) 时分配的 ID。此值不需要跨实例唯一,可以在[0 – 2^28) 范围内任意选择。

  • optixGetInstanceIndex: 返回实例加速结构中基于零的索引。

  • optixGetPrimitiveIndex:当前相交三角形、球体、曲线或自定义几何图形的基元索引 。有关详情,请参阅 OptiX 编程指南 

  • optixGetSbtGASIndex: 当前 GAS 中的 SBT 偏移量 (由构建输入的 sbtIndexOffsetBuffer 指定)。

  • optixGetGASPointerFromHandle:此选项用于检索设备内存中几何加速结构开头的地址 。然后,应用程序可以在 GAS 内存之前按几何图形分配数据块。

着色设置示例 

本节提供了一些着色设置示例,以及处理这些设置的方式。

多种几何类型 

改进后的布局可以轻松适应多种不同类型的几何图形,但 SBT 条目的数量现在取决于几何图形类型和材质着色器的组合数量,因为命中组通过相交程序封装几何图形,并通过命中程序封装材质。对于三种几何类型和两种材质,最多可以有六个 SBT 命中组条目。

多种光线类型 

光线类型的数量也是 SBT 命中组条目数量的乘数。即使是真实示例,光线类型、几何类型和材质类型的乘积通常也在 10 到 100 之间。

单个气体内含多种材质 

有多种方法可以处理此情况,但一种简单的方法是创建辅助查找表,以重定向到着色器参数索引。此表可以通过实例 ID 和 SBT GAS 索引进行索引。

总结 

NVIDIA OptiX 库提供了将着色器和数据绑定到光线追踪应用的灵活机制。使用着色器绑定表在实例级别存储和检索数据是一种简单的方法,适用于简单的应用。但是,避免冗余绑定和数据存储的更优化的方法可以提高性能并避免内存浮肿。