Shaders de jeu 3D pour débutants: Effets

[ La première partie ]

Après avoir traité des bases, dans cette partie de l'article, nous mettons en œuvre des effets tels que les contours des objets, la floraison, le SSAO, le flou, la profondeur de champ, la pixellisation, etc.

Contours



La création de contours autour de la géométrie de la scène donne au jeu un aspect unique qui ressemble à des bandes dessinées ou des dessins animés.

Matériau diffus


Le shader de contour a besoin d'une texture d'entrée pour reconnaître et colorer les bords. Les candidats à une telle texture entrante peuvent être une couleur diffuse à partir de matériaux, des couleurs à partir de textures diffuses, des normales de vertex ou même des couleurs à partir de cartes normales.

uniform struct { vec4 diffuse ; } p3d_Material; out vec4 fragColor; void main() { vec3 diffuseColor = p3d_Material.diffuse.rgb; fragColor = vec4(diffuseColor, 1); } 

Il s'agit d'un petit ombrage de fragment qui rend la couleur diffuse d'un matériau géométrique en une texture de tampon de trame. Cette texture de couleur diffuse provenant du tampon de trame sera la texture d'entrée du shader de chemin.


Il s'agit de la texture de la couleur diffuse du matériau provenant du tampon d'image, qui affiche les couleurs que nous avons définies dans Blender. Le shader contour reconnaîtra les bords de la scène et les coloriera.

Il convient de noter que la couleur diffuse des matériaux ne fonctionnera pas si certaines parties de la scène n'ont pas leur propre couleur diffuse du matériau.

Création d'arêtes



La création d'arêtes est similaire à l'utilisation de filtres de reconnaissance d'arêtes dans GIMP .

Tous les calculs pour cette technique d'ombrage sont effectués dans un fragment shader. Pour créer des contours pour le vertex shader, il suffit de passer quatre sommets du maillage rectangulaire à la sortie pour s'adapter à l'écran.

 // ... uniform sampler2D materialDiffuseTexture; // ... vec2 texSize = textureSize(materialDiffuseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy; // ... 

Avant de commencer à reconnaître les bords, vous devez préparer la texture entrante, avec laquelle nous travaillerons. Puisque la texture a une taille d'écran, nous pouvons calculer les coordonnées UV, connaissant les coordonnées du fragment et la taille de la texture entrante.

  // ... int separation = 1; // ... 

separation peut être personnalisée selon vos goûts. Plus la séparation est grande, plus les bords ou les lignes sont épais.

  // ... float threshold = 0; // ... vec4 mx = vec4(0); vec4 mn = vec4(1); int x = -1; int y = -1; for (int i = 0; i < 9; ++i) { vec4 color = texture ( materialDiffuseTexture , (texCoord + (vec2(x, y) * separation)) / texSize ); mx = max(color, mx); mn = min(color, mn); x += 1; if (x >= 2) { x = -1; y += 1; } } float alpha = ((mx.r + mx.g + mx.b) / 3) - ((mn.r + mn.g + mn.b) / 3); if (alpha > threshold) { alpha = 1; } // ... 


La technique de reconnaissance des bords trouve des changements dans les couleurs de la texture entrante. En se concentrant sur le fragment actuel, il utilise la fenêtre de fragments 3x3 pour trouver les couleurs les plus claires et les plus sombres des neuf échantillons. Ensuite, elle soustrait de la luminosité d'une couleur la luminosité d'une autre, obtenant leur différence.

  // ... vec3 lineRgb = vec3(0.012, 0.014, 0.022); // ... vec4 lineColor = vec4(lineRgb, alpha); // ... fragColor = lineColor; // ... 

Cette différence est utilisée dans le canal alpha de la couleur de sortie. S'il n'y a pas de différence, le bord ou la ligne n'est pas tracé. S'il y a une différence, le bord est tracé.

  // ... float threshold = 0; // ... if (alpha > threshold) { alpha = 1; } // ... 

Essayez de tester la valeur seuil. Maintenant, c'est zéro. Toute valeur différente de zéro devient un bord; ce seuil peut être modifié. Ceci est particulièrement utile pour les textures entrantes plus bruyantes avec de petites différences. Dans le cas d'une texture entrante bruyante, vous devez généralement créer des contours uniquement pour les grandes différences.

