当前位置: 首页 > news >正文

Vulkan 学习(20)---- UniformBuffer 的使用

目录

      • UniformBuffer
        • DescriptorSetLayout 和 VkBuffer
        • 顶点着色器定义
        • 描述符布局(DescriptorSetLayout)
        • 创建 UniformBuffer
        • 描述符池(DescriptorSet Pool)
        • 描述符集(DescriptorSet)
        • 更新描述符集
        • 使用描述符集
        • 使用多个 Descriptor

UniformBuffer

本篇文档是通过 Uniform Buffer 的使用进一步加深对 DescriptorSet 的理解
Vulkan 中,描述符是一种在着色器中访问资源(比如缓冲区,图像,采样器等)的机制或者协议

每个描述符(Descriptor)对应一个资源,代表 GPU 内存中的资源,比如 Uniform Bufferstorage Buffer, TextureSampler

Vulkan 描述符集(VkDescriptorSet)表示着色器可以与之交互的资源的集合,着色器是通过描述符读取和解析资源中的数据,着色器中的绑定点和相应的描述符集中的绑定点必须一一对应
描述符集

DescriptorSetLayout 和 VkBuffer

现在我们已经可以传递顶点的属性(坐标和颜色等)给到顶点着色器,对于一些所有顶点都共享的属性,比如顶点的变换矩阵,将其作为顶点属性为每一个顶点都传递一份显然是很低效的
Vulkan 提供了资源描述符(resource descriptor)来解决这个问题,资源描述符是用来在着色器中访问缓冲和图像数据的一种方式,我们可以将变换矩阵存储在一个缓冲中,然后通过描述符在着色器中访问它,使用描述符需要进行下面三部分的设置:

  • 在管线(pipeline Creation)创建时指定描述符布局(DescriptorSetLayout)
  • 从描述符池(DescriptorSet Pool)中份分配描述符集(DescriptorSet)
  • 渲染时绑定描述符集(update DescriptorSet)

描述符布局(DescriptorSetLayout)用于指定可以被管线访问的资源类型,类似于渲染流程指定可以被访问的附着类型

描述符集指定要绑定到描述符上的缓冲和图像资源,类似于帧缓存指定绑定到渲染流程附着上的图像视图
(just like a framebuffer specifies the actual image views to bind to render pass attachments)

Note: 本质上是一种定义资源如何访问的机制或者协议

最后将描述符集绑定到绘制的指令上,类似绑定顶点缓冲和帧缓存到绘制指令上

有多种类型的描述符,在这里, 只使用到了 Uniform 缓冲对象(UBO), 也有其他类型的描述符,它们的使用方式和 Uniform 缓冲对象类似

我们先用结构体定义我们在着色器中使用的 Uniform 的数据:

struct UniformBufferObject {glm::mat4 model;glm::mat4 view;glm::mat4 proj;
}

我们将要使用的 uniform 数据复制到 VkBuffer 中,然后通过一个 uniform 缓冲对象描述符(DescriptorSet)在顶点着色器中访问它:

layout(binding = 0) uniform UniformBufferObejct {mat4 model;mat4 view;mat4 proj;
}void main() {gl_Position = ubo.proj * ubo.view *ubo.model*vec4(inPostion, 0.01.0)fragColor = inColor;
}

在现在的 demo 中,我们在每一帧更新模型(Model),视图(View),投影矩阵(Projection),可以让矩阵在三维空间内进行旋转

顶点着色器定义
#version 450
#extension GL_ARB_separate_shader_object :enablelayout(binding = 0) uniform UniformBufferObject {mat4 model;mat4 view;mat4 proj;
}layout(location = 0) in vec2 inPostion;
layout(location = 1) in vec3 inColor;layout(location = 0) out vec3 fragColorout gl_PerVertex {vec4 gl_Postion;
}void main() {gl_Position = ubo.proj + ubo.view + ubo.model * vec4(inPostion, 001.0);fragColor = inColor;
}

