Uniform_buffers/Descriptor layout and buffer

本文介绍了在Vulkan图形API中如何使用描述符和统一缓冲区对象(UBO)来传递全局变量,如模型视图投影矩阵,给顶点着色器。文章详细阐述了描述符布局、创建描述符集布局、更新统一缓冲区数据以及在渲染过程中如何绑定这些资源,从而实现3D图形的变换处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

Introduction

Vertex shader

Descriptor set layout

Uniform buffer

Updating uniform data


Introduction

我们现在可以将任意属性传递给每个顶点的顶点着色器,但是全局变量呢?从本章开始,我们将继续学习3D图形,这需要模型视图投影矩阵。我们可以将其包含为顶点数据,但这会浪费内存,而且每当变换发生变化时,都需要更新顶点缓冲区。变换可以很容易地改变每一帧。

在Vulkan中解决这一问题的正确方法是使用资源描述符。描述符是着色器自由访问缓冲区和图像等资源的一种方式。我们将设置一个包含变换矩阵的缓冲区,并让顶点着色器通过描述符访问它们。描述符的使用包括三个部分:

  • 在管道创建期间指定描述符布局
  • 从描述符池分配描述符集
  • 在渲染期间绑定描述符集

描述符布局指定管道将访问的资源类型,就像渲染过程指定将访问的附件类型一样。描述符集指定将绑定到描述符的实际缓冲区或图像资源,就像帧缓冲区指定要绑定到渲染过程附件的实际图像视图一样。然后像顶点缓冲区和帧缓冲区一样,为绘图命令绑定描述符集。

描述符有多种类型,但在本章中,我们将使用统一缓冲区对象(UBO)。我们将在以后的章节中讨论其他类型的描述符,但基本过程是相同的。假设我们有一个C结构中的顶点着色器所需的数据,如下所示:

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

然后,我们可以将数据复制到VkBuffer,并通过顶点着色器中的统一缓冲区对象描述符访问它,如下所示:

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

我们将在每一帧更新模型、视图和投影矩阵,以使上一章中的矩形在3D中旋转。

Vertex shader

修改顶点着色器以包括上面指定的统一缓冲区对象。我假设您熟悉MVP转换。如果没有,请参阅第一章中提到的资源。

#version 450

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

注意,统一、输入和输出声明的顺序无关紧要。绑定指令类似于属性的位置指令。我们将在描述符布局中引用此绑定。具有gl_Position的线被更改为使用变换来计算剪辑坐标中的最终位置。与2D三角形不同,剪辑坐标的最后一个分量可能不是1,当转换为屏幕上的最终规格化设备坐标时,这将导致分割。这在透视投影中用作透视分割,对于使较近的对象看起来比较远的对象更大至关重要。

Descriptor set layout

下一步是在C++端定义UBO,并告诉Vulkan顶点着色器中的这个描述符。

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

我们可以使用GLM中的数据类型精确匹配着色器中的定义。矩阵中的数据与着色器期望的方式是二进制兼容的,因此我们可以稍后将UniformBufferObject存储到VkBuffer中。

我们需要提供着色器中用于管道创建的每个描述符绑定的详细信息,就像我们必须为每个顶点属性及其位置索引所做的那样。我们将设置一个新函数来定义所有这些信息,称为createDescriptorSetLayout。应该在创建管道之前调用它,因为我们需要它。

void initVulkan() {
    ...
    createDescriptorSetLayout();
    createGraphicsPipeline();
    ...
}

...

void createDescriptorSetLayout() {

}

每个绑定都需要通过VkDescriptorSetLayoutBinding结构来描述。

void createDescriptorSetLayout() {
    VkDescriptorSetLayoutBinding uboLayoutBinding{};
    uboLayoutBinding.binding = 0;
    uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    uboLayoutBinding.descriptorCount = 1;
}