Code source



Brouillard



Le brouillard (ou brume, comme on l'appelle dans Blender) ajoute une brume atmosphérique à la scène, créant de mystérieuses parties saillantes ramollies. Les parties saillantes apparaissent lorsqu'une géométrie tombe soudainement dans la pyramide de visibilité de la caméra.

 // ... uniform struct p3d_FogParameters { vec4 color ; float start ; float end ; } p3d_Fog; // ... 

Panda3D a une structure de données pratique qui contient tous les paramètres de brouillard, mais vous pouvez les transférer manuellement vers votre shader.

  // ... float fogIntensity = clamp ( ( p3d_Fog.end - vertexPosition.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; // ... 

Dans l'exemple de code, un modèle linéaire est utilisé pour calculer la luminosité du brouillard lorsque vous vous éloignez de la caméra. Au lieu de cela, vous pouvez utiliser le modèle exponentiel. La luminosité du brouillard est nulle avant ou au début du brouillard. Lorsque la position du sommet approche de la fin du brouillard, fogIntensity rapproche de l'unité. Pour tous les sommets après la fin du brouillard, fogIntensity limité à 1 à partir du dessus.

  // ... fragColor = mix ( outputColor , p3d_Fog.color , fogIntensity ); // ... 

En fonction de la luminosité du brouillard, nous mélangeons la couleur du brouillard avec la couleur de sortie. À fogIntensity que fogIntensity approche l'unité, il y aura de moins en moins outputColor et de plus en plus de couleur de brouillard. Lorsque fogIntensity atteint l'unité, seule la couleur du brouillard restera.

Brouillard sur les contours




 // ... uniform sampler2D positionTexture; // ... vec4 position = texture(positionTexture, texCoord / texSize); float fogIntensity = clamp ( ( p3d_Fog.end - position.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; vec4 lineWithFogColor = mix ( lineColor , p3d_Fog.color , fogIntensity ); fragColor = vec4(lineWithFogColor.rgb, alpha); // ... 

Le Path Shader applique du brouillard aux couleurs des bords pour une image plus holistique. S'il ne le faisait pas, la géométrie des contours serait obscurcie par le brouillard, ce qui serait étrange. Cependant, il crée toujours des contours sur les bords les plus externes de la géométrie de la scène avec le fraisage, car les bords dépassent la géométrie - là où il n'y a pas de positions de sommet.

positionTexture est une texture de tampon d'image qui contient les positions des sommets de l'espace de vue. Vous en saurez plus lorsque nous implémenterons le shader SSAO.

Code source



Bloom



Ajouter de la floraison à la scène peut créer une illusion convaincante du modèle d'éclairage. Les objets émetteurs de lumière deviennent plus convaincants et les reflets lumineux reçoivent une quantité supplémentaire de rayonnement.

  //... float separation = 3; int samples = 15; float threshold = 0.5; float amount = 1; // ... 

Vous pouvez personnaliser ces paramètres à votre guise. La séparation augmente la taille du flou. Les échantillons déterminent la force du flou. Le seuil détermine ce qui sera et ne sera pas affecté par cet effet. La quantité contrôle la quantité de floraison produite.

  // ... int size = samples; int size2 = size * size; int x = 0; int y = 0; // ... float value = 0; vec4 result = vec4(0); vec4 color = vec4(0); // ... for (int i = 0; i < size2; ++i) { // ... } // ... 

Cette technique commence par passer des samples taille d'une fenêtre sur des samples centrés par rapport au fragment actuel. Il ressemble à une fenêtre utilisée pour créer des chemins.

  // ... color = texture ( bloomTexture , ( gl_FragCoord.xy + vec2(x * separation, y * separation) ) / texSize ); value = ((0.3 * color.r) + (0.59 * color.g) + (0.11 * color.b)); if (value < threshold) { color = vec4(0); } result += color; // ... 

Ce code obtient la couleur de la texture entrante et transforme les valeurs du rouge, du vert et du bleu en une valeur en niveaux de gris. Si la valeur en niveaux de gris est inférieure au seuil, elle rejette cette couleur, la rendant noire.

En passant par tous les échantillons dans la fenêtre, il accumule toutes leurs valeurs en result .

  // ... result = result / size2; // ... 

Ayant terminé la collecte des échantillons, il divise la somme des échantillons de couleur par le nombre d'échantillons prélevés. Le résultat est la couleur médiane du fragment lui-même et de ses voisins. Ce faisant, pour chaque fragment, nous obtenons une image floue. Ce type de flou est appelé flou de boîte.


Vous voyez ici le processus d'exécution de l'algorithme de floraison.

Code source



Screen Space Ambient Occlusion (SSAO)



SSAO est l'un de ces effets que vous ne connaissez pas, mais dès que vous savez que vous ne pouvez plus vivre sans eux. Il peut transformer une scène médiocre en une scène incroyable! Dans les scènes statiques, l'occlusion ambiante peut être intégrée dans la texture, mais pour les scènes plus dynamiques, nous avons besoin d'un shader. SSAO est l'une des techniques d'ombrage les plus sophistiquées, mais une fois que vous l'aurez compris, vous deviendrez un maître shader.

Notez que le terme «espace d'écran» dans le titre n'est pas entièrement correct, car tous les calculs ne sont pas effectués dans l'espace d'écran.

Données entrantes


Le shader SSAO aura besoin de l'entrée suivante.

  • Vecteurs de positions des sommets dans l'espace d'observation.
  • Vecteurs normaux aux sommets de l'espace de visualisation.
  • Exemples de vecteurs dans l'espace tangent.
  • Vecteurs de bruit dans l'espace tangent.
  • La matrice de projection sur l'objectif de la caméra.

Poste



Il n'est pas nécessaire de stocker les positions des sommets dans la texture du tampon de trame. Nous pouvons les recréer à partir du tampon de profondeur de la caméra . J'écris un guide pour les débutants, nous n'utiliserons donc pas cette optimisation et nous nous mettrons immédiatement au travail. Dans votre implémentation, vous pouvez facilement utiliser le tampon de profondeur.

 PT(Texture) depthTexture = new Texture("depthTexture"); depthTexture->set_format(Texture::Format::F_depth_component32); PT(GraphicsOutput) depthBuffer = graphicsOutput->make_texture_buffer("depthBuffer", 0, 0, depthTexture); depthBuffer->set_clear_color(LVecBase4f(0, 0, 0, 0)); NodePath depthCameraNP = window->make_camera(); DCAST(Camera, depthCameraNP.node())->set_lens(window->get_camera(0)->get_lens()); PT(DisplayRegion) depthBufferRegion = depthBuffer->make_display_region(0, 1, 0, 1); depthBufferRegion->set_camera(depthCameraNP); 

Si vous décidez d'utiliser le tampon de profondeur, voici comment vous pouvez le configurer dans Panda3D.

 in vec4 vertexPosition; out vec4 fragColor; void main() { fragColor = vertexPosition; } 

Voici un shader simple pour rendre les positions des sommets dans l'espace de visualisation dans une texture de tampon de trame. Une tâche plus difficile consiste à ajuster la texture du tampon de trame de sorte que les composants du vecteur fragment obtenu par celui-ci ne soient pas limités à l'intervalle [0, 1] , et que chacun ait une précision suffisamment élevée (un nombre suffisamment important de bits). Par exemple, si une sorte de position de sommet interpolée est <-139.444444566, 0.00000034343, 2.5> , vous ne pouvez pas l'enregistrer dans la texture en tant que <0.0, 0.0, 1.0> .

  // ... FrameBufferProperties fbp = FrameBufferProperties::get_default(); // ... fbp.set_rgba_bits(32, 32, 32, 32); fbp.set_rgb_color(true); fbp.set_float_color(true); // ... 

Voici un exemple de code qui prépare une texture de tampon de trame pour stocker les positions des sommets. Il a besoin de 32 bits pour le rouge, le vert, le bleu et l'alpha, il désactive donc la restriction des valeurs par l'intervalle [0, 1] . L'appel à set_rgba_bits(32, 32, 32, 32) définit le volume binaire et désactive la restriction.

  glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGB32F , 1200 , 900 , 0 , GL_RGB , GL_FLOAT , nullptr ); 

