Skip to content

Depth Texture Lookup

First, we start by preparing resources for 3 render passes.

The first render pass will render a cube to the swapchain color attachment and a depth attachment. We request attachments to be cleared when we begin this render pass.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    // Scene Cube Pass
    {
        // Create a vertex shader and fragment shader (spir-v only for now)
        const auto vertexShaderPath = KDGpu::assetPath() + "/shaders/examples/depth_texture_lookup/cube.vert.spv";
        auto vertexShader = m_device.createShaderModule(KDGpuExample::readShaderFile(vertexShaderPath));

        const auto fragmentShaderPath = KDGpu::assetPath() + "/shaders/examples/depth_texture_lookup/cube.frag.spv";
        auto fragmentShader = m_device.createShaderModule(KDGpuExample::readShaderFile(fragmentShaderPath));

        // Create a pipeline layout (array of bind group layouts)
        m_rotationPushConstantRange = {
            .size = sizeof(glm::mat4),
            .shaderStages = ShaderStageFlagBits::VertexBit,
        };

        m_sceneCubePipelineLayout = m_device.createPipelineLayout(PipelineLayoutOptions{
                .pushConstantRanges = {
                        m_rotationPushConstantRange,
                },
        });

        // Create a pipeline
        m_sceneCubePipeline = m_device.createGraphicsPipeline(GraphicsPipelineOptions{
                .shaderStages = {
                        { .shaderModule = vertexShader, .stage = ShaderStageFlagBits::VertexBit },
                        { .shaderModule = fragmentShader, .stage = ShaderStageFlagBits::FragmentBit } },
                .layout = m_sceneCubePipelineLayout,
                .vertex = {},
                .renderTargets = { { .format = m_swapchainFormat } },
                .depthStencil = { .format = m_depthFormat, .depthWritesEnabled = true, .depthCompareOperation = CompareOperation::Less },
                .primitive = { .topology = PrimitiveTopology::TriangleList },
        });

        // Most of the render pass is the same between frames. The only thing that changes, is which image
        // of the swapchain we wish to render to. So set up what we can here, and in the render loop we will
        // just update the color texture view.
        m_sceneCubePassOptions = {
            .colorAttachments = {
                    {
                            .view = {}, // Not setting the swapchain texture view just yet
                            .clearValue = { 0.3f, 0.3f, 0.3f, 1.0f },
                    },
            },
            .depthStencilAttachment = {
                    .view = m_depthTextureView,
            },
        };
    }

Filename: depth_texture_lookup/depth_texture_lookup.cpp

The second render pass will clear the color attachment and sample from the previously filled depth buffer. Note that this render pass has no depth attachment. A BindGroup that can hold a CombinedImageSampler is allocated in order to feed the depth texture to the shader.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    // Depth Lookup Pass
    {
        // Create a sampler to be used when sampling the detph texture
        m_depthTextureSampler = m_device.createSampler();

        // Create a vertex shader and fragment shader (spir-v only for now)
        const auto vertexShaderPath = KDGpu::assetPath() + "/shaders/examples/depth_texture_lookup/textured_quad.vert.spv";
        auto vertexShader = m_device.createShaderModule(KDGpuExample::readShaderFile(vertexShaderPath));

        const auto fragmentShaderPath = KDGpu::assetPath() + "/shaders/examples/depth_texture_lookup/textured_quad.frag.spv";
        auto fragmentShader = m_device.createShaderModule(KDGpuExample::readShaderFile(fragmentShaderPath));

        m_depthLookupBindGroupLayout = m_device.createBindGroupLayout(BindGroupLayoutOptions{
                .bindings = {
                        {
                                .binding = 0,
                                .resourceType = ResourceBindingType::CombinedImageSampler,
                                .shaderStages = ShaderStageFlags(ShaderStageFlagBits::FragmentBit),
                        },
                },
        });

        // Create a pipeline layout (array of bind group layouts)
        m_depthLookupPipelineLayout = m_device.createPipelineLayout(PipelineLayoutOptions{
                .bindGroupLayouts = { m_depthLookupBindGroupLayout },
        });

        m_depthLookupPipeline = m_device.createGraphicsPipeline(GraphicsPipelineOptions{
                .shaderStages = {
                        { .shaderModule = vertexShader, .stage = ShaderStageFlagBits::VertexBit },
                        { .shaderModule = fragmentShader, .stage = ShaderStageFlagBits::FragmentBit } },
                .layout = m_depthLookupPipelineLayout,
                .vertex = {},
                .renderTargets = { { .format = m_swapchainFormat } },
                .primitive = { .topology = PrimitiveTopology::TriangleList },
        });

        // Create a bindGroup to hold the uniform containing the texture and sampler
        m_depthTextureBindGroup = m_device.createBindGroup(BindGroupOptions{
                .layout = m_depthLookupBindGroupLayout,
                .resources = {
                        {
                                .binding = 0,
                                .resource = TextureViewSamplerBinding{ .textureView = m_depthTextureView, .sampler = m_depthTextureSampler },
                        },
                },
        });

        // Most of the render pass is the same between frames. The only thing that changes, is which image
        // of the swapchain we wish to render to. So set up what we can here, and in the render loop we will
        // just update the color texture view.
        m_depthLookupPassOptions = {
            .colorAttachments = {
                    {
                            .view = {}, // Not setting the swapchain texture view just yet
                            .loadOperation = AttachmentLoadOperation::Load, // Don't clear color
                            .initialLayout = TextureLayout::ColorAttachmentOptimal,
                    },
            },
        };
    }

