IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel Vulkan complet


précédentsommairesuivant

X. Génération de mipmaps

X-A. Introduction

Notre programme peut maintenant charger et afficher des modèles 3D. Dans ce chapitre, nous allons ajouter une fonctionnalité : la génération des mipmaps. Les mipmaps sont largement utilisées dans les jeux et les logiciels de rendu et Vulkan nous donne un contrôle complet sur leur création.

Les mipmaps sont des réductions précalculées de vos images. Chaque nouvelle image a une taille correspondant à la moitié de la taille de l’image précédente. Les mipmaps sont utilisées comme technique de niveau de détail (Level of Detail (LOD)) : les objets au loin utiliseront des textures avec des images mipmaps de plus petite taille. En utilisant des images réduites, la vitesse de rendu sera améliorée et les artefacts tels que le moiré seront réduits. Voici un exemple de mipmaps :

Un exemple de mipmaps

X-B. Création des images

Avec Vulkan, chaque niveau de mipmap est stocké dans les différents niveaux de mipmap de la ressource de type VkImage. Le niveau 0 correspond à l’image originale et les niveaux suivants sont appelés chaînes de mip (mip chain).

Le nombre de niveaux de mipmap doit être indiqué lors de la création de la ressource de type VkImage. Jusqu’à présent, nous définissions cette valeur à 1. Nous devons maintenant calculer le nombre de niveaux à partir des dimensions de l’image. D’abord, ajoutez une variable membre pour stocker ce nombre :

 
Sélectionnez
...
uint32_t mipLevels;
VkImage textureImage;
...

La valeur de la variable mipLevels ne peut être déterminée qu’une fois la texture chargée par la fonction createTextureImage() :

 
Sélectionnez
int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
...
mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;

Ce code permet de calculer le nombre de niveaux dans notre chaîne de mip. La fonction max() récupère la plus grande dimension de l’image. La fonction log2() détermine combien de fois cette dimension peut être divisée par 2. La fonction floor() gère le cas où la dimension utilisée n'est pas un multiple de deux. On ajoute 1 afin de prendre en compte le niveau de l’image d’origine.

Pour utiliser cette valeur, nous devons modifier les fonctions createImage(), createImageView() et transitionImageLayout() afin d’y spécifier le nombre de niveaux de mip. Ajoutez un paramètre mipLevels à ces fonctions :

 
Sélectionnez
void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {
    ...
    imageInfo.mipLevels = mipLevels;
    ...
}
 
Sélectionnez
VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels) {
    ...
    viewInfo.subresourceRange.levelCount = mipLevels;
    ...
 
Sélectionnez
void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels) {
    ...
    barrier.subresourceRange.levelCount = mipLevels;
    ...

Nous devons aussi mettre à jour ces appels pour utiliser les bonnes valeurs :

 
Sélectionnez
createImage(swapChainExtent.width, swapChainExtent.height, 1, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
...
createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
 
Sélectionnez
swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);
...
depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1);
...
textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels);
 
Sélectionnez
transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 1);
...
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);

X-C. Génération des mipmaps

Notre texture a maintenant plusieurs niveaux de mipmaps, mais le tampon intermédiaire ne peut être utilisé que pour remplir le niveau 0. Les données des autres niveaux ne sont toujours pas définies. Pour les remplir, nous devons générer les données des mipmaps à partir du seul niveau que nous avons. Nous allons utiliser la commande vkCmdBlitImage. Cette commande effectue une copie tout en permettant le redimensionnement et des opérations de filtrages. Nous pouvons l’appeler plusieurs fois pour copier les données de chaque niveau dans notre texture.

La commande vkCmdBlitImage est considérée comme une opération de transfert. Nous devons donc indiquer à Vulkan que nous souhaitons utiliser la texture aussi bien comme source que comme destination d’un transfert. Ajoutez le bit VK_IMAGE_USAGE_TRANSFER_SRC_BIT aux utilisations souhaitées lors de la création de l'image.

 
Sélectionnez
...
createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
...

