狸村 Mystic Web · 狸村
目录

引言

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

顶点着色器

首先,修改顶点着色器,使其不再在着色器代码本身中包含顶点数据。 顶点着色器通过按正确顺序在结构体中声明的顶点缓冲来获取输入。

struct VSInput {
    float2 inPosition;
    float3 inColor;
};

struct VSOutput
{
    float4 pos : SV_Position;
    float3 color;
};

[shader("vertex")]
VSOutput vertMain(VSInput input) {
    VSOutput output;
    output.pos = float4(input.inPosition, 0.0, 1.0);
    output.color = input.inColor;
    return output;
}

[shader("fragment")]
float4 fragMain(VSOutput vertIn) : SV_TARGET {
    return float4(vertIn.color, 1.0);
}

inPositioninColor 变量是 顶点属性(vertex attributes)。 它们是按顶点在顶点缓冲中指定的属性,就像我们之前使用两个数组手动为每个顶点指定位置和颜色一样。

顶点数据

我们将顶点数据从着色器代码移动到程序代码中的一个数组。 首先包含 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 结构体来指定一个顶点数据数组。 我们使用了与之前完全相同的位置和颜色值,但现在它们合并到了一个顶点数组中。这称为顶点属性的 交叉存取(interleaving)

绑定描述

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

第一种结构体是 vk::VertexInputBindingDescription,我们将在 Vertex 结构体中添加一个成员函数,用正确的数据填充它。

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

    static vk::VertexInputBindingDescription getBindingDescription()
    {
      return {.binding = 0, .stride = sizeof(Vertex), .inputRate = vk::VertexInputRate::eVertex};
    }
};

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

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

  • vk::VertexInputRate::eVertex :在每个顶点之后移动到下一个数据条目

  • vk::VertexInputRate::eInstance:在每个实例之后移动到下一个数据条目

我们不打算使用实例化渲染,因此我们将坚持使用逐顶点数据。

属性描述

描述如何处理顶点输入的第二种结构体是 vk::VertexInputAttributeDescription。 我们将在 Vertex 中添加另一个辅助函数来填充这些结构体。

#include <array>

...

    static std::array<vk::VertexInputAttributeDescription, 2> getAttributeDescriptions()
    {
      return {{{.location = 0, .binding = 0, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(Vertex, pos)},
               {.location = 1, .binding = 0, .format = vk::Format::eR32G32B32Sfloat, .offset = offsetof(Vertex, color)}}};
    }
}

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

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

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

  • floatvk::Format::eR32Sfloat

  • float2vk::Format::eR32G32Sfloat

  • float3vk::Format::eR32G32B32Sfloat

  • float4vk::Format::eR32G32B32A32Sfloat

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

  • int2vk::Format::eR32G32Sint,一个包含 2 个分量的 32 位有符号整数向量

  • uint4vk::Format::eR32G32B32A32Uint,一个包含 4 个分量的 32 位无符号整数向量

  • doublevk::Format::eR64Sfloat,一个双精度(64 位)浮点数

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

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

管线顶点输入

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

auto                                     bindingDescription    = Vertex::getBindingDescription();
auto                                     attributeDescriptions = Vertex::getAttributeDescriptions();
vk::PipelineVertexInputStateCreateInfo   vertexInputInfo{.vertexBindingDescriptionCount   = 1,
                                                         .pVertexBindingDescriptions      = &bindingDescription,
                                                         .vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescriptions.size()),
                                                         .pVertexAttributeDescriptions    = attributeDescriptions.data()};

管线现在已准备好接受 vertices 容器格式的顶点数据,并将其传递到我们的顶点着色器。 下一步是创建一个顶点缓冲并将顶点数据移入其中,以便 GPU 能够访问它。

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。