狸村 Mystic Web · 狸村
目录

目标:为模型的几何体创建*底层加速结构*(BLAS),并创建*顶层加速结构*(TLAS)来实例化这些 BLAS。 将 TLAS 绑定到着色器,以便我们可以使用 Ray Query。

在场景中投射光线时,我们需要一个优化结构,能够快速识别光线击中了哪个三角形。GPU 使用加速结构将几何体分组到包围盒中,从而可以跳过场景中的大部分区域。光线遍历沿树状结构进行,高效地缩小到相交的三角形。 具体实现取决于 GPU,对用户来说是不透明的。

38 bounding boxes

我们的场景是一个从 OBJ 文件加载的简单 3D 模型(桌子上的植物)。提供的代码已经将模型的顶点、索引、法线和纹理加载到缓冲中。 它将模型分为多个子网格,每个子网格都有自己的材质。

BLAS 保存一个网格或对象的几何体(三角形)。 TLAS 保存 BLAS 的实例(带变换),以构成完整的场景。 我们将为每个不同的网格/材质创建一个 BLAS,并创建一个引用它们的 TLAS。 Ray Query 将使用 TLAS。 在 Vulkan 中,构建加速结构涉及几个步骤:描述几何体、查询构建大小、分配缓冲、创建 AS 句柄,然后发出构建命令。

任务 2:为每个子网格创建 BLAS

转到 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 所需的内存大小。我们需要分配:

  1. 用于 BLAS 本身的缓冲。

  2. 另一个用于构建过程中使用的临时空间(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];

下图总结了到目前为止我们创建的所有结构和缓冲:

38 TASK02 blas structures

综合起来,我们需要提交一个命令缓冲来在 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);
38 TASK02 blas build

现在,场景中的每个模型都有一个 BLAS。接下来,我们需要将它们全部组合到一个 TLAS 中,该 TLAS 将被片段着色器使用。

任务 3:创建包含 BLAS 实例的 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 所需的主要更改:

38 TASK03 tlas structures

为了准备 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。

任务 4:将加速结构绑定到着色器

为了使加速结构在着色器中可用,我们需要为 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

你不会看到视觉上的变化,但请放心,你的加速结构现在已经设置好,可以在片段着色器中使用了。

导航

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。