Apprenez OpenGL. Leçon 5.10 - Occlusion ambiante de l'espace d'écran

OGL3

SSAO


Le sujet de l'éclairage de fond a été soulevé par nous dans une leçon sur les bases de l'éclairage , mais seulement en passant. Permettez-moi de vous rappeler que la composante d'arrière-plan de l'éclairage est essentiellement une valeur constante qui est ajoutée à tous les calculs de l'éclairage de la scène pour simuler le processus de diffusion de la lumière . Dans le monde réel, la lumière subit de nombreux reflets avec des degrés d'intensité variables, ce qui conduit à un éclairage tout aussi inégal de parties de la scène éclairées indirectement. Évidemment, une fusée à intensité constante n'est pas très plausible.

Un type de calcul approximatif de l'ombrage de l'éclairage indirect est l'algorithme d' occlusion ambiante (AO ), qui simule l'atténuation de l'éclairage indirect au voisinage des coins, des rides et d'autres irrégularités de surface. Ces éléments, en général, se chevauchent considérablement par la géométrie adjacente et laissent donc moins de rayons de lumière s'échapper à l'extérieur, obscurcissant ces zones.

Vous trouverez ci-dessous une comparaison du rendu sans et en utilisant l'algorithme AO. Faites attention à la façon dont l'intensité de l'éclairage d'arrière-plan diminue au voisinage des coins des murs et des autres coupures prononcées de la surface:


Bien que l'effet ne soit pas très visible, la présence de l'effet dans toute la scène lui donne du réalisme en raison de l'illusion supplémentaire de profondeur créée par les petits détails de l'effet d'auto-occultation.


Il convient de noter que les algorithmes de calcul de l'AO sont assez gourmands en ressources, car ils nécessitent une analyse de la géométrie environnante. Dans une mise en œuvre naïve, il serait possible d'émettre simplement beaucoup de rayons à chaque point de la surface et de déterminer le degré de son ombrage, mais cette approche atteint très rapidement la limite de ressources acceptable pour les applications interactives. Heureusement, en 2007, Crytek a publié un document décrivant sa propre approche de la mise en œuvre de l'algorithme Screen-Space Ambient Occlusion (SSAO ) utilisé dans la version finale de Crysis. L'approche a calculé le degré d'ombrage dans l'espace d'écran, en utilisant uniquement le tampon de profondeur actuel au lieu de données réelles sur la géométrie environnante. Une telle optimisation a radicalement accéléré l'algorithme par rapport à l'implémentation de référence et en même temps a donné des résultats pour la plupart plausibles, ce qui a fait de cette approche de calcul approximatif de l'ombrage d'arrière-plan une industrie de facto standard.

Le principe sur lequel l'algorithme est basé est assez simple: pour chaque fragment d'un quad en plein écran, le facteur d'occlusion est calculé en fonction des valeurs de profondeur des fragments environnants. Le coefficient d'ombrage calculé est ensuite utilisé pour réduire l'intensité de l'éclairage de fond (jusqu'à l'exclusion complète). L'obtention d'un coefficient nécessite de collecter des données de profondeur à partir d'une pluralité d'échantillons de la région sphérique entourant le fragment en question et de comparer ces valeurs de profondeur avec la profondeur du fragment en question. Le nombre d'échantillons ayant une profondeur supérieure au fragment actuel détermine directement le coefficient d'ombrage. Regardez ce diagramme:


Ici, chaque point gris se trouve à l'intérieur d'un certain objet géométrique et contribue donc à la valeur du coefficient d'ombrage. Plus il y a d'échantillons à l'intérieur de la géométrie des objets environnants, moins sera l'intensité résiduelle de l'ombrage d'arrière-plan dans cette zone.

