news 2026/6/4 21:09:11

Vulkan Dynamic Uniform Buffers 详解:从普通 UBO 到动态偏移的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vulkan Dynamic Uniform Buffers 详解:从普通 UBO 到动态偏移的工程实践

一、前言

在 Vulkan 中,如果我们想给 Shader 传递一些每帧、每个物体都会变化的数据,最常见的方式之一就是使用 Uniform Buffer。

例如在渲染 100 个物体时,每个物体都有自己的模型矩阵model,但它们共享同一个相机矩阵view和投影矩阵projection。如果用最朴素的方式,我们可能会为每个物体创建一个独立的 Uniform Buffer,或者为每个物体创建一套独立的 Descriptor Set。

这种方式可以工作,但并不优雅:

  1. Descriptor Set 数量会变多;

  2. Buffer 对象数量会变多;

  3. CPU 更新和管理成本会上升;

  4. 渲染大量物体时,绑定开销和资源管理复杂度会明显增加。

因此 Vulkan 提供了 Dynamic Uniform Buffer,也就是动态 uniform buffer。它允许我们把多个物体的 Uniform 数据放进一个大的 Buffer 中,在绘制不同物体时,只通过一个动态偏移量dynamic offset指向当前物体的数据。

一句话概括:

Dynamic Uniform Buffer 的核心思想是:
“一个大 Uniform Buffer,存放多个对象的数据;绘制时通过动态 offset 选择其中某一段数据。”


二、普通 Uniform Buffer 的问题

假设我们有如下 UBO 结构:

struct ObjectUBO { glm::mat4 model; };

每个物体都需要一个不同的model矩阵。

如果场景中有 100 个物体,普通做法可能是:

Object 0 -> UniformBuffer 0 -> DescriptorSet 0 Object 1 -> UniformBuffer 1 -> DescriptorSet 1 Object 2 -> UniformBuffer 2 -> DescriptorSet 2 ... Object 99 -> UniformBuffer 99 -> DescriptorSet 99

这样做的问题很明显:资源数量太多。

更好的想法是:

一个大 Buffer: +------------------+ | Object 0 的 UBO | +------------------+ | Object 1 的 UBO | +------------------+ | Object 2 的 UBO | +------------------+ | ... | +------------------+ | Object 99 的 UBO | +------------------+

绘制第 0 个物体时,Shader 读取 Buffer 的第 0 段。

绘制第 1 个物体时,Shader 读取 Buffer 的第 1 段。

绘制第 99 个物体时,Shader 读取 Buffer 的第 99 段。

这就是 Dynamic Uniform Buffer 要解决的问题。


三、Dynamic Uniform Buffer 是什么?

Dynamic Uniform Buffer 使用的 Descriptor 类型是:

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC

它和普通 Uniform Buffer 的区别在于:

普通 Uniform Buffer:

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER

绑定 Descriptor Set 后,Shader 读取的位置基本固定。

动态 Uniform Buffer:

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC

绑定 Descriptor Set 时,可以额外传入一个动态偏移量:

vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 1, &dynamicOffset );

这里的dynamicOffset会告诉 Vulkan:

“这一次绘制时,不要从 Buffer 开头读取,而是从 Buffer 的某个偏移位置开始读取。”

所以它适合这种场景:

同一个 Descriptor Set 同一个大 Buffer 不同 draw call 使用不同 dynamic offset

四、Dynamic Uniform Buffer 的整体结构

一个典型的 Dynamic Uniform Buffer 渲染流程如下:

CPU 端: 1. 创建一个大的 VkBuffer 2. 按照对齐要求切分 Buffer 3. 把每个物体的 UBO 数据写入不同位置 Descriptor 端: 4. Descriptor 类型设置为 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 5. Descriptor 指向整个大 Buffer,或者指向其中一个逻辑范围 绘制端: 6. 每绘制一个物体,计算 dynamicOffset 7. 调用 vkCmdBindDescriptorSets 传入 dynamicOffset 8. 调用 vkCmdDraw / vkCmdDrawIndexed

可以理解为:

VkBuffer: +--------------------------+ offset = 0 * alignedSize | Object 0 UBO | +--------------------------+ offset = 1 * alignedSize | Object 1 UBO | +--------------------------+ offset = 2 * alignedSize | Object 2 UBO | +--------------------------+ | ... | +--------------------------+ 绘制 Object i: dynamicOffset = i * alignedSize

五、为什么需要对齐?

Dynamic Uniform Buffer 最容易出错的地方就是对齐。

Vulkan 设备会规定一个限制:

VkPhysicalDeviceLimits::minUniformBufferOffsetAlignment

这个值表示 dynamic uniform buffer 的 offset 必须满足的最小对齐要求。

例如某些 GPU 上:

minUniformBufferOffsetAlignment = 256

如果你的 UBO 结构大小是:

sizeof(ObjectUBO) = 64

你不能简单地让:

Object 0 offset = 0 Object 1 offset = 64 Object 2 offset = 128 Object 3 offset = 192

因为 64、128、192 不一定满足设备要求。若设备要求 256 字节对齐,那么合法布局应该是:

Object 0 offset = 0 Object 1 offset = 256 Object 2 offset = 512 Object 3 offset = 768

虽然中间会浪费一些空间,但这是 Vulkan 对动态 UBO 的硬性要求。


六、计算对齐后的 UBO 大小

通常我们会写一个函数来计算对齐后的大小:

VkDeviceSize getAlignedSize(VkDeviceSize originalSize, VkDeviceSize alignment) { if (alignment == 0) { return originalSize; } return (originalSize + alignment - 1) & ~(alignment - 1); }

使用方式:

VkPhysicalDeviceProperties deviceProperties{}; vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); VkDeviceSize minAlignment = deviceProperties.limits.minUniformBufferOffsetAlignment; VkDeviceSize objectUBOSize = sizeof(ObjectUBO); VkDeviceSize dynamicAlignment = getAlignedSize(objectUBOSize, minAlignment);

如果:

sizeof(ObjectUBO) = 64 minUniformBufferOffsetAlignment = 256

那么:

dynamicAlignment = 256

最终大 Buffer 的总大小:

VkDeviceSize bufferSize = objectCount * dynamicAlignment;

七、Shader 中如何声明?

在 GLSL 中,Dynamic Uniform Buffer 和普通 Uniform Buffer 的写法没有本质区别。

例如顶点着色器:

#version 450 layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inColor; layout(set = 0, binding = 0) uniform CameraUBO { mat4 view; mat4 proj; } cameraUBO; layout(set = 0, binding = 1) uniform ObjectUBO { mat4 model; } objectUBO; layout(location = 0) out vec3 fragColor; void main() { gl_Position = cameraUBO.proj * cameraUBO.view * objectUBO.model * vec4(inPosition, 1.0); fragColor = inColor; }

注意:

layout(set = 0, binding = 1) uniform ObjectUBO { mat4 model; } objectUBO;

这段 Shader 并不知道自己读取的是普通 UBO 还是 Dynamic UBO。Dynamic 的概念主要发生在 Vulkan API 绑定 Descriptor 的时候。


八、Descriptor Set Layout 配置

创建 Descriptor Set Layout 时,需要把某个 binding 设置为:

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC

例如:

VkDescriptorSetLayoutBinding cameraLayoutBinding{}; cameraLayoutBinding.binding = 0; cameraLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; cameraLayoutBinding.descriptorCount = 1; cameraLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; cameraLayoutBinding.pImmutableSamplers = nullptr; VkDescriptorSetLayoutBinding objectLayoutBinding{}; objectLayoutBinding.binding = 1; objectLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; objectLayoutBinding.descriptorCount = 1; objectLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; objectLayoutBinding.pImmutableSamplers = nullptr; std::array<VkDescriptorSetLayoutBinding, 2> bindings = { cameraLayoutBinding, objectLayoutBinding }; VkDescriptorSetLayoutCreateInfo layoutInfo{}; layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size()); layoutInfo.pBindings = bindings.data(); VkDescriptorSetLayout descriptorSetLayout; vkCreateDescriptorSetLayout( device, &layoutInfo, nullptr, &descriptorSetLayout );

