Apprenez OpenGL. Leçon 5.8 - Bloom

OGL3

Bloom


En raison de la plage de luminosité limitée disponible pour les moniteurs conventionnels, la tâche d'afficher de manière convaincante des sources de lumière vive et des surfaces fortement éclairées est difficile par définition. L'une des méthodes courantes pour mettre en évidence les zones lumineuses sur le moniteur est une technique qui ajoute un halo de lueur autour des objets lumineux, donnant l'impression de «diffuser» la lumière à l'extérieur de la source lumineuse. En conséquence, l'observateur donne l'impression d'une luminosité élevée de ces zones éclairées ou sources de lumière.

L'effet décrit d'un halo et de la sortie de lumière au-delà de la source est obtenu par une technique de post-traitement appelée bloom . L'application de l'effet ajoute un halo de lueur caractéristique à toutes les zones lumineuses de la scène affichée, comme le montre l'exemple ci-dessous:



Bloom ajoute un indice visuel distinctif à l'image sur la luminosité significative des objets couverts par le halo de l'effet appliqué. Appliqué de manière sélective et dans une mesure précise (auquel de nombreux jeux, hélas, ne peuvent pas faire face), l'effet peut améliorer considérablement l'expressivité visuelle de l'éclairage utilisé dans la scène, ainsi que rajouter du drame dans certaines situations.

Cette technique fonctionne en conjonction avec le rendu HDR presque comme un ajout évident. Apparemment, à cause de cela, beaucoup de gens mélangent à tort ces deux termes à l'interchangeabilité totale. Cependant, ces techniques sont complètement indépendantes et sont utilisées à des fins différentes. Il est possible d'implémenter la floraison en utilisant le tampon d'image par défaut avec une profondeur de couleur de 8 bits, tout comme l'application du rendu HDR sans recourir à l'utilisation de la floraison. La seule chose est que le rendu HDR vous permet d'implémenter l'effet de manière plus efficace (nous verrons cela plus tard).

Pour mettre en œuvre la floraison, la scène illuminée est d'abord rendue de la manière habituelle. Ensuite, un tampon couleur HDR et un tampon couleur contenant uniquement des parties lumineuses de la scène sont extraits. Cette image extraite de la partie lumineuse est ensuite floue et superposée au-dessus de l'image HDR d'origine de la scène.

Pour le rendre plus clair, nous analyserons le processus étape par étape. Rendez une scène contenant 4 sources de lumière vive affichées sous forme de cubes colorés. Tous ont une valeur de luminosité comprise entre 1,5 et 15,0. Si le tampon de couleur est sorti sur le HDR, le résultat est le suivant:


De ce tampon couleur HDR, nous extrayons tous les fragments dont la luminosité dépasse une limite prédéterminée. Il en résulte une image ne contenant que des zones très éclairées:


De plus, cette image de zones lumineuses est floue. La gravité de l'effet est essentiellement déterminée par la force et le rayon du filtre de flou appliqué:


L'image floue des zones lumineuses qui en résulte est à la base de l'effet final des halos autour des objets lumineux. Cette texture est simplement mélangée avec l'image HDR originale de la scène. Les zones lumineuses étant floues, leur taille a augmenté, ce qui donne finalement un effet visuel de luminosité qui dépasse les limites des sources lumineuses:


Comme vous pouvez le voir, la floraison n'est pas la technique la plus sophistiquée, mais atteindre sa haute qualité visuelle et sa fiabilité n'est pas toujours facile. Pour l'essentiel, l'effet dépend de la qualité et du type de filtre de flou appliqué. Même de petits changements dans les paramètres du filtre peuvent changer considérablement la qualité finale de l'équipement.

Ainsi, les actions ci-dessus nous donnent un algorithme étape par étape de l'effet de post-traitement pour l'effet de floraison. L'image ci-dessous résume les actions requises:


Tout d'abord, nous avons besoin d'informations sur les parties lumineuses de la scène en fonction d'une valeur seuil donnée. Voilà ce que nous allons faire.

Extraire les faits saillants


Donc, pour commencer, nous devons obtenir deux images basées sur notre scène. Il serait naïf de rendre deux fois, mais utilisez la méthode MRT ( Multiple Render Targets ) plus avancée: nous spécifions plus d'une sortie dans le fragment shader final, et grâce à cela, deux images peuvent être extraites en une seule passe! Pour spécifier dans quel tampon de couleur le shader sera sorti, le spécificateur de disposition est utilisé:

layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; 

Bien sûr, la méthode ne fonctionnera que si nous avons préparé plusieurs tampons pour l'écriture. En d'autres termes, pour implémenter plusieurs sorties du fragment shader, le tampon de trame utilisé à ce moment doit contenir un nombre suffisant de tampons de couleur connectés. Si nous passons à la leçon sur le tampon de trame , il est rappelé que lors de la liaison de la texture en tant que tampon de couleur, nous pourrions indiquer le numéro de la pièce jointe . Jusqu'à présent, nous n'avions pas besoin d'utiliser une pièce jointe autre que GL_COLOR_ATTACHMENT0 , mais cette fois GL_COLOR_ATTACHMENT1 sera utile, car nous avons besoin de deux objectifs pour enregistrer à la fois:

 //       unsigned int hdrFBO; glGenFramebuffers(1, &hdrFBO); glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); unsigned int colorBuffers[2]; glGenTextures(2, colorBuffers); for (unsigned int i = 0; i < 2; i++) { glBindTexture(GL_TEXTURE_2D, colorBuffers[i]); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); //     glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0 ); } 

De plus, en appelant glDrawBuffers , vous devrez explicitement dire à OpenGL que nous allons sortir vers plusieurs tampons. Sinon, la bibliothèque ne sortira que la première pièce jointe, ignorant les opérations d'écriture sur les autres pièces jointes. En tant qu'argument de la fonction, un tableau d'identificateurs des pièces jointes utilisées à partir de l'énumération correspondante est transmis:

 unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; glDrawBuffers(2, attachments); 

Pour ce tampon de trame, tout shader de fragment qui spécifie un spécificateur d' emplacement pour ses sorties écrit dans le tampon de couleur correspondant. Et c'est une excellente nouvelle, car de cette façon, nous évitons la passe de rendu inutile pour extraire des données sur les parties lumineuses de la scène - vous pouvez tout faire à la fois dans un seul shader:

 #version 330 core layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; [...] void main() { [...] //      FragColor = vec4(lighting, 1.0); //         //   -    ,    float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722)); if(brightness > 1.0) BrightColor = vec4(FragColor.rgb, 1.0); else BrightColor = vec4(0.0, 0.0, 0.0, 1.0); } 

Dans ce fragment, la partie contenant le code typique de calcul de l'éclairage est omise. Son résultat est écrit dans la première sortie du shader - la variable FragColor . Ensuite, la couleur résultante du fragment est utilisée pour calculer la valeur de luminosité. Pour cela, une traduction pondérée en niveaux de gris est réalisée (par multiplication scalaire, on multiplie les composantes correspondantes des vecteurs et on les additionne, conduisant à une valeur unique). Ensuite, lorsque la luminosité d'un fragment d'un certain seuil est dépassée, nous enregistrons sa couleur dans la deuxième sortie du shader. Pour les cubes remplaçant des sources lumineuses, ce shader est également exécuté.

Après avoir compris l'algorithme, nous pouvons comprendre pourquoi cette technique fonctionne si bien avec le rendu HDR. Le rendu au format HDR permet aux composants de couleur d'aller au-delà de la limite supérieure de 1,0, ce qui vous permet d'ajuster de manière plus flexible le seuil de luminosité en dehors de l'intervalle standard [0., 1.], offrant la possibilité d'affiner les parties de la scène qui sont considérées comme lumineuses. Sans utiliser HDR, vous devrez vous contenter d'un seuil de luminosité dans l'intervalle [0., 1.], ce qui est tout à fait acceptable, mais conduit à une coupure de la luminosité plus «nette», ce qui rend souvent la floraison trop intrusive et flashy (imaginez-vous sur un champ de neige en haute montagne) .

Une fois le shader exécuté, deux tampons cibles contiendront une image normale de la scène, ainsi qu'une image contenant uniquement des zones lumineuses.


L'image des zones lumineuses doit maintenant être traitée en utilisant le flou. Vous pouvez accomplir cela avec un simple filtre rectangulaire ( boîte ), qui a été utilisé dans la section de post-traitement de la leçon de tampon de trame . Mais un bien meilleur résultat est obtenu par filtrage de Gauss .