Filename: depth_texture_lookup/depth_texture_lookup.cpp

Next we prepare our last render pass which will use the color and depth attachments but won't clear any of these attachments. This pass will only be used to render the ImGui overlay.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    // ImGui Overlay Pass
    {
        m_overlayPassOptions = {
            .colorAttachments = {
                    { .view = {}, // Not setting the swapchain texture view just yet
                      .loadOperation = AttachmentLoadOperation::Load, // Don't clear color
                      .initialLayout = TextureLayout::ColorAttachmentOptimal,
                      .finalLayout = TextureLayout::PresentSrc },
            },
            .depthStencilAttachment = {
                    .view = m_depthTextureView,
                    .depthLoadOperation = AttachmentLoadOperation::Load, // Load the depth buffer as is, don't clear it
                    .initialLayout = TextureLayout::DepthStencilAttachmentOptimal,
            },
        };
    }

Filename: depth_texture_lookup/depth_texture_lookup.cpp

Rendering

The rendering follows the same order as that of the render passes resources creation detailed above.

First we begin by updating the TextureView for the color attachment of each RenderPass options.

1
2
3
4
5
    auto commandRecorder = m_device.createCommandRecorder();

    m_sceneCubePassOptions.colorAttachments[0].view = m_swapchainViews.at(m_currentSwapchainImageIndex);
    m_depthLookupPassOptions.colorAttachments[0].view = m_swapchainViews.at(m_currentSwapchainImageIndex);
    m_overlayPassOptions.colorAttachments[0].view = m_swapchainViews.at(m_currentSwapchainImageIndex);

Filename: depth_texture_lookup/depth_texture_lookup.cpp

Then we start recording commands for the first RenderPass which draws a cube and fills up the depth attachment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    // Draw Cube

    static float angle = 0.0f;
    const float angularSpeed = 3.0f; // degrees per second
    const float dt = engine()->deltaTimeSeconds();
    angle += angularSpeed * dt;
    if (angle > 360.0f)
        angle -= 360.0f;

    glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), glm::radians(angle), glm::vec3(1.0f, 1.0f, 1.0f));

    auto opaquePass = commandRecorder.beginRenderPass(m_sceneCubePassOptions);
    opaquePass.setPipeline(m_sceneCubePipeline);
    opaquePass.pushConstant(m_rotationPushConstantRange, &rotation);
    opaquePass.draw(DrawCommand{ .vertexCount = 36 });
    opaquePass.end();

Filename: depth_texture_lookup/depth_texture_lookup.cpp

Next, we record commands for the depth lookup render pass which will draw a full screen quad and sample from the depth attachment.

Note that in order to be able to sample from the depth attachment we first have to transition the depth attachment texture from the DepthStencilAttachmentOptimal layout to the ShaderReadOnlyOptimal layout.

We don't update the bind group for this pass every frame but rather only when we know the depth texture has changed, which is every time the swapchain has been resized.

Subsequently, after having recorded the draw commands, we will transition back to the appropriate layout for the render pass that follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    // Only process depth lookup pass fragments once we are sure scene cube fragments have written to the depth buffer
    // Transition the depthTexture to a correct readable layout
    commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
            .srcStages = PipelineStageFlags(PipelineStageFlagBit::AllGraphicsBit),
            .srcMask = AccessFlagBit::DepthStencilAttachmentWriteBit,
            .dstStages = PipelineStageFlags(PipelineStageFlagBit::FragmentShaderBit),
            .dstMask = AccessFlags(AccessFlagBit::ShaderReadBit),
            .oldLayout = m_depthLayout,
            .newLayout = TextureLayout::ShaderReadOnlyOptimal,
            .texture = m_depthTexture,
            .range = {
                    .aspectMask = TextureAspectFlagBits::DepthBit | TextureAspectFlagBits::StencilBit,
                    .levelCount = 1,
            },
    });

    // Draw Quad that displays depth lookup
    auto depthLookupPass = commandRecorder.beginRenderPass(m_depthLookupPassOptions);
    depthLookupPass.setPipeline(m_depthLookupPipeline);
    depthLookupPass.setBindGroup(0, m_depthTextureBindGroup);
    depthLookupPass.draw(DrawCommand{ .vertexCount = 6 });
    depthLookupPass.end();

    // Layout gets reset when we resize as the depthTexture is recreated
    if (m_depthLayout == TextureLayout::Undefined)
        m_depthLayout = TextureLayout::DepthStencilAttachmentOptimal;

    // Transition the depthTexture back to an appropriate depthBuffer layout
    commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
            .srcStages = PipelineStageFlags(PipelineStageFlagBit::BottomOfPipeBit),
            .dstStages = PipelineStageFlags(PipelineStageFlagBit::TopOfPipeBit),
            .oldLayout = TextureLayout::ShaderReadOnlyOptimal,
            .newLayout = m_depthLayout,
            .texture = m_depthTexture,
            .range = {
                    .aspectMask = TextureAspectFlagBits::DepthBit | TextureAspectFlagBits::StencilBit,
                    .levelCount = 1,
            },
    });

Filename: depth_texture_lookup/depth_texture_lookup.cpp

Lastly, we begin the overlay render pass and record the commands that draw up the overlay.


Updated on 2024-12-15 at 00:01:56 +0000