Voici un appel similaire sur OpenGL. GL_RGB32F définit les bits et désactive la restriction.

Si le tampon de couleur a une virgule fixe, les composants des valeurs initiale et finale, ainsi que les indices de mélange, avant de calculer l'équation de mélange sont limités à [0, 1] ou [-1, 1], respectivement, pour les tampons de couleur normalisés non signés normalisés et signés. Si le tampon de couleur a une virgule flottante, la restriction n'est pas respectée.

Source


Ici vous voyez les positions des sommets; l'axe y est vers le haut.

N'oubliez pas que Panda3D définit l'axe z comme un vecteur pointant vers le haut, tandis que dans OpenGL, l'axe y recherche. Le shader de position affiche les positions des sommets avec un z vers le haut, car dans Panda3D
le paramètre gl-coordinate-system default est configuré.

Normal



Pour l'orientation correcte des échantillons obtenus dans le shader SSAO, nous avons besoin des normales aux sommets. L'exemple de code génère plusieurs vecteurs d'échantillonnage répartis sur l'hémisphère, mais vous pouvez utiliser la sphère et résoudre complètement le problème du besoin de normales.

 in vec3 vertexNormal; out vec4 fragColor; void main() { vec3 normal = normalize(vertexNormal); fragColor = vec4(normal, 1); } 