前两个字段指定着色器中使用的绑定和描述符的类型,这是一个统一的缓冲区对象。着色器变量可以表示统一缓冲区对象的数组,descriptorCount指定数组中的值数。例如,这可以用于为骨骼动画的骨骼中的每个骨骼指定变换。我们的MVP转换在一个统一的缓冲区对象中,因此我们使用的描述符计数为1。

uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

我们还需要指定描述符将在哪个着色器阶段被引用。stageFlags字段可以是VkShaderStageFlagBits值或值VK_SHADER_STAGE_ALL_GRAPHICS的组合。在我们的例子中,我们只是从顶点着色器引用描述符。

uboLayoutBinding.pImmutableSamplers = nullptr; // Optional

pImmutableSamplers字段仅与图像采样相关的描述符相关,我们稍后将对此进行讨论。您可以将其保留为默认值。

所有的描述符绑定都组合到一个VkDescriptorSetLayout对象中。在pipelineLayout上方定义新的类成员:

VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;

然后我们可以使用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!");
}

我们需要在管道创建期间指定描述符集布局,以告诉Vulkan着色器将使用哪些描述符。描述符集布局在管道布局对象中指定。修改VkPipelineLayoutCreateInfo以引用布局对象:

VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

您可能想知道为什么可以在这里指定多个描述符集布局,因为一个描述符集已经包含了所有绑定。我们将在下一章继续讨论这个问题,在那里我们将研究描述符池和描述符集。

描述符布局应该保持不变,同时我们可以创建新的图形管道,即直到程序结束:

void cleanup() {
    cleanupSwapChain();

    vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);

    ...
}

Uniform buffer

在下一章中,我们将指定包含着色器的UBO数据的缓冲区,但我们需要先创建此缓冲区。我们将在每一帧将新数据复制到统一缓冲区,因此使用分段缓冲区没有任何意义。在这种情况下,它只会增加额外的开销,可能会降低性能而不是提高性能。

我们应该有多个缓冲区,因为多个帧可能同时在飞行中,我们不想在前一帧仍在读取缓冲区时更新缓冲区以准备下一帧!因此,我们需要有与运行中的帧一样多的统一缓冲区,并写入GPU当前未读取的统一缓冲。

为此,为uniformBuffers和uniformBuffersMemory添加新的类成员:

VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
std::vector<void*> uniformBuffersMapped;

类似地,创建一个在createIndexBuffer之后调用的新函数createUniformBuffers,并分配缓冲区:

void initVulkan() {
    ...
    createVertexBuffer();
    createIndexBuffer();
    createUniformBuffers();
    ...
}

...

void createUniformBuffers() {
    VkDeviceSize bufferSize = sizeof(UniformBufferObject);

    uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
    uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
    uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);

    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]);
    }
}

我们在创建后立即使用vkMapMemory映射缓冲区,以获得一个指针,稍后我们可以将数据写入该指针。在应用程序的整个生命周期中,缓冲区一直映射到该指针。这种技术被称为“持久映射”,适用于所有Vulkan实现。不必在每次需要更新时都映射缓冲区,这会提高性能,因为映射不会释放。

统一数据将用于所有绘制调用,因此只有当我们停止渲染时,包含它的缓冲区才会被破坏。

void cleanup() {
    ...

    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        vkDestroyBuffer(device, uniformBuffers[i], nullptr);
        vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
    }

    vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);

    ...

}

Updating uniform data

创建一个新函数updateUniformBuffer,并在提交下一帧之前从drawFrame函数添加对它的调用:

void drawFrame() {
    ...

    updateUniformBuffer(currentFrame);

    ...

    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

    ...
}

...

void updateUniformBuffer(uint32_t currentImage) {

}

此函数将在每帧生成一个新的变换,以使几何体旋转。我们需要包含两个新的标头来实现此功能:

#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <chrono>

