狸村 Mystic Web · 狸村
目录

目标:通过每帧使用更新后的实例变换重建 TLAS,确保物体动画时阴影能够更新。

为了考虑物体的动画,我们需要在物体移动或变化时更新 TLAS。 这涉及更新实例变换并重建 TLAS。 我们将在 updateTopLevelAS() 函数中执行此操作,该函数每帧都会使用当前的模型矩阵调用。

任务 6:为动画更新 TLAS

首先,我们需要使用当前的模型矩阵更新实例变换。通过遍历 instances 向量并为每个实例设置变换,然后更新实例缓冲来完成。

vk::TransformMatrixKHR tm{};
auto &M = model;
tm.matrix = std::array<std::array<float,4>,3>{{
        std::array<float,4>{M[0][0], M[1][0], M[2][0], M[3][0]},
        std::array<float,4>{M[0][1], M[1][1], M[2][1], M[3][1]},
        std::array<float,4>{M[0][2], M[1][2], M[2][2], M[3][2]}
}};

// TASK06:更新实例以使用新的变换矩阵。
for (auto & instance : instances) {
        instance.setTransform(tm);
}

接下来,我们需要为 TLAS 构建准备几何体数据。这与我们创建 TLAS 时所做的类似,但现在我们将使用更新后的实例缓冲。我们还需要将构建 mode 更改为 eUpdate,并同时定义源 TLAS 和目标 TLAS。这指示实现就地更新现有的 TLAS,而不是创建一个新的。当仅发生微小变化(如变换)时,这更高效:

        // 准备几何体(实例)数据
        auto instancesData = vk::AccelerationStructureGeometryInstancesDataKHR{
            .arrayOfPointers = vk::False,
            .data = instanceAddr
        };

        vk::AccelerationStructureGeometryDataKHR geometryData(instancesData);

        vk::AccelerationStructureGeometryKHR tlasGeometry{
            .geometryType = vk::GeometryTypeKHR::eInstances,
            .geometry = geometryData
        };

        // TASK06:注意用于就地重建 TLAS 的新参数
        vk::AccelerationStructureBuildGeometryInfoKHR tlasBuildGeometryInfo{
            .type = vk::AccelerationStructureTypeKHR::eTopLevel,
            .flags = vk::BuildAccelerationStructureFlagBitsKHR::eAllowUpdate,
            .mode = vk::BuildAccelerationStructureModeKHR::eUpdate,
            .srcAccelerationStructure = tlas,
            .dstAccelerationStructure = tlas,
            .geometryCount = 1,
            .pGeometries = &tlasGeometry
        };

        vk::BufferDeviceAddressInfo scratchAddressInfo{ .buffer = *tlasScratchBuffer };
        vk::DeviceAddress scratchAddr = device.getBufferAddressKHR(scratchAddressInfo);
        tlasBuildGeometryInfo.scratchData.deviceAddress = scratchAddr;

我们可以继续重用同一个 scratch 缓冲。注意,还需要另一个实现提示,即标志 eAllowUpdate,用于指定我们打算更新此 TLAS。我们还需要重新访问 createAccelerationStructures() 函数,在第一次创建 TLAS 时添加此标志:

        vk::AccelerationStructureBuildGeometryInfoKHR tlasBuildGeometryInfo{
            .type = vk::AccelerationStructureTypeKHR::eTopLevel,
            .flags = vk::BuildAccelerationStructureFlagBitsKHR::eAllowUpdate, // <---- TASK06
            .mode = vk::BuildAccelerationStructureModeKHR::eBuild,
            .geometryCount = 1,
            .pGeometries = &tlasGeometry
        };

接下来,我们需要为 TLAS 准备构建范围。这与我们创建 TLAS 时所做的类似:

        // 准备 TLAS 的构建范围
        vk::AccelerationStructureBuildRangeInfoKHR tlasRangeInfo{
            .primitiveCount = primitiveCount,
            .primitiveOffset = 0,
            .firstVertex = 0,
            .transformOffset = 0
        };

最后,我们可以发出命令来重建 TLAS。然而,这里需要一项主要更改,即关于同步。由于我们每帧都调用 updateTopLevelAS(),我们需要一个构建前内存屏障,以确保在构建开始之前,对加速结构的任何先前写入传输或上一帧的着色器读取都已完成:

        // 重建 TLAS
        auto cmd = beginSingleTimeCommands();

        // 构建前屏障
        vk::MemoryBarrier preBarrier {
            .srcAccessMask = vk::AccessFlagBits::eAccelerationStructureWriteKHR | vk::AccessFlagBits::eTransferWrite | vk::AccessFlagBits::eShaderRead,
            .dstAccessMask = vk::AccessFlagBits::eAccelerationStructureReadKHR | vk::AccessFlagBits::eAccelerationStructureWriteKHR
        };

        cmd->pipelineBarrier(
            vk::PipelineStageFlagBits::eAccelerationStructureBuildKHR | vk::PipelineStageFlagBits::eTransfer | vk::PipelineStageFlagBits::eFragmentShader, // srcStageMask
            vk::PipelineStageFlagBits::eAccelerationStructureBuildKHR, // dstStageMask
            {}, // dependencyFlags
            preBarrier, // memoryBarriers
            {}, // bufferMemoryBarriers
            {} // imageMemoryBarriers
        );

        cmd->buildAccelerationStructuresKHR({ tlasBuildGeometryInfo }, { &tlasRangeInfo });