Évidemment, la qualité et le réalisme de l'effet dépendent directement du nombre d'échantillons prélevés. Avec un petit nombre d'échantillons, la précision de l'algorithme diminue et conduit à l'apparition d'un artefact de bande ou de « bande » dû à des transitions brusques entre régions avec des coefficients d'ombrage très différents. Un grand nombre d'échantillons tue simplement les performances. La randomisation du noyau des échantillons permet d'obtenir des résultats quelque peu similaires pour réduire légèrement le nombre d'échantillons requis. Une réorientation par rotation vers un angle aléatoire d'un ensemble de vecteurs d'échantillonnage est implicite. Cependant, l'introduction de l'aléatoire pose immédiatement un nouveau problème sous la forme d'un motif de bruit notable, qui nécessite l'utilisation de filtres de flou pour lisser le résultat. Ci-dessous, un exemple de l'algorithme (auteur - John Chapman ) et de ses problèmes typiques: bandes et modèle de bruit.


Comme on peut le voir, une bande notable due au petit nombre d'échantillons est bien supprimée en introduisant une randomisation de l'orientation des échantillons.

L'implémentation SSAO spécifique de Crytek avait un style visuel reconnaissable. Étant donné que les spécialistes de Crytek utilisaient un noyau sphérique de l'échantillon, cela affectait même les surfaces planes telles que les murs, les rendant ombragées - car la moitié du volume du noyau de l'échantillon était submergée sous la géométrie. Ci-dessous est une capture d'écran d'une scène de Crysis montrée en niveaux de gris basée sur la valeur du facteur d'ombrage. Ici, l'effet de la "grisaille" est clairement visible:


Pour éviter cet effet, nous allons passer du noyau sphérique de l'échantillon à un hémisphère orienté le long de la normale à la surface:


Lors de l'échantillonnage à partir d'un tel hémisphère à orientation normale, nous n'avons pas à prendre en compte les fragments se trouvant sous la surface de la surface adjacente dans le calcul du coefficient d'ombrage. Cette approche supprime les ombres inutiles, en général, donne des résultats plus réalistes. Cette leçon utilisera l'approche hémisphérique et un code un peu plus raffiné de la brillante leçon SSAO de John Chapman .

Tampon de données brutes


Le processus de calcul du facteur d'ombrage dans chaque fragment nécessite la disponibilité de données sur la géométrie environnante. Plus précisément, nous avons besoin des données suivantes:

  • Positionner le vecteur pour chaque fragment;
  • Vecteur normal pour chaque fragment;
  • Couleur diffuse pour chaque fragment;
  • Le cœur de l'échantillon
  • Un vecteur de rotation aléatoire pour chaque fragment utilisé pour réorienter le noyau de l'échantillon.

En utilisant des données sur les coordonnées du fragment dans l'espace des espèces, nous pouvons orienter l'hémisphère du noyau de l'échantillon le long du vecteur normal spécifié dans l'espace des espèces pour le fragment actuel. Ensuite, le noyau résultant est utilisé pour faire des échantillons avec divers décalages à partir d'une texture qui stocke des données sur les coordonnées des fragments. Nous faisons de nombreux échantillons dans chaque fragment, et pour chaque échantillon que nous faisons, nous comparons sa valeur de profondeur avec la valeur de profondeur du tampon de coordonnées de fragment pour estimer la quantité d'ombrage. La valeur résultante est ensuite utilisée pour limiter la contribution du composant d'arrière-plan dans le calcul d'éclairage final. En utilisant un vecteur de rotation aléatoire par fragments, nous pouvons réduire considérablement le nombre d'échantillons requis pour obtenir un résultat décent, et cela sera ensuite démontré.


Le SSAO étant un effet réalisé dans l'espace écran, il est possible d'effectuer un calcul direct en effectuant un quadrillage plein écran. Mais alors nous n'aurons pas de données sur la géométrie de la scène. Pour contourner cette restriction, nous rendrons toutes les informations nécessaires dans la texture, qui seront ensuite utilisées dans le shader SSAO pour accéder à des informations géométriques et autres sur la scène. Si vous avez suivi attentivement ces leçons, vous devez déjà savoir dans l'approche décrite l'apparence de l'algorithme d'ombrage différé. C'est en grande partie pourquoi l'effet SSAO en tant que natif apparaît dans le rendu avec un ombrage différé - après tout, les textures qui stockent les coordonnées et les normales sont déjà disponibles dans le G-buffer.

