一文讲清楚 Vulkan 描述符集

Vulkan 描述符集

Vulkan 中,描述符(Descriptor)是一种用于在着色器中访问资源(如缓冲区、图像、采样器等)的机制或协议。

每个描述符对应一个资源,代表 GPU 内存中的资源,如 uniform buffer、storage buffer、纹理、采样器等。

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

Vulkan 描述符集是不能被直接创建的。它们首先需要从一个特定的缓冲池中被分配得到,这个池叫作描述符池(VkDescriptorPool),这个有点类似于内存池的概念。

一文讲清楚 Vulkan 描述符集

描述符池负责分配新的描述符对象。换句话说,它相当于一组描述符的集合,新的描述符集就是从这些描述符中分配得到的。

描述符池对于内存分配效率较低的场合是非常有用的,它可以直接分配多组描述符集而不需要调用全局同步操作。

在创建描述符集之前,需要定义描述符集的布局(VkDescriptorSetLayout)。布局指定了每个描述符的类型、数量、绑定点等信息。

描述符集布局定义了描述符集中资源的结构,确保描述符集能正确地绑定到管线上。两个拥有相同布局的描述符集被视为兼容的和可相互交换的。

描述符集布局和管线布局

一文讲清楚 Vulkan 描述符集

上文讲到描述符集布局 VkDescriptorSetLayout 指定了每个描述符的类型、数量、绑定点等信息,那么又跟管线布局 VkPipelineLayout 有什么关系呢?

管线布局是对着色器资源绑定的全局描述,代表了图形管线可以访问的所有资源的集合。

创建Vulkan渲染管线的时候需要设置管线布局,它描述了在渲染过程中着色器如何访问资源,包括描述符集和推送常量等。

管线布局可以包含一个或多个描述符集布局和推送常量描述(推送常量可以更新着色器中的常量数据,后面会再展开讲),下面是创建管线布局的结构体(这个结构体在前文图形管线的创建中用到):

typedef struct VkPipelineLayoutCreateInfo {
    VkStructureType                 sType;                  // 结构体类型,必须是 VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO
    const void*                     pNext;                  // 指向扩展结构体的指针,通常为 nullptr
    VkPipelineLayoutCreateFlags     flags;                  // 管线布局创建标志,通常为 0
    uint32_t                        setLayoutCount;         // 绑定的描述符集布局数量
    const VkDescriptorSetLayout*    pSetLayouts;            // 指向描述符集布局数组的指针
    uint32_t                        pushConstantRangeCount; // 推送常量范围数量
    const VkPushConstantRange*      pPushConstantRanges;    // 指向推送常量范围数组的指针
} VkPipelineLayoutCreateInfo;

无论是管线布局还是描述符集布局,本质上都是对资源的一种描述,其本身并不占用资源。

创建和使用描述符集

创建描述符集步骤

  1. 定义描述符集布局:
    • 使用 `VkDescriptorSetLayoutBinding` 结构体定义每个描述符的类型、数量和绑定点。
    • 调用 `vkCreateDescriptorSetLayout` 创建描述符集布局。
  2. 创建描述符池:
    • 使用 `VkDescriptorPoolSize` 指定描述符池中的每种描述符类型的数量。
    • 调用 `vkCreateDescriptorPool` 创建描述符池。
  3. 分配描述符集:
    • 调用 `vkAllocateDescriptorSets` 从描述符池中分配描述符集。
  4. 更新描述符集:
    • 使用 `VkWriteDescriptorSet` 结构体更新描述符集中的描述符,绑定实际的资源(如缓冲区、纹理)。
  5. 绑定描述符集
    • 在渲染过程中,调用 `vkCmdBindDescriptorSets` 将描述符集绑定到管线,着色器便可以访问描述符集中指定的资源。

我们先搞个着色器的脚本,然后对着这个着色器脚本创建描述符集布局和描述符集,主要是为了展示着色器和描述符集的关系是多么地密切,一目了然哈。

顶点着色器:

#version 450  // 指定GLSL的版本号为450,对应于OpenGL 4.5或Vulkan 1.0

// 输入属性
layout(location = 0) in vec3 inPos;     // 顶点位置,location = 0 表示从顶点输入中读取第一个属性
layout(location = 1) in vec2 inUV;      // 纹理坐标,location = 1 表示从顶点输入中读取第二个属性
layout(location = 2) in vec3 inNormal;  // 顶点法线,location = 2 表示从顶点输入中读取第三个属性

// Uniform 缓冲对象 (UBO),用于传递投影、模型、视图矩阵
layout(binding = 0) uniform UBO 
{
    mat4 mvp;
} ubo;  // `ubo` 是这个 uniform 块的实例名,着色器中通过它访问矩阵

// 输出变量,传递到片段着色器
layout(location = 0) out vec2 outUV;  // 纹理坐标输出,location = 0 表示传递给片段着色器的第一个输出变量

// 顶点着色器的主函数
void main() 
{
    outUV = inUV;  // 将输入的纹理坐标传递给输出变量 `outUV`

    // 计算最终顶点位置并赋值给gl_Position
    gl_Position = ubo.mvp * vec4(inPos.xyz, 1.0);  
    // 将输入的顶点位置转换为世界坐标系中的位置,再转换为观察空间坐标系,最后转换为裁剪空间坐标系,并传递给gl_Position
}

片段着色器:

#version 450  // 指定GLSL的版本号为450,对应于OpenGL 4.5或Vulkan 1.0

// Uniform 变量,用于传递2D纹理采样器
layout (binding = 1) uniform sampler2D samplerColor;
// sampler2D:用于在片段着色器中采样2D纹理的统一变量
// binding = 1:指定了该采样器在着色器中的绑定点为1

// 输入变量,从顶点着色器传递过来的纹理坐标
layout (location = 0) in vec2 inUV;  
// location = 0:指定输入变量的位置为0

// 输出变量,片段的最终颜色
layout (location = 0) out vec4 outFragColor;  
// location = 0:指定输出变量的位置为0,表示片段着色器输出的颜色

// 片段着色器的主函数
void main() 
{
    // 通过采样2D纹理 `samplerColor`,使用插值后的纹理坐标 `inUV`,获取纹理颜色,并将其赋值给 `outFragColor`
    outFragColor = texture(samplerColor, inUV, 0.0);
    // `texture()` 函数:在指定的 `samplerColor` 纹理上采样,使用纹理坐标 `inUV`,第三个参数 0.0 是一个 LOD(细节层次)的偏移量,这里不使用 LOD 偏移
}

上面脚本中有 2 个绑定点,binding = 0 是 UBO( MVP 矩阵) ,另一个 binding = 1 是纹理采样器,我们需要使用 2 个描述符。

我们通过 VkDescriptorSetLayoutBinding 描述符集布局绑定来设置描述符的信息:

// 定义 Uniform Buffer Object (UBO) 的描述符集布局绑定
VkDescriptorSetLayoutBinding uboLayoutBinding{};
uboLayoutBinding.binding = 0; // 绑定点 0
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; // 描述符类型为 Uniform Buffer
uboLayoutBinding.descriptorCount = 1; // 仅绑定一个描述符
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; // 该 UBO 将用于顶点着色器

// 定义 Combined Image Sampler 的描述符集布局绑定
VkDescriptorSetLayoutBinding samplerLayoutBinding{};
samplerLayoutBinding.binding = 1; // 绑定点 1
samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; // 描述符类型为 Combined Image Sampler
samplerLayoutBinding.descriptorCount = 1; // 仅绑定一个描述符
samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; // 该采样器将用于片段着色器

基于描述符集布局绑定创建描述符集布局:

// 将两个绑定定义组合到一个数组中
std::array<VkDescriptorSetLayoutBinding, 2> bindings = {uboLayoutBinding, samplerLayoutBinding};

// 创建描述符集布局
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); // 创建描述符集布局

创建描述符池:

// 创建描述符池,用于分配描述符集
std::array<VkDescriptorPoolSize, 2> poolSizes{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; // 池中的第一个类型为 Uniform Buffer
poolSizes[0].descriptorCount = 1; // 分配一个此类型的描述符

poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; // 池中的第二个类型为 Combined Image Sampler
poolSizes[1].descriptorCount = 1; // 分配一个此类型的描述符

VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; // 结构体类型
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size()); // 描述符池大小数量
poolInfo.pPoolSizes = poolSizes.data(); // 指向描述符池大小数组
poolInfo.maxSets = 1; // 仅创建一个描述符集

VkDescriptorPool descriptorPool;
vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool); // 创建描述符池

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

// 分配描述符集
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; // 结构体类型
allocInfo.descriptorPool = descriptorPool; // 从先前创建的描述符池中分配
allocInfo.descriptorSetCount = 1; // 分配一个描述符集
allocInfo.pSetLayouts = &descriptorSetLayout; // 指向描述符集布局

VkDescriptorSet descriptorSet;
vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet); // 分配描述符集

更新描述符集(一个是 Buffer ,另一个是纹理采样器+图像视图):

// 更新描述符集中的 UBO 部分
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffer; // 指向包含 UBO 的缓冲区
bufferInfo.offset = 0; // 缓冲区起始偏移量
bufferInfo.range = sizeof(UniformBufferObject); // 缓冲区范围

VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; // 结构体类型
descriptorWrite.dstSet = descriptorSet; // 目标描述符集
descriptorWrite.dstBinding = 0; // 目标绑定点 0
descriptorWrite.dstArrayElement = 0; // 数组中的第一个元素
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; // 描述符类型
descriptorWrite.descriptorCount = 1; // 更新一个描述符
descriptorWrite.pBufferInfo = &bufferInfo; // 指向包含 UBO 信息的结构体

// 更新描述符集中的 Combined Image Sampler 部分
VkDescriptorImageInfo imageInfo{};
imageInfo.sampler = textureSampler; // 指向已创建的采样器
imageInfo.imageView = textureImageView; // 指向已创建的图像视图
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; // 图像布局

VkWriteDescriptorSet samplerDescriptorWrite{};
samplerDescriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; // 结构体类型
samplerDescriptorWrite.dstSet = descriptorSet; // 目标描述符集
samplerDescriptorWrite.dstBinding = 1; // 目标绑定点 1
samplerDescriptorWrite.dstArrayElement = 0; // 数组中的第一个元素
samplerDescriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; // 描述符类型
samplerDescriptorWrite.descriptorCount = 1; // 更新一个描述符
samplerDescriptorWrite.pImageInfo = &imageInfo; // 指向包含图像采样器信息的结构体

// 更新描述符集
std::array<VkWriteDescriptorSet, 2> descriptorWrites = {descriptorWrite, samplerDescriptorWrite};
vkUpdateDescriptorSets(device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);

我们后续在录制渲染指令到指令缓存时(后面章节会讲到),通过 vkCmdBindDescriptorSets 来绑定相应的描述符集:

// 在命令缓冲区中绑定描述符集
vkCmdBindDescriptorSets(
    commandBuffer,                   // 目标命令缓冲区
    VK_PIPELINE_BIND_POINT_GRAPHICS, // 指定为图形管线
    pipelineLayout,                  // 绑定的管线布局
    0,                               // 第一个描述符集的索引
    1,                               // 绑定一个描述符集
    &descriptorSet,                  // 指向描述符集的指针
    0,                               // 动态偏移量数量
    nullptr                          // 没有动态偏移量
);

好了,关于描述符集的介绍到此为止,我们完成了闭环。

参考

《Vulkan 学习指南》 — [新加坡] 帕敏德·辛格(Parminder Singh)

《Vulkan 应用开发指南》— [美] 格拉汉姆·塞勒斯(Graham Sellers)等 译者:李晓波 等

进技术交流群,扫码添加我的微信:Byte-Flow

字节流动

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/52262.html

(0)

相关推荐

发表回复

登录后才能评论