Comme le shader de position, le shader normal est très simple. N'oubliez pas de normaliser les normales aux sommets et n'oubliez pas qu'elles sont dans l'espace de visualisation.


Les normales aux sommets sont montrées ici; l'axe y est vers le haut.

Rappelons que Panda3D considère l'axe z comme le vecteur ascendant et OpenGL vers l'axe y. Le shader normal affiche les positions des sommets avec l'axe z pointant vers le haut, car le gl-coordinate-system default configuré dans Panda3D.

Échantillons


Pour déterminer la valeur d'occlusion ambiante pour tout fragment unique, nous devons échantillonner la zone environnante.

  // ... for (int i = 0; i < numberOfSamples; ++i) { LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) ).normalized(); float rand = randomFloats(generator); sample[0] *= rand; sample[1] *= rand; sample[2] *= rand; float scale = (float) i / (float) numberOfSamples; scale = lerp(0.1, 1.0, scale * scale); sample[0] *= scale; sample[1] *= scale; sample[2] *= scale; ssaoSamples.push_back(sample); } // ... 

L'exemple de code génère 64 échantillons aléatoires répartis dans un hémisphère. Ces ssaoSamples seront passés au shader SSAO.

  LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 ).normalized(); 

Si vous souhaitez répartir vos échantillons sur une sphère, modifiez alors l'intervalle de la composante aléatoire z pour qu'il passe de moins un à un.

Le bruit


  // ... for (int i = 0; i < 16; ++i) { LVecBase3f noise = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , 0.0 ); ssaoNoise.push_back(noise); } // ... 

Afin de bien couvrir la zone échantillonnée, nous devons générer des vecteurs de bruit. Ces vecteurs de bruit peuvent faire tourner des échantillons autour du haut de la surface.

Occlusion ambiante



SSAO accomplit sa tâche en échantillonnant l'espace d'observation autour du fragment. Plus il y a d'échantillons sous la surface, plus la couleur du fragment est foncée. Ces échantillons sont situés dans le fragment et indiquent dans la direction générale de la normale au sommet. Chaque échantillon est utilisé pour rechercher une position dans la texture de la position du tampon de trame. La position retournée est comparée à l'échantillon. Si l'échantillon est plus éloigné de la caméra que la position, alors l'échantillon vers le fragment est occlus.


Ici, vous voyez un espace au-dessus de la surface échantillonnée pour l'occlusion.

  // ... float radius = 1.1; float bias = 0.026; float lowerRange = -2; float upperRange = 2; // ... 

