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

Vulkan学习笔记10—描述符与统一缓冲区

一、资源描述符

描述符是着色器自由访问缓冲区和图像等资源的一种方式。

描述符的使用包括三个部分

  • 在管线创建期间指定描述符集布局

  • 从描述符池中分配描述符集

  • 在渲染期间绑定描述符集

描述符集布局指定管线将要访问的资源类型,就像渲染通道指定将要访问的附件类型一样。

描述符集指定将绑定到描述符的实际缓冲区或图像资源,就像帧缓冲指定要绑定到渲染通道附件的实际图像视图一样

二、统一缓冲区对象 (UBO)

UBO 是许多种类型描述符的一种。

将数据复制到 VkBuffer,并通过顶点着色器的统一缓冲区对象描述符访问它,在 VkTypes 中新增类型定义 UBO:

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

修改顶点着色器:

#version 450// 全局变换矩阵
layout(binding = 0) uniform UBO {mat4 model;     // 模型变换mat4 view;      // 视图变换mat4 proj;      // 投影变换
} ubo;// 输入属性
layout(location = 0) in vec2 pos;  // 顶点位置
layout(location = 1) in vec3 col;  // 顶点颜色// 输出到片段着色器
layout(location = 0) out vec3 fragCol;void main() {// 计算最终裁剪空间坐标(包含透视除法)gl_Position = ubo.proj * ubo.view * ubo.model * vec4(pos, 0.0, 1.0);fragCol = col; // 传递颜色到片段着色器
}

Uniform、in、out 的声明顺序不影响着色器运行。binding 与 location 作用类似,用于在描述符集布局中定位资源。gl_Position 计算引入了模型 - 视图 - 投影变换链,其结果的 w 分量(通常由透视投影矩阵生成)可能不为 1,这会触发透视除法(NDC = 裁剪坐标 /w),是实现近大远小透视效果的关键。

在创建图形管线之前新增创建描述符布局:

// 创建描述符布局
{VkDescriptorSetLayoutBinding uboLayoutBinding{};uboLayoutBinding.binding = 0;uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;uboLayoutBinding.descriptorCount = 1;uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;VkDescriptorSetLayout descriptorSetLayout;VkPipelineLayout pipelineLayout;VkDescriptorSetLayoutCreateInfo layoutInfo{};layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;layoutInfo.bindingCount = 1;layoutInfo.pBindings = &uboLayoutBinding;if (vkCreateDescriptorSetLayout(vkcontext->device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {throw std::runtime_error("创建描述符集布局失败!");}}

并在管线布局对象中指定描述符集布局:

// 创建图形管线
{    ...VkPipelineLayoutCreateInfo pipelineLayoutInfo{};pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;pipelineLayoutInfo.setLayoutCount = 1;pipelineLayoutInfo.pushConstantRangeCount = 0;pipelineLayoutInfo.pSetLayouts = &vkcontext->descriptorSetLayout; // 指定描述符布局if (vkCreatePipelineLayout(vkcontext->device, &pipelineLayoutInfo, nullptr, &vkcontext->pipelineLayout)!= VK_SUCCESS) {throw std::runtime_error("创建管线布局失败!");}...
}

在创建索引缓冲后面新增创建统一缓冲:

//  创建统一缓冲
{VkDeviceSize bufferSize = sizeof(UBO);vkcontext->uniformBuffers.resize(MAX_CONCURRENT_FRAMES);vkcontext->uniformBuffersMemory.resize(MAX_CONCURRENT_FRAMES);vkcontext->uniformBuffersMapped.resize(MAX_CONCURRENT_FRAMES);for (size_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {createBuffer(vkcontext->physicalDevice, vkcontext->device, bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vkcontext->uniformBuffers[i], vkcontext->uniformBuffersMemory[i]);vkMapMemory(vkcontext->device, vkcontext->uniformBuffersMemory[i], 0, bufferSize, 0, &vkcontext->uniformBuffersMapped[i]);}
}
  1. 为每帧渲染创建独立的统一缓冲,支持多帧并行处理;
  2. 使用主机可见且连贯的内存,允许 CPU 直接修改缓冲内容;
  3. 通过内存映射技术,避免了频繁的数据传输,提高性能;
  4. 统一缓冲通常用于存储 MVP 矩阵、光照参数等需要频繁更新的着色器常量数据。

在停止渲染后销毁统一缓冲区:

void vkClean(VkContext* vkcontext) {cleanupSwapChain(vkcontext);for (size_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {vkDestroyBuffer(vkcontext->device, vkcontext->uniformBuffers[i], nullptr);vkFreeMemory(vkcontext->device, vkcontext->uniformBuffersMemory[i], nullptr);}...
}

为 HelloRect 新增 UBO 属性,新增 update 方法添加 MVP 变换矩阵。

//------------HelloRect.h--------------------
#pragma once
#include <vector>#include "renderer/VkContext.h"using namespace renderer;class HelloRect {
public:HelloRect();void update(VkContext&);const std::vector<Vertex> vertices;const std::vector<uint16_t> indices;UBO ubo;
};//------------HelloRect.cpp------------------
HelloRect::HelloRect(): vertices({{{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}},{{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}},{{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}},{{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}}}), indices({0, 1, 2, 2, 3, 0}), ubo({}) {}void HelloRect::update(VkContext& vkcontext) {static auto startTime = std::chrono::high_resolution_clock::now();auto currentTime = std::chrono::high_resolution_clock::now();float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));ubo.proj = glm::perspective(glm::radians(45.0f), vkcontext.swapChainExtent.width / (float) vkcontext.swapChainExtent.height, 0.1f, 10.0f);ubo.proj[1][1] *= -1; // GLM 最初是为 OpenGL 设计的,其中裁剪坐标的 Y 坐标是反转的。补偿这种情况的最简单方法是翻转投影矩阵中 Y 轴缩放因子的符号。如果不这样做,则图像将倒置渲染。
}
int main() {initWindow();vkcontext.window = window;HelloRect app;vkcontext.vertexData = (void*)app.vertices.data();vkcontext.vertexNum = app.vertices.size();vkcontext.vertexBufferSize = app.vertices.size() * sizeof(Vertex);vkcontext.indicesData = (void*)app.indices.data();vkcontext.indicesNum = app.indices.size();vkcontext.indexBufferSize = app.indices.size() * sizeof(app.indices[0]);vkcontext.ubo = &app.ubo;if (!vkInit(&vkcontext)) {throw std::runtime_error("Vulkan 初始化失败!");}try {while (!glfwWindowShouldClose(window)) {glfwPollEvents();app.update(vkcontext);vkRender(&vkcontext);}...
}

三、描述符池和描述符集

为每个 VkBuffer 资源创建一个描述符集,以将其绑定到统一缓冲区描述符。描述符集不能直接创建,它们必须像命令缓冲区一样从池中分配,下面紧接着统一缓冲后面添加创建描述池代码:

 // 创建描述符池
{VkDescriptorPoolSize poolSize{};poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;poolSize.descriptorCount = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES);VkDescriptorPoolCreateInfo poolInfo{};poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;poolInfo.poolSizeCount = 1;poolInfo.pPoolSizes = &poolSize;poolInfo.maxSets = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES);if (vkCreateDescriptorPool(vkcontext->device, &poolInfo, nullptr, &vkcontext->descriptorPool) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor pool!");}}
// 创建描述符集 - 连接着色器与资源(如统一缓冲、纹理等)的桥梁
{// 为每个并发帧创建相同布局的描述符集std::vector<VkDescriptorSetLayout> layouts(MAX_CONCURRENT_FRAMES, vkcontext->descriptorSetLayout);// 描述符集分配信息VkDescriptorSetAllocateInfo allocInfo{};allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; // 结构体类型allocInfo.descriptorPool = vkcontext->descriptorPool; // 从哪个描述符池分配allocInfo.descriptorSetCount = static_cast<uint32_t>(MAX_CONCURRENT_FRAMES); // 分配数量allocInfo.pSetLayouts = layouts.data(); // 使用的布局数组// 调整容器大小存储描述符集句柄vkcontext->descriptorSets.resize(MAX_CONCURRENT_FRAMES);// 分配描述符集if (vkAllocateDescriptorSets(vkcontext->device, &allocInfo, vkcontext->descriptorSets.data()) != VK_SUCCESS) {throw std::runtime_error("分配描述符集失败!");}// 为每个描述符集更新缓冲区信息for (size_t i = 0; i < MAX_CONCURRENT_FRAMES; i++) {// 描述统一缓冲的信息VkDescriptorBufferInfo bufferInfo{};bufferInfo.buffer = vkcontext->uniformBuffers[i]; // 指定使用的缓冲bufferInfo.offset = 0; // 偏移量(从缓冲起始位置)bufferInfo.range = sizeof(UBO); // 范围(使用整个UBO大小)// 描述如何更新描述符集VkWriteDescriptorSet descriptorWrite{};descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; // 结构体类型descriptorWrite.dstSet = vkcontext->descriptorSets[i]; // 目标描述符集descriptorWrite.dstBinding = 0; // 绑定点(对应着色器中的layout(binding=0))descriptorWrite.dstArrayElement = 0; // 数组元素索引(若有多个)descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; // 描述符类型descriptorWrite.descriptorCount = 1; // 描述符数量descriptorWrite.pBufferInfo = &bufferInfo; // 缓冲信息指针// 更新描述符集 - 将统一缓冲与描述符集绑定vkUpdateDescriptorSets(vkcontext->device, 1, &descriptorWrite, 0, nullptr);}
}

核心功能:

  1. 从描述符池中分配多个描述符集 (每个帧一个)
  2. 每个描述符集使用相同的布局 (descriptorSetLayout)
  3. 将之前创建的统一缓冲 (uniformBuffers) 绑定到描述符集
  4. 通过描述符集将 CPU 更新的数据传递给 GPU 着色器

关键概念

  • 描述符集 (Descriptor Set):是一组描述符的集合,描述符是着色器访问资源的抽象
  • 描述符池 (Descriptor Pool):预分配的内存池,用于高效创建描述符集
  • 描述符布局 (Descriptor Set Layout):定义了描述符集的结构,对应着色器中的 layout 声明
  • 绑定点 (Binding):着色器中使用 layout (binding=X) 指定的资源位置

在 recordCommandBuffer 函数的 vkCmdDrawIndexed 调用前添加 vkCmdBindDescriptorSets 将每一帧的正确描述符集绑定到着色器中的描述符。

void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex, VkContext* vkcontext) {VkCommandBufferBeginInfo beginInfo{};...vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, vkcontext->pipelineLayout, 0, 1, &vkcontext->descriptorSets[vkcontext->currentFrame], 0, nullptr);vkCmdDrawIndexed(commandBuffer, vkcontext->indicesNum, 1, 0, 0, 0);vkCmdEndRenderPass(commandBuffer);if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {throw std::runtime_error("录制命令缓冲失败!");}
}