Comme pour les autres opérations sur les images, la commande vkCmdBlitImage dépend de l’agencement de l'image sur laquelle elle opère. Nous pourrions effectuer une transition de l’intégralité de l'image vers VK_IMAGE_LAYOUT_GENERAL, mais cela serait certainement lent. Pour obtenir des performances optimales, l’image source doit être dans l’agencement VK_IMAGE_LAYOUT_TRANSFER_SCR_OPTIMAL et l’image de destination doit être avec l’agencement VK_IMAGE_LAYOUT_DST_OPTIMAL. Vulkan nous permet de faire une transition de chaque niveau de mip indépendamment. Chaque copie ne travaillera que sur deux niveaux de mip à la fois, donc nous pouvons effectuer une transition entre chaque opération pour toujours avoir l’agencement optimal.

La fonction transitionImageLayout() n'effectue des transitions d’agencement que sur l’intégralité de l’image. Nous devons donc ajouter des barrières de pipeline. Supprimez la transition existante vers VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL de la fonction createTextureImage() :

 
Sélectionnez
...
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
    copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
...

Une fois cela fait, tous les niveaux de la texture sont dans l’agencement VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL. Chaque niveau sera ensuite transitionné vers VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL après que la commande de copie a fini de lire dans l’image.

Nous allons maintenant écrire la fonction pour générer les mipmaps :

 
Sélectionnez
void generateMipmaps(VkImage image, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.image = image;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 1;
    barrier.subresourceRange.levelCount = 1;

    endSingleTimeCommands(commandBuffer);
}

Nous allons réaliser plusieurs transitions et donc réutiliser la structure VkImageMemoryBarrier. Les champs remplis ci-dessus seront valides pour toutes les barrières. Les champs subresourceRange.mipLevel, oldLayout, newLayout, srcAccessMask et dstAccessMask seront modifiés à chaque transition

 
Sélectionnez
int32_t mipWidth = texWidth;
int32_t mipHeight = texHeight;

for (uint32_t i = 1; i < mipLevels; i++) {

}

Cette boucle enregistre les commandes VkCmdBlitImage. Notez que la boucle commence à 1, et pas à 0.

 
Sélectionnez
barrier.subresourceRange.baseMipLevel = i - 1;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;

vkCmdPipelineBarrier(commandBuffer,
    VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
    0, nullptr,
    0, nullptr,
    1, &barrier);

Tout d'abord, nous effectuons une transition du niveau i - 1 vers l’agencement VK_IMAGE_LAYOUT_TRANSFER_SCR_OPTIMAL. Cette transition attendra le remplissage du niveau i - 1, que ce soit par la commande précédente ou par la fonction vkCmdCopyBufferToImage(). La commande de copie actuelle attendra l’exécution de la transition.

 
Sélectionnez
VkImageBlit blit{};
blit.srcOffsets[0] = { 0, 0, 0 };
blit.srcOffsets[1] = { mipWidth, mipHeight, 1 };
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.srcSubresource.mipLevel = i - 1;
blit.srcSubresource.baseArrayLayer = 0;
blit.srcSubresource.layerCount = 1;
blit.dstOffsets[0] = { 0, 0, 0 };
blit.dstOffsets[1] = { mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1 };
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.dstSubresource.mipLevel = i;
blit.dstSubresource.baseArrayLayer = 0;
blit.dstSubresource.layerCount = 1;

Nous devons maintenant indiquer les régions concernées par la commande de copie. Le niveau de mip source est i - 1 et le niveau de destination est i. Les deux éléments du tableau scrOffsets déterminent la région 3D à partir de laquelle les données seront copiées. Le tableau dstOffsets, définit la région recevant les données de la copie. Les dimensions X et Y de dstOffsets[1] sont divisées par 2, car chaque niveau de mip est deux fois moins grand que le niveau précédemment. La dimension Z de srcOffsets[1] et dstOffsets[1] doit être 1, car notre image 2D a une profondeur de 1.

 
Sélectionnez
vkCmdBlitImage(commandBuffer,
    image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
    image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    1, &blit,
    VK_FILTER_LINEAR);

Nous enregistrons la commande de copie. Notez que la variable textureImage est utilisée aussi bien comme source que comme destination. C’est que nous voulons copier différents niveaux de mip de la même image. Le niveau de mip source vient de passer à l’agencement VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL et le niveau destination est encore avec l’agencement VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL obtenu grâce à la fonction createTextureImage().

Lorsque vous utilisez une queue dédiée au transfert (comme suggéré dans ce chapitreTampon intermédiaire), la commande vkCmdBlitImage doit être enregistrée dans une queue graphique.

