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

Tutoriel Vulkan complet


précédentsommairesuivant

XI. Multiéchantillonnage

XI-A. Introduction

Notre programme peut maintenant générer plusieurs niveaux de détails pour les textures et ainsi supprimer quelques artefacts lors du rendu d’objets lointains. L’image est plus nette, mais si vous faites attention, vous remarquerez des motifs en dents de scie sur les bordures des objets dessinés. C’est d’autant plus vrai dans nos premiers programmes où nous affichions un carré :

Affichage des coordonnées de texture

Cet effet indésirable s'appelle crénelage (aliasing). Il est la conséquence du nombre limité de pixels lors du rendu. Comme il n’existe aucun écran ayant une résolution illimitée, il sera toujours visible. Il existe plusieurs méthodes pour corriger cela et ce chapitre se concentrera sur l’une des plus populaires : l’anticrénelage par multiéchantillonnage (multisample anti-aliasing (MSAA)).

Dans un rendu classique, la couleur d'un pixel est déterminée à partir d'un unique échantillon, en général le centre du pixel. Si une ligne passe à travers certains pixels sans couvrir le point utilisé par l’échantillon, alors le pixel restera blanc, provoquant cet effet d’escalier.

[ALT-PASTOUCHE]

Le MSAA consiste à utiliser plusieurs points pour échantillonner un pixel et ainsi déterminer sa couleur. Comme on peut s'y attendre, plus on utilise de points, meilleur est le résultat, mais cela consomme aussi plus de ressources.

[ALT-PASTOUCHE]

Pour notre implémentation, nous allons utiliser le maximum de points possible. Selon votre application, cela peut ne pas être la meilleure approche et il peut être judicieux d’utiliser moins d’échantillons afin d’obtenir de meilleures performances.

XI-B. Récupération du nombre maximal d’échantillons

Commençons par déterminer le nombre maximal d’échantillons supporté par la carte graphique. Les cartes graphiques modernes supportent au moins huit échantillons, mais ce nombre n’est pas une norme. Nous allons stocker ce nombre dans une variable membre :

 
Sélectionnez
...
VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT;
...

Par défaut nous n'utilisons qu'un échantillon par pixel, ce qui correspond à ne pas utiliser de multiéchantillonnage. Dans un tel cas, l’image finale ne sera pas impactée. Le nombre d’échantillons maximum supporté par la carte peut être obtenu à partir de la structure de type VkPhysicalDeviceProperties associée au périphérique choisi. Nous utilisons aussi un tampon de profondeur. Nous devons donc prendre en compte le nombre d’échantillons pour la couleur et pour la profondeur. Le plus haut nombre d’échantillons supporté par les deux (&) sera le maximum supporté. Ajoutez une fonction pour récupérer cette information :

 
Sélectionnez
VkSampleCountFlagBits getMaxUsableSampleCount() {
    VkPhysicalDeviceProperties physicalDeviceProperties;
    vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);

    VkSampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts;
    if (counts & VK_SAMPLE_COUNT_64_BIT) { return VK_SAMPLE_COUNT_64_BIT; }
    if (counts & VK_SAMPLE_COUNT_32_BIT) { return VK_SAMPLE_COUNT_32_BIT; }
    if (counts & VK_SAMPLE_COUNT_16_BIT) { return VK_SAMPLE_COUNT_16_BIT; }
    if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; }
    if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; }
    if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; }

    return VK_SAMPLE_COUNT_1_BIT;
}

Nous allons utiliser cette fonction pour définir la variable msaaSamples pendant le processus de la sélection du GPU. Nous devons modifier la fonction pickPhysicalDevice() :

 
Sélectionnez
void pickPhysicalDevice() {
    ...
    for (const auto& device : devices) {
        if (isDeviceSuitable(device)) {
            physicalDevice = device;
            msaaSamples = getMaxUsableSampleCount();
            break;
        }
    }
    ...
}

XI-C. Configurer une cible de rendu