类似地,我们需要一个构建后屏障,以确保构建期间对加速结构的所有写入对后续读取或着色器访问可见:

        // 构建后屏障
        vk::MemoryBarrier postBarrier {
            .srcAccessMask = vk::AccessFlagBits::eAccelerationStructureWriteKHR,
            .dstAccessMask = vk::AccessFlagBits::eAccelerationStructureReadKHR | vk::AccessFlagBits::eShaderRead
        };

        cmd->pipelineBarrier(
            vk::PipelineStageFlagBits::eAccelerationStructureBuildKHR, // srcStageMask
            vk::PipelineStageFlagBits::eAccelerationStructureBuildKHR | vk::PipelineStageFlagBits::eFragmentShader, // dstStageMask
            {}, // dependencyFlags
            postBarrier, // memoryBarriers
            {}, // bufferMemoryBarriers
            {} // imageMemoryBarriers
        );

        endSingleTimeCommands(*cmd);

这些屏障对于正确的同步至关重要,可以防止竞争条件,并确保加速结构对于光线追踪着色器处于有效状态。

验证该函数在 drawFrame() 中更新模型矩阵后被调用:

        updateUniformBuffer(frameIndex);
        // TASK06:使用当前模型矩阵更新 TLAS
        updateTopLevelAS(ubo.model);

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

#define LAB_TASK_LEVEL 6

现在阴影应该正确更新,因为加速结构和几何体动画是同步的:

38 TASK07 shadows dynamic

作为参考,下面是此阶段完整着色器的样子:

点击展开着色器
struct VSInput {
    float3 inPosition;
    float3 inColor;
    float2 inTexCoord;
    float3 inNormal;
};

struct UniformBuffer {
    float4x4 model;
    float4x4 view;
    float4x4 proj;
    float3   cameraPos;
};
[[vk::binding(0,0)]]
ConstantBuffer<UniformBuffer> ubo;

// TASK05:加速结构绑定
[[vk::binding(1,0)]]
RaytracingAccelerationStructure accelerationStructure;

[[vk::binding(2,0)]]
StructuredBuffer<uint> indexBuffer;

[[vk::binding(3,0)]]
StructuredBuffer<float2> uvBuffer;

struct InstanceLUT {
    uint materialID;
    uint indexBufferOffset;
};
[[vk::binding(4,0)]]
StructuredBuffer<InstanceLUT> instanceLUTBuffer;

struct VSOutput
{
    float4 pos : SV_Position;
    float3 fragColor;
    float2 fragTexCoord;
    float3 fragNormal;
    float3 worldPos;
};

[shader("vertex")]
VSOutput vertMain(VSInput input) {
    VSOutput output;
    output.pos = mul(ubo.proj, mul(ubo.view, mul(ubo.model, float4(input.inPosition, 1.0))));
    output.fragColor = input.inColor;
    output.fragTexCoord = input.inTexCoord;
    output.fragNormal = input.inNormal;
    output.worldPos = mul(ubo.model, float4(input.inPosition, 1.0)).xyz;
    return output;
}

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

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

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

static const float3 lightDir = float3(-6.0, 0.0, 6.0);

// 小 epsilon 以避免自相交
static const float EPSILON = 0.01;

// TASK05:实现 Ray Query 阴影
bool in_shadow(float3 P)
{
    // 从世界位置向光源构建阴影光线
    RayDesc shadowRayDesc;
    shadowRayDesc.Origin = P;
    shadowRayDesc.Direction = normalize(lightDir);
    shadowRayDesc.TMin = EPSILON;
    shadowRayDesc.TMax = 1e4;

    // 初始化用于阴影的 RayQuery
    RayQuery<RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES |
             RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH> sq;
    let rayFlags = RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES |
             RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH;

    sq.TraceRayInline(accelerationStructure, rayFlags, 0xFF, shadowRayDesc);

    sq.Proceed();

    // 如果阴影光线命中不透明三角形,我们认为该像素处于阴影中
    bool hit = (sq.CommittedStatus() == COMMITTED_TRIANGLE_HIT);

    return hit;
}

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

   float3 P = vertIn.worldPos;

   bool inShadow = in_shadow(P);

   // 如果在阴影中则变暗
   if (inShadow) {
       baseColor.rgb *= 0.2;
   }

   return baseColor;
}

注意:Ray Query 与光线追踪管线:注意我们是如何直接在片段着色器中添加光线追踪效果(阴影)的。我们不需要单独的光线生成着色器或任何新管线。这就是 Ray Query(也称为内联光线追踪)的强大之处:我们将光线遍历集成到现有的渲染管线中。这保持了着色器逻辑的统一,并避免了额外的 GPU 着色器启动。在许多移动 GPU 上,这种方法不仅更方便,而且是必要的:如前所述,当前的移动设备大多支持 Ray Query 而不支持完整的光线管线,并且它们在片段着色器中高效地运行 Ray Query。这是我们在本实验中重点关注 Ray Query 的关键原因。

导航

Vulkan 是 Khronos Group Inc. 的注册商标

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