顶点输入描述
介绍
在接下来的几章中,我们将用内存中的顶点缓冲区替换顶点着色器中的硬编码顶点数据。我们将从最简单的方法开始,创建一个 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);
}
inPosition 和 inColor 变量是 顶点属性。它们是在顶点缓冲区中按每个顶点指定的属性,就像我们使用两个数组手动为每个顶点指定位置和颜色一样。
顶点数据
我们将顶点数据从着色器代码移动到程序代码中的数组。首先包含 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 vk::VertexInputBindingDescription getBindingDescription() {
return { 0, sizeof(Vertex), vk::VertexInputRate::eVertex };
}
};
顶点绑定描述了在顶点之间从内存加载数据的速率。它指定了数据条目之间的字节数,以及是在每个顶点之后还是在每个实例之后移动到下一个数据条目。
我们所有的每顶点数据都打包在一个数组中,因此我们只会有一个绑定。binding 参数指定绑定在绑定数组中的索引。stride 参数指定从一个条目到下一个条目的字节数,inputRate 参数可以有以下值之一:
-
VK_VERTEX_INPUT_RATE_VERTEX:每个顶点后移动到下一个数据条目 -
VK_VERTEX_INPUT_RATE_INSTANCE:每个实例后移动到下一个数据条目
我们不会使用实例化渲染,所以我们将坚持使用每顶点数据。
属性描述
描述如何处理顶点输入的第二个结构是 VkVertexInputAttributeDescription。我们将向 Vertex 添加另一个辅助函数来填充这些结构体。
#include <array>
...
static std::array<vk::VertexInputAttributeDescription, 2> getAttributeDescriptions() {
return {
vk::VertexInputAttributeDescription( 0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, pos) ),
vk::VertexInputAttributeDescription( 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) )
};
}
正如函数原型所示,将会有两个这样的结构体。属性描述结构体描述了如何从源自绑定描述的顶点数据块中提取顶点属性。我们有两个属性,位置和颜色,所以我们需要两个属性描述结构体。
binding 参数告诉 Vulkan 每顶点数据来自哪个绑定。location 参数引用顶点着色器中输入的 location 指令。位置为 0 的顶点着色器中的输入是位置,它有两个 32 位浮点组件。
format 参数描述属性的数据类型。有点令人困惑的是,格式是使用与颜色格式相同的枚举指定的。以下着色器类型和格式通常一起使用:
-
float:VK_FORMAT_R32_SFLOAT -
float2:VK_FORMAT_R32G32_SFLOAT -
float3:VK_FORMAT_R32G32B32_SFLOAT -
float4:VK_FORMAT_R32G32B32A32_SFLOAT
如你所见,你应该使用颜色通道数量与着色器数据类型中的组件数量匹配的格式。允许使用比着色器中组件数量更多的通道,但它们将被静默丢弃。如果通道数量低于组件数量,则 BGA 组件将使用默认值 (0, 0, 1)。颜色类型(SFLOAT、UINT、SINT)和位宽也应与着色器输入的类型匹配。请参见以下示例:
-
int2:VK_FORMAT_R32G32_SINT,一个 2 组件的 32 位有符号整数向量 -
uint4:VK_FORMAT_R32G32B32A32_UINT,一个 4 组件的 32 位无符号整数向量 -
double:VK_FORMAT_R64_SFLOAT,一个双精度(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 = attributeDescriptions.size(), .pVertexAttributeDescriptions = attributeDescriptions.data() };
管线现在已准备好接受 vertices 容器格式的顶点数据并将其传递给我们的顶点着色器。如果你现在启用验证层运行程序,你会看到它抱怨没有顶点缓冲区绑定到绑定。下一步 是创建一个顶点缓冲区并将顶点数据移动到其中,以便 GPU 能够访问它。
C++ 代码 / slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器