Comme certaines autres techniques, le shader SSAO a plusieurs paramètres de contrôle qui peuvent être modifiés pour obtenir l'apparence souhaitée. un biais est ajouté à la distance entre l'échantillon et la caméra. Ce paramètre peut être utilisé pour lutter contre les taches. Le rayon augmente ou diminue la zone de couverture de l'espace échantillon. lowerRange et upperRange modifient la plage standard de la métrique de facteur de [0, 1] à n'importe quelle valeur que vous sélectionnez. En augmentant la plage, vous pouvez augmenter le contraste.

  // ... vec4 position = texture(positionTexture, texCoord); vec3 normal = texture(normalTexture, texCoord).xyz; int noiseX = int(gl_FragCoord.x - 0.5) % 4; int noiseY = int(gl_FragCoord.y - 0.5) % 4; vec3 random = noise[noiseX + (noiseY * 4)]; // ... 

Nous obtenons la position, le vecteur normal et aléatoire pour une utilisation ultérieure. Rappelons que dans l'exemple de code, 16 vecteurs aléatoires ont été créés. Un vecteur aléatoire est sélectionné en fonction de la position à l'écran des fragments actuels.

  // ... vec3 tangent = normalize(random - normal * dot(random, normal)); vec3 binormal = cross(normal, tangent); mat3 tbn = mat3(tangent, binormal, normal); // ... 

En utilisant un vecteur aléatoire et un vecteur normal, nous collectons la matrice de la tangente, binormale et normale. Nous avons besoin de cette matrice pour transformer les vecteurs échantillons de l'espace tangent à l'espace d'enquête.

  // ... float occlusion = NUM_SAMPLES; for (int i = 0; i < NUM_SAMPLES; ++i) { // ... } // ... 

Ayant une matrice, le shader peut parcourir tous les échantillons de la boucle, en soustrayant le nombre de non ouverts.

  // ... vec3 sample = tbn * samples[i]; sample = position.xyz + sample * radius; // ... 

À l'aide de la matrice, placez l'échantillon à côté de la position du sommet / fragment et mettez-le à l'échelle par le rayon.

  // ... vec4 offset = vec4(sample, 1.0); offset = lensProjection * offset; offset.xyz /= offset.w; offset.xyz = offset.xyz * 0.5 + 0.5; // ... 

En utilisant la position de l'échantillon dans l'espace d'observation, nous le transformons de l'espace d'observation à l'espace d'écrêtage, puis à l'espace UV.

 -1 * 0.5 + 0.5 = 0 1 * 0.5 + 0.5 = 1 

N'oubliez pas que les composants de l'espace d'écrêtage sont compris entre moins un et un, et que les coordonnées UV sont comprises entre zéro et un. Pour convertir les coordonnées de l'espace d'écrêtage en coordonnées UV, multipliez-les par une seconde et ajoutez une seconde.

  // ... vec4 offsetPosition = texture(positionTexture, offset.xy); float occluded = 0; if (sample.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; } // ... 

En utilisant les coordonnées de décalage UV obtenues en projetant l'échantillon 3D sur la texture de position 2D, nous trouvons le vecteur de position correspondant. Cela nous amène de l'espace de visualisation à l'espace de détourage à l'espace UV, puis de retour à l'espace de visualisation. Le shader exécute cette boucle pour déterminer s'il existe une géométrie derrière l'échantillon, à l'emplacement de l'échantillon ou devant l'échantillon. Si l'échantillon est situé devant ou dans une certaine géométrie, cet échantillon n'est tout simplement pas pris en compte par rapport au fragment chevauché. Si l'échantillon est derrière une géométrie, cet échantillon est pris en compte par rapport au fragment chevauché.

  // ... float intensity = smoothstep ( 0.0 , 1.0 , radius / abs(position.y - offsetPosition.y) ); occluded *= intensity; occlusion -= occluded; // ... 

Ajoutez maintenant du poids à cette position échantillonnée en fonction de sa distance à l'intérieur ou à l'extérieur du rayon. Ensuite, soustrayez cet échantillon de la métrique d'occlusion car cela suppose que tous les échantillons se sont chevauchés avant la boucle.

  // ... occlusion /= NUM_SAMPLES; // ... fragColor = vec4(vec3(occlusion), position.a); // ... 

