当前位置: 首页 > 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://www.xdnf.cn/news/1488619.html

相关文章:

  • 【系统分析师】第7章-基础知识:软件工程(核心总结)
  • 计算机毕设选题:基于Python+Django的B站数据分析系统的设计与实现【源码+文档+调试】
  • 阿里云上启动enclave 并与宿主机通信
  • 韧性双核系统:个人与关系的共生进化框架
  • 2024理想算法岗笔试笔记
  • HTTP中Payload的含义解析
  • MySQL集群高可用架构——组复制 (MGR)
  • Set集合
  • matrix-breakout-2-morpheus靶机渗透
  • 【从零开始学习Redis】秒杀优化——阻塞队列、消息队列实现异步秒杀
  • 虚拟机之CentOS、网络设置的有趣问题
  • openpyxl和excel数据驱动
  • C++20格式化字符串:std::format的使用与实践
  • 大坝安全监测中的单北斗GNSS变形监测系统应用解析
  • 宋红康 JVM 笔记 Day14|垃圾回收概述
  • Android --- AOSP源码导入Android Studio
  • 使用 Doxygen 生成 C++ 与 Python 项目文档
  • 腾讯云TDSQL-C 与传统MySQL对比
  • tf_keras包
  • 【工具变量】地级市中小企业数字化转型月度DID数据集(2022.1-2025.7)
  • 设计模式:模板方法模式(Template Method Pattern)
  • 设计模式:状态模式(State Pattern)
  • 【数据分析】一种用于校正微生物组数据中批次效应的多变量框架
  • 人工智能学习:Transformer架构
  • 简单的说一说前端开发语言React
  • 学习字符串
  • NW506NW507美光固态闪存NW525NW539
  • AI时代的软件开发革命:吴恩达关于快速工程的深度思考
  • WebGL2初识
  • 开源 C++ QT Widget 开发(十三)IPC通讯--本地套接字 (Local Socket)