uniforminout 定义在着色器中出现的顺序可以是任意的,任意代码中 binding 修饰符类似于我们对顶点属性使用的 location 修饰符,我们会在描述符布局引用这个 binding

gl_Position 使用变换矩阵最终得到矩形在三维空间内的裁剪坐标

描述符布局(DescriptorSetLayout)

我们需要在管线创建的时候提供着色器使用的每一个描述符绑定信息,
首先需要使用 createDescriptorSetLayout 的函数,并在管线创建前调用

void initVulkan() {createDecriptorSetLayout();createGraphicPipeline();
}void createDescriptorSetLayout() {
}

使用 vkDescriptorSetLayoutBinding 结构体来描述每一个绑定操作

void createDescriptorSetLayout() {VkSescriptorSetLayoutBinding ubolayoutBinding = {};uboLayoutBinding.binding = 0;uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;uboLayoutBinding.descriptorCount = 1;
}

bindingdescriptorType 用于指定着色器使用的描述符绑定和描述符类型,这里我们指定的是一个 uniform 缓冲对象,
也可以使用 uniform 数组传递到着色器中,我们可以使用数组来制定骨骼动画(skeletal aniamtion)中使用的所有变换矩阵,
我们的 MVP 矩阵只需要使用一个 uniform 缓冲对象,所以我们将 descriptorCount 的值设置为 1

uboLayoutBinding.pImmutableSamplers = nullptr;
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

我们还需要指定描述符在哪一个着色器阶段被使用,stageFlags 这里我们只是在 Vertex Shader 中使用,
pImmutableSamplers 成员变量仅用于和图像采样相关的描述符

调用 vkCreateDescriptorSetLayout 函数创建 VkDescriptorSetLayout 对象,vkCreateDescriptorSetLayout 函数以 VkDescriptorSetLayoutCreateInfo结构体作为参数

VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor set layout!");
}

同时我们需要在创建 GraphicPipeline 的时候指定 DescriptorSetLayout,也可以指定多个 DescriptorSetLayout

VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
创建 UniformBuffer

我们需要创建包含 UniformBuffer 的缓冲对象(Uniform Buffer),然后在每一帧中将新的 UBO 数据复制到 uniform 缓冲,由于需要频繁的更新数据,使用暂存并不会带来性能的提升

由于我们需要并行渲染多帧的缘故,我们需要多个 uniform 缓冲,来满足多帧并行渲染的需要,我们可以并行渲染每一帧或者一个交换链图像使用独立的 uniform 缓冲对象

VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;void createUniformBuffer() {VkDeviceSize bufferSize = sizeof(UniformBufferObject);uniformBuffers.resize(swapChainImages.size());uniformBuffersMemory.resize(swapChainImages.size());for (size_t i = 0; i < swapChainImages.size(); i++) {createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]);}
}

最后更新 UniformBuffer 只需要将数据拷贝到 UniformBuffer Memory 对象的虚拟地址空间中

void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage]0sizeof(ubo)0&data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
描述符池(DescriptorSet Pool)

描述符集不能被直接创建,需要通过描述符池(DescriptorSet Pool)来分配,这里使用 createDescriptorPool 的函数来进行描述符池的创建

我们使用 VkDescriptorPoolSize 来决定 我们使用的 DescriptorSet 类型和数量
poolSize 是根据 swapChainImages 中的 image 的数量来决定的

VkDescriptorPoolSize poolSize = {};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());VkDescriptorPool descriptorPool;...if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor pool!");
}
描述符集(DescriptorSet)

DescriptorSet 的分配(Allocate)需要我们使用 vkAllocateDescriptorSets 分配出来,我们使用 VkDescriptorSetAllocateInfo 结构体
需要指定分配 DescriptorSet 使用的 DescriptorSetPool,需要分配的描述符集数量,以及它们使用的 DescriptorSetLayout