Divisez le nombre de chevauchements par le nombre d'échantillons pour convertir l'indicateur d'occlusion de l'intervalle [0, NUM_SAMPLES] à l'intervalle [0, 1] . Zéro signifie une occlusion complète, les unités signifient aucune occlusion. Maintenant, affectez la métrique d'occlusion à la couleur du fragment, et c'est tout.

Veuillez noter que dans l'exemple de code, le canal alpha se voit attribuer la valeur alpha de la texture de position du tampon de trame pour éviter le chevauchement de l'arrière-plan.

Flou



La texture du tampon de trame SSAO est un peu bruyante, vous devez donc la rendre floue pour le lissage.

  // ... for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( ssaoTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ).rgb; xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

Le shader de flou SSAO est un flou de boîte régulier. Comme le shader de floraison, il dessine une fenêtre sur la texture entrante et fait la moyenne de chaque fragment avec les valeurs de ses voisins.

Notez que parameters.x est un paramètre de séparation.

Couleur ambiante


  // ... vec2 ssaoBlurTexSize = textureSize(ssaoBlurTexture, 0).xy; vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize; float ssao = texture(ssaoBlurTexture, ssaoBlurTexCoord).r; vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex * ssao; // ... 

Le dernier défi pour SSAO réside à nouveau dans les calculs d'éclairage. Nous voyons ici comment l'occlusion se trouve dans le tampon de texture SSAO de texture et est incluse dans le calcul de la lumière ambiante.

Code source



Profondeur de champ



La profondeur de champ est également un tel effet, après avoir appris lequel, vous ne pouvez pas vous en passer. D'un point de vue artistique, vous pouvez l'utiliser pour attirer l'attention du spectateur sur un objet spécifique. Mais dans le cas général, la profondeur de champ au prix d'un petit effort ajoute une grande part de réalisme.

En bref


La première étape consiste à rendre la scène complètement nette. Rendez-le à la texture du tampon d'image. Ce sera l'une des valeurs d'entrée pour la profondeur du tampon de champ.

Flou


  // ... vec4 result = vec4(0); for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( blurTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ); xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

La deuxième étape consiste à rendre la scène floue comme si elle était complètement floue. Comme avec Bloom et SSAO, vous pouvez utiliser le flou de boîte. Rendez cette scène défocalisée à la texture du tampon d'image. Ce sera une autre valeur d'entrée pour le shader de profondeur de champ.

Notez que parameters.x est un paramètre de séparation.

Confusion




  // ... float focalLengthSharpness = 100; float blurRate = 6; // ... 

Vous pouvez personnaliser ces options à votre goût. focalLengthSharpness affecte la défocalisation de la scène à la distance focale. Plus la focalLengthSharpness , plus la scène sera défocalisée à la distance focale. blurRate affecte la vitesse de flou de la scène lorsque vous vous éloignez de la distance focale. Plus le blurRate flou est petit, moins la scène sera floue blurRate s'éloignant du point AF.

  // ... vec4 focusColor = texture(focusTexture, texCoord); vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord); // ... 

Nous aurons besoin de couleurs au point et dans une image défocalisée.

  // ... vec4 position = texture(positionTexture, texCoord); // ... 

Nous pouvons également avoir besoin de la position du sommet dans l'espace de visualisation. Vous pouvez réappliquer la texture des positions à partir du tampon de trame utilisé pour SSAO.

  // ... float blur = clamp ( pow ( blurRate , abs(position.y - focalLength.x) ) / focalLengthSharpness , 0 , 1 ); // ... fragColor = mix(focusColor, outOfFocusColor, blur); // ... 

Et ici, la confusion a lieu. Le plus proche blurd'un, plus il utilisera outOfFocusColor. Une valeur de zéro blursignifie que ce fragment est entièrement au point. Avec blur >= 1ce fragment est complètement défocalisé.

Code source



Postérisation



La postérisation, ou échantillonnage des couleurs, est le processus de réduction du nombre de couleurs uniques dans une image. Vous pouvez utiliser ce shader pour donner au jeu un look comique ou rétro. Si vous le combinez avec un contour, vous obtenez un vrai style caricatural.

  // ... float levels = 8; // ... 