Flou gaussien


La leçon de post-traitement nous a donné une idée du flou en utilisant une moyenne de couleurs simple de fragments d'images adjacents. Cette méthode de flou est simple, mais l'image résultante peut sembler plus attrayante. Le flou gaussien est basé sur la courbe de distribution en forme de cloche du même nom: les valeurs élevées de la fonction sont situées plus près du centre de la courbe et tombent des deux côtés. Mathématiquement, une courbe gaussienne peut être exprimée avec différents paramètres, mais la forme générale de la courbe reste la suivante:


Le flou avec des poids basés sur les valeurs de la courbe de Gauss est beaucoup mieux qu'un filtre rectangulaire: en raison du fait que la courbe a une plus grande surface au voisinage de son centre, ce qui correspond à des poids plus grands pour les fragments près du centre du noyau du filtre. En prenant, par exemple, le noyau 32x32, nous utiliserons les facteurs de pondération les plus petits, plus le fragment est éloigné du noyau central. C'est cette caractéristique de filtre qui donne un résultat de flou gaussien visuellement plus satisfaisant.

La mise en œuvre du filtre nécessitera un tableau bidimensionnel de coefficients de pondération, qui pourrait être rempli sur la base de l'expression bidimensionnelle décrivant la courbe gaussienne. Cependant, nous rencontrerons immédiatement un problème de performances: même un noyau de flou relativement petit dans un fragment 32x32 nécessitera 1024 échantillons de texture pour chaque fragment de l'image traitée!

Heureusement pour nous, l'expression de la courbe gaussienne a une caractéristique mathématique très pratique - la séparabilité, qui permettra de faire deux expressions unidimensionnelles à partir d'une expression bidimensionnelle qui décrivent les composantes horizontale et verticale. Cela permettra de brouiller à son tour dans deux approches: horizontalement, puis verticalement avec des ensembles de poids correspondant à chacune des directions. L'image résultante sera la même que lors du traitement d'un algorithme bidimensionnel, mais elle nécessitera beaucoup moins de puissance de traitement du processeur vidéo: au lieu de 1024 échantillons de la texture, nous n'avons besoin que de 32 + 32 = 64! C'est l'essence même de la filtration gaussienne en deux passes.


Pour nous, tout cela signifie une chose: le flou d'une image devra être fait deux fois, et ici l'utilisation d'objets de tampon de cadre sera utile. Nous appliquons la technique dite de ping-pong: il y a quelques objets de tampon d'image et le contenu du tampon de couleur d'un tampon d'image est rendu avec un certain traitement dans le tampon de couleur du tampon d'image actuel, puis le tampon d'image source et le récepteur d'image tampon sont échangés et ce processus est répété un certain nombre de fois. En fait, le tampon d'image actuel pour afficher l'image est simplement changé, et avec lui, la texture actuelle à partir de laquelle l'échantillonnage est effectué pour le rendu. L'approche vous permet de brouiller l'image d'origine en la plaçant dans le premier tampon de trame, puis de brouiller le contenu du premier tampon de trame, de la placer dans la seconde, puis de brouiller la seconde, de la placer dans la première, etc.

Avant de passer au code de réglage du tampon de trame, jetons un coup d'œil au code de shader de flou gaussien:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D image; uniform bool horizontal; uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); void main() { //     vec2 tex_offset = 1.0 / textureSize(image, 0); //    vec3 result = texture(image, TexCoords).rgb * weight[0]; if(horizontal) { for(int i = 1; i < 5; ++i) { result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i]; result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i]; } } else { for(int i = 1; i < 5; ++i) { result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i]; result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i]; } } FragColor = vec4(result, 1.0); } 

Comme vous pouvez le voir, nous utilisons un échantillon assez petit de coefficients de la courbe gaussienne, qui sont utilisés comme poids pour les échantillons horizontalement ou verticalement par rapport au fragment actuel. Le code a deux branches principales divisant l'algorithme en passe verticale et horizontale en fonction de la valeur de l'uniforme horizontal . Le décalage pour chaque échantillon est défini égal à la taille de texel, qui est définie comme l'inverse de la taille de texture (une valeur de type vec2 retournée par la fonction textureSize ()).

