顶点输入描述

简介

在接下来的几章中,我们将用内存中的顶点缓冲区替换顶点着色器中硬编码的顶点数据。我们将从创建 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 中找到有关布局限定符的更多信息。

顶点数据

我们将顶点数据从着色器代码移动到程序代码中的数组。首先包含 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能够访问它。