这里有两个 binding:

binding = 0 -> 普通 Uniform Buffer,存放相机数据 binding = 1 -> Dynamic Uniform Buffer,存放每个物体的数据

通常相机数据每帧只需要一份,而物体数据需要很多份,所以物体矩阵更适合放进 Dynamic Uniform Buffer。


九、创建 Dynamic Uniform Buffer

首先定义每个物体的数据结构:

struct ObjectUBO { glm::mat4 model; };

然后根据物体数量计算 Buffer 大小:

uint32_t objectCount = 100; VkDeviceSize objectUBOSize = sizeof(ObjectUBO); VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(physicalDevice, &properties); VkDeviceSize minAlignment = properties.limits.minUniformBufferOffsetAlignment; VkDeviceSize dynamicAlignment = getAlignedSize(objectUBOSize, minAlignment); VkDeviceSize bufferSize = objectCount * dynamicAlignment;

然后创建 Buffer:

createBuffer( bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, dynamicUniformBuffer, dynamicUniformBufferMemory );

这里为了简单演示,使用了:

VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT VK_MEMORY_PROPERTY_HOST_COHERENT_BIT

这表示 CPU 可以直接映射并写入这块内存。

在性能要求更高的项目中,也可以使用 staging buffer 或者 ring buffer 等方式进一步优化。


十、写入多个物体的 UBO 数据

因为每个物体的数据之间需要按照dynamicAlignment对齐,所以不能直接使用普通数组下标写入。

正确写法通常是:

void* data = nullptr; vkMapMemory( device, dynamicUniformBufferMemory, 0, bufferSize, 0, &data ); for (uint32_t i = 0; i < objectCount; i++) { ObjectUBO objectUBO{}; objectUBO.model = glm::mat4(1.0f); objectUBO.model = glm::translate( objectUBO.model, glm::vec3(i * 2.0f, 0.0f, 0.0f) ); char* destination = reinterpret_cast<char*>(data) + i * dynamicAlignment; memcpy(destination, &objectUBO, sizeof(ObjectUBO)); } vkUnmapMemory(device, dynamicUniformBufferMemory);

关键是这一句:

char* destination = reinterpret_cast<char*>(data) + i * dynamicAlignment;

它表示第i个物体的 UBO 数据写入到:

i * dynamicAlignment

这个偏移位置。


十一、更新 Descriptor Set

Dynamic Uniform Buffer 仍然需要写入 Descriptor Set。

VkDescriptorBufferInfo cameraBufferInfo{}; cameraBufferInfo.buffer = cameraUniformBuffer; cameraBufferInfo.offset = 0; cameraBufferInfo.range = sizeof(CameraUBO); VkDescriptorBufferInfo objectBufferInfo{}; objectBufferInfo.buffer = dynamicUniformBuffer; objectBufferInfo.offset = 0; objectBufferInfo.range = sizeof(ObjectUBO);

然后写入 Descriptor:

std::array<VkWriteDescriptorSet, 2> descriptorWrites{}; descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; descriptorWrites[0].dstSet = descriptorSet; descriptorWrites[0].dstBinding = 0; descriptorWrites[0].dstArrayElement = 0; descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; descriptorWrites[0].descriptorCount = 1; descriptorWrites[0].pBufferInfo = &cameraBufferInfo; descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; descriptorWrites[1].dstSet = descriptorSet; descriptorWrites[1].dstBinding = 1; descriptorWrites[1].dstArrayElement = 0; descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; descriptorWrites[1].descriptorCount = 1; descriptorWrites[1].pBufferInfo = &objectBufferInfo; vkUpdateDescriptorSets( device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr );

这里需要注意:

objectBufferInfo.offset = 0; objectBufferInfo.range = sizeof(ObjectUBO);

很多初学者会疑惑:为什么 offset 不写成某个物体的偏移?

