Vulkan Dynamic Uniform Buffers 详解:从普通 UBO 到动态偏移的工程实践
一、前言
在 Vulkan 中,如果我们想给 Shader 传递一些每帧、每个物体都会变化的数据,最常见的方式之一就是使用 Uniform Buffer。
例如在渲染 100 个物体时,每个物体都有自己的模型矩阵model,但它们共享同一个相机矩阵view和投影矩阵projection。如果用最朴素的方式,我们可能会为每个物体创建一个独立的 Uniform Buffer,或者为每个物体创建一套独立的 Descriptor Set。
这种方式可以工作,但并不优雅:
Descriptor Set 数量会变多;
Buffer 对象数量会变多;
CPU 更新和管理成本会上升;
渲染大量物体时,绑定开销和资源管理复杂度会明显增加。
因此 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_DYNAMICStorage 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 中非常稳定且高效的一种资源组织方式。