Créez deux tampons de cadre contenant un tampon de couleur en fonction de la texture:

 unsigned int pingpongFBO[2]; unsigned int pingpongBuffer[2]; glGenFramebuffers(2, pingpongFBO); glGenTextures(2, pingpongBuffer); for (unsigned int i = 0; i < 2; i++) { glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]); glBindTexture(GL_TEXTURE_2D, pingpongBuffer[i]); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongBuffer[i], 0 ); } 

Après avoir obtenu la texture HDR de la scène et extrait la texture des zones lumineuses, nous remplissons le tampon de couleur de l'un des deux tampons d'image préparés avec la texture de luminosité et commençons le processus de ping-pong dix fois (cinq fois verticalement, cinq horizontalement):

 bool horizontal = true, first_iteration = true; int amount = 10; shaderBlur.use(); for (unsigned int i = 0; i < amount; i++) { glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]); shaderBlur.setInt("horizontal", horizontal); glBindTexture( GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal] ); RenderQuad(); horizontal = !horizontal; if (first_iteration) first_iteration = false; } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

À chaque itération, nous sélectionnons et ancrons l'un des tampons d'image en fonction du flou horizontal ou vertical de cette itération, et le tampon de couleur de l'autre tampon d'image est ensuite utilisé comme texture d'entrée pour le shader de flou. À la première itération, nous devons utiliser explicitement une image contenant des zones lumineuses ( luminositéTexture ) - sinon les deux tampons de cadre de ping-pong resteront vides. Après dix passes, l'image d'origine prend la forme de cinq flous par un filtre gaussien complet. L'approche utilisée nous permet de changer facilement le degré de flou: plus il y a d'itérations de ping-pong, plus le flou est fort.

Dans notre cas, le résultat du flou ressemble à ceci:


Pour terminer l'effet, il ne reste plus qu'à combiner l'image floue avec l'image HDR d'origine de la scène.

Mélange de textures


Ayant à portée de main la texture HDR de la scène rendue et la texture floue des zones surexposées, tout ce dont vous avez besoin pour réaliser le fameux effet de floraison ou lueur est de combiner ces deux images. Le shader de fragment final (très similaire à celui présenté dans la leçon sur le format HDR ) fait exactement cela - il mélange additivement deux textures:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D scene; uniform sampler2D bloomBlur; uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(scene, TexCoords).rgb; vec3 bloomColor = texture(bloomBlur, TexCoords).rgb; hdrColor += bloomColor; // additive blending //   vec3 result = vec3(1.0) - exp(-hdrColor * exposure); //     - result = pow(result, vec3(1.0 / gamma)); FragColor = vec4(result, 1.0); } 

Ce qu'il faut rechercher: le mixage est effectué avant d'appliquer le mappage de tonalité . Cela traduira correctement la luminosité supplémentaire de l'effet dans la plage LDR ( Low Dynamic Range ), tout en conservant la distribution de luminosité relative dans la scène.

Le résultat du traitement - toutes les zones lumineuses ont reçu un effet de lueur notable:


Les cubes qui remplacent les sources lumineuses sont désormais beaucoup plus lumineux et transmettent mieux l'impression d'une source lumineuse. Cette scène est assez primitive, car la mise en œuvre de l'effet d'un enthousiasme spécial ne provoquera pas, mais dans les scènes complexes avec un éclairage réfléchi, une floraison qualitativement réalisée peut être un élément visuel crucial qui ajoute du drame.

Le code source de l'exemple est ici .

Je note que la leçon a utilisé un filtre assez simple avec seulement cinq échantillons dans chaque direction. En faisant plus d'échantillons dans un rayon plus grand ou en effectuant plusieurs itérations du filtre, vous pouvez visuellement améliorer l'effet. De plus, il convient de dire que visuellement la qualité de l’effet entier dépend directement de la qualité de l’algorithme de flou utilisé. En améliorant le filtre, vous pouvez obtenir une amélioration significative et l'effet global. Par exemple, un résultat plus impressionnant est montré par la combinaison de plusieurs filtres avec différentes tailles de noyau ou différentes courbes gaussiennes. Les ressources suivantes de Kalogirou et EpicGames expliquent comment améliorer la qualité de la floraison en modifiant le flou gaussien.

Ressources supplémentaires


Source: https://habr.com/ru/post/fr420375/


All Articles