原因是:
Descriptor Set 只描述这个 Buffer 的基本绑定信息,真正选择第几个物体的数据,是在绘制时通过dynamicOffset完成的。


十二、绘制时传入 dynamic offset

绘制多个物体时,核心代码如下:

for (uint32_t i = 0; i < objectCount; i++) { uint32_t dynamicOffset = static_cast<uint32_t>(i * dynamicAlignment); vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 1, &dynamicOffset ); vkCmdDrawIndexed( commandBuffer, indexCount, 1, 0, 0, 0 ); }

重点是:

uint32_t dynamicOffset = static_cast<uint32_t>(i * dynamicAlignment);

第 0 个物体:

dynamicOffset = 0

第 1 个物体:

dynamicOffset = dynamicAlignment

第 2 个物体:

dynamicOffset = 2 * dynamicAlignment

第 N 个物体:

dynamicOffset = N * dynamicAlignment

这样,所有物体都使用同一个 Descriptor Set,但每次 draw call 读取的 UBO 数据不同。


十三、Dynamic Offset 的绑定顺序

如果一个 Descriptor Set 中有多个 Dynamic Uniform Buffer,或者同时使用 Dynamic Storage Buffer,那么dynamicOffsets数组的顺序必须和 Descriptor Set Layout 中动态 descriptor 的顺序一致。

例如:

set = 0, binding = 0 -> 普通 UBO set = 0, binding = 1 -> Dynamic UBO set = 0, binding = 2 -> Dynamic UBO

那么绑定时需要传入两个 dynamic offset:

uint32_t dynamicOffsets[2] = { offsetForBinding1, offsetForBinding2 };

调用:

vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 2, dynamicOffsets );

如果只有一个 Dynamic Uniform Buffer,那么就只传一个 offset。


十四、完整绘制逻辑示意

整体渲染流程可以总结为:

初始化阶段: 1. 获取 minUniformBufferOffsetAlignment 2. 计算 dynamicAlignment 3. 创建 objectCount * dynamicAlignment 大小的 VkBuffer 4. 创建 Descriptor Set Layout 5. binding 使用 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 6. 分配 Descriptor Set 7. vkUpdateDescriptorSets 写入 Buffer 信息 每帧更新阶段: 1. 更新 CameraUBO 2. 遍历所有物体 3. 将每个 ObjectUBO 写入大 Buffer 的对应偏移位置 命令录制阶段: 1. 绑定 Pipeline 2. 绑定 Vertex Buffer / Index Buffer 3. 遍历所有物体 4. 计算 dynamicOffset = i * dynamicAlignment 5. vkCmdBindDescriptorSets 传入 dynamicOffset 6. vkCmdDrawIndexed

十五、Dynamic Uniform Buffer 与 Push Constants 的区别

Dynamic Uniform Buffer 和 Push Constants 都可以用于传递小规模数据,但两者定位不同。

1. Push Constants

Push Constants 适合传递非常小、频繁变化的数据。

例如:

struct PushConstantData { glm::mat4 model; };

优点:

1. 使用简单 2. 不需要创建 Buffer 3. 更新开销低 4. 非常适合少量数据

缺点:

1. 容量很小 2. 设备限制通常比较严格 3. 不适合大量物体数据

2. Dynamic Uniform Buffer

Dynamic Uniform Buffer 适合存储较多对象的 per-object 数据。

优点:

1. 可以存放大量物体数据 2. Descriptor Set 数量少 3. 适合批量渲染多个对象 4. 资源管理比每物体一个 UBO 更整洁

缺点:

1. 需要处理内存对齐 2. 需要手动计算 dynamic offset 3. 如果每个 draw call 都绑定 descriptor,仍然存在一定 CPU 开销

简单选择原则:

数据很小、对象数量少:Push Constants 对象数量多、每个对象都有独立矩阵/材质参数:Dynamic Uniform Buffer 数据量更大、结构更复杂:Storage Buffer

十六、Dynamic Uniform Buffer 与 Storage Buffer 的区别

Dynamic Uniform Buffer 的数据通常只读,并且受Uniform Buffer相关限制约束。

Storage Buffer 使用:

VK_DESCRIPTOR_TYPE_STORAGE_BUFFER

或者:

VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC

Storage Buffer 的容量通常更大,也更加灵活,可以在 Shader 中读写。

对比:

Uniform Buffer: 适合小规模、结构清晰、频繁读取的常量数据。 例如 view/proj/model、材质参数、灯光参数等。 Storage Buffer: 适合大规模数组数据、实例数据、粒子数据、骨骼矩阵、GPU 计算结果等。

如果只是传递每个物体的model matrix,Dynamic Uniform Buffer 是很合理的选择。

如果要传递成千上万个实例的数据,或者数据结构非常大,Storage Buffer 往往更合适。


十七、常见错误与排查方法

错误一:没有按照 minUniformBufferOffsetAlignment 对齐

错误写法:

dynamicOffset = i * sizeof(ObjectUBO);

正确写法:

dynamicOffset = i * dynamicAlignment;

其中:

dynamicAlignment >= sizeof(ObjectUBO)

并且满足设备对齐要求。


错误二:Descriptor 类型写错

如果 Descriptor Set Layout 中写成了:

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER

但绑定时却传入 dynamic offset,就会出现错误。

Dynamic Uniform Buffer 必须使用:

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC

错误三:dynamicOffset 数量不匹配

如果 Descriptor Set 中有一个 dynamic descriptor,那么:

dynamicOffsetCount = 1

如果有两个 dynamic descriptor,那么:

dynamicOffsetCount = 2

不能多,也不能少。


错误四:Buffer 总大小不足

假设:

objectCount = 100 dynamicAlignment = 256

那么 Buffer 至少需要:

100 * 256 = 25600 bytes

如果只分配:

100 * sizeof(ObjectUBO)

很容易越界或者造成渲染异常。


错误五:range 设置不合理

DescriptorBufferInfo 中的:

objectBufferInfo.range

通常设置为单个对象 UBO 的大小:

objectBufferInfo.range = sizeof(ObjectUBO);

也可以根据需求设置为更大的范围,但需要确保 offset 和 range 访问不越界。

对于初学者,建议先使用:

offset = 0 range = sizeof(ObjectUBO)

然后通过 dynamic offset 指向具体对象。


错误六:CPU 写入数据后 GPU 没有正确看到

如果使用的内存不是HOST_COHERENT,那么 CPU 写入后需要调用:

vkFlushMappedMemoryRanges

如果使用:

VK_MEMORY_PROPERTY_HOST_COHERENT_BIT

则通常不需要手动 flush。

不过从工程角度看,仍然要理解 Vulkan 的显式同步模型。Vulkan 不会自动帮你处理所有 CPU/GPU 数据可见性问题。


十八、一个更完整的类设计思路

可以将 Dynamic Uniform Buffer 封装成一个类:

class DynamicUniformBuffer { public: void create( VkPhysicalDevice physicalDevice, VkDevice device, uint32_t objectCount ); void updateObject(uint32_t index, const ObjectUBO& ubo); VkBuffer getBuffer() const; VkDeviceSize getDynamicAlignment() const; VkDeviceSize getOffset(uint32_t index) const; private: VkDevice device = VK_NULL_HANDLE; VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; void* mapped = nullptr; uint32_t objectCount = 0; VkDeviceSize objectSize = sizeof(ObjectUBO); VkDeviceSize dynamicAlignment = 0; VkDeviceSize bufferSize = 0; };

更新某个对象:

void DynamicUniformBuffer::updateObject( uint32_t index, const ObjectUBO& ubo ) { char* destination = reinterpret_cast<char*>(mapped) + index * dynamicAlignment; memcpy(destination, &ubo, sizeof(ObjectUBO)); }

获取 offset:

VkDeviceSize DynamicUniformBuffer::getOffset(uint32_t index) const { return index * dynamicAlignment; }

绘制时:

uint32_t dynamicOffset = static_cast<uint32_t>(dynamicUBO.getOffset(i)); vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 1, &dynamicOffset );

这样可以让主渲染逻辑更清晰。


十九、适合使用 Dynamic Uniform Buffer 的场景

Dynamic Uniform Buffer 特别适合以下场景:

1. 多个物体共享同一个 Pipeline; 2. 多个物体共享同一个 Descriptor Set Layout; 3. 每个物体都有不同的 model 矩阵; 4. 每个物体有少量独立材质参数; 5. 想减少 Descriptor Set 数量; 6. 想把 per-object 数据集中管理; 7. 不想为每个物体单独创建一个 Uniform Buffer。

例如:

渲染 100 个立方体; 渲染多个 glTF 节点; 渲染多个模型实例; 渲染多个带有不同材质参数的小物体; 渲染场景中的多个 transform object。

二十、不适合使用 Dynamic Uniform Buffer 的场景

Dynamic Uniform Buffer 并不是万能的。

以下场景可能不适合:

1. 数据量特别大; 2. 每个对象的数据结构复杂且变化频繁; 3. 需要在 Shader 中随机访问大量对象数据; 4. 需要 GPU 端写入数据; 5. 想做大规模 GPU-driven rendering; 6. 每次 draw call 绑定 dynamic offset 成为 CPU 瓶颈。

这些情况下可以考虑:

1. Storage Buffer; 2. Instancing; 3. Push Constants; 4. Descriptor Indexing; 5. GPU-driven rendering; 6. Multi-draw indirect。

二十一、Dynamic Uniform Buffer 和 glTF 模型渲染

在 glTF 模型加载中,Dynamic Uniform Buffer 很常见。

一个 glTF 文件通常由多个 Node 组成,每个 Node 都有自己的变换矩阵:

glTF Scene ├── Node 0 -> model matrix 0 ├── Node 1 -> model matrix 1 ├── Node 2 -> model matrix 2 └── Node 3 -> model matrix 3

如果每个 Node 都创建一个单独的 UBO,会让资源管理变复杂。

更好的方式是:

CameraUBO: 存放 view / projection Dynamic ObjectUBO: 存放每个 node 的 model matrix MaterialUBO 或 StorageBuffer: 存放材质参数

绘制 glTF Node 时:

for (uint32_t nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) { uint32_t dynamicOffset = static_cast<uint32_t>(nodeIndex * dynamicAlignment); vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 1, &dynamicOffset ); drawNode(commandBuffer, nodes[nodeIndex]); }

这样每个 Node 使用同一个 Descriptor Set,但是通过不同的 dynamic offset 读取不同的模型矩阵。


二十二、性能分析

Dynamic Uniform Buffer 的主要收益是减少资源绑定复杂度,而不是完全消除 draw call 开销。

它可以减少:

1. Descriptor Set 数量; 2. Uniform Buffer 对象数量; 3. Descriptor 更新次数; 4. CPU 端资源管理复杂度。

但是它不能减少:

1. draw call 数量; 2. 每次 vkCmdBindDescriptorSets 的调用; 3. Pipeline 切换成本; 4. 顶点处理成本; 5. 片元处理成本。

如果你的场景中有大量相同 Mesh 的实例,Instancing 可能更合适。

如果你的场景中有大量不同 Mesh,并且追求极限性能,则可能需要进一步考虑:

1. Multi Draw Indirect; 2. GPU Culling; 3. Descriptor Indexing; 4. Bindless Resource; 5. Mesh Shader; 6. GPU-driven Pipeline。

Dynamic Uniform Buffer 更像是 Vulkan 初中级阶段非常实用的一种资源组织方法。


二十三、推荐的资源组织方式

对于一个基础 Vulkan Renderer,可以这样设计 Descriptor:

set = 0:Frame / Camera 级别数据 binding = 0:CameraUBO binding = 1:LightUBO set = 1:Object 级别数据 binding = 0:Dynamic ObjectUBO set = 2:Material / Texture 级别数据 binding = 0:BaseColor Texture binding = 1:Normal Texture binding = 2:Sampler