Le dernier paramètre permet de fournir un filtre de type VkFilter à utiliser lors de la copie. Nous avons accès aux mêmes options de filtrage qu’avec la structure VkSampler. Nous utilisons la valeur VK_FILTER_LINEAR, pour activer l’interpolation linéaire.

 
Sélectionnez
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

vkCmdPipelineBarrier(commandBuffer,
    VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
    0, nullptr,
    0, nullptr,
    1, &barrier);

Ce code attend que le niveau i – 1 passe à l’agencement VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. Cette transition attend que la copie finisse. Toutes les opérations d’échantillonnage vont attendre que la transition se termine.

 
Sélectionnez
    ...
    if (mipWidth > 1) mipWidth /= 2;
    if (mipHeight > 1) mipHeight /= 2;
}

À la fin de la boucle, nous divisons les dimensions du niveau de mip actuel par 2. Nous vérifions les dimensions avant d’effectuer la division pour s’assurer que la dimension ne soit jamais 0. Ainsi, nous gérons le cas où les images ne sont pas carrées pour lesquelles une des deux dimensions serait 1 avant l’autre. Lorsque cela se produit, la dimension est toujours 1 pour les niveaux restants.

 
Sélectionnez
    barrier.subresourceRange.baseMipLevel = mipLevels - 1;
    barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

    vkCmdPipelineBarrier(commandBuffer,
        VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
        0, nullptr,
        0, nullptr,
        1, &barrier);

    endSingleTimeCommands(commandBuffer);
}

Avant de terminer avec le tampon de commandes, nous devons insérer une dernière barrière. Cette barrière permet d’effectuer la transition du dernier niveau de mip de l’agencement VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL vers VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. Ce cas n’est pas pris en compte par la boucle, car nous ne copions jamais le dernier niveau de mip.

Appelez la fonction generateMipmaps() dans la fonction createTextureImage() :

 
Sélectionnez
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
    copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
//transition vers l’agencement VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL lors de la génération des mipmaps
...
generateMipmaps(textureImage, texWidth, texHeight, mipLevels);

Les mipmaps de notre image sont maintenant complètement remplies.

X-D. Support du filtrage linéaire

Il est très pratique d’utiliser une fonction telle que vkCmdBlitImage() pour générer tous les niveaux de mip, mais il n’est malheureusement pas garanti qu’elle soit supportée sur toutes les plateformes. La fonction repose sur le support du filtrage linéaire pour le format d’image utilisé. Le support de cette fonctionnalité peut être vérifié avec la fonction vkGetPhysicalDeviceFormatProperties(). Nous effectuons cette vérification dans la fonction generateMipmaps().

Ajoutez d'abord un paramètre qui indique le format de l'image :

 
Sélectionnez
void createTextureImage() {
    ...

    generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_SRGB, texWidth, texHeight, mipLevels);
}

void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {

    ...
}

Dans la fonction generateMipmaps(), utilisez la fonction vkGetPhysicalDeviceFormatProperties() pour récupérer les propriétés du format de la texture :

 
Sélectionnez
void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {

    // Vérifie si le format supporte le filtrage linéaire
    VkFormatProperties formatProperties;
    vkGetPhysicalDeviceFormatProperties(physicalDevice, imageFormat, &formatProperties);

    ...

La structure VkFormatProperties possède les trois champs : linearTilingFeatures, optimalTilingFeature et bufferFeaetures. Chacun décrit l'utilisation possible du format suivant la façon dont l’image est utilisée. Nous créons nos textures avec le format de tiling optimal, donc nous devons utiliser le champ optimalTilingFeatures. Le support du filtrage linéaire est indiqué par le bit VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT.

 
Sélectionnez
if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) {
    throw std::runtime_error("Le format de la texture ne supporte pas le filtrage linéaire !");
}

Si le format de texture ne supporte pas le filtrage linéaire, vous avez deux possibilités :

  • implémenter une fonction cherchant un format avec le support du filtrage linéaire dans les opérations de copie ;
  • implémenter la génération des mipmaps de manière logicielle, notamment grâce à une bibliothèque telle que stb_image_resize. Chaque image peut être chargée dans une image, de la même façon que vous avez chargé l’image originale.