glm/gtc/matrix_transform.hpp标头公开了可用于生成模型转换(如glm::rotate)、视图转换(如glm::lookAt)和投影转换(如glm::perspective)的函数。GLM_FORCE_RIANS定义对于确保glm::rotate等函数使用弧度作为参数是必要的,以避免任何可能的混淆。

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();
}

updateUniformBuffer函数将从一些逻辑开始,以计算渲染以浮点精度开始后的时间(以秒为单位)。

现在我们将在统一缓冲区对象中定义模型、视图和投影变换。模型旋转将是使用时间变量围绕Z轴的简单旋转:

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);

我选择使用45度垂直视野的透视投影。其他参数是纵横比、近视图平面和远视图平面。使用当前交换链范围计算纵横比非常重要,以考虑调整大小后窗口的新宽度和高度。

ubo.proj[1][1] *= -1;

GLM最初是为OpenGL设计的,其中剪辑坐标的Y坐标是反转的。最简单的补偿方法是翻转投影矩阵中Y轴比例因子上的符号。如果不执行此操作,则图像将呈现为倒置。

现在定义了所有转换,因此我们可以将统一缓冲区对象中的数据复制到当前的统一缓冲区。这与我们对顶点缓冲区的处理方式完全相同,只是没有暂存缓冲区。如前所述,我们只映射一次统一缓冲区,因此我们可以直接写入它,而无需再次映射:

memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo));

这样使用UBO并不是将频繁更改的值传递给着色器的最有效方法。将一小部分数据缓冲区传递给着色器的一种更有效的方法是推送常量。我们可以在未来的章节中讨论这些问题。

在下一章中,我们将研究描述符集,它实际上将VkBuffers绑定到统一缓冲区描述符,以便着色器可以访问该转换数据。

