Vulkan_Ray Tracing 06_着色器绑定表

。本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。

一、Shader Binding Table

1.1 SBT概述

首先我们来简要的说一下SBT:

SBT: 包含了在光线追踪场景中可能被调用的 所有着色器集合 ,以及那些可能被传给shader的嵌入式参数。其实SBT就是一个array, array里面每个槽是一个ShaderRecord。每个ShaderRecord里装了这些东西: shader句柄, 若干函数句柄,函数输入参数。

ShaderRecord有以下几种类型:

  • RayGenerationRecord :其中的shader句柄指向一个RayGenerationShader;

  • HitGroupRecord:里面装着3个函数句柄,分别对应3个shader: Intersection Shader / Any-Hit Shader / Closet-Hit Shader;

  • MissRecord:里面装着shader句柄指向一个MissShader。

如下图所示:
在这里插入图片描述
图里右边那个数组就是上文的 array,里面装着 ShaderRecord,HitGroupRecord的具体计算公式:

在这里插入图片描述

示例图中上半部分,有两个BLAS,这个其实两个instance(可以理解为一堆geometry的集合)。vulkan会给每个instance分配一个BLAS结构。图中那个兔子是一个geometry,茶壶和自由女神像分别是一个geometry,这只是一个示范,有两个instance,第一个instance中有1个geometry,第二instance中有两个geometry;其中每个geometry可以理解为一个模型,里面有一堆三角形数组。

  1. I_offset: 的含义就是instance编号,从0 开始 ;
  2. G_ID: 是 geometry在它所在的instance中的序号(0开始) ,
  3. R_stride和R_offset : R表示ray是光线的意思,R_offset 就表示是哪个类型的光线;或者叫光线的类型序号,R_stride 是光线类型数。
    VulkanRaytracing支持多个不同类型的光线,比如从视点发出的光线和从某个光源发出的光线;那么就是2种光线类型,则R_stride = 2,R_offset 分别为 0 和 1 。

最后的公式已经列出来了,其实就是 根据这些这些参数,vulkan会根据这个公式算出HitRecord的位置(这个计算逻辑是写死的),然后让对应的geometry 用这个HitRecord来shadering。

但是这些参数是用户通过vulkanAPI输入的,所以用户必须要保证按照这些参数和这个公式算出来的index对应的HitRecord是你想要的HitRecord, 而这个HitRecord也是通过API配置的。

1.2 SBT代码实现

在光栅化管线的设置中,绘制相应对象之前会绑定当前着色器及其关联资源,然后可以为某些其他对象绑定另一个着色器和资源集,依此类推。由于光线追踪可以随时击中场景的任何表面,因此所有着色器必须同时可用。

着色器绑定表其实就是是光线追踪过程的“蓝图”。它允许我们选择使用从哪个光线生成着色器开始,如果没有找到交点的时候,它会控制哪个未命中着色器要执行,以及可以选择对每个实例执行哪些命中着色器组。实例和着色器组之间的这种关联是在设置几何体时创建的:对于每个实例,我们hitGroupId在 TLAS 中提供了一个。该值用于计算与该实例的命中组对应的 SBT 中的索引。每个实例组之间所需的步幅计算通过下边几个参数来确定:

  • PhysicalDeviceRayTracingPipelinePropertiesKHR::shaderGroupHandleSize
  • PhysicalDeviceRayTracingPipelinePropertiesKHR::shaderGroupBaseAlignment
  • 用户提供的shaderRecordEXT数据的大小(可自定义设置,在本例中未使用)。

1.2.1 SBT句柄对齐

SBT 是一个最多包含四个数组的集合,其中包含光线追踪管线中使用的着色器组的句柄。每个数组用于光线生成、未命中、命中和可调用(此处未使用)着色器组。在示例中,我们将创建一个缓冲区来存储前三组的数组,且每种类型只有一个着色器,因此每个“数组”只是一组着色器的句柄。

缓冲区将具有以下结构,并且在调用时vkCmdTraceRaysKHR函数时将使用该结构:
在这里插入图片描述

我们将确保所有起始组都以对齐shaderGroupBaseAlignment的地址开始,并且组中的每个实例都与shaderGroupHandleAlignment字节对齐。所有组都与shaderGroupHandleAlignment字节大小对齐。

内存大小和字节对齐问题
SBT创建时需要特别注意shaderGroup大小和shader指针字节对齐问题。Vulkan自身不能保证shader指针或组大小对齐,所以我们必须手动向上对齐。使用groupHandleSize作为 stride步长
可能恰好适用于你的硬件,但并不能保证所有硬件都适用。在句柄大小小于对齐的硬件上,可以在不使用额外内存的情况下插入一些 shaderRecordEXT数据。

所以需要使用以下公式将大小四舍五入到下一个对齐方式:

 template < class integral >
constexpr integral align_up(integral x, size_t a) noexcept
{
  return integral((x + (integral(a) - 1)) & ~integral(a - 1));
}

特殊情况
RayGen 大小和步幅需要具有相同的值。

我们首先在HelloVulkan类中添加SBT创建方法和SBT缓冲区本身的声明:

void           createRtShaderBindingTable();

nvvk::Buffer                    m_rtSBTBuffer;
VkStridedDeviceAddressRegionKHR m_rgenRegion{};
VkStridedDeviceAddressRegionKHR m_missRegion{};
VkStridedDeviceAddressRegionKHR m_hitRegion{};
VkStridedDeviceAddressRegionKHR m_callRegion{};

在createRtShaderBindingTable()函数开始处,我们统计shadergroup组相关的信息。因为目前只有一个光线生成着色器,所以我们添加常数1。

//--------------------------------------------------------------------------------------------------
// The Shader Binding Table (SBT)
// 获取所有的着色指针,并将它们写入SBT缓冲区
void HelloVulkan::createRtShaderBindingTable()
{
  uint32_t missCount{1};
  uint32_t hitCount{1};
  auto     handleCount = 1 + missCount + hitCount;
  uint32_t handleSize  = m_rtProperties.shaderGroupHandleSize;

下面设置每个组的步幅和大小。除了光线生成着色器组,其余组的步幅将是与shaderGroupHandleAlignment大小对齐。 每个组的大小则是组中实例个数总大小与shaderGroupBaseAlignment对齐。

// SBT(缓冲区)需要对起始组和组中的句柄指针进行对齐。  
uint32_t handleSizeAligned = nvh::align_up(handleSize, m_rtProperties.shaderGroupHandleAlignment);

m_rgenRegion.stride = nvh::align_up(handleSizeAligned, m_rtProperties.shaderGroupBaseAlignment);
m_rgenRegion.size   = m_rgenRegion.stride;  // The size member of pRayGenShaderBindingTable must be equal to its stride member
m_missRegion.stride = handleSizeAligned;
m_missRegion.size   = nvh::align_up(missCount * handleSizeAligned, m_rtProperties.shaderGroupBaseAlignment);
m_hitRegion.stride  = handleSizeAligned;
m_hitRegion.size    = nvh::align_up(hitCount * handleSizeAligned, m_rtProperties.shaderGroupBaseAlignment);

然后我们获取着色器组中每个管线的句柄指针。

// 获取 shader group 指针
uint32_t             dataSize = handleCount * handleSize;
std::vector<uint8_t> handles(dataSize);
auto result = vkGetRayTracingShaderGroupHandlesKHR(m_device, m_rtPipeline, 0, handleCount, dataSize, handles.data());
assert(result == VK_SUCCESS);

接下来我们将创建用于保存句柄指针数据的缓冲区。需要注意的是创建 SBT 缓冲区时需要包含VK_BUFFER_USAGE_SHADER_BINDING_TABLE_BIT_KHR标志。为了追踪光线,我们还需要 SBT 的地址,所以还需要设置VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT标志。

// SBT缓冲区创建.
VkDeviceSize sbtSize = m_rgenRegion.size + m_missRegion.size + m_hitRegion.size + m_callRegion.size;
m_rtSBTBuffer        = m_alloc.createBuffer(sbtSize,
                                     VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
                                         | VK_BUFFER_USAGE_SHADER_BINDING_TABLE_BIT_KHR,
                                     VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
m_debug.setObjectName(m_rtSBTBuffer.buffer, std::string("SBT"));  

之后,我们存储每个着色器组的设备地址。由于我们不使用可调用对象,因此我们将其保留为 0。

// 获取每组SBT的地址
VkBufferDeviceAddressInfo info{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, m_rtSBTBuffer.buffer};
VkDeviceAddress           sbtAddress = vkGetBufferDeviceAddress(m_device, &info);
m_rgenRegion.deviceAddress           = sbtAddress;
m_missRegion.deviceAddress           = sbtAddress + m_rgenRegion.size;
m_hitRegion.deviceAddress            = sbtAddress + m_rgenRegion.size + m_missRegion.size;

下边的 lambda 函数将返回指向之前获取到的句柄的指针。我们将使用此函数将数据从句柄复制到 SBT 缓冲区中。

// 快速获取句柄指针数据
auto getHandle = [&] (int i) { return handles.data() + i * handleSize; };

由于我们的缓冲区对主机可见,我们将映射其内存以准备数据复制。

// 映射SBT缓冲区到CPU并创建临时指针。
auto*    pSBTBuffer = reinterpret_cast<uint8_t*>(m_alloc.map(m_rtSBTBuffer));
uint8_t* pData{nullptr};
uint32_t handleIdx{0};

复制 RayGen 指针,后续根据此指针拷贝数据。

// Raygen
pData = pSBTBuffer;
memcpy(pData, getHandle(handleIdx++), handleSize);

将指针设置到未命中组的开头并复制所有未命中句柄指针。我们现在只有一个未命中组,但是为了后续我们添加更多未命中的着色器,故采用 for 循环处理。

// Miss
pData = pSBTBuffer + m_rgenRegion.size;
for(uint32_t c = 0; c < missCount; c++)
{
  memcpy(pData, getHandle(handleIdx++), handleSize);
  pData += m_missRegion.stride;
}

以同样的方式,复制命中组的句柄数据。

// Hit
pData = pSBTBuffer + m_rgenRegion.size + m_missRegion.size;
for(uint32_t c = 0; c < hitCount; c++)
{
  memcpy(pData, getHandle(handleIdx++), handleSize);
  pData += m_hitRegion.stride;
}

完成并释放数据。

 m_alloc.unmap(m_rtSBTBuffer);
  m_alloc.finalizeAndReleaseStaging();
}

和其他资源一样,我们在destroyResources相应位置释放 SBT 缓冲区:

  m_alloc.destroy(m_rtSBTBuffer);

着色器顺序
和光栅化管线一样,不要求光线生成、未命中和命中组按顺序出现。但一般情况下,我们构建 SBT 中对应实例的索引为 0、1 、2以对应构建管线时结构体VkPipelineShaderStageCreateInfo 的数组索引也为 0、1 、 2 。再次声明,SBT 的顺序不需要与管线着色器阶段顺序匹配。

SBT创建封装
每个组的所有实例可以从我们创建光线追踪管线时的结构体VkPhysicalDeviceRayTracingPipelinePropertiesKHR中获取到。从这个结构中获取信息的优点是我们不必遵循特定的顺序。

最后,在main函数中,我们现在添加着色器绑定表的构建函数:

  helloVk.createRtShaderBindingTable();
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值