目录
20 layout and buffer
顶点着色器
描述符集布局
21 统一缓冲区
更新统一数据
22 Descriptor pool and sets
描述符池
描述符集
使用描述符集
对齐要求
20 layout and buffer
我们现在可以将任意属性传递给每个顶点的顶点着色器,模型-视图-投影矩阵将其作为顶点数据包含在内,但这是一种内存浪费。资源描述符描述符是着色器自由访问缓冲区和图像等资源的一种方式。我们将设置一个包含转换矩阵的缓冲区,并让顶点着色器通过描述符访问它们。描述符的使用包括三个部分:
- 在管道创建期间指定描述符布局
- 从描述符池中分配一个描述符集
- 渲染时绑定描述符集
描述符布局指定管道将访问的资源类型,就像渲染通道指定将访问的附件类型一样。描述符集指定绑定到描述符的实际缓冲区或图像资源,就像帧缓冲区指定要绑定到渲染通道附件的实际图像视图一样。描述符集然后像顶点缓冲区和帧缓冲区一样绑定到绘图命令。
描述符有多种类型,但在本章中我们将使用统一缓冲区对象 (UBO)。
顶点着色器
修改顶点着色器以包含上面指定的统一缓冲区对象。
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};//顶点着色器在 C 结构中拥有的数据
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
//uniform请注意, ,in和声明的顺序out无关紧要
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
描述符集布局
下一步是在 C++ 端定义 UBO,并在顶点着色器中将此描述符告知 Vulkan。
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
我们需要提供有关着色器中用于管道创建的每个描述符绑定的详细信息,设置一个新函数来定义所有这些信息,称为createDescriptorSetLayout
. 它应该在管道创建之前调用
成员变量
//所有描述符绑定都组合到一个 VkDescriptorSetLayout对象中。
VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding{};
//前两个字段指定binding在着色器中使用的和描述符的类型,它是一个统一的缓冲区对象
uboLayoutBinding.binding = 0;
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;//指定数组中值的数量
//我们的 MVP 转换是在一个单一的统一缓冲对象中
//指定在哪个着色器阶段将引用描述符。该字段可以是值或值stageFlags的组合。
//在我们的例子中,我们只引用来自顶点着色器的描述符
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
uboLayoutBinding.pImmutableSamplers = nullptr; // Optional
//该pImmutableSamplers字段仅与图像采样相关的描述符相关
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!");
}
}
我们需要在管道创建期间指定描述符集布局,以告知 Vulkan 着色器将使用哪些描述符。描述符集布局在管道布局对象中指定。修改VkPipelineLayoutCreateInfo 以引用布局对象:
pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
描述符布局应该在我们创建新的图形管道时保持不变,即直到程序结束:
void cleanup() {
cleanupSwapChain();
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
...
}
21 统一缓冲区
为着色器指定包含 UBO 数据的缓冲区,但我们需要先创建此缓冲区。我们将每帧都将新数据复制到统一缓冲区,因此拥有暂存缓冲区并没有任何意义。在这种情况下,它只会增加额外的开销,并且可能会降低性能而不是提高性能。
为uniformBuffers
, 和添加新的类成员uniformBuffersMemory
:
std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
std::vector<void*> uniformBuffersMapped;
//创建一个createUniformBuffers在之后调用 createIndexBuffer并分配缓冲区的新函数
void createUniformBuffers() {
VkDeviceSize bufferSize = sizeof(UniformBufferObject);
uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);
//我们在创建后立即映射缓冲区,vkMapMemory以获取一个指针,稍后我们可以将数据写入该指针
//缓冲区在应用程序的整个生命周期内保持映射到此指针。这种技术称为“持久映射”
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; 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]);
vkMapMemory(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]);
}
}
//统一数据将用于所有绘制调用,因此包含它的缓冲区应该只在我们停止渲染时销毁。
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}
更新统一数据
在drawFrame里
提交下一帧之前调用updateUniformBuffer,
此函数将在每一帧生成一个新的变换,使几何体旋转。我们需要包含两个新的标头来实现此功能:
#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <chrono>
//标glm/gtc/matrix_transform.hpp头公开了可用于生成模型变换
//标准chrono库标头公开函数以进行精确计时。
//我们将使用它来确保无论帧速率如何,几何体每秒旋转 90 度。
void updateUniformBuffer(uint32_t currentImage)
{
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();
}//以秒为单位计算自渲染开始以来以浮点精度计算的时间。
我们现在将在统一缓冲区对象中定义模型、视图和投影转换。模型旋转将是使用变量围绕 Z 轴的简单旋转time
:
UniformBufferObject ubo{};
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
该glm::rotate
函数将现有的变换、旋转角度和旋转轴作为参数。构造函数glm::mat4(1.0f)
返回一个单位矩阵。使用旋转角度time * glm::radians(90.0f)
达到每秒旋转 90 度的目的。
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));
对于视图转换,我决定以 45 度角从上方查看几何体。该glm::lookAt
函数将眼睛位置、中心位置和上轴作为参数。
ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);
ubo.proj[1][1] *= -1;GLM 最初是为 OpenGL 设计的,其中剪辑坐标的 Y 坐标是倒置的。最简单的补偿方法是翻转投影矩阵中 Y 轴比例因子的符号。
22 Descriptor pool and sets
描述符池
不能直接创建描述符集,它们必须像命令缓冲区一样从池中分配.我们将编写一个新函数createDescriptorPool
来设置它。
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
//描述符集将包含哪些描述符类型以及它们的数量
poolSize.descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
//我们将为每一帧分配这些描述符之一。此池大小结构由 main 引用VkDescriptorPoolCreateInfo:
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
//指定可以分配的描述符集的最大数量
poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
//添加一个新的类成员来存储描述符池的句柄并调用 vkCreateDescriptorPool创建它
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor pool!");
}
描述符集
我们现在可以自己分配描述符集。为此添加一个函数:createDescriptorSets
std::vector<VkDescriptorSetLayout> layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo{};
//指定要分配的描述符池、要分配的描述符集的数量以及它们基于的描述符布局
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
allocInfo.pSetLayouts = layouts.data();
//添加一个类成员来保存描述符集句柄并分配它们 vkAllocateDescriptorSets
descriptorSets.resize(MAX_FRAMES_IN_FLIGHT);
if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate descriptor sets!");
}
您不需要显式清理描述符集,因为它们会在描述符池被销毁时自动释放。调用 vkAllocateDescriptorSets将分配描述符集,每个描述符集都有一个统一的缓冲区描述符。
void cleanup() {
...
vkDestroyDescriptorPool(device, descriptorPool, nullptr);
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
...
}
现在已经分配了描述符集,但仍然需要配置其中的描述符
//添加一个循环来填充每个描述符:
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
}
//前两个字段指定要更新的描述符集和绑定。
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;//统一的缓冲区绑定索引0
descriptorWrite.dstArrayElement = 0;
//指定描述符的类型。可以一次更新数组中的多个描述符
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pBufferInfo = &bufferInfo;//该pBufferInfo字段用于引用缓冲区数据的描述符
descriptorWrite.pImageInfo = nullptr; // Optional引用图像数据的描述符
descriptorWrite.pTexelBufferView = nullptr; // Optional引用缓冲区视图的描述符
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
使用描述符集
我们现在需要更新recordCommandBuffer
函数,以实际将每个帧的正确描述符集绑定到着色器中的描述符vkCmdBindDescriptorSets。这需要在调用之前完成vkCmdDrawIndexed:
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[currentFrame], 0, nullptr);
与顶点和索引缓冲区不同,描述符集不是图形管道所独有的。因此,我们需要指定是否要将描述符集绑定到图形或计算管道。下一个参数是描述符所基于的布局。接下来的三个参数指定第一个描述符集的索引、要绑定的集的数量以及要绑定的集的数组。最后两个参数指定用于动态描述符的偏移量数组。
再次运行您的程序,您现在应该看到以下内容:
如果projection没有乘-1
对齐要求
到目前为止我们忽略的一件事是 C++ 结构中的数据应该如何与着色器中的统一定义匹配。
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
//4*4*4 = 64 对齐
//model偏移量为0,view偏移量为 64,proj偏移量为 128。所有这些都是 16 的倍数
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
//尝试将结构和着色器修改为如下所示:
struct UniformBufferObject {
glm::vec2 foo;
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
//开头vec2,其大小仅为 8 个字节
//可以使用alignasC++11 中引入的说明符
layout(binding = 0) uniform UniformBufferObject {
vec2 foo;
mat4 model;
mat4 view;
mat4 proj;
} ubo;
重新编译你的着色器和你的程序并运行它,你会发现你到目前为止工作的彩色方块已经消失了!那是因为我们没有考虑对齐要求。
Vulkan 期望结构中的数据以特定方式在内存中对齐,例如:
- 标量必须按 N 对齐(= 4 个字节,给定 32 位浮点数)。
vec2
必须对齐 2N(= 8 字节)vec3
orvec4
必须对齐 4N(= 16 字节)- 嵌套结构必须按其成员的基本对齐方式对齐(四舍五入为 16 的倍数)。
- 矩阵
mat4
必须与vec4
具有相同的对齐方式。可以使用alignasC++11 中引入的说明符
一种方法可以在大多数时候不必考虑这些对齐要求。我们可以GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
在包含 GLM 之前定义:
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
#include <glm/glm.hpp>
不幸的是,如果您开始使用嵌套结构,此方法可能会失效。考虑 C++ 代码中的以下定义:
struct Foo {
glm::vec2 v;
};
struct UniformBufferObject {
Foo f1;
Foo f2;
};
struct Foo {
vec2 v;
};
layout(binding = 0) uniform UniformBufferObject {
Foo f1;
Foo f2;
} ubo;
//在这种情况下f2,将有一个偏移量8,而它应该有一个16偏移
//因为它是一个嵌套结构。在这种情况下,您必须自己指定对齐方式:
struct UniformBufferObject {
Foo f1;
alignas(16) Foo f2;
};