Skip to content

Dynamic Uniform Buffer

This example showcases how raw data can be written to uniform buffers and then be indexed with bind groups. In this case it is used to store the transformation matrices of four triangles and render each one using the same index and vertex buffers. The only input that's necessary is the index of the object's transform matrix in the buffer. Take a look at the following render code:

1
2
3
4
5
6
7
    for (size_t i = 0; i < entityCount; ++i) {
        // Bind Group and provide offset into the Dynamic UBO that holds all the transform matrices
        const uint32_t dynamicUBOOffset = i * m_dynamicUBOByteStride;
        opaquePass.setBindGroup(0, m_transformBindGroup, m_pipelineLayout, { dynamicUBOOffset });
        const DrawIndexedCommand drawCmd = { .indexCount = 3 };
        opaquePass.drawIndexed(drawCmd);
    }

Filename: dynamic_ubo/dynamic_ubo_triangles.cpp

entityCount is 4, corresponding to the triangles. During this process of creating the draw commands for each triangle, only the offset of the bindgroup is changed.

This buffer of transforms is modified every frame in updateScene:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    // Each frame we want to rotate the triangle a little
    static float angle = 0.0f;
    angle += 0.1f;
    if (angle > 360.0f)
        angle -= 360.0f;

    std::vector<uint8_t> rawTransformData(entityCount * m_dynamicUBOByteStride, 0U);

    // Update EntityCount matrices into the single buffer we have
    for (size_t i = 0; i < entityCount; ++i) {
        auto transform = glm::mat4(1.0f);
        transform = glm::translate(transform, glm::vec3(-0.7f + i * 0.5f, 0.0f, 0.0f));
        transform = glm::scale(transform, glm::vec3(0.2f));
        transform = glm::rotate(transform, glm::radians(angle + 45.0f * i), glm::vec3(0.0f, 0.0f, 1.0f));

        std::memcpy(rawTransformData.data() + i * m_dynamicUBOByteStride, &transform, sizeof(glm::mat4));
    }

    auto bufferData = m_transformDynamicUBOBuffer.map();
    std::memcpy(bufferData, rawTransformData.data(), rawTransformData.size());
    m_transformDynamicUBOBuffer.unmap();

Filename: dynamic_ubo/dynamic_ubo_triangles.cpp

In order to achieve this, all of the buffers must be mapped to CPU address space: the vertex buffer, index buffer, and transform uniform buffer. We do not use uploadBufferData like we did in the previous examples for most of these buffers. Instead, we use the map, memcpy, and unmap pattern. Uploading the index data looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        const BufferOptions bufferOptions = {
            .size = 3 * sizeof(uint32_t),
            .usage = BufferUsageFlagBits::IndexBufferBit,
            .memoryUsage = MemoryUsage::CpuToGpu
        };
        m_indexBuffer = m_device.createBuffer(bufferOptions);
        std::vector<uint32_t> indexData = { 0, 1, 2 };
        auto bufferData = m_indexBuffer.map();
        std::memcpy(bufferData, indexData.data(), indexData.size() * sizeof(uint32_t));
        m_indexBuffer.unmap();

Filename: dynamic_ubo/dynamic_ubo_triangles.cpp

Additionally, we must configure the bind group format to hold a dynamic uniform buffer by setting the resource type to KDGpu::ResourceBindingType::DynamicUniformBuffer

1
2
3
4
5
6
7
8
9
    // Create bind group layout consisting of a single binding holding a UBO
    // clang-format off
    const BindGroupLayoutOptions bindGroupLayoutOptions = {
        .bindings = {{
            .binding = 0,
            .resourceType = ResourceBindingType::DynamicUniformBuffer,
            .shaderStages = ShaderStageFlags(ShaderStageFlagBits::VertexBit)
        }}
    };

Filename: dynamic_ubo/dynamic_ubo_triangles.cpp

And the bind group itself needs to contain a KDGpu::DynamicUniformBufferBinding instead of a KDGpu::UniformBufferBinding.

1
2
3
4
5
6
7
8
9
    const BindGroupOptions bindGroupOptions = {
        .layout = bindGroupLayout,
        .resources = {{
            .binding = 0,
            // We are dealing with a Dynamic UBO expected to hold
            // a set of transform matrices. The size we specify for the binding is the size of a single entry in the buffer
            .resource = DynamicUniformBufferBinding{ .buffer = m_transformDynamicUBOBuffer, .size = uint32_t(m_dynamicUBOByteStride) }
        }}
    };

Filename: dynamic_ubo/dynamic_ubo_triangles.cpp


Updated on 2023-12-22 at 00:05:36 +0000