目录
Introduction
在接下来的几章中,我们将用内存中的顶点缓冲区替换顶点着色器中的硬编码顶点数据。我们将从创建CPU可见缓冲区并使用memcpy将顶点数据直接复制到其中的最简单方法开始,然后我们将了解如何使用暂存缓冲区将顶点数据复制到高性能内存中。
Vertex shader
首先更改顶点着色器,使其不再包含着色器代码本身中的顶点数据。顶点着色器使用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变量是顶点属性。它们是在顶点缓冲区中指定per-vertex的特性,就像我们使用两个数组手动指定每个顶点的位置和颜色一样。确保重新编译顶点着色器!
就像fragColor一样,布局(location=x)注释为输入分配索引,我们稍后可以使用这些索引来引用它们。重要的是要知道,某些类型(如dvec3 64位矢量)使用多个时隙。这意味着后面的索引必须至少高出2:
layout(location = 0) in dvec3 inPosition;
layout(location = 2) in vec3 inColor;
您可以在OpenGLwiki中找到有关布局限定符的更多信息。
Vertex data
我们正在将顶点数据从着色器代码移动到程序代码中的数组中。首先包括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}}
};
现在使用“顶点”结构指定顶点数据数组。我们使用的位置和颜色值与之前完全相同,但现在它们被合并为一个顶点数组。这称为交错顶点属性。
Binding descriptions
下一步是告诉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;
我们所有的每个顶点数据都打包在一个数组中,所以我们只需要一个绑定。绑定参数指定绑定数组中绑定的索引。步幅参数指定从一个条目到下一个条目的字节数,inputRate参数可以具有以下值之一:
- VK_VERTEX_INPUT_RATE_VERTEX:移动到每个顶点后的下一个数据项
- VK_VERTEX_INPUT_RATE_INSTANCE:移动到每个实例后的下一个数据条目
我们不会使用实例化渲染,所以我们将坚持逐顶点数据。
Attribute descriptions
描述如何处理顶点输入的第二个结构是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 per-vertex数据来自哪个绑定。位置参数引用顶点着色器中输入的位置指令。位置为0的顶点着色器中的输入是位置,它有两个32位浮点组件。
format参数描述属性的数据类型。有点令人困惑的是,这些格式是使用与颜色格式相同的枚举指定的。以下着色器类型和格式通常一起使用:
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位有符号整数的双分量向量
- 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);
颜色属性的描述方式大致相同。
Pipeline vertex input
我们现在需要通过引用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();
管道现在可以接受顶点容器格式的顶点数据,并将其传递给顶点着色器。如果您现在在启用验证层的情况下运行程序,您将看到它抱怨没有顶点缓冲区绑定到绑定。下一步是创建顶点缓冲区并将顶点数据移动到其中,以便GPU能够访问它。