std::vector<VkDescriptorSetLayout>
layouts(swapChainImages.size(), descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size());
allocInfo.pSetLayouts = layouts.data();VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;
...
descriptorSets.resize(swapChainImages.size());
if (vkAllocateDescriptorSets(device, &allocInfo, &descriptorSets[0]) != VK_SUCCESS) {throw std::runtime_error("failed to allocate descriptor sets!");
}

DescriptorSet 会在 DescriptorSetPool 销毁的时候自动被销毁,所以不需要我们显式的清除
vkAllocateDescriptorSets 函数分配地描述符集对象,每一个都带有 uniform 缓冲描述符(对应一个 uniformvkBuffer)

我们通过 vkDescriptorBufferInfo 结构体来配置引用的 vkBuffer

VkDescriptorBufferInfo 结构体可指定缓冲对象和可以访问的数据范围

for (size_t i = 0; i < swapChainImages.size(); i++) {VkDescriptorBufferInfo bufferInfo = {};bufferInfo.buffer = uniformBuffers[i];bufferInfo.offset = 0;bufferInfo.range = sizeof(UniformBufferObject);
}

如果需要使用整个缓冲,可以使将 range 成员变量范围设置为 VK_WHOLE_SIZE

更新描述符集
VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr; // Optional
descriptorWrite.pTexelBufferView = nullptr; // Optional

dstSetdstBinding 成员变量用于指定要更新的 DescriptorSet 和 绑定点(bindings)
需要注意的是DescriptorSet可以使用数组,所以我们需要指定数组的第一个元素作为索引,这里我们没有使用,所以将索引指定为 0

pBufferInfo 成员变量用于指定描述符引用的缓冲数据,pImageInfo 成员变量用于指定描述符引用的图像数据,
pTexelBufferView 成员变量 用于指定描述符引用的缓冲视图,这里我们只使用了 pBufferInfo 成员变量

最后使用 vkUpdateDescriptorSets 更新描述符集

vkUpdateDescriptorSets(device, 1&descriptorWrite, 0, nullptr);

vkUpdateDescriptorSets 函数可以接受两个数组作为参数;
VkWriteDescriptorSet 结构体数组和 VkCopyDescriptorSet 结构体数组,后者被用来复制(copy)描述符对

使用描述符集

现在修改 createCommandBuffer 函数为每个交换链图像绑定对应的描述符集,这需要调用 cmdBindDescriptorSets 完成,需要在调用 vkCmdDrawIndexed 函数之前调用这个函数

vkCmdBindDescriptorSets(commandBuffers[i],
VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 01&descriptorSets[i]0, nullptr);
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size())1000);

和顶点缓冲,索引缓冲不同,描述符集合并不是图像管线所独有的,所以我们需要指定我们绑定的是图形管线还是计算管线,管线之后的参数是描述符使用的布局
后面的三个参数用于指定: 描述符集的第一个元素索引,绑定的描述符集的个数,以及用于绑定的描述符集数组,最后两个参数用于指定动态描述符的数组偏移

使用多个 Descriptor

DescriptorSet 本身就是集合的概念,也就是可以创建 Descriptor 数组对应到一个 DescriptorSet 的绑定点上

VkDescriptorSetLayoutBinding binding = {};
binding.binding = 0; // 绑定点
binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
binding.descriptorCount = 8; // 绑定了 8 个 uniform buffer
binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &binding;
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout);// glsl access descriptorset array
layout(set = 0, binding = 0) uniform UniformBuffer {mat4 model;vec4 color;
} ubo[8];void main() {mat4 modelMatrix = ubo[3].model; // 访问第4个元素vec4 objectColor = ubo[gl_InstanceIndex].color; // 按实例索引访问
}

也可以在一个绑定点上使用不同的 DescriptorSet index,对应的 glsl 代码如下

// 三个不同的descriptor set,但都使用binding = 0
layout(set = 0, binding = 0) uniform UniformBuffer { ... } cameraUBO;
layout(set = 1, binding = 0) uniform UniformBuffer { ... } modelUBO;  
layout(set = 2, binding = 0) uniform sampler2D albedoTexture;