Il est à noter que ce n’est pas une bonne pratique de générer les mipmaps au cours de l’exécution. Habituellement, elles sont prégénérées et stockées dans le fichier de la texture avec le niveau de base afin d’améliorer les vitesses de chargement. L’implémentation logicielle du redimensionnement et le chargement des différents niveaux à partir d’un fichier sont laissés comme exercice pour le lecteur.

X-E. Échantillonneur

Une ressource de type VkImage contient les données de mipmap. Une ressource de type VkSampler contrôle comment les données sont lues lors du rendu. Vulkan nous fournit les propriétés suivantes : minLod, maxLod, mipLodBias et mipmapMode (où « Lod » signifie « Level of Detail » (niveau de détail)). Pendant l’échantillonnage d'une texture, l’échantillonneur sélectionne le niveau de mip suivant le pseudo-code suivant :

 
Sélectionnez
lod = getLodLevelFromScreenSize(); // plus petit lorsque l’objet est proche, peut être négatif
lod = clamp(lod + mipLodBias, minLod, maxLod);

level = clamp(floor(lod), 0, texture.mipLevels - 1);  // limité par rapport au nombre de niveaux de mip dans la texture

if (mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST) {
    color = sample(level);
} else {
    color = blend(sample(level), sample(level + 1));
}

Si la valeur de samplerInfo.mipmapMode est VK_SAMPLER_MIPMAP_MODE_NEAREST, la variable lod sélectionne le niveau de mip à partir duquel échantillonner. Si le mode est VK_SAMPLER_MIPMAP_MODE_LINEAR, lod sélectionne deux niveaux de mip pour effectuer l’échantillonnage. Le résultat correspond au mélange linéaire des couleurs des deux niveaux.

L'opération d'échantillonnage est aussi affectée par lod :

 
Sélectionnez
if (lod <= 0) {
    color = readTexture(uv, magFilter);
} else {
    color = readTexture(uv, minFilter);
}

Si l'objet est proche de la caméra, magFilter est utilisé comme filtre. Si l'objet est distant, minFilter sera utilisé. Normalement, lod est un nombre positif et devient nul lorsque l’objet est proche de la caméra. mipLodBias permet de forcer Vulkan à utiliser un lod particulier et donc, un niveau de mip plus petit que ce qu’il aurait utilisé habituellement.

Pour voir les bénéfices du mipmapping, nous devons définir de nouvelles valeurs pour notre échantillonneur textureSampler. Nous avons déjà fourni les valeurs de minFilter et magFilter pour utiliser le filtrage linéaire VK_FILTER_LINEAR. Il nous reste à choisir les valeurs de minLod, maxLod, mipLodBias et mipmapMode.

 
Sélectionnez
void createTextureSampler() {
    ...
    samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    samplerInfo.minLod = 0.0f; // Optionnel
    samplerInfo.maxLod = static_cast<float>(mipLevels);
    samplerInfo.mipLodBias = 0.0f; // Optionnel
    ...
}

Pour utiliser la totalité des niveaux de mipmaps, nous mettons minLod à 0.0f et maxLod au nombre de niveaux de mip. Nous n'avons aucune raison de changer la valeur de lod avec mipLodBias, alors nous pouvons le mettre à 0.0f.

Lancez votre programme et vous devriez voir ceci :

[ALT-PASTOUCHE]

Notre scène est si simple qu'il n'y a pas de différence majeure. Il y a quelques différences subtiles si vous regardez avec attention :

[ALT-PASTOUCHE]

La différence la plus évidente est le texte sur les papiers. Avec les mipmaps, les écritures sont plus lisses. Sans les mipmaps, les écritures ont des bordures dures et des trous, à cause du moiré.

Vous pouvez jouer avec les paramètres de l’échantillonneur et voir leur effet sur le mipmapping. Par exemple, en changeant la valeur de minLod, vous pouvez forcer l’échantillonneur à ne pas utiliser les niveaux de mip les plus bas :

 
Sélectionnez
samplerInfo.minLod = static_cast<float>(mipLevels / 2);

Ce qui donne :

[ALT-PASTOUCHE]

C’est ce que donneront les niveaux de mip les plus hauts qui seront utilisés lorsque les objets sont loin de la caméra.

Code C++ / Vertex shader / Fragment shader


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Contributeurs Github et Developpez.com et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.