Dans cette leçon, l'effet est implémenté au-dessus d'une version légèrement simplifiée du code de la leçon sur l' éclairage différé . Si vous ne vous êtes pas encore familiarisé avec les principes de l'éclairage différé, je vous recommande fortement de vous tourner vers cette leçon.

Étant donné que l'accès aux informations de fragment sur les coordonnées et les normales devrait déjà être disponible en raison du tampon G, le shader de fragment de l'étape de traitement de la géométrie est assez simple:

#version 330 core layout (location = 0) out vec4 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; void main() { //        gPosition = FragPos; //       gNormal = normalize(Normal); //    -   gAlbedoSpec.rgb = vec3(0.95); } 

Étant donné que l'algorithme SSAO est un effet dans l'espace d'écran et que le facteur d'ombrage est calculé en fonction de la zone visible de la scène, il est judicieux d'effectuer des calculs dans l'espace d'affichage. Dans ce cas, la variable FragPos obtenue à partir du vertex shader stocke la position exactement dans la fenêtre. Il convient de s'assurer que les coordonnées et les normales sont stockées dans le G-buffer dans l'espace de vue, car tous les autres calculs y seront effectués.

Il y a la possibilité de restaurer le vecteur de position sur la base uniquement d'une profondeur de fragment connue et d'une certaine quantité de magie mathématique, qui est décrite, par exemple, dans le blog de Matt Pettineo. Bien sûr, cela nécessite un coût de calcul important, mais cela élimine le besoin de stocker les données de position dans le G-buffer, ce qui prend beaucoup de mémoire vidéo. Cependant, dans un souci de simplicité de l'exemple de code, nous laisserons cette approche à l'étude personnelle.

La texture du tampon de couleur gPosition est configurée comme suit:

 glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); 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_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 

Cette texture stocke les coordonnées des fragments et peut être utilisée pour obtenir des données de profondeur pour chaque point à partir du noyau des échantillons. Je note que la texture utilise un format de données à virgule flottante - cela permettra aux coordonnées des fragments de ne pas être réduites à l'intervalle [0., 1.]. Faites également attention au mode de répétition - GL_CLAMP_TO_EDGE est défini. Ceci est nécessaire pour éliminer la possibilité de ne pas suréchantillonner volontairement l'espace de l'écran. Le dépassement de l'intervalle principal des coordonnées de texture nous donnera des données de position et de profondeur incorrectes.

Ensuite, nous nous engagerons dans la formation d'un noyau hémisphérique des échantillons et la création d'une méthode d'orientation aléatoire.

Création d'un hémisphère à orientation normale


Ainsi, la tâche consiste à créer un ensemble de points d'échantillonnage situés à l'intérieur d'un hémisphère orienté le long de la normale à la surface. Étant donné que la création d'un échantillon de noyau pour toutes les directions possibles de la normale est impossible à calculer, nous utilisons la transition vers l' espace tangent , où la normale est toujours représentée comme un vecteur dans la direction du demi-axe positif Z.


En supposant que le rayon de l'hémisphère est un processus unique, la formation d'un noyau d'un échantillon de 64 points ressemble à ceci:

 //      0.0 - 1.0 std::uniform_real_distribution<float> randomFloats(0.0, 1.0); std::default_random_engine generator; std::vector<glm::vec3> ssaoKernel; for (unsigned int i = 0; i < 64; ++i) { glm::vec3 sample( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) ); sample = glm::normalize(sample); sample *= randomFloats(generator); float scale = (float)i / 64.0; ssaoKernel.push_back(sample); } 

Ici, nous sélectionnons au hasard les coordonnées x et y dans l'intervalle [-1., 1.] et la coordonnée z dans l'intervalle [0., 1.] (si l'intervalle est le même que pour x et y , nous obtiendrions un noyau sphérique échantillonnage). Les vecteurs d'échantillons résultants seront limités aux hémisphères, car le noyau de l'échantillon sera finalement orienté le long de la normale à la surface.

À l'heure actuelle, tous les points d'échantillonnage sont répartis de manière aléatoire à l'intérieur du noyau, mais pour des raisons de qualité de l'effet, les échantillons plus proches de l'origine du noyau devraient contribuer davantage au calcul du coefficient d'ombrage. Cela peut être réalisé en modifiant la distribution des points d'échantillonnage formés en augmentant leur densité près de l'origine. Cette tâche est facilement accomplie à l'aide de la fonction d'interpolation d'accélération:

 scale = lerp(0.1f, 1.0f, scale * scale); sample *= scale; ssaoKernel.push_back(sample); } 

