8.3 索引化绘制
简单的吧连续的顶点给入管线并不总是你所想要的。在大多数几何网格中,很多顶点不止被使用一次。一个完全连接的网格也许会让多个三角形共用一个顶点。甚至一个立方体中三个相邻的三角形共享一个顶点。在你的顶点缓冲区中为每一个顶点指定三次是非常浪费的。除了这个,一些Vulkan实现也许很聪明,如果看到相同的一个顶点出现多次,在后面的过程中会跳过对相同的顶点的处理,并使用之前顶点着色器调用的结果。
要想这样做,Vulkan允许索引化绘制。vkCmdDraw()使用索引化和vkCmdDrawIndexed()是等价的,vkCmdDrawIndexed()的原型是:
voidvkCmdDrawIndexed (
VkCommandBuffer commandBuffer,
uint32_t indexCount,
uint32_t instanceCount,
uint32_t firstIndex,
int32_t vertexOffset,
uint32_t firstInstance);
vkCmdDrawIndexed()的第一个参数是将被执行的命令所在的命令缓冲区的handle。然而,相比于简单从零开始,vkCmdDrawIndexed()从一个索引缓冲区中取出索引。索引缓冲区是一个通常的缓冲区对象,通过vkCmdBindIndexBuffer()绑定到命令缓冲区,命令原型为:
voidvkCmdBindIndexBuffer (
VkCommandBuffer commandBuffer,
VkBuffer buffer,
VkDeviceSize offset,
VkIndexType indexType);
索引缓冲区所绑定的命令缓冲区通过commandBuffer指定,包含索引数据的缓冲区通过buffer指定。从offset开始的缓冲区的一部分可以绑定到命令缓冲区。绑定的部分总是拓展到缓冲区对象的结尾。在索引缓冲区上没有绑定检查,Vulkan将读取你告诉它的索引数量。然而,读取数据时将不会突破缓冲区对象的尾部。
缓冲区内的索引的类型通过indexType指定。这是VkIndexType枚举的一个成员,所有成员如下:
•VK_INDEX_TYPE_UINT16: Unsigned 16-bit integers
•VK_INDEX_TYPE_UINT32: Unsigned 32-bit integers
当你调用vkCmdDrawIndexed()时,Vulkan将从当前绑定的索引缓冲区中offset位置开始读取数据
offset+ firstIndex * sizeof(index)
其中,sizeof(index)对于VK_INDEX_TYPE_UINT16来说是2,对VK_INDEX_TYPE_UINT32来说是4。代码将会从索引缓冲区中读取indexCount个连续的整形,然后把它们加上vertexOffset。加法操作总是以32位执行的,不管当前绑定的索引缓冲区的索引类型。如果加法操作在无符号32整形上溢出了,结果是未知的,所以要避免这种状况。
Figure 8.1语义化的展示了数据流
Figure8.1: Index Data Flow
注意,当索引类型是VK_INDEX_TYPE_UINT32时,索引数值的最大范围也许并不受支持。可查看调用vkGetPhysicalDeviceProperties(). 获取的VkPhysicalDeviceLimits结构的maxDrawIndexedIndexValue域来做检查。这个数值被保证至少是224-1,也可以高达232-1。
为了演示如何高效率的使用索引数据,Listing 8.2展示了用索引和非索引方式绘制一个立方体所需数据的不同。
Listing8.2: Indexed Cube Data
//Raw, non-indexed data
static const float vertex_positions[] =
{
-0.25f, 0.25f, -0.25f,
-0.25f, -0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, 0.25f, -0.25f,
-0.25f, 0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, -0.25f, 0.25f,
0.25f, 0.25f, -0.25f,
0.25f, -0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
0.25f, 0.25f, -0.25f,
0.25f, -0.25f, 0.25f,
-0.25f, -0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
-0.25f, -0.25f, 0.25f,
-0.25f, 0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
-0.25f, -0.25f, 0.25f,
-0.25f, -0.25f, -0.25f,
-0.25f, 0.25f, 0.25f,
-0.25f, -0.25f, -0.25f,
-0.25f, 0.25f, -0.25f,
-0.25f, 0.25f, 0.25f,
-0.25f, -0.25f, 0.25f,
0.25f, -0.25f, 0.25f,
0.25f, -0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
-0.25f, -0.25f, -0.25f,
-0.25f, -0.25f, 0.25f,
-0.25f, 0.25f, -0.25f,
0.25f, 0.25f, -0.25f,
0.25f, 0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
-0.25f, 0.25f, 0.25f,
-0.25f, 0.25f, -0.25f
};
static const uint32_t vertex_count = sizeof(vertex_positions) /
(3 * sizeof(float));
// Indexed vertex data
static const float indexed_vertex_positions[] =
{
-0.25f, -0.25f, -0.25f,
-0.25f, 0.25f, -0.25f,
0.25f, -0.25f, -0.25f,
0.25f, 0.25f, -0.25f,
0.25f, -0.25f, 0.25f,
0.25f, 0.25f, 0.25f,
-0.25f, -0.25f, 0.25f,
-0.25f, 0.25f, 0.25f,
};
// Index buffer
static const uint16_t vertex_indices[] =
{
0, 1, 2,
2, 1, 3,
2, 3, 4,
4, 3, 5,
4, 5, 6,
6, 5, 7,
6, 7, 0,
0, 7, 1,
6, 0, 2,
2, 4, 6,
7, 5, 3,
7, 3, 1
};
static const uint32_tindex_count = vkcore::utils::arraysize(vertex_indices);
如你在Listing 8.2中所见,绘制一个立方体的所需数据很小。只有存储八个不同顶点的数据,和36个用来引用它们的索引数据。随着场景的复杂几何尺寸在增长,存储量也可能相当大。在这个简单的例子中,非索引化的顶点数据有36个顶点,每一个都包含三个4字节元素,总共是432字节的数据。同时,索引化数据有12个顶点,每一个都包含三个4字节元素,加上36个索引,每一个消耗2字节。索引化的立方体产生了168字节的数据。
除了使用索引化数据来节省空间,很多Vulkan实现包含了顶点缓存,可以重复利用顶点着色器之前计算的结果。如果顶点数据是非索引化的,那么管线必须假设他们都是不同的。然而,当顶点被索引化,两个具有相同索引的顶点是相同的。在任何一个闭合的网格中,同一个顶点将会出现多次,因为有多个图元会共享它。重用可以节省不少的计算量。
8.3.1 只索引绘制
在你的顶点着色器中可以直接访问到当前顶点的原生索引。这个索引在SPIR-V中是以VertexIndex修饰的变量,通过GLSL中内置的变量gl_VertexIndex生成的。这包含了索引缓冲区()的内容加上传递给vkCmdDrawIndexed()的vertexOffset的值。
你可以使用
8.3.2 重置索引