VkContext中 新增成员:

std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
std::vector<void*> uniformBuffersMapped;
UBO* ubo;VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;

现在构建运行直接黑屏,原因是投影矩阵中进行了 Y 翻转,顶点现在是以逆时针顺序而不是顺时针顺序绘制的。这会导致背面剔除生效,并阻止任何几何图形被绘制。要解决背面剔除导致的渲染问题,只需在图形管线配置中修改光栅化状态:

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;

现在构建运行效果:

内存对齐要求

Vulkan 希望你结构中的数据以一种特定的方式在内存中对齐,例如

  • 标量必须按 N 对齐(对于 32 位浮点数,N = 4 字节)。

  • 一个 vec2 必须按 2N 对齐(= 8 字节)

  • 一个 vec3 或 vec4 必须按 4N 对齐(= 16 字节)

  • 一个嵌套结构必须按其成员的基本对齐方式对齐,并向上舍入到 16 的倍数。

  • 一个 mat4 矩阵必须与一个 vec4 具有相同的对齐方式。

始终明确指定对齐方式,这样,你就不会被对齐错误引起的奇怪症状所迷惑。

struct UniformBufferObject {alignas(16) glm::mat4 model;alignas(16) glm::mat4 view;alignas(16) glm::mat4 proj;
};

当前代码分支: 08_uniformbuffer

相关文章:

  • 使用nvm管理npm和pnpm
  • 支持selenium的chrome driver更新到137.0.7151.119
  • Java课堂笔记11
  • 生产者-消费者模式在不同操作系统上的行为差异
  • 分布式选举算法<一> Bully算法
  • 要在 Linux 不联网服务器 上部署并运行 Gitee 上的 vue-vben-admin 项目,并且该项目使用的是 pnpm 管理依赖
  • LLM 支持的基于意图的分类 网络钓鱼电子邮件
  • 设计模式精讲 Day 6:适配器模式(Adapter Pattern)
  • 华为云Flexus+DeepSeek征文 | 基于DeepSeek-R1强化学习的多模态AI Agent企业级应用开发实战:从理论到生产的完整解决方案
  • 在MATLAB中绘制阵列天线的散射方向图
  • ChangeNotifierProvider 本质上也是 Widget
  • 我的256天创作纪念日
  • 二、OpenCV的第一个程序
  • Arduino入门教程:9、蜂鸣器
  • CppCon 2017 学习:CNL: A Compositional Numeric Library
  • Vue3 × DataV:三步上手炫酷数据可视化组件库
  • 机器学习 (ML) 基础入门指南
  • 李宏毅2025《机器学习》第一讲-生成式AI:技术突破和未来发展
  • 伪造GPS信号多种方式尝试-HackRF
  • 《MyBatis-Day02》
  • 没有平台没有网站怎么做外贸/郑州网站seo服务
  • wordpress下载慢/seo免费课程视频
  • pinterest的优点/seo 的作用和意义
  • 自学设计的网站/百度云官网登录首页
  • wordpress项目下载/seo是啥意思
  • 阿里巴巴做网站费用计入/广州新塘网站seo优化