最后再更新一下 DescriptorSet 的示意图,加深理解:
DescriptorSet


文章转载自:

http://DKtWCpV2.dqspq.cn
http://KAJ79aDT.dqspq.cn
http://vYLra5dl.dqspq.cn
http://2C34jirn.dqspq.cn
http://3DoeUbPP.dqspq.cn
http://SczAnX1N.dqspq.cn
http://TME4u0ba.dqspq.cn
http://EAF8x0cg.dqspq.cn
http://Wxh4BaOb.dqspq.cn
http://Bi559M7c.dqspq.cn
http://T4pMUlAo.dqspq.cn
http://N7vSSYdJ.dqspq.cn
http://zoqEJBfZ.dqspq.cn
http://AWGG3QP4.dqspq.cn
http://Qlj8vcNL.dqspq.cn
http://O1dUzRcj.dqspq.cn
http://USFn90hi.dqspq.cn
http://lEBkDmB8.dqspq.cn
http://Xrq6DRSX.dqspq.cn
http://GTBkNX8s.dqspq.cn
http://YaKzhsQU.dqspq.cn
http://7Z2fi699.dqspq.cn
http://LlgNuMmY.dqspq.cn
http://wQB0pNAb.dqspq.cn
http://gd6yjGdv.dqspq.cn
http://qeJfRQH8.dqspq.cn
http://JTDo3dlp.dqspq.cn
http://TO3iKCMP.dqspq.cn
http://JtszVM8N.dqspq.cn
http://wvP2JvjH.dqspq.cn
http://www.dtcms.com/a/371937.html

相关文章:

  • 微信小程序中实现AI对话、生成3D图像并使用xr-frame演示
  • 【不背八股】9.MySQL知识点汇总
  • MySQL6
  • 论文阅读:ICLR 2021 BAG OF TRICKS FOR ADVERSARIAL TRAINING
  • GD32自学笔记:4.ADC
  • LeetCode 522.最长特殊序列2
  • CentOS 7.2 虚机 ssh 登录报错在重启后无法进入系统
  • 腾讯混元 3D 2.0 Windows 便携版:低显存需求下的高效文/图生3D体验
  • 火山 RTC 引擎15 拉流 推流 地址生成器 、合流转推 开关
  • CesiumJS详解:打造专业级Web 3D地球仪与地图的JavaScript库
  • 数据结构:顺序表与链表
  • C++ 前缀和 高频笔试考点 实用技巧 牛客 DP34 [模板] 前缀和 题解 每日一题
  • kotlin - 平板分屏,左右拖动,2个Activity计算宽度,使用ActivityOptions、Rect(三)
  • 【软考架构】第七章 系统架构设计基础知识-7.2基于架构的软件开发方法:Architecture-Based Software Design,ABSD
  • Dify 从入门到精通(第 81/100 篇):Dify 的多模态模型监控(高级篇)
  • 2019年11月系统架构设计师真题及解析摘要
  • 基于Django的“社区爱心养老管理系统”设计与开发(源码+数据库+文档+PPT)
  • IO性能篇(二):文件读写的四种分类
  • 超越模仿,探寻智能的本源:从人类认知机制到下一代自然语言处理
  • 计算机视觉(十二):人工智能、机器学习与深度学习
  • 去中心化投票系统开发教程 第五章:测试与部署
  • 自然语言处理之第一课语言转换方法
  • 移动端代理配置:iOS和Android设备代理设置完全指南
  • 【面试向】区块链介绍
  • 第十四届蓝桥杯青少组C++选拔赛[2023.2.12]第二部分编程题(4、最大空白区)
  • keycloak redirect_url重定向配置
  • Archon01-项目部署
  • 基于Python的餐厅推荐系统【2026最新】
  • OpenManus项目安装与使用教程详解
  • 《sklearn机器学习——管道和复合估计器》回归中转换目标