目录
- UniformBuffer
- DescriptorSetLayout 和 VkBuffer
- 顶点着色器定义
- 描述符布局(DescriptorSetLayout)
- 创建 UniformBuffer
- 描述符池(DescriptorSet Pool)
- 描述符集(DescriptorSet)
- 更新描述符集
- 使用描述符集
- 使用多个 Descriptor
UniformBuffer
本篇文档是通过 Uniform Buffer
的使用进一步加深对 DescriptorSet
的理解
Vulkan
中,描述符是一种在着色器中访问资源(比如缓冲区,图像,采样器等)的机制或者协议
每个描述符(Descriptor
)对应一个资源,代表 GPU
内存中的资源,比如 Uniform Buffer
, storage Buffer
, Texture
,Sampler
等
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.0, 1.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, 0,0, 1.0);fragColor = inColor;
}
uniform
,in
和 out
定义在着色器中出现的顺序可以是任意的,任意代码中 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;
}
binding
和 descriptorType
用于指定着色器使用的描述符绑定和描述符类型,这里我们指定的是一个 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], 0, sizeof(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
缓冲描述符(对应一个 uniform
的 vkBuffer
)
我们通过 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
dstSet
和 dstBinding
成员变量用于指定要更新的 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, 0, 1, &descriptorSets[i], 0, nullptr);
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
和顶点缓冲,索引缓冲不同,描述符集合并不是图像管线所独有的,所以我们需要指定我们绑定的是图形管线还是计算管线,管线之后的参数是描述符使用的布局
后面的三个参数用于指定: 描述符集的第一个元素索引,绑定的描述符集的个数,以及用于绑定的描述符集数组,最后两个参数用于指定动态描述符的数组偏移
使用多个 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
的示意图,加深理解: