如何在单个 Vulkan 渲染过程中重复更新多个对象的统一数据并使更新同步?

How to repeatedly update a uniform data for number of objects inside a single Vulkan render pass and make the update synchronized?

我正在尝试将我的 OpenGL 3D 游戏引擎移植到 Vulkan。游戏场景中有大量的3D对象,每个对象都有自己的属性(模型矩阵、灯光等),并且对象是完全动态的,这意味着在游戏过程中,一些3D对象可能会进来,而另一些可能会被移除.使用 OpenGL,我将 3D 对象的属性分组到着色器中的统一缓冲区(代码简化):


    layout(std140, set = 0, binding = 0) uniform object_attrib 
    {
        vec3 light_pos;
        vec3 light_color;
        mat4 model;
        mat4 view_projection;
        ...
    } params;

我现在要做的是为游戏场景中的每个 3D 对象使用这个单一的统一缓冲区,以通过 Vulkan 渲染它们。

我在 begin-render-pass 和 end-render-pass 中使用单个 Vulkan 渲染通道,我使用 for-each 循环遍历每个 3D 对象并执行以下操作来渲染它们。请参阅下面的伪代码。


    vkBeginCommandBuffer(cmdBuffer, ...);
        vkCmdBeginRenderPass(cmdBuffer, ...);
            for(object3D obj : scene->objects)
            { 
                // Step 1 - update object's uniform data by memcpy()
                _updateUniformBuffer(obj); 

                // Step 2 - build draw command for this object
                // bind vertex buffer, bind index buffer, bind pipeline, ..., draw
                _buildDrawCommands(obj);
            }
        vkCmdEndRenderPass(cmdBuffer, ...);
    vkEndCommandBuffer(cmdBuffer, ...);
    vkQueueSubmit(...); // Finally, submit the commands to queue to render the scene

显然,我的解决方案将不起作用,因为缓冲区中的所有 Vulkan 命令仅在调用 vkQueueSubmit() 后才在 GPU 上执行。但是对 _updateUniformBuffer(obj) 的调用(通过 memcpy(...))是 "interleaved" 带有命令记录的,它会立即执行,因此序列被打乱,最后每个对象都不会获得自己的属性。

所以可能会有疑问,Vulkan 的解决方案是什么,可以在单个渲染过程中为每个对象重复正确更新统一缓冲区,并确保每个对象都获得正确的属性数据?

在我 post 这个问题之前,我尝试考虑以下解决方案,但其中 none 似乎是一个不错的解决方案:

因为您正在记录绘图命令及其以制服形式的输入数据,对于场景中的所有对象,在它们执行和读取它们的输入数据之前,没有办法为所有对象进行存储在某处分配的统一缓冲区的版本。 OpenGL ES 驱动程序为您做这件事:当您更新制服时,它们会在内部分配新的 space,将新制服写入其中,然后更新内部指针,以便下一次调用将使用新的制服数据以前的统一数据。

在 Vulkan 中,你可以自己做,你的第三个想法最接近正确的方法。有一些变化,但最直接的变化之一是:

创建一个大的 VkBuffer 并将其绑定到内存。它应该足够大以处理 typical/average 帧的所有统一数据。从零偏移量开始,对于每次抽取,在当前偏移量处写入新制服,在描述符集中重新绑定 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,动态偏移量指向新制服数据,然后更新偏移量下一次抽奖的制服将放在你刚刚使用过的制服之后。

在每一帧结束时(假设每帧一个命令缓冲区),记住你在缓冲区中到达了多远,并将其与表示该命令缓冲区完成的事件相关联。该事件将告诉您何时可以覆盖该帧中使用的缓冲区区域。如果在足够的 space 再次可用之前,您最终需要更多 space 用于制服,您可以创建一个新的 VkBuffer 并开始使用它,最终在其数据退役时恢复到原始状态。通过这种方式,您最终可以得到一个由多个 VkBuffer 组成的统一数据的动态大小的环形缓冲区。