Vous pouvez expérimenter avec ce paramètre. Plus il est grand, plus il restera de fleurs.

  // ... vec4 texColor = texture(posterizeTexture, texCoord); // ... 

Nous aurons besoin de la couleur entrante.

  // ... vec3 grey = vec3((texColor.r + texColor.g + texColor.b) / 3.0); vec3 grey1 = grey; grey = floor(grey * levels) / levels; texColor.rgb += (grey - grey1); // ... 

Je n'ai pas vu une telle méthode de postérisation. Après l'avoir vérifié, j'ai vu qu'il crée de plus beaux résultats par rapport aux méthodes conventionnelles. Pour réduire la palette de couleurs, convertissez d'abord la couleur en une valeur en niveaux de gris. Nous discrétisons la couleur en la liant à l'un des niveaux. Nous calculons la différence entre la valeur discrétisée en niveaux de gris et la valeur non discrétisée en niveaux de gris. Ajoutez cette différence à la couleur d'entrée. Cette différence est la quantité par laquelle la couleur doit augmenter / diminuer pour obtenir une valeur discrétisée en niveaux de gris.

  // ... fragColor = texColor; // ... 

N'oubliez pas d'attribuer la valeur de la couleur d'entrée à la couleur du fragment.

Ombrage cel



La postérisation peut donner à une image l'apparence de l'ombrage cel, car l'ombrage cel est le processus de discrétisation des couleurs diffuses et diffuses en nuances discrètes. Nous voulons utiliser uniquement des couleurs diffuses solides sans détails fins de la carte normale et une petite valeur levels.

Code source



Pixélisation



La pixellisation d'un jeu 3D peut lui donner une apparence intéressante, ou vous faire gagner du temps qui aurait été nécessaire pour créer manuellement tout le pixel art. Combinez-le avec la postérisation pour créer un véritable look rétro.

  // ... int pixelSize = 5; // ... 

Vous pouvez ajuster vous-même la taille des pixels. Plus elle est grande, plus l'image sera rugueuse.



  // ... float x = int(gl_FragCoord.x) % pixelSize; float y = int(gl_FragCoord.y) % pixelSize; x = floor(pixelSize / 2.0) - x; y = floor(pixelSize / 2.0) - y; x = gl_FragCoord.x + x; y = gl_FragCoord.y + y; // ... 

Cette technique attache chaque fragment au centre de sa fenêtre de taille de pixel non superposée la plus proche. Ces fenêtres s'alignent au-dessus de la texture entrante. Les fragments au centre de la fenêtre déterminent la couleur des autres fragments de leur fenêtre.

  // ... fragColor = texture(pixelizeTexture, vec2(x, y) / texSize); // ... 

Après avoir déterminé les coordonnées du fragment souhaité à utiliser, prenez sa couleur dans la texture entrante et affectez-la à la couleur du fragment.

Code source



Aiguiser



L'effet de netteté (netteté) augmente le contraste sur les bords de l'image. Il est utile lorsque les graphismes s'avèrent trop mous.

  // ... float amount = 0.8; // ... 

En modifiant la valeur, nous pouvons contrôler l'amplitude de la netteté du résultat. Si la valeur est zéro, l'image ne changera pas. Avec des valeurs négatives, l'image commence à paraître étrange.

  // ... float neighbor = amount * -1; float center = amount * 4 + 1; // ... 

Les fragments adjacents sont multipliés par amount * -1. Le fragment actuel est multiplié par amount * 4 + 1.

  // ... vec3 color = texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 1) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x - 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 0) / texSize).rgb * center + texture(sharpenTexture, vec2(gl_FragCoord.x + 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y - 1) / texSize).rgb * neighbor ; // ... 

Les fragments voisins sont en haut, en bas, à gauche et à droite. Après avoir multiplié les voisins et le fragment actuel par leurs valeurs, le résultat est additionné.

  // ... fragColor = vec4(color, texture(sharpenTexture, texCoord).a); // ... 

Cette quantité est la couleur finale du fragment.

Code source



Grain de film



