Index buffer
Introduction
Les modèles 3D que vous serez susceptibles d'utiliser dans des applications réelles partagerons le plus souvent des vertices communs à plusieurs triangles. Cela est d'ailleurs le cas avec un simple rectangle :
Un rectangle est composé de triangles, ce qui signifie que nous aurions besoin d'un vertex buffer avec 6 vertices. Mais nous dupliquerions alors des vertices, aboutissant à un gachis de mémoire. Dans des modèles plus complexes, les vertices sont en moyenne en contact avec 3 triangles, ce qui serait encore pire. La solution consiste à utiliser un index buffer.
Un index buffer est essentiellement un tableau de références vers le vertex buffer. Il vous permet de réordonner ou de dupliquer les données de ce buffer. L'image ci-dessus démontre l'utilité de cette méthode.
Création d'un index buffer
Dans ce chapitre, nous allons ajouter les données nécessaires à l'affichage d'un rectangle. Nous allons ainsi rajouter une coordonnée dans le vertex buffer et créer un index buffer. Voici les données des sommets au complet :
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}}
};
Le coin en haut à gauche est rouge, celui en haut à droite est vert, celui en bas à droite est bleu et celui en bas à
gauche est blanc. Les couleurs seront dégradées par l'interpolation du rasterizer. Nous allons maintenant créer le
tableau indices
pour représenter l'index buffer. Son contenu correspond à ce qui est présenté dans l'illustration.
const std::vector<uint16_t> indices = {
0, 1, 2, 2, 3, 0
};
Il est possible d'utiliser uint16_t
ou uint32_t
pour les valeurs de l'index buffer, en fonction du nombre d'éléments
dans vertices
. Nous pouvons nous contenter de uint16_t
car nous n'utilisons pas plus de 65535 sommets différents.
Comme les données des sommets, nous devons placer les indices dans un VkBuffer
pour que le GPU puisse y avoir accès.
Créez deux membres donnée pour référencer les ressources du futur index buffer :
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;
La fonction createIndexBuffer
est quasiment identique à 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);
}
Il n'y a que deux différences : bufferSize
correspond à la taille du tableau multiplié par sizeof(uint16_t)
, et
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
est remplacé par VK_BUFFER_USAGE_INDEX_BUFFER_BIT
. À part ça tout est
identique : nous créons un buffer intermédiaire puis le copions dans le buffer final local au GPU.
L'index buffer doit être libéré à la fin du programme depuis cleanup
.
void cleanup() {
cleanupSwapChain();
vkDestroyBuffer(device, indexBuffer, nullptr);
vkFreeMemory(device, indexBufferMemory, nullptr);
vkDestroyBuffer(device, vertexBuffer, nullptr);
vkFreeMemory(device, vertexBufferMemory, nullptr);
...
}
Utilisation d'un index buffer
Pour utiliser l'index buffer lors des opérations de rendu nous devons modifier un petit peu createCommandBuffers
. Tout
d'abord il nous faut lier l'index buffer. La différence est qu'il n'est pas possible d'avoir plusieurs index buffers. De
plus il n'est pas possible de subdiviser les sommets en leurs coordonnées, ce qui implique que la modification d'une
seule coordonnée nécessite de créer un autre sommet le vertex buffer.
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);
Un index buffer est lié par la fonction vkCmdBindIndexBuffer
. Elle prend en paramètres le buffer, le décalage dans ce
buffer et le type de donnée. Pour nous ce dernier sera VK_INDEX_TYPE_UINT16
.
Simplement lier le vertex buffer ne change en fait rien. Il nous faut aussi mettre à jour les commandes d'affichage
pour indiquer à Vulkan comment utiliser le buffer. Supprimez l'appel à vkCmdDraw
, et remplacez-le par
vkCmdDrawIndexed
:
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
Le deuxième paramètre indique le nombre d'indices. Le troisième est le nombre d'instances à invoquer (ici 1
car nous
n'utilisons par cette technique). Le paramètre suivant est un décalage dans l'index buffer, sachant qu'ici il ne
fonctionne pas en octets mais en indices. L'avant-dernier paramètre permet de fournir une valeur qui sera ajoutée à tous
les indices juste avant de les faire correspondre aux vertices. Enfin, le dernier paramètre est un décalage pour le
rendu instancié.
Lancez le programme et vous devriez avoir ceci :
Vous savez maintenant économiser la mémoire en réutilisant les vertices à l'aide d'un index buffer. Cela deviendra crucial pour les chapitres suivants dans lesquels vous allez apprendre à charger des modèles complexes.
Nous avons déjà évoqué le fait que le plus de buffers possibles devraient être stockés dans un seul emplacement
mémoire. Il faudrait dans l'idéal allez encore plus loin :
les développeurs des drivers recommandent également que vous
placiez plusieurs buffers dans un seul et même VkBuffer
, et que vous utilisiez des décalages pour les différencier
dans les fonctions comme vkCmdBindVertexBuffers
. Cela simplifie la mise des données dans des caches car elles sont
regroupées en un bloc. Il devient même possible d'utiliser la même mémoire pour plusieurs ressources si elles ne sont
pas utilisées en même temps et si elles sont proprement mises à jour. Cette pratique s'appelle d'ailleurs aliasing, et
certaines fonctions Vulkan possèdent un paramètre qui permet au développeur d'indiquer s'il veut utiliser la technique.