La fonction lerp () est définie comme:

 float lerp(float a, float b, float f) { return a + f * (b - a); } 

Une telle astuce nous donne une distribution modifiée, où la plupart des points d'échantillonnage se trouvent près de l'origine du noyau.


Chacun des vecteurs échantillons obtenus sera utilisé pour déplacer les coordonnées du fragment dans l'espace des espèces afin d'obtenir des données sur la géométrie environnante. Pour obtenir des résultats décents lorsque vous travaillez dans la fenêtre, vous aurez peut-être besoin d'un nombre impressionnant d'échantillons, ce qui affectera inévitablement les performances. Cependant, l'introduction de bruit pseudo-aléatoire ou la rotation des vecteurs d'échantillonnage dans chaque fragment traité réduira considérablement le nombre requis d'échantillons de qualité comparable.

Rotation aléatoire du noyau de l'échantillon


Ainsi, l'introduction de l'aléatoire dans la distribution des points dans le noyau de l'échantillon peut réduire considérablement l'exigence du nombre de ces points pour obtenir un effet de qualité décent. Il serait possible de créer un vecteur de rotation aléatoire pour chaque fragment de la scène, mais c'est trop cher de mémoire. Il est plus efficace de créer une petite texture contenant un ensemble de vecteurs de rotation aléatoire, puis de l’utiliser avec le mode de répétition GL_REPEAT .

Créez un tableau 4x4 et remplissez-le de vecteurs de rotation aléatoire orientés le long du vecteur normal dans l'espace tangent:

 std::vector<glm::vec3> ssaoNoise; for (unsigned int i = 0; i < 16; i++) { glm::vec3 noise( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); ssaoNoise.push_back(noise); } 

Puisque le noyau est aligné le long du demi-axe positif Z dans l'espace tangent, nous laissons la composante z égale à zéro - cela assurera la rotation uniquement autour de l'axe Z.

Ensuite, créez une texture 4x4 et remplissez-la avec notre tableau de vecteurs de rotation. Assurez-vous d'utiliser le mode de relecture GL_REPEAT pour le pavage de texture:

 unsigned int noiseTexture; glGenTextures(1, &noiseTexture); glBindTexture(GL_TEXTURE_2D, noiseTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 

Eh bien, nous avons maintenant toutes les données nécessaires à la mise en œuvre directe de l'algorithme SSAO!

Shader SSAO


Un shader d'effet sera exécuté pour chaque fragment d'un quadruple plein écran, calculant le coefficient d'ombrage dans chacun d'eux. Étant donné que les résultats seront utilisés dans une autre étape de rendu qui crée l'éclairage final, nous devrons créer un autre objet framebuffer pour stocker le résultat du shader:

 unsigned int ssaoFBO; glGenFramebuffers(1, &ssaoFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); unsigned int ssaoColorBuffer; glGenTextures(1, &ssaoColorBuffer); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0); 

Comme le résultat de l'algorithme est le seul nombre réel dans [0., 1.], pour le stockage, il suffira de créer une texture avec le seul composant disponible. C'est pourquoi GL_RED est défini comme format interne pour le tampon de couleur.

En général, le processus de rendu d'étape SSAO ressemble à ceci:

 //  :  G- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); [...] glBindFramebuffer(GL_FRAMEBUFFER, 0); //  G-      SSAO glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, noiseTexture); shaderSSAO.use(); SendKernelSamplesToShader(); shaderSSAO.setMat4("projection", projection); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0); //  :    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shaderLightingPass.use(); [...] glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); [...] RenderQuad(); 

Le shader SSAO accepte les textures du tampon G dont il a besoin en entrée, ainsi que la texture du bruit et le noyau de l'échantillon:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D texNoise; uniform vec3 samples[64]; uniform mat4 projection; //             //      1280x720 const vec2 noiseScale = vec2(1280.0/4.0, 720.0/4.0); void main() { [...] } 

