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

Glsl 如何重复更新单个Vulkan渲染过程中对象数量的统一数据并使更新同步?,glsl,shader,vulkan,Glsl,Shader,Vulkan,我正在尝试将我的OpenGL 3D游戏引擎移植到Vulkan。游戏场景中有大量3D对象,每个对象都有自己的属性(模型矩阵、灯光等),并且对象是完全动态的,这意味着在游戏过程中可能会出现一些3D对象,而其他对象可能会被移除。使用OpenGL,我将3D对象的属性分组到着色器中的统一缓冲区中(代码简化): 我现在要做的是对游戏场景中的每个3D对象使用这个统一的缓冲区,通过Vulkan渲染它们。 我使用一个Vulkan渲染过程,在开始渲染过程和结束渲染过程中,我使用for-each循环遍历每个3D对象,

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

我现在要做的是对游戏场景中的每个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

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

所以,可能的问题是,Vulkan如何正确地为单个渲染过程中的每个对象重复更新统一缓冲区,并确保每个对象获得正确的属性数据

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

  • 使用“每个对象渲染过程”并使用“围栏”确保一个对象完全渲染,直到我开始渲染下一个对象。如果有1000个对象,每帧将有1000个渲染过程?这是不可能的
  • 我可以在一个渲染过程中重复提交命令缓冲区吗?我的意思是,在为一个对象生成绘制命令以渲染该对象之后,立即提交命令缓冲区,使用fence确保渲染完成,然后转到下一个对象。这将有一个渲染过程和1000个vkQueueSubmit()调用
  • 使用动态统一缓冲区创建一个巨大的统一缓冲区,包含1000个对象的数据。由于对象编号是动态的,因此很难实现
  • 使用推力常数?这也是不可能的,因为最大数据大小只有128字节

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

在Vulkan,你可以自己做,你的第三个想法是最接近正确的方式。有几种变体,但最简单的一种是:

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


在每帧结束时(假设每帧有一个命令缓冲区),记住您在缓冲区中走了多远,并将其与表示该命令缓冲区完成的事件相关联。该事件将告诉您何时可以覆盖该帧中使用的缓冲区区域。如果在重新获得足够的可用空间之前,您需要更多的空间来放置制服,您可以创建一个新的VkBuffer并开始使用它,最终在其数据失效时返回到原始状态。这样,您就可以得到由多个Vkbuffer组成的统一数据动态大小的环形缓冲区。

值得注意的是,“巨大”的统一缓冲区通常比逐顶点数据小几个数量级,因此,不要害怕保守,分配一个比您认为需要的更大的块。它通常有助于以字节表示大小,1024x1024 RGB纹理是4MB,一千个256字节的统一块每个是256KB。GPU的可用内存通常是GB。谢谢大家给我提供的有用建议。我认为动态UBO似乎是最可行的解决方案。但是,该解决方案添加了一些额外的工作来保持每对象偏移。既然游戏场景或CAD应用程序中的对象是动态的(不断进出),那么为5000个对象预先分配UBO,然后使用唯一的每个对象ID来执行动态偏移计算是一个好主意,还是有一个更聪明的方法?我不会保留每个对象的偏移,我会每次绘制。偏移量只是“下一次绘制的制服放置位置”,每次绘制对象时,对象使用的偏移量可能会改变。嗨,Jesse,我想我已经了解了动态缓冲区解决方案的概念,但对于下半部分,我不是很清楚-如何实现“由多个VKBuffer组成的统一数据的动态大小的环形缓冲区”。你能推荐一个Internet上的示例项目,可以给我提供更多的实现细节吗?谢谢。

    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