目标:为模型的几何体创建*底层加速结构*(BLAS),并创建*顶层加速结构*(TLAS)来实例化这些 BLAS。 将 TLAS 绑定到着色器,以便我们可以使用 Ray Query。
在场景中投射光线时,我们需要一个优化结构,能够快速识别光线击中了哪个三角形。GPU 使用加速结构将几何体分组到包围盒中,从而可以跳过场景中的大部分区域。光线遍历沿树状结构进行,高效地缩小到相交的三角形。 具体实现取决于 GPU,对用户来说是不透明的。
我们的场景是一个从 OBJ 文件加载的简单 3D 模型(桌子上的植物)。提供的代码已经将模型的顶点、索引、法线和纹理加载到缓冲中。 它将模型分为多个子网格,每个子网格都有自己的材质。
BLAS 保存一个网格或对象的几何体(三角形)。 TLAS 保存 BLAS 的实例(带变换),以构成完整的场景。 我们将为每个不同的网格/材质创建一个 BLAS,并创建一个引用它们的 TLAS。 Ray Query 将使用 TLAS。 在 Vulkan 中,构建加速结构涉及几个步骤:描述几何体、查询构建大小、分配缓冲、创建 AS 句柄,然后发出构建命令。
转到 createAccelerationStructures 的定义。在这里,我们将首先为模型中的每个子网格创建 BLAS。代码已经有一个遍历 submeshes 的循环,其中包含几何体数据。
首先,我们需要描述 BLAS 的几何体。为此使用 vk::AccelerationStructureGeometryKHR 结构体:
// 准备几何体数据
auto trianglesData = vk::AccelerationStructureGeometryTrianglesDataKHR{
.vertexFormat = vk::Format::eR32G32B32Sfloat,
.vertexData = vertexAddr,
.vertexStride = sizeof(Vertex),
.maxVertex = submesh.maxVertex,
.indexType = vk::IndexType::eUint32,
.indexData = indexAddr + submesh.indexOffset * sizeof(uint32_t)
};
vk::AccelerationStructureGeometryDataKHR geometryData(trianglesData);
vk::AccelerationStructureGeometryKHR blasGeometry{
.geometryType = vk::GeometryTypeKHR::eTriangles,
.geometry = geometryData,
.flags = vk::GeometryFlagBitsKHR::eOpaque
};
然后将其记录到构建信息结构体中:
vk::AccelerationStructureBuildGeometryInfoKHR blasBuildGeometryInfo{
.type = vk::AccelerationStructureTypeKHR::eBottomLevel,
.mode = vk::BuildAccelerationStructureModeKHR::eBuild,
.geometryCount = 1,
.pGeometries = &blasGeometry,
};
接下来,我们需要查询 BLAS 的内存需求:
// TASK02:查询此 BLAS 所需的内存大小
vk::AccelerationStructureBuildSizesInfoKHR blasBuildSizes =
device.getAccelerationStructureBuildSizesKHR(
vk::AccelerationStructureBuildTypeKHR::eDevice,
blasBuildGeometryInfo,
{ primitiveCount }
);
此辅助函数使用 vkGetAccelerationStructureBuildSizesKHR(),并返回 BLAS 所需的内存大小。我们需要分配:
用于 BLAS 本身的缓冲。
另一个用于构建过程中使用的临时空间(scratch space)的缓冲。
然后我们可以创建这些缓冲,并将它们存储在持久数组中,因为稍后还会用到。
我们还需要创建 BLAS 句柄本身,这通过 vk::AccelerationStructureCreateInfoKHR 和使用 vkCreateAccelerationStructureKHR() 的设备辅助函数完成。句柄存储在向量中供以后使用(记住,每个子网格需要一个):
// TASK02:创建并存储 BLAS 句柄
vk::AccelerationStructureCreateInfoKHR blasCreateInfo{
.buffer = blasBuffers[i],
.offset = 0,
.size = blasBuildSizes.accelerationStructureSize,
.type = vk::AccelerationStructureTypeKHR::eBottomLevel,
};
blasHandles.emplace_back(device.createAccelerationStructureKHR(blasCreateInfo));
// 将 BLAS 句柄保存到构建信息结构体中
blasBuildGeometryInfo.dstAccelerationStructure = blasHandles[i];
下图总结了到目前为止我们创建的所有结构和缓冲:
综合起来,我们需要提交一个命令缓冲来在 GPU 上构建 BLAS。这通过 vkCmdBuildAccelerationStructuresKHR() 完成,它接受构建信息和一个范围。范围增加了灵活性,可以一次性构建多个几何体,但这里每个 BLAS 只有一个几何体,因此保持简单:
// TASK02:准备 BLAS 的构建范围
vk::AccelerationStructureBuildRangeInfoKHR blasRangeInfo{
.primitiveCount = primitiveCount,
.primitiveOffset = 0,
.firstVertex = firstVertex,
.transformOffset = 0
};
最后,准备并提交一个命令缓冲,它将保存一个有效的底层加速结构句柄:
// TASK02:构建 BLAS
auto cmd = beginSingleTimeCommands();
cmd->buildAccelerationStructuresKHR({ blasBuildGeometryInfo }, { &blasRangeInfo });
endSingleTimeCommands(*cmd);
现在,场景中的每个模型都有一个 BLAS。接下来,我们需要将它们全部组合到一个 TLAS 中,该 TLAS 将被片段着色器使用。
现在我们有了 BLAS,需要创建一个引用它们的 TLAS。TLAS 将保存 BLAS 的实例,允许我们使用变换(位置、旋转、缩放)将它们放置在场景中。
我们可以在创建 BLAS 的同一子网格循环中创建实例。对于每个子网格,我们将创建一个引用相应 BLAS 句柄的实例。为此使用 vk::AccelerationStructureInstanceKHR 结构体:
// TASK03:为 TLAS 创建 BLAS 实例
vk::AccelerationStructureDeviceAddressInfoKHR addrInfo{
.accelerationStructure = *blasHandles[i]
};
vk::DeviceAddress blasDeviceAddr = device.getAccelerationStructureAddressKHR(addrInfo);
vk::AccelerationStructureInstanceKHR instance{
.transform = tm,
.mask = 0xFF,
.accelerationStructureReference = blasDeviceAddr
};
instances.push_back(instance);
注意,我们需要使用 vkGetAccelerationStructureDeviceAddressKHR() 获取 BLAS 的设备地址。我们暂时将变换矩阵设置为单位矩阵,稍后将在实验中重新讨论。
现在所有实例都存储在一个向量中,我们需要为 TLAS 准备实例数据。这涉及创建一个保存实例数据的缓冲。
使用与 BLAS 非常相似的方法,我们需要为 TLAS 构建准备数据、查询缓冲大小、分配缓冲、创建 TLAS 句柄,并发出构建命令。下图突出了 TLAS 所需的主要更改:
为了准备 TLAS 的几何体数据,我们将使用 vk::GeometryTypeKHR::eInstances 来表示我们正在从 BLAS 的实例构建 TLAS:
// TASK03:准备几何体(实例)数据
auto instancesData = vk::AccelerationStructureGeometryInstancesDataKHR{
.arrayOfPointers = vk::False,
.data = instanceAddr
};
vk::AccelerationStructureGeometryDataKHR geometryData(instancesData);
vk::AccelerationStructureGeometryKHR tlasGeometry{
.geometryType = vk::GeometryTypeKHR::eInstances,
.geometry = geometryData
};
然后将其记录到构建信息结构体中:
vk::AccelerationStructureBuildGeometryInfoKHR tlasBuildGeometryInfo{
.type = vk::AccelerationStructureTypeKHR::eTopLevel,
.mode = vk::BuildAccelerationStructureModeKHR::eBuild,
.geometryCount = 1,
.pGeometries = &tlasGeometry
};
接下来,我们需要查询 TLAS 的内存需求:
// TASK03:查询此 TLAS 所需的内存大小
vk::AccelerationStructureBuildSizesInfoKHR tlasBuildSizes =
device.getAccelerationStructureBuildSizesKHR(
vk::AccelerationStructureBuildTypeKHR::eDevice,
tlasBuildGeometryInfo,
{ primitiveCount }
);
然后再次创建必要的缓冲。
为了创建 TLAS 句柄,我们像之前一样使用 vkCreateAccelerationStructureKHR():
// TASK03:创建并存储 TLAS 句柄
vk::AccelerationStructureCreateInfoKHR tlasCreateInfo{
.buffer = tlasBuffer,
.offset = 0,
.size = tlasBuildSizes.accelerationStructureSize,
.type = vk::AccelerationStructureTypeKHR::eTopLevel,
};
tlas = device.createAccelerationStructureKHR(tlasCreateInfo);
// 将 TLAS 句柄保存到构建信息结构体中
tlasBuildGeometryInfo.dstAccelerationStructure = tlas;
再一次,我们需要为 TLAS 准备构建范围。这与 BLAS 类似,但现在我们使用实例计数。然后我们可以提交命令缓冲来构建 TLAS:
// TASK03:准备 TLAS 的构建范围
vk::AccelerationStructureBuildRangeInfoKHR tlasRangeInfo{
.primitiveCount = primitiveCount,
.primitiveOffset = 0,
.firstVertex = 0,
.transformOffset = 0
};
// TASK03:构建 TLAS
auto cmd = beginSingleTimeCommands();
cmd->buildAccelerationStructuresKHR({ tlasBuildGeometryInfo }, { &tlasRangeInfo });
endSingleTimeCommands(*cmd);
完成!你现在已经创建了一个 TLAS,它引用模型中所有子网格的 BLAS。TLAS 已准备好用于片段着色器中的 Ray Query。
为了使加速结构在着色器中可用,我们需要为 TLAS 添加一个描述符集绑定。这可以在 createDescriptorSetLayout() 函数中完成(你可以暂时忽略更高的绑定):
// TASK04:加速结构使用绑定 1
std::array global_bindings = {
vk::DescriptorSetLayoutBinding( 0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, nullptr),
vk::DescriptorSetLayoutBinding( 1, vk::DescriptorType::eAccelerationStructureKHR, 1, vk::ShaderStageFlagBits::eFragment, nullptr),
vk::DescriptorSetLayoutBinding( 2, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment, nullptr),
vk::DescriptorSetLayoutBinding( 3, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment, nullptr),
vk::DescriptorSetLayoutBinding( 4, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment, nullptr)
};
接下来,我们需要更新描述符集以绑定 TLAS。这可以在 updateDescriptorSets() 函数中完成:
vk::WriteDescriptorSetAccelerationStructureKHR asInfo{
.accelerationStructureCount = 1,
.pAccelerationStructures = {&*tlas}
};
vk::WriteDescriptorSet asWrite{
.pNext = &asInfo,
.dstSet = globalDescriptorSets[i],
.dstBinding = 1,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eAccelerationStructureKHR
};
然后在调用 vkUpdateDescriptorSets() 时将 TLAS 包含在列表中:
std::array<vk::WriteDescriptorSet, 4> descriptorWrites{bufferWrite, asWrite, indexBufferWrite, uvBufferWrite};
device.updateDescriptorSets(descriptorWrites, {});
最后,在着色器中添加相应的属性:
// TASK04:加速结构绑定
[[vk::binding(1,0)]]
RaytracingAccelerationStructure accelerationStructure;
使用以下设置重新构建并运行:
#define LAB_TASK_LEVEL 4
你不会看到视觉上的变化,但请放心,你的加速结构现在已经设置好,可以在片段着色器中使用了。
上一篇:动态渲染
下一篇:Ray Query 阴影
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。