Notez la variable noiseScale . Notre petite texture avec du bruit devrait être carrelée sur toute la surface de l'écran, mais puisque les coordonnées de texture TexCoords sont dans [0., 1.] cela ne se produira pas sans notre intervention. À ces fins, nous calculons le facteur pour les coordonnées de texture, qui se trouve comme le rapport de la taille de l'écran à la taille de la texture de bruit:

 vec3 fragPos = texture(gPosition, TexCoords).xyz; vec3 normal = texture(gNormal, TexCoords).rgb; vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz; 

Puisque lors de la création de la texture de bruit texNoise , nous avons défini le mode de répétition sur GL_REPEAT , il sera maintenant répété plusieurs fois sur la surface de l'écran. Avec randomVec , fragPos et les valeurs normales , nous pouvons créer une matrice de transformation TBN de l'espace tangent à l'espace des espèces:

 vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal); 

En utilisant le processus de Gram-Schmidt, nous créons une base orthogonale inclinée de manière aléatoire dans chaque fragment en fonction de la valeur aléatoire randomVec . Un point important: dans ce cas, peu importe pour nous que la matrice TBN soit précisément orientée le long de la surface du triangle (comme dans le cas de la cartographie de parallaxe, environ Per.), Nous n'avons donc pas besoin de données tangentes et bi-tangentes précalculées.

Ensuite, nous parcourons le tableau du noyau de l'échantillon, convertissons chaque vecteur échantillon de l'espace tangent à l'espace des espèces et obtenons sa somme avec la position actuelle du fragment. Ensuite, nous comparons la valeur de profondeur de la quantité résultante avec la valeur de profondeur obtenue par échantillonnage à partir de la texture G-buffer correspondante.

Bien que cela semble déroutant, passons par les étapes:

 float occlusion = 0.0; for(int i = 0; i < kernelSize; ++i) { //     vec3 sample = TBN * samples[i]; //      - sample = fragPos + sample * radius; [...] } 

Ici, kernelSize et radius sont des variables qui contrôlent les caractéristiques de l'effet. Dans ce cas, ils sont respectivement de 64 et 0,5. À chaque itération, nous traduisons le vecteur de base de l'échantillon dans l'espace des espèces. Ensuite, nous ajoutons à la valeur obtenue du déplacement de l'échantillon dans l'espace des espèces la valeur de la position du fragment dans l'espace des espèces. Dans ce cas, la valeur de décalage est multipliée par la variable de rayon, qui contrôle le rayon du cœur de l'échantillon d'effet SSAO.

Après ces étapes, nous devons convertir le vecteur échantillon résultant en espace d'écran, afin de pouvoir sélectionner dans la texture du tampon G qui stocke les positions et les profondeurs des fragments en utilisant la valeur projetée obtenue. Puisque l' échantillon est dans la fenêtre, nous avons besoin de la matrice de projection projection :

 vec4 offset = vec4(sample, 1.0); offset = projection * offset; //     offset.xyz /= offset.w; //   offset.xyz = offset.xyz * 0.5 + 0.5; //    [0., 1.] 

Après la conversion en espace de clip, nous effectuons manuellement la division en perspective en divisant simplement les composants xyz par le composant w . Le vecteur résultant en coordonnées de périphérique normalisées ( NDC ) est traduit dans l'intervalle de valeurs [0., 1.] afin qu'il puisse être utilisé comme coordonnées de texture:

 float sampleDepth = texture(gPosition, offset.xy).z; 

Nous utilisons les composants xy du vecteur échantillon pour sélectionner dans la texture les positions du G-buffer. Nous obtenons la valeur de profondeur (composantes z ) correspondant au vecteur échantillon lorsqu'il est vu depuis la position de l'observateur (il s'agit du premier fragment visible non blindé). Si en même temps la profondeur d'échantillonnage obtenue est supérieure à la profondeur stockée, alors on augmente le coefficient d'ombrage:

 occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0); 

Notez le décalage de biais , qui est ajouté à la profondeur du fragment d'origine (défini dans l'exemple à 0,025). Ce décalage n'est pas toujours requis, mais la présence d'une variable vous permet de contrôler l'apparence de l'effet SSAO et, dans certaines situations, supprime les problèmes d'ondulations dans les zones ombrées.