Avec le MSAA, chaque pixel est échantillonné à partir d’un tampon hors écran qui est ensuite affiché à l’écran. Ce nouveau tampon est légèrement différent des images sur lesquelles nous avons dessiné jusqu’à présent : il a la possibilité de stocker plus d’un échantillon par pixel. Une fois le tampon multiéchantillonné créé, il doit être utilisé pour déterminer le tampon d’image classique (qui stocke uniquement un échantillon par pixel). C’est pourquoi nous devons créer une cible de rendu supplémentaire et modifier le processus de rendu. Nous n'avons besoin que d’une cible de rendu, car seule une opération de rendu s’effectue à la fois, tout comme pour le tampon de profondeur. Ajoutez les variables membres suivantes :

 
Sélectionnez
...
VkImage colorImage;
VkDeviceMemory colorImageMemory;
VkImageView colorImageView;
...

Cette nouvelle image doit pouvoir stocker le nombre d’échantillons voulus par pixel. Nous devons donc passer ce nombre à la structure VkImageCreateInfo lors de sa création. Modifiez la fonction createImage() pour ajouter le paramètre numSamples :

 
Sélectionnez
void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {
    ...
    imageInfo.samples = numSamples;
    ...

Pour le moment, mettez à jour tous les appels à cette fonction avec VK_SAMPLE_COUNT_1_BIT. Nous utiliserons la valeur adéquate par la suite.

 
Sélectionnez
createImage(swapChainExtent.width, swapChainExtent.height, 1, VK_SAMPLE_COUNT_1_BIT, 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_SAMPLE_COUNT_1_BIT, 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);

Nous allons maintenant créer un tampon de couleurs multiéchantillonné. Ajoutez une fonction nommée createColorResources et notez que nous passons la variable msaaSamples à la fonction createImage(). Nous n'utilisons qu'un seul niveau de mipmap, ce qui est forcé par la spécification de Vulkan pour les images multiéchantillonnées. De plus, ce tampon de couleurs n’a pas besoin de mipmap, car elle n’est pas utilisée comme texture.

 
Sélectionnez
void createColorResources() {
    VkFormat colorFormat = swapChainImageFormat;

    createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, colorImage, colorImageMemory);
    colorImageView = createImageView(colorImage, colorFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);
}

Pour une question de cohérence, nous appelons cette fonction juste avant la fonction createDepthResource() :

 
Sélectionnez
void initVulkan() {
    ...
    createColorResources();
    createDepthResources();
    ...
}

Nous avons maintenant un tampon de couleurs multiéchantillonné. Occupons-nous de la profondeur. Modifiez la fonction createDepthResources() et mettez à jour le nombre d’échantillons utilisés :

 
Sélectionnez
void createDepthResources() {
    ...
    createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
    ...
}

Comme nous avons créé de nouvelles ressources, nous devons les libérer :

 
Sélectionnez
void cleanupSwapChain() {
    vkDestroyImageView(device, colorImageView, nullptr);
    vkDestroyImage(device, colorImage, nullptr);
    vkFreeMemory(device, colorImageMemory, nullptr);
    ...
}

Mettez également à jour la fonction recreateSwapChain() afin de reconstruire une nouvelle image pour les couleurs avec la bonne résolution lorsque la fenêtre est redimensionnée :

 
Sélectionnez
void recreateSwapChain() {
    ...
    createGraphicsPipeline();
    createColorResources();
    createDepthResources();
    ...
}

Nous avons fini le paramétrage initial du MSAA. Nous devons maintenant utiliser ces nouvelles ressources dans le pipeline, le tampon d’images et la passe de rendu !

XI-D. Ajout de nouvelles attaches