Le grain du film (à petites doses, et pas comme dans l'exemple) peut ajouter du réalisme, qui est invisible jusqu'à ce que cet effet soit supprimé. Ce sont généralement les imperfections qui rendent l'image générée numériquement plus convaincante.

Notez que le grain du film est généralement le dernier effet appliqué au cadre avant d'être affiché.

Valeur


  // ... float amount = 0.1; // ... 

amountcontrôle la visibilité du grain du film. Plus la valeur est élevée, plus il y a de «neige» sur l'image.

Luminosité aléatoire


 // ... uniform float osg_FrameTime; //... float toRadians = 3.14 / 180; //... float randomIntensity = fract ( 10000 * sin ( ( gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime ) * toRadians ) ); // ... 

Ce morceau de code calcule la luminosité aléatoire nécessaire pour ajuster la valeur.

 Time Since F1 = 00 01 02 03 04 05 06 07 08 09 10 Frame Number = F1 F3 F4 F5 F6 osg_FrameTime = 00 02 04 07 08 

Valeur osg_FrameTime fournie par Panda3D. Un temps d'image est un horodatage contenant des informations sur le nombre de secondes écoulées depuis la première image. L'exemple de code l'utilise pour animer le grain du film, qui osg_FrameTimesera différent dans chaque image.

  // ... ( gl_FragCoord.x + gl_FragCoord.y * 8009 // Large number here. // ... 

Pour les grains statiques, les films doivent être remplacés par un osg_FrameTimegrand nombre. Pour éviter de voir des modèles, vous pouvez essayer différents nombres.



  // ... * sin ( ( gl_FragCoord.x + gl_FragCoord.y * someNumber // ... 

Pour créer des points ou des taches de grain de film, les coordonnées, x et y sont utilisées. Si vous utilisez x, seules les lignes verticales seront affichées, si vous utilisez y, seules les lignes horizontales seront affichées.

Dans le code, une coordonnée est multipliée par une autre pour détruire la symétrie diagonale.


Bien sûr, vous pouvez vous débarrasser du multiplicateur de coordonnées et obtenir un effet de pluie tout à fait acceptable.

Notez que pour animer l'effet pluie, multipliez la sortie sinpar osg_FrameTime.

Expérimentez avec les coordonnées x et y pour changer la direction de la pluie. Pour une douche descendante, ne laissez que la coordonnée x.

 input = (gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime) * toRadians frame(10000 * sin(input)) = fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = 

sinutilisé comme fonction de hachage. Les coordonnées des fragments sont hachées avec les valeurs de sortie sin. Grâce à cela, une propriété pratique apparaît - quelles que soient les données d'entrée (grandes ou petites), l'intervalle de sortie sera compris entre moins un et un.

 fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = fract(6400.723818964882) = 0.723818964882 

sinen combinaison avec fractégalement utilisé comme générateur de nombres pseudo aléatoires.

 >>> [floor(fract(4 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 0, 1, 2, 2, 3, 4, 4, 5, 6] >>> [floor(fract(10000 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 4, 8, 0, 2, 1, 7, 0, 0, 5] 

Regardez d'abord la première rangée de chiffres, puis la seconde. Chaque ligne est déterministe, mais le motif est moins visible dans la seconde que dans la seconde. Par conséquent, malgré le fait que la sortie est fract(10000 * sin(...))déterministe, le modèle est reconnu beaucoup plus faible.


Ici, nous voyons comment le facteur sinest d'abord 1, puis 10, puis 100, puis 1000.

Lorsque le multiplicateur des valeurs de sortie augmente, le sinmodèle devient moins perceptible. Pour cette raison, le code sinest multiplié par 10 000.

Couleur du fragment


  // ... vec2 texSize = textureSize(filmGrainTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 color = texture(filmGrainTexture, texCoord); // ... 

Convertissez les coordonnées du fragment en coordonnées UV. En utilisant ces coordonnées UV, nous recherchons la couleur de texture pour le fragment actuel.

  // ... amount *= randomIntensity; color.rgb += amount; // ... 

Modifiez la valeur en une luminosité aléatoire et ajoutez-la à la couleur.

  // ... fragColor = color; // ... 

Définissez la couleur du fragment, et c'est tout.

Code source



Remerciements


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


All Articles