简介

在接下来的几章中,我们将把顶点着色器中硬编码的顶点数据替换为内存中的顶点缓冲。我们将从创建 CPU 可见缓冲并使用 memcpy 直接将顶点数据复制到其中的最简单方法开始,之后我们将了解如何使用暂存缓冲将顶点数据复制到高性能内存中。

顶点着色器

首先更改顶点着色器,使其不再包含着色器代码本身的顶点数据。顶点着色器使用 in 关键字从顶点缓冲获取输入。

#version 450

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

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

inPositioninColor 变量是顶点属性。它们是每个顶点在顶点缓冲中指定的属性,就像我们使用两个数组手动指定每个顶点的位置和颜色一样。请务必重新编译顶点着色器!

就像 fragColor 一样,layout(location = x) 注解为我们可以稍后引用它们的输入分配索引。重要的是要知道,某些类型(如 dvec3 64 位向量)使用多个。这意味着它之后的索引必须至少高 2

layout(location = 0) in dvec3 inPosition;
layout(location = 2) in vec3 inColor;

您可以在 OpenGL wiki 中找到有关 layout 限定符的更多信息。

顶点数据

我们将顶点数据从着色器代码移动到程序代码中的数组中。首先包含 GLM 库,该库为我们提供了线性代数相关的类型,如向量和矩阵。我们将使用这些类型来指定位置和颜色向量。

#include <glm/glm.hpp>

创建一个名为 Vertex 的新结构,其中包含我们将在顶点着色器内部使用的两个属性

struct Vertex {
    glm::vec2 pos;
    glm::vec3 color;
};

GLM 方便地为我们提供了与着色器语言中使用的向量类型完全匹配的 C++ 类型。

const std::vector<Vertex> vertices = {
    {{0.0f, -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}}
};

现在使用 Vertex 结构来指定顶点数据数组。我们使用的位置和颜色值与之前完全相同,但现在它们组合成一个顶点数组。这被称为交错顶点属性。

绑定描述

下一步是告诉 Vulkan 一旦将此数据格式上传到 GPU 内存后,如何将其传递给顶点着色器。需要两种类型的结构来传达此信息。

第一个结构是 VkVertexInputBindingDescription,我们将向 Vertex 结构添加一个成员函数,以使用正确的数据填充它。

struct Vertex {
    glm::vec2 pos;
    glm::vec3 color;

    static VkVertexInputBindingDescription getBindingDescription() {
        VkVertexInputBindingDescription bindingDescription{};

        return bindingDescription;
    }
};

顶点绑定描述了在整个顶点中从内存加载数据的速率。它指定了数据条目之间的字节数,以及在每个顶点之后还是在每个实例之后移动到下一个数据条目。

VkVertexInputBindingDescription bindingDescription{};
bindingDescription.binding = 0;
bindingDescription.stride = sizeof(Vertex);
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

我们每个顶点的数据都打包在一个数组中,因此我们只有一个绑定。binding 参数指定绑定数组中绑定的索引。stride 参数指定从一个条目到下一个条目的字节数,inputRate 参数可以具有以下值之一

  • VK_VERTEX_INPUT_RATE_VERTEX:在每个顶点之后移动到下一个数据条目
  • VK_VERTEX_INPUT_RATE_INSTANCE:在每个实例之后移动到下一个数据条目

我们不会使用实例化渲染,因此我们将坚持使用每个顶点的数据。

属性描述

描述如何处理顶点输入的第二个结构是 VkVertexInputAttributeDescription。我们将向 Vertex 添加另一个辅助函数来填充这些结构。

#include <array>

...

static std::array<VkVertexInputAttributeDescription, 2> getAttributeDescriptions() {
    std::array<VkVertexInputAttributeDescription, 2> attributeDescriptions{};

    return attributeDescriptions;
}

如函数原型所示,将有两个这样的结构。属性描述结构描述了如何从来自绑定描述的顶点数据块中提取顶点属性。我们有两个属性,位置和颜色,因此我们需要两个属性描述结构。

attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);

binding 参数告诉 Vulkan 每个顶点数据来自哪个绑定。location 参数引用顶点着色器中输入的 location 指令。顶点着色器中位置 0 的输入是位置,它有两个 32 位浮点分量。

format 参数描述了属性的数据类型。有点令人困惑的是,格式使用与颜色格式相同的枚举来指定。以下着色器类型和格式通常一起使用

  • floatVK_FORMAT_R32_SFLOAT
  • vec2VK_FORMAT_R32G32_SFLOAT
  • vec3VK_FORMAT_R32G32B32_SFLOAT
  • vec4VK_FORMAT_R32G32B32A32_SFLOAT

如您所见,您应该使用颜色通道数量与着色器数据类型中的分量数量匹配的格式。允许使用比着色器中的分量数量更多的通道,但它们将被静默丢弃。如果通道数量少于分量数量,则 BGA 分量将使用 (0, 0, 1) 的默认值。颜色类型(SFLOATUINTSINT)和位宽也应与着色器输入的类型匹配。请参见以下示例

  • ivec2VK_FORMAT_R32G32_SINT,一个 2 分量 32 位有符号整数向量
  • uvec4VK_FORMAT_R32G32B32A32_UINT,一个 4 分量 32 位无符号整数向量
  • doubleVK_FORMAT_R64_SFLOAT,一个双精度(64 位)浮点数

format 参数隐式定义了属性数据的大小(以字节为单位),而 offset 参数指定了从每个顶点数据的开始处读取的字节数。绑定一次加载一个 Vertex,位置属性 (pos) 从此结构的开头偏移 0 个字节。这是使用 offsetof 宏自动计算的。

attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1;
attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[1].offset = offsetof(Vertex, color);

颜色属性的描述方式大致相同。

管线顶点输入

我们现在需要设置图形管线以接受此格式的顶点数据,方法是在 createGraphicsPipeline 中引用这些结构。找到 vertexInputInfo 结构并修改它以引用这两个描述

auto bindingDescription = Vertex::getBindingDescription();
auto attributeDescriptions = Vertex::getAttributeDescriptions();

vertexInputInfo.vertexBindingDescriptionCount = 1;
vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescriptions.size());
vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;
vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data();

现在,管线已准备好接受 vertices 容器格式的顶点数据,并将其传递给我们的顶点着色器。如果您现在启用验证层运行程序,您会看到它抱怨没有顶点缓冲绑定到绑定。下一步是创建顶点缓冲并将顶点数据移动到其中,以便 GPU 能够访问它。

C++ 代码 / 顶点着色器 / 片段着色器