Mais ce n'est pas tout, car une telle implémentation conduit à des artefacts perceptibles. Il se manifeste dans les cas où un fragment situé près du bord d'une certaine surface est considéré. Dans de telles situations, lors de la comparaison des profondeurs, l'algorithme capturera inévitablement les profondeurs des surfaces, qui peuvent se situer très loin derrière celle considérée. À ces endroits, l'algorithme augmentera considérablement par erreur le degré d'ombrage, ce qui créera des halos sombres visibles sur les bords des objets. L'artefact est traité en introduisant une vérification de distance supplémentaire (un exemple de John Chapman ):


La vérification limitera la contribution au coefficient d'ombrage uniquement pour les valeurs de profondeur situées dans le rayon de l'échantillon:

 float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth)); occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0) * rangeCheck; 

Nous utilisons également la fonction GLSL smoothstep () , qui implémente une interpolation douce du troisième paramètre entre le premier et le second. En même temps, renvoyer 0 si le troisième paramètre est inférieur ou égal au premier, ou 1 si le troisième paramètre est supérieur ou égal au second. Si la différence de profondeur est dans le rayon , alors sa valeur sera lissée en douceur dans l'intervalle [0., 1.] conformément à cette courbe:


Si nous utilisions des limites claires dans les conditions de vérification de la profondeur, cela ajouterait des artefacts sous forme de limites nettes aux endroits où les valeurs de la différence de profondeurs sont en dehors des limites du rayon .

Avec la touche finale, nous normalisons la valeur du coefficient d'ombrage en utilisant la taille du noyau de l'échantillon et enregistrons le résultat. Nous inversons également la valeur finale en la soustrayant de l'unité, afin que vous puissiez utiliser la valeur finale directement pour moduler le composant d'arrière-plan de l'éclairage sans étapes supplémentaires:

 } occlusion = 1.0 - (occlusion / kernelSize); FragColor = occlusion; 

Pour une scène avec une nanosuit couchée qui nous est familière, l'exécution du shader SSAO donne la texture suivante:


Comme vous pouvez le voir, l'effet de l'ombrage d'arrière-plan crée une bonne illusion de profondeur. Seule l'image de sortie du shader vous permet déjà de distinguer les détails du costume et de vous assurer qu'il repose vraiment sur le sol et ne lévite pas à une certaine distance de celui-ci.

Néanmoins, l'effet est loin d'être idéal, car le motif de bruit introduit par la texture des vecteurs à rotation aléatoire est facilement perceptible. Pour lisser le résultat du calcul SSAO, nous appliquons un filtre de flou.

Ombrage d'arrière-plan flou


Après avoir construit le résultat de SSAO et avant le mélange final de l'éclairage, il est nécessaire de brouiller la texture qui stocke les données sur le coefficient d'ombrage. Pour ce faire, nous aurons un autre framebuffer:

 unsigned int ssaoBlurFBO, ssaoColorBufferBlur; glGenFramebuffers(1, &ssaoBlurFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO); glGenTextures(1, &ssaoColorBufferBlur); glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0); 

La mise en mosaïque d'une texture de bruit dans l'espace d'écran fournit des caractéristiques d'aléatoire bien définies que vous pouvez utiliser à votre avantage lors de la création d'un filtre de flou:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D ssaoInput; void main() { vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0)); float result = 0.0; for (int x = -2; x < 2; ++x) { for (int y = -2; y < 2; ++y) { vec2 offset = vec2(float(x), float(y)) * texelSize; result += texture(ssaoInput, TexCoords + offset).r; } } FragColor = result / (4.0 * 4.0); } 

Le shader transite simplement les texels de la texture SSAO avec un décalage de -2 à +2, ce qui correspond à la taille réelle de la texture de bruit. Le décalage est égal à la taille exacte d'un texel: la fonction textureSize () est utilisée pour le calcul, qui renvoie vec2 avec les dimensions de la texture spécifiée. T.O. Le shader fait simplement la moyenne des résultats stockés dans la texture, ce qui donne un flou rapide et assez efficace:


Au total, nous avons une texture avec des données d'ombrage d'arrière-plan pour chaque fragment sur l'écran - tout est prêt pour l'étape de réduction finale de l'image!

Appliquer un ombrage d'arrière-plan


L'étape d'application du coefficient d'ombrage dans le calcul final de l'éclairage est étonnamment simple: pour chaque fragment, il suffit de multiplier simplement la valeur de la composante de fond de la source lumineuse par le coefficient d'ombrage de la texture préparée. Vous pouvez prendre un shader prêt à l'emploi avec le modèle Blinn-Fong de la leçon sur l' ombrage différé et le corriger un peu:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; uniform sampler2D ssao; struct Light { vec3 Position; vec3 Color; float Linear; float Quadratic; float Radius; }; uniform Light light; void main() { //    G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Diffuse = texture(gAlbedo, TexCoords).rgb; float AmbientOcclusion = texture(ssao, TexCoords).r; //   -    //   :   -  vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion); vec3 lighting = ambient; //    (0, 0, 0)   - vec3 viewDir = normalize(-FragPos); //   vec3 lightDir = normalize(light.Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color; //   vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0); vec3 specular = light.Color * spec; //   float dist = length(light.Position - FragPos); float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist); diffuse *= attenuation; specular *= attenuation; lighting += diffuse + specular; FragColor = vec4(lighting, 1.0); } 

Il n'y a que deux changements majeurs: la transition vers les calculs dans la fenêtre et la multiplication du composant d'éclairage d'arrière-plan par la valeur d' AmbientOcclusion . Un exemple de scène avec un seul point bleu:


Le code source complet est ici .

La manifestation de l'effet SSAO dépend fortement de paramètres tels que kernelSize , radius et polarisation , souvent les affiner est une évidence pour l'artiste de travailler sur un lieu / scène particulier. Il n'y a pas de «meilleure» combinaison universelle de paramètres: pour certaines scènes, un petit rayon du noyau de l'échantillon est bon, tandis que d'autres bénéficient de l'augmentation du rayon et du nombre d'échantillons. L'exemple utilise 64 points d'échantillonnage, ce qui, franchement, est redondant, mais vous pouvez toujours modifier le code et voir ce qui se passe avec un plus petit nombre d'échantillons.

En plus des uniformes répertoriés responsables de la définition de l'effet, il est possible de contrôler explicitement la gravité de l'effet d'ombrage d'arrière-plan. Pour ce faire, il suffit d'élever le coefficient à un degré contrôlé par un autre uniforme:

 occlusion = 1.0 - (occlusion / kernelSize); FragColor = pow(occlusion, power); 

Je vous conseille de passer un peu de temps sur le jeu avec les paramètres, car cela donnera une meilleure compréhension de la nature des changements dans l'image finale.

Pour résumer, il convient de dire que bien que l'effet visuel de l'utilisation de SSAO soit plutôt subtil, mais dans les scènes avec un éclairage bien placé, il ajoute indéniablement une fraction notable de réalisme. Avoir un tel outil dans votre arsenal est certainement précieux.

Ressources supplémentaires


  1. Tutoriel SSAO : Un excellent article de leçon de John Chapman, sur la base duquel le code de cette leçon est construit.
  2. Connaissez vos artefacts SSAO : Un article très précieux montrant avec lucidité non seulement les problèmes les plus urgents avec la qualité SSAO, mais aussi les moyens de les résoudre. Lecture recommandée.
  3. SSAO avec reconstruction de profondeur : Addendum à la leçon principale de SSAO par OGLDev concernant une technique couramment utilisée pour restaurer les coordonnées des fragments en fonction de la profondeur. L'importance de cette approche est due aux économies de mémoire importantes dues au manque de nécessité de stocker des positions dans le G-buffer. L'approche est si universelle qu'elle s'applique aux SSAO dans la mesure où.

PS : Nous avons un télégramme conf pour la coordination des transferts. Si vous avez un désir sérieux d'aider à la traduction, alors vous êtes les bienvenus!

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


All Articles