一:概述
在接下来的几篇文章中,我们将把顶点着色器中的硬编码顶点数据替换为内存中的顶点缓冲区。我们将从创建一个CPU可见的缓冲区并使用memcpy直接将顶点数据复制到其中的最简单方法开始,然后我们将看到如何使用临时缓冲区将顶点数据复制到高性能内存中。
更多内容请参考 一文带你了解GPU编程从入门到精通-****博客
二:顶点着色器
首先,将顶点着色器更改为不再在着色器代码中包含顶点数据。顶点着色器使用 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;
-
}
inPosition 和 inColor 变量是顶点属性。它们是每个顶点在顶点缓冲区中指定的属性,就像我们使用两个数组手动为每个顶点指定位置和颜色一样。确保重新编译顶点着色器!
就像fragColor一样,layout(location = x)注释为输入分配索引,我们可以在后续使用它们。重要的是要知道某些类型,如dvec3 64位向量,占用多个索引位置。这意味着后面的索引必须至少偏移2:
-
layout(location = 0) in dvec3 inPosition;
-
layout(location = 2) in vec3 inColor;
您可以在 OpenGL wiki 上找到有关布局限定符的更多信息。
三:顶点数据
我们将顶点数据从着色器代码移动到我们程序代码的一个数组中。首先包含 GLM 库,它为我们提供了与线性代数相关的类型,如向量和矩阵。我们将使用这些类型来指定位置和颜色向量。
#include <glm/>
创建一个新的结构体 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}}
-
};
现在使用顶点结构来指定一个顶点数据数组。我们使用的位置信息和颜色值与之前完全相同,但现在它们组合成一个顶点数组。这被称为交错顶点属性。
四:绑定描述
下一步是告诉Vulkan如何将此数据格式传递给顶点着色器,一旦它被上传到GPU内存中。需要两种结构来传达这些信息。
第一个结构是 VkVertexInputBindingDescription,我们将向 Vertex 结构添加一个成员函数,以用正确的数据填充它。
-
struct Vertex {
-
glm::vec2 pos;
-
glm::vec3 color;
-
-
static VkVertexInputBindingDescription getBindingDescription() {
-
VkVertexInputBindingDescription bindingDescription{};
-
-
return bindingDescription;
-
}
-
};
顶点绑定描述了在顶点之间以何种速率从内存加载数据。它指定了数据项之间的字节数,以及在每个顶点之后还是在每个实例之后移动到下一个数据项。
-
VkVertexInputBindingDescription bindingDescription{};
-
= 0;
-
= sizeof(Vertex);
-
= VK_VERTEX_INPUT_RATE_VERTEX;
我们所有的每个顶点数据都打包在一个数组中,因此我们只会有一个绑定。绑定参数指定数组中绑定的索引。步幅参数指定从一个顶点数据到下一个顶点数据的字节数,输入速率参数可以具有以下值之一:
VK_VERTEX_INPUT_RATE_VERTEX: 每个顶点后移动到下一个顶点数据
VK_VERTEX_INPUT_RATE_INSTANCE: 每个实例后移动到下一个顶点数据
在这里我们不打算使用实例化渲染,因此我们使用每个顶点(per-vertex)的数据。
五:属性描述
第二个结构是描述如何处理顶点输入的 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);
绑定参数告诉Vulkan每个顶点数据来自哪个绑定。位置参数引用了顶点着色器中输入的位置信息。在顶点着色器中,位置为0的输入是位置,它有两个32位浮点成员(vec2)。
格式参数描述了属性的数据类型。有点令人困惑的是,格式使用与颜色格式相同的枚举进行指定。以下着色器类型和格式通常一起使用:
-
float: VK_FORMAT_R32_SFLOAT
-
vec2: VK_FORMAT_R32G32_SFLOAT
-
vec3: VK_FORMAT_R32G32B32_SFLOAT
-
vec4: VK_FORMAT_R32G32B32A32_SFLOAT
如您所见,您应该使用颜色通道数量与着色器数据类型中组件数量相匹配的格式。允许使用比着色器分量数量更多的通道,但这些通道将被静默丢弃。如果通道数量低于组件数量,则BGA组件将使用默认值(0, 0, 1)。颜色类型(SFLOAT、UINT、SINT)和位宽也应与着色器输入的类型相匹配。请参见以下示例:
ivec2: VK_FORMAT_R32G32_SINT,32位带符号整数的2分量向量
uvec4: VK_FORMAT_R32G32B32A32_UINT,32位无符号整数的4分量向量
double: VK_FORMAT_R64_SFLOAT,双精度(64位)浮点数
format参数隐式定义属性数据的字节大小,offset参数指定从每顶点数据开始读取的字节数。绑定一次加载一个顶点,并且位置属性(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();
-
-
= 1;
-
= static_cast<uint32_t>(attributeDescriptions.size());
-
= &bindingDescription;
-
= attributeDescriptions.data();
管线现在可以接受顶点格式的顶点数据,并将其传递给顶点着色器。如果您现在在启用了验证层的情况下运行程序,您将看到它报告没有绑定到绑定的顶点缓冲区。下一篇文章是创建一个顶点缓冲区,并将顶点数据移动到其中,以便GPU能够访问它。
完整代码:/code/18_vertex_input.cpp