也可以简化成:

set = 0: binding = 0:CameraUBO binding = 1:Dynamic ObjectUBO binding = 2:Sampler binding = 3:Texture

对于初学项目,后一种更容易实现。

对于较大型引擎,前一种分层方式更清晰。


二十四、最小示例总结

1. 定义 UBO

struct ObjectUBO { glm::mat4 model; };

2. 获取对齐要求

VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(physicalDevice, &properties); VkDeviceSize alignment = properties.limits.minUniformBufferOffsetAlignment;

3. 计算对齐大小

VkDeviceSize dynamicAlignment = getAlignedSize(sizeof(ObjectUBO), alignment);

4. 创建大 Buffer

VkDeviceSize bufferSize = objectCount * dynamicAlignment;

5. Descriptor 类型

descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;

6. 写 Descriptor

bufferInfo.buffer = dynamicUniformBuffer; bufferInfo.offset = 0; bufferInfo.range = sizeof(ObjectUBO);

7. 绘制时传入 offset

uint32_t dynamicOffset = i * dynamicAlignment; vkCmdBindDescriptorSets( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 1, &dynamicOffset );

二十五、结语

Dynamic Uniform Buffer 是 Vulkan 中非常重要的资源管理技术。它的核心并不复杂:

把多个对象的 Uniform 数据放进一个大 Buffer; 每个对象的数据按照设备要求对齐; 绘制时通过 dynamic offset 指向当前对象的数据。

它解决的是“多个对象如何高效共享一个 Descriptor Set 和一个 Uniform Buffer”的问题。

对于 Vulkan 初学者来说,掌握 Dynamic Uniform Buffer 具有很高的实践价值。它不仅能帮助我们理解 Vulkan 的 Descriptor 系统,也能为后续学习 glTF 渲染、实例化渲染、材质系统、GPU-driven rendering 打下基础。

可以这样记住它:

普通 Uniform Buffer:绑定一次,读取固定位置。 Dynamic Uniform Buffer:绑定同一个 Buffer,但每次绘制可以动态改变读取位置。

在真实项目中,Dynamic Uniform Buffer 常用于:

1. 每个物体的 model 矩阵; 2. 每个物体的材质参数; 3. 每个 glTF node 的变换数据; 4. 多对象共享 Descriptor Set 的渲染系统。

只要注意对齐、Buffer 大小、Descriptor 类型和 dynamic offset 的正确使用,Dynamic Uniform Buffer 就是 Vulkan 中非常稳定且高效的一种资源组织方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/4 21:08:07

STC15W单片机驱动OLED同步显示温度与实时时钟(DS18B20+I2C)

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;一套开箱即用的51单片机温时双显实现方案&#xff0c;主控为STC15W系列芯片&#xff0c;通过单总线协议稳定读取DS18B20数字温度传感器数据&#xff0c;同时利用I2C接口驱动OLED屏幕&#xff0c;实时刷新当前温…

作者头像 李华
网站建设 2026/6/4 21:01:05

DeepSeek-V4长记忆与强Agent技术解析:低成本高可靠AI工作流构建

1. 项目概述&#xff1a;这不是一次普通升级&#xff0c;而是一次能力边界的重新定义“DeepSeek-V4来了&#xff1a;长记忆强Agent&#xff0c;还便宜”——看到这个标题&#xff0c;我第一时间没去点开链接&#xff0c;而是把手机倒扣在桌面上&#xff0c;泡了杯浓茶。干这行十…

作者头像 李华
网站建设 2026/6/4 21:01:02

销售易开发者技能包上线丨0代码开发新能力,业务更满意

“客户 7 天没回访&#xff0c;系统能不能自动提醒&#xff1f;”“重点客户状态变化后&#xff0c;能不能自动触发主管审批&#xff1f;”在企业业务经营中&#xff0c;业务用户几乎每天都会向IT提出类似需求。一句简单诉求&#xff0c;落地却要历经需求沟通、方案确认、开发配…

作者头像 李华