Commençons par la passe de rendu. Modifiez la fonction createRenderPass() et mettez à jour les attaches de couleur et de profondeur dans la structure de création de la passe de rendu :

 
Sélectionnez
void createRenderPass() {
    ...
    colorAttachment.samples = msaaSamples;
    colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    ...
    depthAttachment.samples = msaaSamples;
    ...

Remarquez que nous avons changé l'agencement final (finalLayout) de VK_IMAGE_LAYOUT_PRESENT_SRC_KHR pour VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL. En effet, les images multiéchantillonnées ne peuvent être directement présentées. Nous devons les convertir en une image plus classique. Cette contrainte ne s’applique pas au tampon de profondeur, car nous ne l’affichons dans aucun cas. Par conséquent, nous devons ajouter une unique attache pour les couleurs que nous appelons attache de conversion :

 
Sélectionnez
    ...
    VkAttachmentDescription colorAttachmentResolve{};
    colorAttachmentResolve.format = swapChainImageFormat;
    colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT;
    colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    colorAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    colorAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    ...

La passe de rendu doit maintenant être configurée pour convertir l’image multiéchantillonnée en attache classique. Créez une nouvelle référence d’attache pointant sur le tampon de couleurs qui nous servira de destination :

 
Sélectionnez
    ...
    VkAttachmentReference colorAttachmentResolveRef{};
    colorAttachmentResolveRef.attachment = 2;
    colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    ...

Modifiez la variable membre de la structure de sous-passe pResolveAttachments pour indiquer la nouvelle référence d’attache. C’est suffisant pour que la passe de rendu effectue une conversion, ce qui nous permet d’afficher l’image à l’écran :

 
Sélectionnez
    ...
    subpass.pResolveAttachments = &colorAttachmentResolveRef;
    ...

Fournissez ensuite la nouvelle attache de couleurs à la structure de création de la passe de rendu.

 
Sélectionnez
    ...
    std::array<VkAttachmentDescription, 3> attachments = {colorAttachment, depthAttachment, colorAttachmentResolve};
    ...

Modifiez ensuite la fonction createFramebuffer() et ajoutez la nouvelle vue d’image à la liste :

 
Sélectionnez
void createFrameBuffers() {
        ...
        std::array<VkImageView, 3> attachments = {
            colorImageView,
            depthImageView,
            swapChainImageViews[i]
        };
        ...
}

Enfin, il ne reste plus qu’à indiquer (dans la fonction createGraphicsPipeline()) au nouveau pipeline d’utiliser plusieurs échantillons :

 
Sélectionnez
void createGraphicsPipeline() {
    ...
    multisampling.rasterizationSamples = msaaSamples;
    ...
}

Lancez votre programme et vous devriez voir ceci :

[ALT-PASTOUCHE]

Comme pour le mipmapping, la différence n'est pas immédiatement visible. En regardant de plus près, vous remarquerez que les bordures sont moins crénelées et que l’image est plus lisse qu’avant.

[ALT-PASTOUCHE]

La différence est encore plus visible en zoomant sur un bord :

[ALT-PASTOUCHE]

XI-E. Amélioration de la qualité

Notre implémentation du MSAA est limitée et cela peut impacter la qualité de l’image affichée dans des scènes plus détaillées. Par exemple, nous ne corrigeons pas les problèmes potentiels liés au crénelage causé par les shaders : c’est-à-dire que le MSAA n’adoucit que les bordures de la géométrie, mais pas son remplissage. Cela est marquant, lorsque vous affichez un polygone lisse alors que la texture le remplissant sera crénelée, car elle contient des hauts contrastes. Une façon de résoudre ce problème est d’activer l’échantillonnage des fragments (sample shading), qui améliore encore la qualité de l'image au prix de performances encore réduites.

 
Sélectionnez
void createLogicalDevice() {
    ...
    deviceFeatures.sampleRateShading = VK_TRUE; // active la fonctionnalité d'échantillonnage des fragments pour ce périphérique
    ...
}

void createGraphicsPipeline() {
    ...
    multisampling.sampleShadingEnable = VK_TRUE; // active l'échantillonnage des fragments dans le pipeline
    multisampling.minSampleShading = .2f; // fraction minimale pour l'échantillonnage des fragments ; plus la valeur est proche de 1, plus le résultat est doux
    ...
}

Nous n’activons pas l’échantillonnage des fragments dans notre application, mais dans certains cas son activation permet une nette amélioration de la qualité du rendu :

[ALT-PASTOUCHE]

XI-F. Conclusion

Il nous a fallu beaucoup de travail pour en arriver là, mais vous avez maintenant une bonne connaissance des bases de Vulkan. Ces connaissances vous permettent maintenant d'explorer d'autres fonctionnalités, comme :

  • les constantes poussées (push constants) ;
  • le rendu instancié ;
  • les variables uniformes dynamiques ;
  • les descripteurs d'images et d’échantillonneurs séparés ;
  • la mise en cache de pipeline ;
  • la génération des tampons de commandes depuis plusieurs threads :
  • les sous-passes multiples ;
  • les shaders de calcul (compute shaders).

Le programme actuel peut être grandement étendu, par exemple en ajoutant l'éclairage Blinn-Phong, des effets de post-traitement et l’application des ombres. Vous devriez pouvoir apprendre ces techniques depuis des tutoriels conçus pour d'autres bibliothèques, car ces algorithmes fonctionnent de la même façon, quelle que soit la bibliothèque.

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.