狸村 Mystic Web · 狸村
目录

目标:为场景添加对透明物体的支持。我们将实现一个简单的 Alpha 测试,丢弃 alpha 值较低的片段,并在相应的光线追踪阴影中复制此操作。

到目前为止,我们一直将所有几何体视为不透明的。然而,请注意,树叶是使用带有 alpha 通道来定义透明度的纹理渲染的,目前尚未考虑这一点,因此树叶边缘会出现一些暗色像素:

38 TASK08 alphacut before

任务 7:Alpha 裁剪透明度

为了实现 Alpha 裁剪透明度,我们将在片段着色器中丢弃 alpha 值较低的片段。这是处理透明纹理的常用技术,无需复杂的混合或排序:

   float4 baseColor = textures[pc.materialIndex].Sample(textureSampler, vertIn.fragTexCoord);

   // Alpha 测试
   if (baseColor.a < 0.5) discard;
38 TASK08 alphacut after

请注意,阴影目前保持不变。我们需要做的第一件事是,在 createAccelerationStructures() 中,仅当 BLAS 没有 Alpha 透明度时才使用 eOpaque 标志:

vk::AccelerationStructureGeometryKHR blasGeometry{
    .geometryType = vk::GeometryTypeKHR::eTriangles,
    .geometry = geometryData
};

blasGeometry.flags = (submesh.alphaCut) ? vk::GeometryFlagsKHR(0) : vk::GeometryFlagBitsKHR::eOpaque;

使用以下设置重新构建并运行:

#define LAB_TASK_LEVEL 7

你会看到树叶的阴影现在完全消失了!这是因为在所有交点候选中,只有不透明几何体的三角形会自动提交。我们需要实现一种在 Ray Query 阴影中处理透明度并有条件地提交候选交点的方法。

在此之前,让我们先检查一下到目前为止构建的加速结构,了解它们的结构以及每个三角形可用的信息。

任务 8:使用 NVIDIA Nsight Graphics 检查加速结构

RenderDoc 目前不支持检查 Vulkan 加速结构,但我们可以使用 Nsight Graphics 来验证我们的 BLAS/TLAS 是否正确构建。这有助于在我们依赖着色器之前捕获错误(例如,几何体计数错误、偏移错误)。

使用 Nsight:

  1. 指定可执行文件路径:C:\Users\nvidia\S2025_LABS\gensubcur_105\Vulkan-Tutorial\attachments\build\38_ray_tracing\Debug\38_ray_tracing.exe

  2. 指定工作目录:C:\Users\nvidia\S2025_LABS\gensubcur_105\Vulkan-Tutorial\attachments\build\38_ray_tracing

  3. 启动应用程序。

38 TASK04 nsight launch

然后捕获一帧并点击“Start Graphics Debugger”:

38 TASK04 nsight capture

与 RenderDoc 类似,你可以检查事件并找到加速结构构建命令,这将带我们进入加速结构可视化工具:

  1. 使用事件浏览器中的搜索功能过滤并找到 draw 调用。

  2. 选择任意绘制调用,这将打开 API 检查器。

  3. 选择 FS 以找到绑定到我们片段着色器的资源。

  4. 点击加速结构绑定(这将是我们的 TLAS),这将打开“Ray Tracing Inspector”。

38 TASK04 nsight main

在新窗口中,你可以看到 TLAS 及其实例:

  1. 你可以展开每个实例以查看其引用的 BLAS,并检查几何体数据。

  2. 你可能会发现“Orbit Camera”更适合浏览场景。

  3. 你可能需要将“Up Direction”设置为“Z Axis”以匹配模型中使用的坐标系。

  4. 使用“Opaque”几何体的过滤器(如预期的那样,树叶模型将显示为灰色)。

  5. 尝试不同的可视化选项,如“Color by”。

  6. 尝试“Show Mesh Wireframes”和“Show AABB Wireframes”。

38 TASK04 nsight inspector

任务 9:Bindless 资源和实例查找表

到目前为止,我们不需要知道光线击中了哪个三角形,我们只关心是否击中了某些东西。但为了实现透明度,我们需要知道用于着色我们所击中的三角形上的点的纹理的 alpha 值。这样,我们就可以确定是否击中了透明像素,并需要继续遍历,以防在通往光源的路径上击中了它后面的其他不透明三角形。

