简介
注意:演示此功能的源代码可以在我的开源 C++ Vulkan 示例仓库中的新示例中找到。
随着新的 VK_EXT_conditional_rendering 扩展的引入,Vulkan 获得了基于专用缓冲区中存储的值有条件地执行某些渲染和调度命令的能力。
因此,如果对象的可见性发生变化,不再需要重建命令缓冲区,而是只需更改单个缓冲区值即可控制是否执行该对象的渲染命令,而无需触及任何命令缓冲区。
本文和示例将展示此功能的一个简单用例,但与基于缓冲区的其他渲染条件(如间接绘制)一样,也可以使用计算着色器更新此类缓冲区,完全不需要与主机进行任何往返交互。
静态命令缓冲区
在本示例中,我们将渲染一个 glTF 模型的层次化节点结构:
绘制节点的功能通过递归函数实现,如下所示:
void renderNode(vkglTF::Node *node, VkCommandBuffer commandBuffer) {
// 使用多个描述符集传递场景和模型节点矩阵
const std::vector<VkDescriptorSet> descriptorsets = {
scene.descriptorSet,
node->mesh->uniformBuffer.descriptorSet
};
vkCmdBindDescriptorSets(...);
// 使用推送常量传递材质参数
vkCmdPushConstants(...);
// 绘制当前节点
vkCmdDrawIndexed(...);
// 绘制子节点
for (auto child : node->children) {
renderNode(child, commandBuffer);
}
}
在创建命令缓冲区时,为场景中的所有根节点调用此函数:
void buildCommandBuffers() {
vkBeginCommandBuffer(...);
...
// 绑定 glTF 场景的顶点和索引缓冲区
vkCmdBindVertexBuffers(...);
vkCmdBindIndexBuffer(...);
// 递归渲染节点
for (auto node : scene.nodes) {
// 最终会调用 vkCmdDrawIndexed(...);
renderNode(node, commandBuffer);
}
...
vkEndCommandBuffer(commandBuffer);
}
在这种传统设置中,更改单个节点的可见性需要重新构建命令缓冲区。
条件缓冲区
如引言所述,条件缓冲区用于有条件地执行渲染和调度命令。因此,第一步是设置此缓冲区。由于这与设置其他缓冲区(如 uniform、顶点、索引和着色器存储缓冲区)类似,这里不再赘述。
关键点如下:
- 引入了新的缓冲区类型
VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT
- 缓冲区格式固定为连续的 32 位值
- 偏移量也按 32 位对齐
这使得它非常适合典型的 C/C++ 主机结构,例如简单的向量:
std::vector<int32_t> conditionalVisibility;
设置缓冲区本身的代码如下:
VkBufferCreateInfo bufferCI{};
bufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferCI.usage = VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT;
bufferCI.size = sizeof(int32_t) * conditionalVisibility.size();
vkCreateBuffer(device, &bufferCI, nullptr, &conditionalBuffer.buffer);
VkMemoryRequirements memReqs{};
vkGetBufferMemoryRequirements(device, conditionalBuffer.buffer, &memReqs);
VkMemoryAllocateInfo memAllocInfo{};
memAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memAllocInfo.allocationSize = memReqs.size;
memAllocInfo.memoryTypeIndex = getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
vkAllocateMemory(device, &memAllocInfo, nullptr, &conditionalBuffer.memory);
vkBindBufferMemory(device, conditionalBuffer.buffer, conditionalBuffer.memory, 0);
vkMapMemory(device, conditionalBuffer.memory, 0, bufferCI.size, 0, &conditionalBuffer.mapped);
通过这种方式,我们获得了一个与主机应用程序大小和布局匹配的缓冲区。注意,为了更好的性能和更复杂的用例,可以创建一个设备本地缓冲区,并通过暂存缓冲区或命令缓冲区进行更新。
添加条件执行
VK_EXT_conditional_rendering 扩展引入了两个新函数,允许我们标记命令缓冲区的区域以进行条件执行:
void vkCmdBeginConditionalRenderingEXT(VkCommandBuffer commandBuffer, const VkConditionalRenderingBeginInfoEXT* pConditionalRenderingBegin);
和
void vkCmdEndConditionalRenderingEXT(VkCommandBuffer commandBuffer);
将绘制和调度命令包装在此区域内意味着,只有在条件缓冲区中给定偏移处的值为非零时,才会执行这些命令。一个基本示例如下:
VkConditionalRenderingBeginInfoEXT conditionalRenderingBeginInfo{};
conditionalRenderingBeginInfo.sType = VK_STRUCTURE_TYPE_CONDITIONAL_RENDERING_BEGIN_INFO_EXT;
conditionalRenderingBeginInfo.buffer = conditionalBuffer.buffer;
conditionalRenderingBeginInfo.offset = 0;
vkCmdBeginConditionalRenderingEXT(commandBuffer, &conditionalRenderingBeginInfo);
// 仅当条件缓冲区中偏移 0 处的值 != 0 时,才会执行此命令
vkCmdDrawIndexed(...);
vkCmdEndConditionalRenderingEXT(commandBuffer);
conditionalRenderingBeginInfo
结构包含 vkCmdBeginConditionalRenderingEXT
函数用于确定是否执行该区域内命令的参数。
因此,对于此基本示例,如果偏移 0 处的 32 位条件缓冲区值为零,则不会执行 vkCmdDrawIndexed
。
现在将偏移 0 处的缓冲区值更改为 1,绘制命令将被执行:
conditionalVisibility[0] = 1;
memcpy(conditionalBuffer.mapped, &conditionalVisibility, sizeof(conditionalVisibility));
一旦缓冲区与设备同步,绘制调用将在无需更新命令缓冲区的情况下执行。
应用到实际示例
在我们的实际示例中,我们为每个 glTF 场景节点创建一个条件缓冲区,每个节点对应一个 32 位值:
conditionalVisibility.resize(scene.linearNodeCount);
std::fill(conditionalVisibility.begin(), conditionalVisibility.end(), 1);
...
VkBufferCreateInfo bufferCI{};
bufferCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferCI.usage = VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT;
bufferCI.size = sizeof(int32_t) * scene.linearNodeCount;
VK_CHECK_RESULT(vkCreateBuffer(device, &bufferCI, nullptr, &conditionalBuffer.buffer));
使用此设置,每个可见的 glTF 节点通过其唯一节点索引映射到 conditionalVisibility
中的一个条目:
考虑到上述布局,我们现在可以将条件渲染添加到节点绘制函数中:
void renderNode(vkglTF::Node *node, VkCommandBuffer commandBuffer) {
// 使用多个描述符集传递场景和模型节点矩阵
const std::vector<VkDescriptorSet> descriptorsets = {
scene.descriptorSet,
node->mesh->uniformBuffer.descriptorSet
};
vkCmdBindDescriptorSets(...);
// 使用推送常量传递材质参数
vkCmdPushConstants(...);
// 此节点的条件渲染参数
VkConditionalRenderingBeginInfoEXT conditionalRenderingBeginInfo{};
conditionalRenderingBeginInfo.sType = VK_STRUCTURE_TYPE_CONDITIONAL_RENDERING_BEGIN_INFO_EXT;
conditionalRenderingBeginInfo.buffer = conditionalBuffer.buffer;
// 偏移量由此节点的实际索引定义
conditionalRenderingBeginInfo.offset = sizeof(int32_t) * node->index;
// 开始条件渲染区域
vkCmdBeginConditionalRenderingEXT(commandBuffer, &conditionalRenderingBeginInfo);
// 仅当给定偏移处的缓冲区值 != 0 时才会执行
vkCmdDrawIndexed(commandBuffer, primitive->indexCount, 1, primitive->firstIndex, 0, 0);
// 结束此条件渲染区域
vkCmdEndConditionalRenderingEXT(commandBuffer);
for (auto child : node->children) {
renderNode(child, commandBuffer);
}
}
就是这样!通过上述代码,我们现在可以切换每个 glTF 模型节点的可见性,在本例中通过用户界面添加复选框来实现,而无需重新构建命令缓冲区:
结语
尽管在实际应用中,条件渲染的用例可能有限,因为大多数情况下命令缓冲区会不断重新生成,但它是 Vulkan 的一个很好的补充,特别是与计算着色器结合使用时。结合这两者,您可以在 GPU 上进行可见性计算并更新条件缓冲区,而无需与主机进行往返交互。