如何重复更新单个 Vulkan 渲染通道内多个对象的统一数据并使更新同步?

Hon*_*ang 4 shader glsl vulkan

我正在尝试将我的 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;

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

我正在使用单个 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

Run Code Online (Sandbox Code Playgroud)

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

那么可能会有疑问,Vulkan 的解决方案是如何为单个渲染通道中的每个对象重复正确地更新统一缓冲区并确保每个对象获得正确的属性数据?

在我发布这个问题之前,我试图考虑以下解决方案,但似乎没有一个是好的:

  • 使用 render-pass-per-object 并使用 fence 确保一个对象被完全渲染,直到我开始渲染下一个。如果有 1000 个对象,每帧会有 1000 个渲染通道?这是不可能的。
  • 我可以在一个渲染通道内重复提交命令缓冲区吗?我的意思是我在为一个对象构建绘制命令以渲染对象后立即提交命令缓冲区,使用围栏确保渲染完成,然后转到下一个对象。这将有一个渲染通道和 1000 个 vkQueueSubmit() 调用
  • 使用动态统一缓冲区创建一个巨大的统一缓冲区,包含 1000 个对象的数据。由于对象编号是动态的,因此难以实现。
  • 使用推送常量?这也是不可能的,因为最大数据大小仅为 128 字节。

Jes*_*all 5

因为您正在以统一形式记录绘图命令及其输入数据,所以在场景中的所有对象执行并读取其输入数据之前,无法为场景中的所有对象存储所有版本的统一缓冲区分配到某处。OpenGL ES 驱动程序为您执行此操作:当您更新制服时,它们会在内部分配新空间,将新制服写入其中,然后更新内部指针,以便下一次调用将使用新的统一数据而不是以前的统一数据数据。

在 Vulkan 中,您可以自己做这件事,而您的第三个想法最接近正确的方法。有一些变化,但最直接的一种是:

创建一个大的 VkBuffer 并将其绑定到内存。它可能应该足够大以处理典型/平均帧的所有统一数据。从零偏移开始,对于每次绘制,在当前偏移处写入新的制服,在描述符集中重新绑定 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,动态偏移指向新的统一数据,然后更新偏移,以便下一次绘制的制服将放置在您刚刚使用的那些之后。

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