在介绍为此所需的 Ray Query 逻辑之前,让我们先观察一下渲染器如何在着色器中绑定我们需要的一切,并实现一个简单的查找表,将加速结构实例映射到其几何体和纹理数据。

注意,渲染器不会为每个子网格单独绑定材质纹理。相反,它绑定一个纹理数组,并使用材质索引来查找每个子网格的纹理。我们使用推送常量传递材质索引:

        for (auto& sub : submeshes) {
            // TASK09:Bindless 资源
            PushConstant pushConstant = {
                .materialIndex = sub.materialID < 0 ? 0u : static_cast<uint32_t>(sub.materialID),
            };
            commandBuffers[frameIndex].pushConstants<PushConstant>(pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, pushConstant);

            commandBuffers[frameIndex].drawIndexed(sub.indexCount, 1, sub.indexOffset, 0, 0);
        }

然后,在着色器中,我们使用材质索引来采样纹理数组。它们都共享同一个采样器:

[[vk::binding(0,1)]]
SamplerState textureSampler;

[[vk::binding(1,1)]]
Texture2D<float4> textures[];

struct PushConstant {
    uint materialIndex;
};
[push_constant]
PushConstant pc;

[shader("fragment")]
float4 fragMain(VSOutput vertIn) : SV_TARGET {
   float4 baseColor = textures[pc.materialIndex].Sample(textureSampler, vertIn.fragTexCoord);

这是一种称为“bindless 资源”的常用技术,它允许我们减少所需的描述符集和绑定的数量,并使管理具有许多对象的场景中的材质变得更加容易。它需要描述符索引扩展,自 Vulkan 1.2 起,该扩展已成为核心功能。

我们无法在光线遍历中使用推送常量,因为我们的光线可能击中场景中的任何几何体,而不仅仅是我们正在着色的那个。然而,我们可以为每个加速结构实例标记一个自定义索引,然后使用此索引和查找表(LUT)来查找所击中的实例的几何体和纹理。

38 TASK09 instance lut

createAccelerationStructures() 中,当我们遍历模型子网格时,需要向 AccelerationStructureInstanceKHR 结构体添加一个新字段,为每个子网格保存一个唯一索引:

vk::AccelerationStructureInstanceKHR instance{
    .transform = identity,
    .mask = 0xFF,
    .accelerationStructureReference = blasDeviceAddr
};

instances.push_back(instance);

instances[i].instanceCustomIndex = static_cast<uint32_t>(i);

如果你现在运行应用程序并使用 Nsight Graphics 捕获它,你将能够按“Instance Custom Index”着色,以查看分配给每个实例的索引,而之前它们都是相同的:

38 TASK09 instance custom index

然后,填充一个 LUT 条目的向量。使用相同的子网格索引,我们需要为每个子网格存储材质 ID 和索引缓冲偏移量:

// TASK09:存储实例查找表条目
instanceLUTs.push_back({ static_cast<uint32_t>(submesh.materialID), submesh.indexOffset });

与创建 LUT 缓冲相关的其余代码可以在 createDescriptorSets()createInstanceLUTBuffer() 中找到。请注意,相应的绑定已在着色器中定义:

// TASK09:实例查找表
struct InstanceLUT {
    uint materialID;
    uint indexBufferOffset;
};
[[vk::binding(4,0)]]
StructuredBuffer<InstanceLUT> instanceLUTBuffer;

现在我们将看到如何使用这些资源与 Ray Query 来处理透明交点。

任务 10:带 Alpha 测试的 Ray Query

还记得 Proceed() 如何将 RayQuery 对象的状态沿光线推进到下一个交点候选吗?这就是我们将实现 Alpha 测试逻辑的地方。我们将检查用于着色我们所击中的三角形的纹理的 Alpha 值,如果它低于某个阈值,我们将继续遍历以找到下一个不透明三角形。一旦找到,我们就“提交”它,遍历将结束。

首先,将调用替换为一个循环,并从候选命中点检索必要的属性。然后,我们将这些传递给一个辅助函数 intersection_uv,它将检索我们在三角形内击中的点的纹理坐标:

    while (sq.Proceed())
    {
        uint instanceID = sq.CandidateRayInstanceCustomIndex();
        uint primIndex = sq.CandidatePrimitiveIndex();

        float2 uv = intersection_uv(instanceID, primIndex, sq.CandidateTriangleBarycentrics());
    }

以下是此辅助函数的定义:

float2 intersection_uv(uint instanceID, uint primIndex, float2 barycentrics) {
    uint indexOffset = instanceLUTBuffer[NonUniformResourceIndex(instanceID)].indexBufferOffset;

    uint i0 = indexBuffer[indexOffset + (primIndex * 3 + 0)];
    uint i1 = indexBuffer[indexOffset + (primIndex * 3 + 1)];
    uint i2 = indexBuffer[indexOffset + (primIndex * 3 + 2)];

    float2 uv0 = uvBuffer[i0];
    float2 uv1 = uvBuffer[i1];
    float2 uv2 = uvBuffer[i2];

    float w0 = 1.0 - barycentrics.x - barycentrics.y;
    float w1 = barycentrics.x;
    float w2 = barycentrics.y;

    return w0 * uv0 + w1 * uv1 + w2 * uv2;
}
  1. instanceID 允许我们从实例 LUT 中检索 indexBufferOffsetmaterialID

  2. indexBufferOffset 用于查找实例的索引缓冲。请注意,索引缓冲包含场景中所有模型的索引,因此我们需要将其缩小到命中的模型(例如树叶)。

  3. primIndex 是三角形在索引缓冲的实例部分中的索引。

  4. NonUniformResourceIndex() 表示资源索引可能在单个绘制或分派调用中的不同着色器调用之间有所不同,从而防止不必要的编译器优化。

一旦我们将命中缩小到模型内的特定三角形,我们就可以在 uvBuffer 中检索它的纹理坐标,该缓冲包含场景中所有顶点的 UV 坐标。

最后,它根据交点的重心坐标插值命中三角形的纹理坐标。

然后,我们可以使用这些 UV 坐标来采样纹理并检索 Alpha 值:

        uint materialID = instanceLUTBuffer[NonUniformResourceIndex(instanceID)].materialID;
        float4 intersection_color = textures[NonUniformResourceIndex(materialID)].SampleLevel(textureSampler, uv, 0);

根据 Alpha 值,我们可以决定是继续追踪还是提交命中:

        if (intersection_color.a < 0.5) {
            // 如果三角形是透明的,我们继续追踪
            // 以找到下一个不透明三角形。
        } else {
            // 如果我们击中了不透明三角形,则停止追踪。
            sq.CommitNonOpaqueTriangleHit();
        }

完整的 Proceed() 循环应如下所示:

    while (sq.Proceed())
    {
        uint instanceID = sq.CandidateRayInstanceCustomIndex();
        uint primIndex = sq.CandidatePrimitiveIndex();

        float2 uv = intersection_uv(instanceID, primIndex, sq.CandidateTriangleBarycentrics());

        uint materialID = instanceLUTBuffer[NonUniformResourceIndex(instanceID)].materialID;
        float4 intersection_color = textures[NonUniformResourceIndex(materialID)].SampleLevel(textureSampler, uv, 0);

        if (intersection_color.a < 0.5) {
            // 如果三角形是透明的,我们继续追踪
            // 以找到下一个不透明三角形。
        } else {
            // 如果我们击中了不透明三角形,则停止追踪。
            sq.CommitNonOpaqueTriangleHit();
        }
    }

请注意,不透明命中的三角形会自动提交,并且永远不会进入循环。

使用以下设置重新构建并运行:

#define LAB_TASK_LEVEL 10

至此,你已经通过 Ray Query 实现了带透明度的稳健阴影!这是一个重要的特性,对于精细的 Alpha 细节,传统的阴影映射很难做到这一点:

38 TASK10 alphacut shadows

有了支持阴影透明度的所有设置,实现反射等其他效果就变得非常简单了!

导航

Vulkan 是 Khronos Group Inc. 的注册商标

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