<think>好的,我现在需要帮助用户了解VkDescriptorSetLayout。用户之前已经询问过VkPipelineLayoutCreateInfo的初始化,现在又接着问VkDescriptorSetLayout,显然是在学习Vulkan的描述符相关部分。首先,我应该先明确VkDescriptorSetLayout的作用,它是用来描述着色器中的资源绑定的,比如uniform buffers、采样器、存储缓冲区等。 接下来,用户可能需要知道如何创建这个对象。在之前的回答中,我提到了描述符集布局在初始化PipelineLayout时的使用,所以这里需要详细说明如何创建DescriptorSetLayout。需要介绍VkDescriptorSetLayoutCreateInfo结构体的初始化过程,包括绑定数量、绑定数组的配置,比如每个绑定指定类型、数量、着色器阶段等。 然后,用户可能会关心具体的代码示例,比如如何设置VkDescriptorSetLayoutBinding结构体,以及调用vkCreateDescriptorSetLayout函数的步骤。同时,要注意是否需要清理资源,比如使用vkDestroyDescriptorSetLayout,但用户没有直接问,但可能后续会用到,所以可以提一下。 另外,用户可能不清楚描述符集布局和管线布局的关系,这里需要简单解释,说明DescriptorSetLayout是PipelineLayout的一部分,用于告诉管线着色器将访问哪些资源。可能还需要提到实际应用中的例子,比如在渲染循环中如何绑定描述符集。 还要注意用户之前的问题中提到了结构体初始化的细节,比如sType必须正确设置,所以在这里同样需要强调VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO的正确使用,以及pNext和flags的设置,通常默认即可。 用户可能需要知道不同绑定类型的区别,比如uniform buffer和combined image sampler的使用场景,所以在示例中可以给出这两种常见类型的配置。此外,注意描述符数量的设置,尤其是对于纹理数组的情况,需要指定descriptorCount。 最后,要确保回答结构清晰,分点说明创建步骤,注意事项,并给出代码示例,这样用户能一步步跟着操作。同时,提醒资源管理和验证层的潜在错误,帮助用户避免常见问题。</think>在 Vulkan 中,`VkDescriptorSetLayout`(描述符集布局)是**定义着色器如何访问资源(如缓冲区、纹理等)的蓝图**。它通过描述绑定到着色器的资源类型、数量和阶段,最终会被组合到`VkPipelineLayout`中。以下是详细说明: --- ### **核心作用** 1. **资源绑定规范** - 定义着色器访问的资源类型(Uniform Buffer、采样器、存储缓冲区等) - 指定资源绑定点(例如:绑定点 0 = Uniform Buffer,绑定点 1 = 纹理采样器) - 声明资源在着色器中的可见阶段(顶点/片段/计算等) 2. **管线布局基础** - 多个`VkDescriptorSetLayout`可组合成`VkPipelineLayout` - 允许不同管线共享相同的资源绑定结构 --- ### **创建流程**(代码示例) ```cpp // 1. 定义绑定信息数组(每个元素描述一个资源绑定点) VkDescriptorSetLayoutBinding bindings[2] = {}; // 绑定点 0:Uniform Buffer(用于顶点着色器) bindings[0].binding = 0; // 绑定点编号(对应着色器中的 layout(binding=0)) bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; bindings[0].descriptorCount = 1; // 资源数量(例如数组长度,单资源则为1) bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; // 可见阶段 // 绑定点 1:Combined Image Sampler(用于片段着色器) bindings[1].binding = 1; bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bindings[1].descriptorCount = 1; bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; // 2. 创建描述符集布局信息结构体 VkDescriptorSetLayoutCreateInfo layoutInfo = {}; layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutInfo.bindingCount = 2; // 绑定点数量 layoutInfo.pBindings = bindings; // 绑定信息数组 // 3. 创建描述符集布局对象 VkDescriptorSetLayout descriptorSetLayout; VkResult result = vkCreateDescriptorSetLayout( device, // Vulkan 逻辑设备 &layoutInfo, // 布局信息 nullptr, // 分配器回调(通常为nullptr) &descriptorSetLayout ); // 4. 检查创建结果 if (result != VK_SUCCESS) { // 处理错误(例如内存不足或无效参数) } ``` --- ### **关键参数解析** 1. `VkDescriptorSetLayoutBinding` 结构体: - `binding`:对应着色器中的 `layout(binding=N)` 声明 - `descriptorType`:资源类型,常用值包括: - $VK\_DESCRIPTOR\_TYPE\_UNIFORM\_BUFFER$(Uniform 缓冲区) - $VK\_DESCRIPTOR\_TYPE\_COMBINED\_IMAGE\_SAMPLER$(纹理+采样器) - $VK\_DESCRIPTOR\_TYPE\_STORAGE\_BUFFER$(可读写缓冲区) - `stageFlags`:指定哪些着色器阶段可访问(用位或操作组合多个阶段) 2. **特殊配置**: - **动态 Uniform 缓冲区**:设置 `descriptorCount` 为动态数量时需结合 `VkDescriptorBufferInfo` 的动态偏移 - **纹理数组**:设置 `descriptorCount > 1` 时表示绑定多个同类资源(例如 $descriptorCount = 4$ 对应 4 个纹理) --- ### **实际应用场景** 1. **Uniform 数据传递** ```cpp // 顶点着色器的 MVP 矩阵传递 layout(binding = 0) uniform UniformBufferObject { mat4 model; mat4 view; mat4 proj; } ubo; ``` 2. **纹理采样** ```cpp // 片段着色器采样纹理 layout(binding = 1) uniform sampler2D texSampler; ``` --- ### **注意事项** 1. **生命周期管理** - 需在程序退出时通过 `vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr)` 释放资源 2. **验证层错误** - 若绑定点未在着色器中声明或类型不匹配,验证层会报错(例如 `VUID-VkPipelineLayoutCreateInfo-pSetLayouts-00382`) 3. **性能优化** - 复用相同的 `VkDescriptorSetLayout` 可减少资源重复创建 - 通过描述符池(`VkDescriptorPool`)管理实际分配
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值