本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。
一、加速结构
为了减少渲染过程中光线与三角形相交测试的次数、提高效率,使用Vulkan光线追踪需要将几何体组织到一个加速结构 (AS) 中。
一般加速结构创建通常作为分层结构在硬件中实现,如下图与DX12光追对比:
由图可以看出Vulkan光追中允许用户自定义加速结构,即:
- Bottom-Level Acceleration Structure(BLAS):通常BLAS 对应于场景中的单个 3D 模型。BLASes 存储实际的顶点数据。BLAS几何由一个或多个顶点缓冲区构建而成,每个缓冲区都有自己的变换矩阵(与 TLAS 矩阵分开),并且允许我们在单个 BLAS 中存储多个定位模型。需要额外注意的是,如果一个对象在同一个 BLAS 中多次实例化,其几何图形将被复制。这对于提高静态、非实例化场景组件的性能特别有用(根据经验,BLAS 越少越好),其最大实例个数可根据VkPhysicalDeviceAccelerationStructurePropertiesKHR::maxInstanceCount来查询。
- Top-Level Acceleration Structure(TLAS):而 TLAS 则是通过定位(使用 3×4 变换矩阵)单个引用的 BLAS 来构建整个场景。TLAS 将包含对象实例,每个实例都有自己的变换矩阵和对相应 BLAS 的引用。
具体可见如下图示关系:
示例代码加载 OBJ 文件并将其索引、顶点和材质数据存储到ObjModel结构中。多个obj模型由一个ObjInstance结构体进行引用,该结构体内还包含该特定实例的转换矩阵。对于构建Vulkan加速结构来说ObjModel、ObjInstance集合正好可以对应来创建 BLAS 和 TLAS,其绑定创建关系如下图所示:
需要注意的是,在本案例中,创建和引用了一些辅助类来进行创建光追管线必要的一些数据,见原文不再赘述
1.1 BLAS
构建 BLAS 对象的第一步包括将几何数据转换ObjModel为 AS 构建器使用的多个结构。
案例中将所有这些结构置于 nvvk::RaytracingBuilderKHR::BlasInput 中了,并创建了一个新方法:
auto objectToVkGeometryKHR(const ObjModel& model);
在这个函数中会对创建加速结构vkCmdBuildAccelerationStructuresKHR所需的三个结构体进行赋值操作:
-
VkAccelerationStructureGeometryTrianglesDataKHR: 指向保存三角形顶点/索引数据的缓冲区的设备指针,以及解释信息(步幅、数据类型等,类VAO);
-
VkAccelerationStructureGeometryKHR: 几何类型枚举(案例中使用三角形构建)、加速结构构建器的标志和指向VkAccelerationStructureGeometryTrianglesDataKHR的统一体。
-
VkAccelerationStructureBuildRangeInfoKHR: 顶点数组中的索引,用于为 BLAS 提供源输入几何。
可以直观的看定义明了上边说的关系
typedef union VkAccelerationStructureGeometryDataKHR {
VkAccelerationStructureGeometryTrianglesDataKHR triangles;
VkAccelerationStructureGeometryAabbsDataKHR aabbs;
VkAccelerationStructureGeometryInstancesDataKHR instances;
} VkAccelerationStructureGeometryDataKHR;
typedef struct VkAccelerationStructureGeometryKHR {
VkStructureType sType;
const void* pNext;
VkGeometryTypeKHR geometryType;
VkAccelerationStructureGeometryDataKHR geometry;
VkGeometryFlagsKHR flags;
} VkAccelerationStructureGeometryKHR;
其中,注意区分:VkAccelerationStructureGeometryKHR与VkAccelerationStructureBuildRangeInfoKHR,这两个参数都是单独传递给结构体构建器,其关系类似OpenGL中glVertexAttribPointer与glDrawArrays的作用。
上述结构体中的多个对象可以组合成数组并构建到单个 BLAS 中。在此示例中,此数组的长度将始终为 1。
此外每个 BLAS 有多个几何体主要原因是适当地划分具有相交对象的体积将更利于加速结构的处理。这中情况仅适用于大型或复杂的静态对象组。
针对本案例中的情况认为所有对象都是不透明的,并将其指示给构建器以进行潜在优化(更具体地说,这将禁用对 anyhit 着色器的调用)。
具体实现代码如下:
//--------------------------------------------------------------------------------------------------
// 光追几何数据构件(顶点/索引缓冲区数据存入,后续真正创建blas会用到)
//
auto HelloVulkan::objectToVkGeometryKHR(const ObjModel& model)
{
// 顶点/索引缓冲区对应的设备地址
VkDeviceAddress vertexAddress = nvvk::getBufferDeviceAddress(m_device, model.vertexBuffer.buffer);
VkDeviceAddress indexAddress = nvvk::getBufferDeviceAddress(m_device, model.indexBuffer.buffer);
uint32_t maxPrimitiveCount = model.nbIndices / 3;
// 顶点缓冲区相关信息.
VkAccelerationStructureGeometryTrianglesDataKHR triangles{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR};
triangles.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT; // vec3 vertex position data.
triangles.vertexData.deviceAddress = vertexAddress;
triangles.vertexStride = sizeof(VertexObj);
// 索引缓冲区相关信息
triangles.indexType = VK_INDEX_TYPE_UINT32;
triangles.indexData.deviceAddress = indexAddress;
triangles.maxVertex = model.nbVertices;
// 将上述数据标识为包含不透明三角形.
VkAccelerationStructureGeometryKHR asGeom{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR};
asGeom.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR;
asGeom.flags = VK_GEOMETRY_OPAQUE_BIT_KHR;
asGeom.geometry.triangles = triangles;
// 整个array都将用于创建BLAS.
VkAccelerationStructureBuildRangeInfoKHR offset;
offset.firstVertex = 0;
offset.primitiveCount = maxPrimitiveCount;
offset.primitiveOffset = 0;
offset.transformOffset = 0;
// 本案例中一个几何实例创建一个对应的BLAS,实际中可以由多个几何实例创建在一个BLAS中
nvvk::RaytracingBuilderKHR::BlasInput input;
input.asGeometry.emplace_back(asGeom);
input.asBuildOffsetInfo.emplace_back(offset);
//后续创建BLAS中才会真正用到上边的数据
return input;
}
顶点属性
在上面的代码中,我们设定的是顶点缓冲区的起始位置是VertexObj结构体的首个成员。如果存在偏移的话,我们就得手动来调整vertexAddress对应的偏移量。
需要知道的是AS构建只需要位置属性即可;教程的后边内容会学习在光线跟踪时绑定顶点缓冲区并手动查找其他需要的属性。
内存安全
BlasInput结构体本质上是指向顶点缓冲区数据的设备指针;程序不会复制或管理实际的顶点数据。对于本案例,我们所有模型的数据都在启动时加载并保持在内存中,直到 BLAS 被创建。如果您动态加载和卸载较大场景的部分,或者动态生成顶点数据,那么就需要注意动态删除这些buffer数据。
现在可以创建一个方法createBottomLevelAS()为每个对象生成一个nvvk::RaytracingBuilderKHR::BlasInput 对象 ,之后调用buildBlas()方法生成 BLAS:
void createBottomLevelAS();
该方法遍历所有加载的模型生成blas,之后在buildBlas中生成真正的BLAS数据。
void HelloVulkan::createBottomLevelAS()
{
std::vector<nvvk::RaytracingBuilderKHR::BlasInput> allBlas;
allBlas.reserve(m_objModel.size());
for(const auto& obj : m_objModel)
{
auto blas = objectToVkGeometryKHR(obj);
// 一个blas中可以包含多个几何实例,本案例中仅用一个
allBlas.emplace_back(blas);
}
m_rtBuilder.buildBlas(allBlas, VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR);
}
1.1.1 buildBlas()函数说明
这个助手函数已经存在于raytraceKHR_vkpp.hpp:它可以在许多项目中重用,并且是nvpro-samples提供的助手集的一部分。该函数将为每个生成一个 BLAS RaytracingBuilderKHR::BlasInput:
创建BLAS需要以下结构体数据:
-
VkAccelerationStructureBuildGeometryInfoKHR:创建和构建加速结构。在之前objectToVkGeometryKHR()方法中已经创建的VkAccelerationStructureGeometryKHR数组。
-
VkAccelerationStructureBuildRangeInfoKHR: 此参数也在 objectToVkGeometryKHR() 中已经创建好了。
-
VkAccelerationStructureBuildSizesInfoKHR:创建 AS 和暂存缓冲区所需的大小
-
nvvk::AccelKHR: 返回值
上述数据将存储在BuildAccelerationStructure结构体中以方便后续的创建。
在函数开始时,我们只初始化稍后需要的数据。
//--------------------------------------------------------------------------------------------------
// 从BlasInput数组中创建所有的BLAS
// flags是否压缩buffer数据
void nvvk::RaytracingBuilderKHR::buildBlas(const std::vector<BlasInput>& input, VkBuildAccelerationStructureFlagsKHR flags)
{
m_cmdPool.init(m_device, m_queueIndex);
uint32_t nbBlas = static_cast<uint32_t>(input.size());
VkDeviceSize asTotalSize{0}; //所有需分配的BLAS内存大小
uint32_t nbCompactions{0}; // Nb of BLAS requesting compaction
VkDeviceSize maxScratchSize{0}; // Largest scratch size
之后便是对每个BLAS为其所需的BuildAccelerationStructure结构体填充数据,其中包括设置几何数据、构建range、构建所需的内存大小以及暂存缓冲区的大小。为了节省空间,案例中将使用一个公用的缓冲区,因此程序中用Extra info存储了所需的最大暂存内存大小等信息。之后将分配一个这样大小的暂存缓冲区。
// 为生成加速结构生成必要的数据信息
std::vector<BuildAccelerationStructure> buildAs(nbBlas);
for(uint32_t idx = 0; idx < nbBlas; idx++)
{
//填充部分VkAccelerationStructureBuildGeometryInfoKHR结构体用于查询所需构建大小。
//结构体中其余数据在createBlas 中填充
buildAs[idx].buildInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR;
buildAs[idx].buildInfo.mode = VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR;
buildAs[idx].buildInfo.flags = input[idx].flags | flags;
buildAs[idx].buildInfo.geometryCount = static_cast<uint32_t>(input[idx].asGeometry.size());
buildAs[idx].buildInfo.pGeometries = input[idx].asGeometry.data();
// Build range information
buildAs[idx].rangeInfo = input[idx].asBuildOffsetInfo.data();
// 获取创建加速结构所需的大小
std::vector<uint32_t> maxPrimCount(input[idx].asBuildOffsetInfo.size());
for(auto tt = 0; tt < input[idx].asBuildOffsetInfo.size(); tt++)
maxPrimCount[tt] = input[idx].asBuildOffsetInfo[tt].primitiveCount; // Number of primitives/triangles
vkGetAccelerationStructureBuildSizesKHR(m_device, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
&buildAs[idx].buildInfo, maxPrimCount.data(), &buildAs[idx].sizeInfo);
// Extra info
asTotalSize += buildAs[idx].sizeInfo.accelerationStructureSize;
maxScratchSize = std::max(maxScratchSize, buildAs[idx].sizeInfo.buildScratchSize);
nbCompactions += hasFlag(buildAs[idx].buildInfo.flags, VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_COMPACTION_BIT_KHR);
}
在遍历所有 BLAS 之后,我们拥有最大的暂存缓冲区大小,我们将创建它。
// 创建用于保存加速结构构建器临时数据的scratch缓冲区
nvvk::Buffer scratchBuffer =
m_alloc->createBuffer(maxScratchSize, VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
VkBufferDeviceAddressInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, scratchBuffer.buffer};
VkDeviceAddress scratchAddress = vkGetBufferDeviceAddress(m_device, &bufferInfo);
以下部分用于查询每个 BLAS 的实际大小。要想查询 BLAS 实际占用的大小,我们使用的查询类型必须为 VK_QUERY_TYPE_ACCELERATION_STRUCTURE_COMPACTED_SIZE_KHR标识。如果我们想在第二步中压缩加速结构,这是必需的。默认情况下,vkGetAccelerationStructureBuildSizesKHR函数返回的大小为最坏情况的大小。创建后,实际空间可能会更小,并且可以将加速结构复制到完全使用所需的结构。这可以节省超过 50% 的设备内存使用量,这也是为什么要进行压缩buffer的原因。
// 分配一个查询池,用于存储每个BLAS压缩所需的大小。
VkQueryPool queryPool{VK_NULL_HANDLE};
if(nbCompactions > 0) // 是否需要压缩
{
assert(nbCompactions == nbBlas); // Don't allow mix of on/off compaction
VkQueryPoolCreateInfo qpci{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
qpci.queryCount = nbBlas;
qpci.queryType = VK_QUERY_TYPE_ACCELERATION_STRUCTURE_COMPACTED_SIZE_KHR;
vkCreateQueryPool(m_device, &qpci, nullptr, &queryPool);
}
压缩
要使用压缩,BLAS 标志必须为
VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_COMPACTION_BIT_KHR
在单个命令缓冲区中创建所有 BLAS 可能会生效,但它可能会让管道等待并可能产生其他问题。为了避免这个潜在的问题,我们将 BLAS 创建拆分为大约 256MB 所需内存的块。如果我们请求压缩,我们将立即执行,从而限制所需的内存分配。
创建和压缩BLAS 见下文(本案例仅使用了创建)。
// 批量处理创建/压缩BLAS,以允许驻留在有限的内存中
std::vector<uint32_t> indices; // 要创建的BLAS的索引
VkDeviceSize batchSize{0};
VkDeviceSize batchLimit{256'000'000}; // 256 MB
for(uint32_t idx = 0; idx < nbBlas; idx++)
{
indices.push_back(idx);
batchSize += buildAs[idx].sizeInfo.accelerationStructureSize;
// 超过极限或最后一个BLAS元素进行创建操作
if(batchSize >= batchLimit || idx == nbBlas - 1)
{
VkCommandBuffer cmdBuf = m_cmdPool.createCommandBuffer();
cmdCreateBlas(cmdBuf, indices, buildAs, scratchAddress, queryPool);
m_cmdPool.submitAndWait(cmdBuf);
if(queryPool)
{
VkCommandBuffer cmdBuf = m_cmdPool.createCommandBuffer();
cmdCompactBlas(cmdBuf, indices, buildAs, queryPool);
m_cmdPool.submitAndWait(cmdBuf); // 提交命令缓冲区并调用vkQueueWaitIdle
// 销毁非压缩数据
destroyNonCompacted(indices, buildAs);
}
// Reset
batchSize = 0;
indices.clear();
}
}
创建的加速结构保存在下边的m_blas这个对象中,这样就可以用创建的索引来检索它。
for(auto& b : buildAs)
{
m_blas.emplace_back(b.as);
}
最后,我们清理掉内存。
// Clean up
vkDestroyQueryPool(m_device, queryPool, nullptr);
m_alloc->finalizeAndReleaseStaging();
m_alloc->destroy(scratchBuffer);
m_cmdPool.deinit();
cmdCreateBlas:创建BLAS
//--------------------------------------------------------------------------------------------------
// 为' buildAs ' 数组中的所有索引创建BLAS。
// 在buildBlas中创建了BuildAccelerationStructure数组,其中索引数组限制了一次创建BLAS的数量。 这也就限制了压缩BLAS时所需的内存。
void nvvk::RaytracingBuilderKHR::cmdCreateBlas(VkCommandBuffer cmdBuf,
std::vector<uint32_t> indices,
std::vector<BuildAccelerationStructure>& buildAs,
VkDeviceAddress scratchAddress,
VkQueryPool
首先,我们重置查询以确定 BLAS 的实际大小
if(queryPool) // 查询压缩大小
vkResetQueryPool(m_device, queryPool, 0, static_cast<uint32_t>(indices.size()));
uint32_t queryCnt{0};
该函数正在创建由索引块定义的所有 BLAS。
for(const auto& idx : indices)
{
BLAS 的创建包括两个步骤:
- 创建加速结构: 我们使用createAcceleration()创建以上代码部分已经查询大小的缓冲区和加速结构。此步仅是创建缓冲区和加速结构。
- 构建加速结构: 使用加速结构、暂存缓冲区和几何信息,此时是 BLAS 的实际构建。
m_alloc->createAcceleration函数中创建一个由加速结构大小查询所指示大小的缓冲区,需要给它设置VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR和VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT 使用位(后者是必需的,因为 TLAS 构建器将需要 BLAS 的原始地址),并将加速结构绑定到它通过填写 的buffer字段分配的内存VkAccelerationStructureCreateInfoKHR。与缓冲区和图像不同,它们的Vk*句柄的分配和内存绑定是在单独的步骤中完成的,而加速结构通过一次vkCreateAccelerationStructureKHR调用即可创建并绑定到内存。
// 实际分配缓冲区和加速结构
VkAccelerationStructureCreateInfoKHR createInfo{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR};
createInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR;
createInfo.size = buildAs[idx].sizeInfo.accelerationStructureSize; // 需要分配的内存大小
buildAs[idx].as = m_alloc->createAcceleration(createInfo);
NAME_IDX_VK(buildAs[idx].as.accel, idx);
NAME_IDX_VK(buildAs[idx].as.buffer.buffer, idx);
// 上文说的其余需要填充的信息
buildAs[idx].buildInfo.dstAccelerationStructure = buildAs[idx].as.accel; // 设置需要创建的数据位置
buildAs[idx].buildInfo.scratchData.deviceAddress = scratchAddress; // 所有构建都使用相同的scratch缓冲区
// 真正创建BLAS的函数
vkCmdBuildAccelerationStructuresKHR(cmdBuf, 1, &buildAs[idx].buildInfo, &buildAs[idx].rangeInfo);
注意每次构建调用后的屏障设置:这是必要的,因为我们正在构建重用暂存空间,所以我们需要确保在开始下一个构建之前完成前一个构建。我们可以使用多个暂存缓冲区,但这会占用大量内存,并且设备一次只能构建一个 BLAS,因此速度不会更快。
// 由于scratch缓冲区在构建中被重用,我们需要一个屏障来确保一个构建在开始下一个之前已经完成。
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR;
barrier.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR;
vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR, 0, 1, &barrier, 0, nullptr, 0, nullptr);
然后我们只在需要时添加尺寸查询
if(queryPool)
{
// 添加一个查询来查找压缩所需的“实际”内存量
vkCmdWriteAccelerationStructuresPropertiesKHR(cmdBuf, 1, &buildAs[idx].buildInfo.dstAccelerationStructure,
VK_QUERY_TYPE_ACCELERATION_STRUCTURE_COMPACTED_SIZE_KHR, queryPool, queryCnt++);
}
}
}
尽管这种方法具有保持所有 BLAS 独立的优势,但高效构建多个 BLAS 需要分配更大的暂存缓冲区并同时启动多个构建。本教程不使用压缩,这可以显着减少加速结构的内存占用。这儿也是后续可优化的一个重要点。
cmdCompactBlas:压缩BLAS
接下来是进行压缩处理(本案例未压缩)。这部分是可选的,它将把 BLAS 压缩到它实际使用的内存中。我们必须等到所有 BLAS 构建完毕,才能在更合适的内存空间中进行复制。这就是m_cmdPool.submitAndWait(cmdBuf)在调用这个函数之前使用的原因。
//--------------------------------------------------------------------------------------------------
// 基于查询管线获取的大小,创建并替换新的加速结构和缓冲区。
void nvvk::RaytracingBuilderKHR::cmdCompactBlas(VkCommandBuffer cmdBuf,
std::vector<uint32_t> indices,
std::vector<BuildAccelerationStructure>& buildAs,
VkQueryPool queryPool)
{
从广义上讲,压缩的工作原理如下:
-
从查询池中获取实际大小
-
用更小的尺寸创建一个新的加速结构
-
将之前的加速结构复制到新分配的加速结构中
-
销毁之前的加速结构。
具体代码如下:
uint32_t queryCtn{0};
std::vector<nvvk::AccelKHR> cleanupAS;
// 返回压缩后的大小
std::vector<VkDeviceSize> compactSizes(static_cast<uint32_t>(indices.size()));
vkGetQueryPoolResults(m_device, queryPool, 0, (uint32_t)compactSizes.size(), compactSizes.size() * sizeof(VkDeviceSize),
compactSizes.data(), sizeof(VkDeviceSize), VK_QUERY_RESULT_WAIT_BIT);
for(auto idx : indices)
{
buildAs[idx].cleanupAS = buildAs[idx].as; // previous AS to destroy
buildAs[idx].sizeInfo.accelerationStructureSize = compactSizes[queryCtn++]; // 新的压缩尺寸
// 创建一个压缩版本的加速结构
VkAccelerationStructureCreateInfoKHR asCreateInfo{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR};
asCreateInfo.size = buildAs[idx].sizeInfo.accelerationStructureSize;
asCreateInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR;
buildAs[idx].as = m_alloc->createAcceleration(asCreateInfo);
NAME_IDX_VK(buildAs[idx].as.accel, idx);
NAME_IDX_VK(buildAs[idx].as.buffer.buffer, idx);
// 复制原来的BLAS到新的AS中
VkCopyAccelerationStructureInfoKHR copyInfo{VK_STRUCTURE_TYPE_COPY_ACCELERATION_STRUCTURE_INFO_KHR};
copyInfo.src = buildAs[idx].buildInfo.dstAccelerationStructure;
copyInfo.dst = buildAs[idx].as.accel;
copyInfo.mode = VK_COPY_ACCELERATION_STRUCTURE_MODE_COMPACT_KHR;
vkCmdCopyAccelerationStructureKHR(cmdBuf, ©Info);
}
}
1.2 TLAS
TLAS 是光线追踪场景描述的入口点,存储所有实例。为了创建TLAS 我们在类中添加一个新方法:
void createTopLevelAS();
我们用VkAccelerationStructureInstanceKHR结构体表示一个实例,它在赋值给buildBlas数组时存储其单个物体的变换矩阵 ( transform) 其对应 的BLAS( blasId)。它还包含一个在着色过程中可用的实例标识符gl_InstanceCustomIndex,以及表示在命中对象时将调用的着色器的命中组的索引(VkAccelerationStructureInstanceKHR::instanceShaderBindingTableRecordOffset,又叫hitGroupId)。
gl_InstanceId
不要把gl_InstanceID与gl_InstanceCustomIndex搞混淆了.
gl_InstanceID只是相交实例的索引,因为它出现在用于构建 TLAS 的实例数组中。
在这个特定的例子中,我们可以忽略自定义索引,因为 Id
等价于gl_InstanceId(gl_InstanceId指定与当前光线相交的实例的索引,在这种情况下与i 的值相同)。在以后的教程示例中,该值会有所不同。
该索引和命中组的概念与光线追踪管道和着色器绑定表(SBT)的定义相关,本教程稍后将对其进行描述,他们用于确定运行时调用哪些着色器。本案例中我们将为整个场景只使用一个命中组,因此命中组索引始终为 0。
最后,实例可能会使用其VkGeometryInstanceFlagsKHR flags成员指示剔除特性,例如背面剔除。在本案例中,为了简单和独立于输入模型数据的简化,教程中完全禁用剔除。
一旦创建了所有实例对象,我们就会触发 TLAS 构建,指示构建器更喜欢生成针对跟踪性能(而不是 AS 大小)优化的 TLAS。
//--------------------------------------------------------------------------------------------------
void HelloVulkan::createTopLevelAS()
{
std::vector<VkAccelerationStructureInstanceKHR> tlas;
tlas.reserve(m_instances.size());
for(const HelloVulkan::ObjInstance& inst : m_instances)
{
VkAccelerationStructureInstanceKHR rayInst{};
rayInst.transform = nvvk::toTransformMatrixKHR(inst.transform); // 实例的位置矩阵
rayInst.instanceCustomIndex = inst.objIndex; // gl_InstanceCustomIndexEXT
rayInst.accelerationStructureReference = m_rtBuilder.getBlasDeviceAddress(inst.objIndex);
rayInst.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR;
rayInst.mask = 0xFF; // 只有当 rayMask & instance.mask != 0 时才会命中
rayInst.instanceShaderBindingTableRecordOffset = 0; // 我们将对所有对象使用相同的hit group
tlas.emplace_back(rayInst);
}
m_rtBuilder.buildTlas(tlas, VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR);
}
之后,我们需要通过在 末尾添加调用来显式销毁我们创建的对象 HelloVulkan::destroyResources:
// #VKRay
m_rtBuilder.destroy();
getBlasDeviceAddress()
getBlasDeviceAddress()返回加速结构设备的地址blasId。这个id 对应于buildBlas 中创建的 BLAS 。
1.2.1 buildTlas()函数
buildTlas()函数用于构建顶级加速结构并且也被封装成了一个函数可直接使用, 并从Instance对象数组中构建 TLAS 。
我们首先设置命令缓冲区并复制用户的 TLAS 标志。
// 从Instance对象数组中构建 TLAS
// 生成的TLAS将被存储在m_tlas中 ,update表示是否用更新后的矩阵重建Tlas
void buildTlas(const std::vector<VkAccelerationStructureInstanceKHR>& instances,
VkBuildAccelerationStructureFlagsKHR flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR,
bool update = false)
{
// 不能两次调用buildTlas,除非是更新操作
assert(m_tlas.accel == VK_NULL_HANDLE || update);
uint32_t countInstance = static_cast<uint32_t>(instances.size());
// 用来创建TLAS的命令缓冲区
nvvk::CommandPool genCmdBuf(m_device, m_queueIndex);
VkCommandBuffer cmdBuf = genCmdBuf.createCommandBuffer();
接下来,我们需要将 Vulkan 实例上传到设备。
nvvk::CommandPool genCmdBuf(m_device, m_queueIndex);
VkCommandBuffer cmdBuf = genCmdBuf.createCommandBuffer();
// 创建一个保存实际实例数据(matrices++)的缓冲区,供AS构建时候使用
nvvk::Buffer instancesBuffer; // 包含矩阵和BLAS id的实例的缓冲区
instancesBuffer = m_alloc->createBuffer(cmdBuf, instances,
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR);
NAME_VK(instancesBuffer.buffer);
VkBufferDeviceAddressInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, instancesBuffer.buffer};
VkDeviceAddress instBufferAddr = vkGetBufferDeviceAddress(m_device, &bufferInfo);
// 确保在触发加速结构构建之前复制了实例缓冲区的副本
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR;
vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
0, 1, &barrier, 0, nullptr, 0, nullptr);
此时,我们有一个命令缓冲区 ( cmdBuf)、一些实例 ( countInstance) 以及保存所有数据的VkAccelerationStructureInstanceKHR. 有了这些信息,我们调用一个函数来构建 TLAS。此函数将分配一个临时缓冲区,一旦所有工作完成,我们将需要销毁该缓冲区。
// 创建 TLAS
nvvk::Buffer scratchBuffer;
cmdCreateTlas(cmdBuf, countInstance, instBufferAddr, scratchBuffer, flags, update, motion);
// 销毁临时数据
genCmdBuf.submitAndWait(cmdBuf);
m_alloc->finalizeAndReleaseStaging();
m_alloc->destroy(scratchBuffer);
m_alloc->destroy(instancesBuffer);
}
cmdCreateTlas函数的功能则是真正创建TLAS的地方。
//--------------------------------------------------------------------------------------------------
// 在 buildTlas 中最终会调用到此处
void nvvk::RaytracingBuilderKHR::cmdCreateTlas(VkCommandBuffer cmdBuf,
uint32_t countInstance,
VkDeviceAddress instBufferAddr,
nvvk::Buffer& scratchBuffer,
VkBuildAccelerationStructureFlagsKHR flags,
bool update,
bool motion)
{
下一部分是填充用于构建 TLAS 的结构体。它是一种包含许多实例的几何体。
// 接收指向上述上传实例的设备指针 .
VkAccelerationStructureGeometryInstancesDataKHR instancesVk{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR};
instancesVk.data.deviceAddress = instBufferAddr;
// 将上述内容放入VkAccelerationStructureGeometryKHR。 我们需要将实例结构体放入联合中,并将其标记为实例数据.
VkAccelerationStructureGeometryKHR topASGeometry{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR};
topASGeometry.geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR;
topASGeometry.geometry.instances = instancesVk;
// Find sizes
VkAccelerationStructureBuildGeometryInfoKHR buildInfo{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR};
buildInfo.flags = flags;
buildInfo.geometryCount = 1;
buildInfo.pGeometries = &topASGeometry;
buildInfo.mode = update ? VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR : VK_BUILD_ACCELERATION_STRUCTURE_MODE_BUILD_KHR;
buildInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR;
buildInfo.srcAccelerationStructure = VK_NULL_HANDLE;
VkAccelerationStructureBuildSizesInfoKHR sizeInfo{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR};
vkGetAccelerationStructureBuildSizesKHR(m_device, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR, &buildInfo,
&countInstance, &sizeInfo);
我们可以创建加速结构,暂时先不构建它。
VkAccelerationStructureCreateInfoKHR createInfo{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR};
createInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR;
createInfo.size = sizeInfo.accelerationStructureSize;
m_tlas = m_alloc->createAcceleration(createInfo);
NAME_VK(m_tlas.accel);
NAME_VK(m_tlas.buffer.buffer);
构建加速结构时还需要创建一个暂存缓冲区。
// 分配临时内存
scratchBuffer = m_alloc->createBuffer(sizeInfo.buildScratchSize,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
VkBufferDeviceAddressInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, scratchBuffer.buffer};
VkDeviceAddress scratchAddress = vkGetBufferDeviceAddress(m_device, &bufferInfo);
NAME_VK(scratchBuffer.buffer);
最后,我们可以构建加速结构。
// 更新构建信息
buildInfo.srcAccelerationStructure = VK_NULL_HANDLE;
buildInfo.dstAccelerationStructure = m_tlas.accel;
buildInfo.scratchData.deviceAddress = scratchAddress;
// 构建偏移信息:n个实例
VkAccelerationStructureBuildRangeInfoKHR buildOffsetInfo{countInstance, 0, 0, 0};
const VkAccelerationStructureBuildRangeInfoKHR* pBuildOffsetInfo = &buildOffsetInfo;
// 真正调用函数构件TLAS的地方
vkCmdBuildAccelerationStructuresKHR(cmdBuf, 1, &buildInfo, &pBuildOffsetInfo);
}
最后,在main函数中,我们现在可以在初始化光线追踪后立即添加几何实例和加速结构的创建:
// #VKRay
helloVk.initRayTracing();
helloVk.createBottomLevelAS();
helloVk.createTopLevelAS();