如何在单个 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 似乎是一个不错的解决方案:
- 使用逐对象渲染通道并使用栅栏确保一个对象被完全渲染,直到我开始渲染下一个对象。如果有 1000 个对象,每帧将有 1000 个渲染通道?这是不可能的。
- 我可以在一次渲染过程中重复提交命令缓冲区吗?我的意思是我在构建一个对象的绘制命令以渲染对象后立即提交命令缓冲区,使用 fence 确保渲染完成,然后转到下一个对象。这将有一个渲染通道和 1000 个 vkQueueSubmit() 调用
- 使用动态统一缓冲区创建一个巨大的统一缓冲区,其中包含 1000 个对象的数据。由于对象数量是动态的,因此很难实现。
- 使用推送常量?这也是不可能的,因为最大数据大小只有 128 字节。
因为您正在记录绘图命令及其以制服形式的输入数据,对于场景中的所有对象,在它们执行和读取它们的输入数据之前,没有办法为所有对象进行存储在某处分配的统一缓冲区的版本。 OpenGL ES 驱动程序为您做这件事:当您更新制服时,它们会在内部分配新的 space,将新制服写入其中,然后更新内部指针,以便下一次调用将使用新的制服数据以前的统一数据。
在 Vulkan 中,你可以自己做,你的第三个想法最接近正确的方法。有一些变化,但最直接的变化之一是:
创建一个大的 VkBuffer 并将其绑定到内存。它应该足够大以处理 typical/average 帧的所有统一数据。从零偏移量开始,对于每次抽取,在当前偏移量处写入新制服,在描述符集中重新绑定 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,动态偏移量指向新制服数据,然后更新偏移量下一次抽奖的制服将放在你刚刚使用过的制服之后。
在每一帧结束时(假设每帧一个命令缓冲区),记住你在缓冲区中到达了多远,并将其与表示该命令缓冲区完成的事件相关联。该事件将告诉您何时可以覆盖该帧中使用的缓冲区区域。如果在足够的 space 再次可用之前,您最终需要更多 space 用于制服,您可以创建一个新的 VkBuffer 并开始使用它,最终在其数据退役时恢复到原始状态。通过这种方式,您最终可以得到一个由多个 VkBuffer 组成的统一数据的动态大小的环形缓冲区。
我正在尝试将我的 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 似乎是一个不错的解决方案:
- 使用逐对象渲染通道并使用栅栏确保一个对象被完全渲染,直到我开始渲染下一个对象。如果有 1000 个对象,每帧将有 1000 个渲染通道?这是不可能的。
- 我可以在一次渲染过程中重复提交命令缓冲区吗?我的意思是我在构建一个对象的绘制命令以渲染对象后立即提交命令缓冲区,使用 fence 确保渲染完成,然后转到下一个对象。这将有一个渲染通道和 1000 个 vkQueueSubmit() 调用
- 使用动态统一缓冲区创建一个巨大的统一缓冲区,其中包含 1000 个对象的数据。由于对象数量是动态的,因此很难实现。
- 使用推送常量?这也是不可能的,因为最大数据大小只有 128 字节。
因为您正在记录绘图命令及其以制服形式的输入数据,对于场景中的所有对象,在它们执行和读取它们的输入数据之前,没有办法为所有对象进行存储在某处分配的统一缓冲区的版本。 OpenGL ES 驱动程序为您做这件事:当您更新制服时,它们会在内部分配新的 space,将新制服写入其中,然后更新内部指针,以便下一次调用将使用新的制服数据以前的统一数据。
在 Vulkan 中,你可以自己做,你的第三个想法最接近正确的方法。有一些变化,但最直接的变化之一是:
创建一个大的 VkBuffer 并将其绑定到内存。它应该足够大以处理 typical/average 帧的所有统一数据。从零偏移量开始,对于每次抽取,在当前偏移量处写入新制服,在描述符集中重新绑定 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,动态偏移量指向新制服数据,然后更新偏移量下一次抽奖的制服将放在你刚刚使用过的制服之后。
在每一帧结束时(假设每帧一个命令缓冲区),记住你在缓冲区中到达了多远,并将其与表示该命令缓冲区完成的事件相关联。该事件将告诉您何时可以覆盖该帧中使用的缓冲区区域。如果在足够的 space 再次可用之前,您最终需要更多 space 用于制服,您可以创建一个新的 VkBuffer 并开始使用它,最终在其数据退役时恢复到原始状态。通过这种方式,您最终可以得到一个由多个 VkBuffer 组成的统一数据的动态大小的环形缓冲区。