Vulkan Tutorial 22 Index buffer
操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Visual Studio 2017
Introduction
在實際產品的運行環境中3D模型的數據往往共享多個三角形之間的頂點數據。即使繪制一些簡單的圖形也是如此,比如矩形:
繪制矩形需要兩個三角形,通常意味著我們需要6個頂點數據。問題是其中的兩個頂點會重復,導致數據會有50%的冗余。如果更復雜的模型,該問題會更加嚴重,平均每三個三角形就會發生重復頂點使用的情況。解決問題的方法是使用index buffer,即索引緩沖區。
索引緩沖區純粹是一個指向頂點緩沖區的指針數組。它允許我們重排列頂點數據,並復用多個已經存在的頂點數據。上圖顯示了有一個包含四個不重復頂點數據的頂點緩沖區,通過索引緩沖區將如何顯示矩形的情況。前三個索引定義右上角的三角形,最後三個索引定義了左下角的三角形。
Index buffer creation
在本章節中,為了繪制如上圖所示的矩形,我們需要修改頂點數據並添加索引數據。修改後的頂點數據分別代表矩形四個角:
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。
如頂點數據,為了使GPU可以訪問到它們,需要將索引數據上傳到緩沖區VkBuffer。定義兩個類成員保存索引緩沖區的資源:
VkBuffer vertexBuffer; VkDeviceMemory vertexBufferMemory; VkBuffer indexBuffer; VkDeviceMemory indexBufferMemory;
createIndexBuffer函數與之前的createVertexBuffer函數非常類似:
void initVulkan() { ... createVertexBuffer(); createIndexBuffer(); ... } void createIndexBuffer() { VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size(); VkBuffer stagingBuffer; VkDeviceMemory stagingBufferMemory; createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); void* data; vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); memcpy(data, indices.data(), (size_t) bufferSize); vkUnmapMemory(device, stagingBufferMemory); createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory); copyBuffer(stagingBuffer, indexBuffer, bufferSize); vkDestroyBuffer(device, stagingBuffer, nullptr); vkFreeMemory(device, stagingBufferMemory, nullptr); }
僅有的兩個差異。bufferSize現在等於索引數量乘以索引類型的大小,該類型或者是uint16_t,或者是uint32_t。indexBuffer的用法需改用VK_BUFFER_USAGE_INDEX_BUFFER_BIT代替VK_BUFFER_USAGE_VERTEX_BUFFER_BIT。其他的過程是一致的。我們創建暫存緩沖區拷貝頂點數據的內容,並最終拷貝到設備本地索引緩沖區。
索引緩沖區在程序退出的時候需要清理,與頂點緩沖區類似:
void cleanup() { cleanupSwapChain(); vkDestroyBuffer(device, indexBuffer, nullptr); vkFreeMemory(device, indexBufferMemory, nullptr); vkDestroyBuffer(device, vertexBuffer, nullptr); vkFreeMemory(device, vertexBufferMemory, nullptr); ... }
Using an index buffer
使用索引緩沖區繪制需要修改createCommandBuffers函數兩個地方。首先需要綁定索引緩沖區,就像之前的頂點緩沖區一樣。區別是現在僅使用一個索引緩沖區。不幸的是,不可能對每個頂點屬性使用不同的索引,所以即使只有一個屬性不同,我們仍然必須完全復制頂點數據。
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);
索引緩沖區使用vkCmdBindIndexBuffer綁定,它持有索引緩沖區作為參數,還需要偏移量和索引數據的類型。如前所述,可能的類型是VK_INDEX_TYPE_UINT16和VK_INDEX_TYPE_UINT32。
僅僅綁定索引緩沖區不會發生任何改變,我們還需要告知Vulkan在使用索引緩沖區後,對應的繪制命令的變化。移除vkCmdDraw函數,並用vkCmdDrawIndexed替換:
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
該函數的調用與vkCmdDraw非常類似。前兩個參數指定索引的數量和幾何instance數量。我們沒有使用instancing,所以指定1。索引數表示被傳遞到頂點緩沖區中的頂點數量。下一個參數指定索引緩沖區的偏移量,使用1將會導致圖形卡在第二個索引處開始讀取。倒數第二個參數指定索引緩沖區中添加的索引的偏移。最後一個參數指定instancing偏移量,我們沒有使用該特性。
現在運行程序如下所示:
現在我們已經通過索引緩沖區復用了頂點數據。在未來的加載復雜的3D模型數據中,該技術會非常的重要。
在前一章提到,為了更優的分配使用資源,推薦在單個內存中分配多個資源,如緩沖區,但是實際上,我們應該更進一步細化。來自Nvidia的驅動程序開發者建議將多個緩沖區(頂點緩沖區、索引緩沖區)存儲到單個VkBuffer中。並在諸如vkCmdBindVertexBuffers之類的命令中使用偏移量。優點在於,在這種情況下,數據會更加充分的利用緩存,因為它們排列在一塊區域。甚至在同一個渲染操作中可以復用來自相同內存塊的多個資源塊,只要刷新數據即可。該技巧稱為稱為aliasing,一些Vulkan函數有明確的標誌指定這樣做的意圖。
項目代碼 GitHub 地址。
Vulkan Tutorial 22 Index buffer