在实际应用程序中,你将要渲染的 3D 网格通常会在多个三角形之间共享顶点。 即使像绘制矩形这样简单的事情也会发生这种情况:
绘制一个矩形需要两个三角形,这意味着我们需要一个包含六个顶点的顶点缓冲。 问题在于两个顶点的数据需要重复,导致 50% 的冗余。 对于更复杂的网格,这种情况会更严重,顶点平均会被三个三角形重用。 解决这个问题的方法是使用 索引缓冲(index buffer)。
索引缓冲本质上是一个指向顶点缓冲的指针数组。 它允许你重新排列顶点数据,并为多个顶点重用现有数据。 上图展示了如果我们有一个包含四个唯一顶点的顶点缓冲,矩形的索引缓冲应该是什么样子。 前三个索引定义了右上三角形,后三个索引定义了左下三角形的顶点。
在本章中,我们将修改顶点数据并添加索引数据,以绘制一个像图中那样的矩形。 修改顶点数据以表示四个角:
const std::vector<Vertex> vertices = {
{{-0.5f, -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}},
{{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}}
};
左上角是红色,右上角是绿色,右下角是蓝色,左下角是白色。
我们将添加一个新数组 indices 来表示索引缓冲的内容。
它应与图中的索引匹配,以绘制右上三角形和左下三角形。
const std::vector<uint16_t> indices = {
0, 1, 2, 2, 3, 0
};
根据 vertices 中的条目数量,你可以使用 uint16_t 或 uint32_t 作为索引缓冲的类型。
我们现在可以坚持使用 uint16_t,因为我们使用的唯一顶点数少于 65535。
就像顶点数据一样,索引需要上传到 vk::raii::Buffer 中,以便 GPU 能够访问它们。
定义两个新的类成员来保存索引缓冲的资源:
vk::raii::Buffer vertexBuffer = nullptr;
vk::raii::DeviceMemory vertexBufferMemory = nullptr;
vk::raii::Buffer indexBuffer = nullptr;
vk::raii::DeviceMemory indexBufferMemory = nullptr;
我们现在要添加的 createIndexBuffer 函数与 createVertexBuffer 几乎相同:
void initVulkan()
{
...
createVertexBuffer();
createIndexBuffer();
...
}
void createIndexBuffer()
{
vk::DeviceSize bufferSize = sizeof(indices[0]) * indices.size();
auto [stagingBuffer, stagingBufferMemory] =
createBuffer(bufferSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
void *data = stagingBufferMemory.mapMemory(0, bufferSize);
memcpy(data, indices.data(), (size_t) bufferSize);
stagingBufferMemory.unmapMemory();
std::tie(indexBuffer, indexBufferMemory) =
createBuffer(bufferSize, vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal);
copyBuffer(stagingBuffer, indexBuffer, bufferSize);
}
只有两个明显的区别。
bufferSize 现在等于索引数乘以索引类型的大小,即 uint16_t 或 uint32_t。
indexBuffer 的用途应为 vk::BufferUsageFlagBits::eIndexBuffer 而不是 vk::BufferUsageFlagBits::eVertexBuffer,这是合理的。
除此之外,过程完全相同。
我们创建一个暂存缓冲来复制 indices 的内容,然后将其复制到最终的设备本地索引缓冲。
使用索引缓冲进行绘制需要对 recordCommandBuffer 进行两处修改。
首先,我们需要绑定索引缓冲,就像我们对顶点缓冲所做的那样。
区别在于你只能有一个索引缓冲。
遗憾的是,不能为每个顶点属性使用不同的索引,因此即使只有一个属性不同,我们仍然必须完全复制顶点数据。
commandBuffers[frameIndex].bindVertexBuffers(0, *vertexBuffer, {0});
commandBuffers[frameIndex].bindIndexBuffer(*indexBuffer, 0, vk::IndexType::eUint16);
索引缓冲使用 vk::raii::CommandBuffer::bindIndexBuffer 绑定,该函数接受索引缓冲、其中的字节偏移量以及索引数据的类型作为参数。
如前所述,可能的类型是 vk::IndexType::eUint16 和 vk::IndexType::eUint32。
仅仅绑定索引缓冲还不会改变任何东西,我们还需要更改绘制命令,告诉 Vulkan 使用索引缓冲。
删除 vk::raii::CommandBuffer::draw 行,并将其替换为 vk::raii::CommandBuffer::drawIndexed:
commandBuffer.drawIndexed(static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
对此函数的调用与 vk::raii::CommandBuffer::draw 非常相似。
前两个参数指定索引数和实例数。
我们不使用实例化,因此只需指定 1 个实例。
索引数表示将传递给顶点着色器的顶点数量。
下一个参数指定索引缓冲中的偏移量,使用值 1 将导致图形卡从第二个索引开始读取。
倒数第二个参数指定在索引到顶点缓冲之前要添加到顶点索引的偏移量。
最后一个参数指定实例化的偏移量,我们不会使用它。
现在运行你的程序,你应该会看到以下内容:
你现在知道如何通过使用索引缓冲重用顶点来节省内存。 这在未来的章节中将变得尤为重要,届时我们将加载复杂的 3D 模型。
前一章已经提到,你应该从单个内存分配中分配多个资源,如缓冲,但实际上你应该更进一步。
驱动程序开发者建议你也将多个缓冲(如顶点缓冲和索引缓冲)存储到一个 vk::raii::Buffer 中,并在 vk::raii::CommandBuffer::bindVertexBuffers 等命令中使用偏移量。
这样做的优点是,在这种情况下,你的数据更具缓存友好性,因为它们更靠近。
甚至可以在同一渲染操作期间未使用的多个资源之间重用同一块内存,当然前提是它们的数据被刷新。这被称为 别名(aliasing),一些 Vulkan 函数有显式标志来指定你想这样做。
在下一章中,我们将学习如何将频繁变化的参数传递给 GPU。
C++ 代码 / slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。