Implémentation de nuages ​​volumétriques physiquement corrects comme dans Horizon Zero Dawn

Auparavant, les nuages ​​dans les jeux étaient dessinés avec des sprites 2D ordinaires, qui sont toujours tournés dans le sens de la caméra, mais ces dernières années, de nouveaux modèles de cartes vidéo vous permettent de dessiner des nuages ​​physiquement corrects sans pertes de performances notables. On pense que des nuages ​​volumineux dans le jeu ont amené le studio Guerrilla Games avec le jeu Horizon Zero Dawn. Bien sûr, de tels nuages ​​pouvaient être rendus auparavant, mais le studio a formé quelque chose comme une norme industrielle pour les ressources sources et les algorithmes utilisés, et maintenant toute implémentation de nuages ​​volumétriques est en quelque sorte conforme à cette norme.



L'ensemble du processus de rendu des nuages ​​est très bien divisé en étapes et il est important de noter qu'une mise en œuvre inexacte, même sur l'une d'entre elles, peut entraîner de telles conséquences qu'il ne sera pas clair où se trouve l'erreur et comment la corriger, il est donc conseillé de faire une conclusion de contrôle du résultat à chaque fois.

Cartographie des tons, sRGB


Avant de commencer à travailler avec l'éclairage, il est important de faire deux choses:

  1. Avant d'afficher l'image finale à l'écran, appliquez au moins le mappage de tons le plus simple:

    tunedColor=color/(1+color) 

    Cela est nécessaire car les valeurs de couleur calculées seront beaucoup plus grandes que l'unité.
  2. Assurez-vous que le tampon d'image final dans lequel vous dessinez et affiché à l'écran est au format sRGB. Si l'activation du mode sRGB pose problème, la conversion peut être effectuée manuellement dans le shader:

     finalColor=pow(color, vec3(1.0/2.2)) 

    La formule convient à la plupart des cas, mais pas à 100% selon le moniteur. Il est important que la conversion sRGB soit toujours effectuée en dernier.

Modèle d'éclairage


Considérons un espace rempli de matière partiellement transparente de différentes densités. Lorsqu'un rayon de lumière traverse une telle substance, il est exposé à quatre effets: absorption, diffusion, diffusion amplificatrice et auto-rayonnement. Ce dernier se produit dans le cas de processus chimiques dans une substance et n'est pas affecté ici.

Supposons que nous ayons un rayon de lumière qui traverse la matière d'un point A à un point B:


L'absorption

La lumière traversant une substance est absorbée par cette même substance. La fraction de lumière non absorbée peut être trouvée par la formule:


- la lumière restant au point après absorption . - pointez sur le segment AB à distance de A.

Diffusion

Une partie de la lumière sous l'influence de particules de matière change de direction. La fraction de lumière qui n'a pas changé de direction peut être trouvée par la formule:


- fraction de lumière qui n'a pas changé de direction après diffusion en un point .

L'absorption et la dispersion doivent être combinées:


Fonction appelé atténuation ou extinction. Une fonction - fonction de transfert. Il montre combien de lumière reste lors du passage du point A au point B.

En ce qui concerne et : , où C est une certaine constante, qui peut avoir une valeur différente pour chaque canal en RVB, Est la densité du milieu au point .

Maintenant, compliquons la tâche. La lumière se déplace du point A au point B, elle s'éteint pendant le mouvement. Au point X, une partie de la lumière est diffusée dans différentes directions, une des directions correspond à l'observateur au point O. Ensuite, une partie de la lumière diffusée se déplace du point X au point O et s'humidifie à nouveau. Le chemin de la lumière AXO nous intéresse.


La perte de lumière lors du passage de A à X, nous savons: , tout comme nous savons la perte de lumière de X à O - ce . Mais qu'en est-il de la fraction de lumière qui sera diffusée en direction de l'observateur?

Dispersion d'amplification

Si dans le cas d'une diffusion ordinaire, l'intensité lumineuse diminue, alors dans le cas d'une diffusion amplificatrice, elle augmente du fait de la diffusion de la lumière qui s'est produite dans les régions voisines. La quantité totale de lumière provenant des régions voisines peut être trouvée par la formule:


signifie prendre l'intégrale sur la sphère, - fonction de phase - lumière venant de la direction .

Il est assez difficile de calculer la lumière de toutes les directions, cependant, nous savons que la partie originale de la lumière est transportée par notre faisceau AB d'origine. La formule peut être grandement simplifiée:


- l'angle entre le faisceau lumineux et le faisceau d'observation (c'est-à-dire l'angle AXO), - la valeur initiale de l'intensité lumineuse. En résumant tout ce qui précède, nous obtenons la formule:


- lumière entrante - la lumière atteignant l'observateur.

Nous compliquons un peu plus la tâche. Disons que la lumière est émise par une lumière directionnelle, c'est-à-dire le soleil:


Tout se passe comme dans le cas précédent, mais plusieurs fois. La lumière du point A1 est diffusée au point X1 vers l'observateur au point O, la lumière du point A2 est diffusée au point X2 vers l'observateur au point O, etc. On voit que la lumière atteignant l'observateur est égale à la somme:


Ou une expression intégrale plus précise:


Il est important de comprendre qu'ici , c'est-à-dire le segment est divisé en un nombre infini de sections de longueur nulle.

Le ciel


Avec une légère simplification, un rayon de soleil traversant l'atmosphère ne subit que la diffusion, c'est-à-dire .


Et même pas un type de diffusion, mais deux: la diffusion de Rayleigh et la diffusion Mi. La première est causée par des molécules d'air, et la seconde est causée par un aérosol d'eau.

La densité totale de l'air (ou aérosol) à travers lequel passe un rayon de lumière, se déplaçant du point A au point B:
- hauteur d'échelle, h - hauteur actuelle.

Une solution intégrale simple serait:

où dh est la taille de pas avec laquelle l'échantillon de hauteur est prélevé.

Regardez maintenant la figure et utilisez la formule dérivée dans la partie précédente du «modèle d'éclairage»:


L'observateur regarde de O à O '. Nous voulons collecter toute la lumière qui atteint les points X1, X2, ..., Xn, qui y est dispersée, puis atteint l'observateur:


l'intensité de la lumière émise par le soleil, - hauteur au point ; dans le cas du ciel, constante C, qui est en fonction désigné comme .

La solution de l'intégrale peut être la suivante:

Cette formule est valable pour la diffusion de Rayleigh et la diffusion de Mie. En conséquence, les valeurs lumineuses pour chacune des diffusions s'additionnent simplement:


Rayleigh Dispersion



(contient des valeurs pour chaque canal RVB)



Résultat:


Mi scatter



(les valeurs pour tous les canaux RVB sont les mêmes)



Résultat:


Le nombre d'échantillons par segment et sur le segment Vous pouvez en prendre 32 et plus. Le rayon de la Terre est de 6371000 m, l'atmosphère est de 100000 m.

Que faire de tout ça:

  1. Dans chaque pixel de l'écran, nous calculons la direction de l'observateur V
  2. On prend la position de l'observateur O égale à {0, 6371000, 0}
  3. Nous trouvons à la suite de l'intersection du rayon originaire du point O et de la direction de V et de la sphère centrée au point {0,0,0} et d'un rayon de 6471000
  4. Segment de ligne diviser en 32 sections de longueur égale
  5. Pour chaque section, nous calculons la diffusion Rayleigh et la diffusion Mie, et ajoutons tout. De plus, pour calculer nous devrons également diviser le segment 32 parcelles égales dans chaque cas. peut être lu à travers une variable dont la valeur augmente à chaque étape du cycle.

Le résultat final:


Modèle cloud


Nous aurons besoin de plusieurs types de bruit en 3D. Le premier est le bruit du mouvement brownien fractal (fBm) de Perlin:

Résultat pour une tranche 2D:


Le deuxième est le bruit fBm de camouflage de Voronoi.

Résultat pour une tranche 2D:


Pour obtenir le bruit de masquage fBm de Vorley, vous devez inverser le bruit de masquage fBm de Voronoj. Cependant, j'ai légèrement modifié les plages de valeurs à ma discrétion:

 float fbmTiledWorley3(...) { return clamp((1.0-fbmTiledVoronoi3(...))*1.5-0.25, 0.0, 1.0); } 

Le résultat ressemble immédiatement aux structures des nuages:


Pour les nuages, vous devez obtenir deux textures spéciales. Le premier a une taille de 128x128x128 et est responsable du bruit basse fréquence, le second a une taille de 32x32x32 et est responsable du bruit haute fréquence. Chaque texture utilise un seul canal au format R8. Dans certains exemples, 4 canaux de R8G8B8A8 sont utilisés pour la première texture et trois canaux de R8G8B8 pour la seconde, puis les canaux sont mélangés dans un shader. Je ne vois pas l’intérêt, car le mixage peut être fait à l’avance, obtenant ainsi un plus grand succès dans la cohérence du cache.

Pour le mixage, et aussi à certains endroits, la fonction remap () sera utilisée, qui met à l'échelle les valeurs d'une plage à l'autre:

 float remap(float value, float minValue, float maxValue, float newMinValue, float newMaxValue) { return newMinValue+(value-minValue)/(maxValue-minValue)*(newMaxValue-newMinValue); } 

Commençons à préparer la texture avec un bruit basse fréquence:
Canal R - bruit fBm de Perlin
Canal G - bruit FBm Vorley en mosaïque
Canal B - bruit Worley fBm plus petit avec une échelle plus petite
Canal A - Bruit fBm variable de Varley avec une échelle encore plus petite


Le mixage se fait de cette façon:

 finalValue=remap(noise.x, (noise.y * 0.625 + noise.z*0.25 + noise.w * 0.125)-1, 1, 0, 1) 

Résultat pour une tranche 2D:


Maintenant, préparez la texture avec un bruit haute fréquence:
Canal R - bruit FBm Vorley en mosaïque
Canal G - Bruit Vorley fBm à plus petite échelle
Canal B - Bruit Varley taylivaya fBm avec une échelle encore plus petite


 finalValue=noise.x * 0.625 + noise.y*0.25 + noise.z * 0.125; 

Résultat pour une tranche 2D:


Nous avons également besoin d'une carte texture-météo 2D qui déterminera la présence, la densité et la forme des nuages, en fonction des coordonnées de l'espace. Il est peint par des artistes pour affiner la couverture nuageuse. L'interprétation des canaux de couleur de la carte météo peut être différente, dans la version que j'ai prêtée, elle est la suivante:


Canal R - couverture nuageuse à basse altitude
Canal G - couverture nuageuse à haute altitude
Canal B - hauteur maximale des nuages
Canal A - densité des nuages

Nous sommes maintenant prêts à créer une fonction qui renverra la densité des nuages ​​en fonction des coordonnées de l'espace 3D.

A l'entrée, un point dans l'espace avec coordonnées en km

 vec3 position 

Ajoutez immédiatement le décalage au vent

 position.xz+=vec2(0.2f)*ufmParams.time; 

Obtenez les valeurs de la carte météo

 vec4 weather=textureLod(ufmWeatherMap, position.xz/4096.0f, 0); 
On obtient le pourcentage de hauteur (de 0 à 1)

 float height=cloudGetHeight(position); 

Ajoutez un petit arrondi des nuages ​​ci-dessous:
 float SRb=clamp(remap(height, 0, 0.07, 0, 1), 0, 1); 
Nous faisons une diminution linéaire de la densité à 0 avec une hauteur croissante selon le canal B de la carte météo:

 float SRt=clamp(remap(height, weather.b*0.2, weather.b, 1, 0), 0, 1); 
Combinez le résultat:

 float SA=SRb*SRt; 

Ajoutez à nouveau l'arrondi des nuages ​​ci-dessous:

 float DRb=height*clamp(remap(height, 0, 0.15, 0, 1), 0, 1); 

Ajoutez également l'arrondi des nuages ​​sur le dessus:

 float DRt=height*clamp(remap(height, 0.9, 1, 1, 0), 0, 1); 
Nous combinons le résultat, nous ajoutons ici l'influence de la densité de la carte météo et l'influence de la densité, qui est définie via gui:

 float DA=DRb*DRt*weather.a*2*ufmProperties.density; 

Combinez le bruit basse fréquence et haute fréquence de nos textures:

 float SNsample=textureLod(ufmLowFreqNoiseTexture, position/48.0f, 0).x*0.85f+textureLod(ufmHighFreqNoiseTexture, position/4.8f, 0).x*0.15f; 

Dans tous les documents que j'ai lus, la fusion se déroule différemment, mais j'ai bien aimé cette option.

Nous déterminons la quantité de couverture (% du ciel occupé par les nuages), qui est définie via gui, les canaux R et G de la carte météo sont également utilisés:

 float WMc=max(weather.r, clamp(ufmProperties.coverage-0.5, 0, 1)*weather.g*2); 

Calculez la densité finale:

 float d=clamp(remap(SNsample*SA, 1-ufmProperties.coverage*WMc, 1, 0, 1), 0, 1)*DA; 

Fonction entière:

 float cloudSampleDensity(vec3 position) { position.xz+=vec2(0.2f)*ufmParams.time; vec4 weather=textureLod(ufmWeatherMap, position.xz/4096.0f+vec2(0.2, 0.1), 0); float height=cloudGetHeight(position); float SRb=clamp(remap(height, 0, 0.07, 0, 1), 0, 1); float SRt=clamp(remap(height, weather.b*0.2, weather.b, 1, 0), 0, 1); float SA=SRb*SRt; float DRb=height*clamp(remap(height, 0, 0.15, 0, 1), 0, 1); float DRt=height*clamp(remap(height, 0.9, 1, 1, 0), 0, 1); float DA=DRb*DRt*weather.a*2*ufmProperties.density; float SNsample=textureLod(ufmLowFreqNoiseTexture, position/48.0f, 0).x*0.85f+textureLod(ufmHighFreqNoiseTexture, position/4.8f, 0).x*0.15f; float WMc=max(weather.r, clamp(ufmProperties.coverage-0.5, 0, 1)*weather.g*2); float d=clamp(remap(SNsample*SA, 1-ufmProperties.coverage*WMc, 1, 0, 1), 0, 1)*DA; return d; } 

Quelle doit être exactement cette fonction est une question ouverte, car en ignorant les lois auxquelles les nuages ​​obéissent lors de la définition des paramètres, vous pouvez obtenir un résultat très inhabituel et magnifique. Tout dépend de l'application.


Intégration


L'atmosphère de la Terre est divisée en deux couches: interne et externe, entre lesquelles les nuages ​​peuvent être situés. Ces couches peuvent être représentées par des sphères, mais aussi par des plans. Je me suis installé sur les sphères. Pour la première couche, j'ai pris le rayon de sphère de 6415 km, pour la deuxième couche, le rayon de 6435 km. Le rayon de la terre arrondi à 6400 km. Certains paramètres dépendront de l'épaisseur conditionnelle de la partie «nuageuse» de l'atmosphère (20 km).



Contrairement au ciel, les nuages ​​sont opaques et l'intégration nécessite non seulement d'obtenir la couleur, mais également d'obtenir la valeur du canal alpha. Vous avez d'abord besoin d'une fonction qui renvoie la densité totale du nuage à travers laquelle passera un rayon de lumière du soleil.


Personne n'attire l'attention sur cela, mais la pratique a montré qu'il n'est pas du tout nécessaire de prendre en compte l'intégralité du trajet du faisceau, seul l'écart le plus extrême est nécessaire. Nous supposons que les nuages ​​au-dessus d'un segment tronqué n'existent pas du tout.


De plus, nous sommes très limités dans le nombre d'échantillons de densité qui peuvent être réalisés sans nuire aux performances. Guerrilla Games do 6. De plus, dans l'une des présentations, le développeur a déclaré qu'ils dispersent ces échantillons à l'intérieur du cône, et le dernier échantillon est spécialement conçu très loin du reste pour couvrir autant d'espace que possible. Les imprécisions et le bruit qui en résultent seront toujours lissés sur le fond des échantillons voisins, et cela, au contraire, se traduira par une précision accrue.


Au final, je me suis installé sur 4 échantillons qui se trouvent sur la même ligne, mais cette dernière est prise avec un pas multiplié par 6. Le pas est de 20 km * 0,01, soit 200 m.

La fonction est assez simple:

 float cloudSampleDirectDensity(vec3 position, vec3 sunDir) { //   float avrStep=(6435.0-6415.0)*0.01; float sumDensity=0.0; for(int i=0;i<4;i++) { float step=avrStep; //      6 if(i==3) step=step*6.0; //  position+=sunDir*step; //  ,  ,   //  float density=cloudSampleDensity(position)*step; sumDensity+=density; } return sumDensity; } 

Vous pouvez maintenant passer à la partie la plus difficile. Nous déterminons l'observateur à la surface de la Terre au point {0, 6400,0} et trouvons l'intersection du faisceau d'observation avec une sphère de rayon 6415 km et de centre {0,0,0} - nous obtenons le point de départ S.


Voici la version de base de la fonction:

 vec4 mainMarching(vec3 viewDir, vec3 sunDir) { vec3 position; crossRaySphereOutFar(vec3(0.0, 6400.0, 0.0), viewDir, vec3(0.0), 6415.0, position); float avrStep=(6435.0-6415.0)/64.0; for(int i=0;i<128;i++) { position+=viewDir*step; if(length(position)>6435.0) break; } return vec4(0.0); } 

La taille de l'étape est définie comme 20 km / 64, c'est-à-dire dans le cas de la direction strictement verticale du faisceau de l'observateur, nous réaliserons 64 échantillons. Cependant, lorsque cette direction est plus horizontale, les échantillons seront légèrement plus grands, il n'y a donc pas 64 étapes dans le cycle, mais 128 avec une marge.

Au début, nous supposons que la couleur finale est le noir et la transparence est l'unité. À chaque étape, nous augmenterons la valeur de couleur et diminuerons la valeur de transparence. Si la transparence est proche de 0, vous pouvez pré-quitter la boucle:

 vec3 color=vec3(0.0); float transmittance=1.0; … //    //      float density=cloudSampleDensity(position)*avrStep; //   ,   //   float sunDensity=cloudSampleDirectDensity(position, sunDir); //      float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*m2*m3; //       color+=sunColor*light*transmittance; transmittance*=exp(-ufmProperties.attenuation*density); … return vec4(color, 1.0-transmittance); 

ufmProperties.attenuation - Il n'y a que C dans et ufmProperties.attenuation2 est C dans . ufmProperties.sunIntensity - l'intensité de rayonnement du soleil. sunColor - la couleur du soleil.

Résultat:


Un défaut est immédiatement évident - un ombrage sévère. Mais maintenant, nous allons corriger le manque d'éclairage amplifié près du soleil. Cela s'est produit parce que nous n'avons pas ajouté de fonction de phase. Pour calculer la diffusion de la lumière traversant les nuages, la fonction de phase de Hengy-Greenstein est utilisée, qui l'a ouverte en 1941 pour des calculs similaires dans des amas de gaz dans l'espace:


Une digression devrait être faite ici. Selon le modèle d'éclairage canonique, la fonction de phase devrait être une. Cependant, en réalité, le résultat obtenu ne convient à personne et chacun utilise des fonctions à deux phases, et combine même ses valeurs de manière particulière. Je me suis également concentré sur les fonctions à deux phases, mais j'additionne simplement leurs valeurs. La fonction de la première phase a g proche de 1 et vous permet de faire un éclairage lumineux près du soleil. La fonction de la deuxième phase a g proche de 0,5 et vous permet de diminuer progressivement l'illumination dans toute la sphère céleste.

Code mis à jour:

 // cos(theta) float mu=max(0, dot(viewDir, sunDir)); float m11=ufmProperties.phaseInfluence*cloudPhaseFunction(mu, ufmProperties.eccentrisy); float m12=ufmProperties.phaseInfluence2*cloudPhaseFunction(mu, ufmProperties.eccentrisy2); float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*(m11+m12)*m2*m3; 

ufmProperties.eccentrisy, ufmProperties.eccentrisy2 sont des valeurs g

Résultat:


Vous pouvez maintenant commencer le combat avec trop d'ombrage. Elle est présente car nous n'avons pas tenu compte de la lumière des nuages ​​environnants et du ciel, qui est dans la vraie vie.

J'ai résolu ce problème comme ceci:

 return vec4(color+ambientColor*ufmProperties.ambient, 1.0-transmittance); 

Où ambientColor est la couleur du ciel dans la direction du faisceau d'observation, ufmProperties.ambient est le paramètre de réglage.

Résultat:


Reste à résoudre le dernier problème. Dans la vraie vie, plus la vue est horizontale, plus nous voyons un certain brouillard ou brume qui ne nous permet pas de voir des objets très éloignés. Cela doit également se refléter dans le code. J'ai pris le cosinus habituel de l'angle de regard et de la fonction exponentielle. Sur cette base, un certain coefficient de fusion est calculé, ce qui permet une interpolation linéaire entre la couleur résultante et la couleur d'arrière-plan.

 float blending=1.0-exp(-max(0.0, dot(viewDir, vec3(0.0,1.0,0.0)))*ufmProperties.fog); blending=blending*blending*blending; return vec4(mix(ambientColor, color+ambientColor*ufmProperties.ambient, blending), 1.0-transmittance); 

ufmProperties.fog - pour une configuration manuelle.


Fonction récapitulative:

 vec4 mainMarching(vec3 viewDir, vec3 sunDir, vec3 sunColor, vec3 ambientColor) { vec3 position; crossRaySphereOutFar(vec3(0.0, 6400.0, 0.0), viewDir, vec3(0.0), 6415.0, position); float avrStep=(6435.0-6415.0)/64.0; vec3 color=vec3(0.0); float transmittance=1.0; for(int i=0;i<128;i++) { float density=cloudSampleDensity(position)*avrStep; if(density>0.0) { float sunDensity=cloudSampleDirectDensity(position, sunDir); float mu=max(0.0, dot(viewDir, sunDir)); float m11=ufmProperties.phaseInfluence*cloudPhaseFunction(mu, ufmProperties.eccentrisy); float m12=ufmProperties.phaseInfluence2*cloudPhaseFunction(mu, ufmProperties.eccentrisy2); float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*(m11+m12)*m2*m3; color+=sunColor*light*transmittance; transmittance*=exp(-ufmProperties.attenuation*density); } position+=viewDir*avrStep; if(transmittance<0.05 || length(position)>6435.0) break; } float blending=1.0-exp(-max(0.0, dot(viewDir, vec3(0.0,1.0,0.0)))*ufmProperties.fog); blending=blending*blending*blending; return vec4(mix(ambientColor, color+ambientColor*ufmProperties.ambient, blending), 1.0-transmittance); } 

Vidéo de démonstration:


Optimisation et améliorations possibles


Après avoir implémenté l'algorithme de rendu de base, le problème suivant est qu'il fonctionne trop lentement. Ma version a produit 25 fps en full hd sur la radeon rx 480. Les deux approches suivantes pour résoudre le problème ont été suggérées par Guerrilla Games lui-même.

Nous dessinons ce qui est vraiment visible

L'écran est divisé en tuiles de 16 x 16 pixels. Tout d'abord, l'environnement 3D habituel est dessiné. Il s'avère que la majeure partie du ciel est couverte de montagnes ou de gros objets. Par conséquent, vous devez effectuer le calcul uniquement dans les tuiles dans lesquelles les nuages ​​ne sont bloqués par rien.

Reprojection

Lorsque la caméra est immobile, il s'avère que les nuages ​​en général ne peuvent pas être mis à jour. Cependant, si la caméra a bougé, cela ne signifie pas que nous devons mettre à jour tout l'écran. Tout est déjà dessiné, il vous suffit de reconstruire l'image en fonction des nouvelles coordonnées. La recherche d'anciennes coordonnées sur de nouvelles, à travers les matrices de projection et de visualisation des images actuelles et précédentes, s'appelle projection. Ainsi, dans le cas d'un décalage de caméra, nous transférons simplement les couleurs en fonction des nouvelles coordonnées. Dans les cas où ces coordonnées indiquent hors écran, les nuages ​​doivent être redessinés honnêtement.

Mise à jour partielle

Je n'aime pas l'idée de la reprojection, car avec un virage serré de la caméra, il peut s'avérer que les nuages ​​devront être rendus pour un tiers de l'écran, ce qui peut provoquer un décalage. Je ne sais pas comment Guerrilla Games a géré cela, mais au moins dans Horizon Zero Dawn, lorsque vous contrôlez le joystick, la caméra se déplace en douceur et il n'y a aucun problème avec des sauts brusques. Par conséquent, en tant qu'expérience, j'ai trouvé ma propre approche. Les nuages ​​sont dessinés sur une carte cubique, en 5 faces, car le fond ne nous intéresse pas.Le côté de la carte cubique a une résolution réduite égale aux ⅔ de la hauteur de l'écran. Chaque face de la carte cubique est divisée en tuiles 8x8. Chaque image sur chaque face est mise à jour avec un seul des 64 pixels dans chaque mosaïque. Cela donne des artefacts perceptibles lors de changements soudains, mais parce que les nuages ​​sont assez statiques, alors une telle astuce est invisible. En conséquence, le radeon rx 480 produit 500 fps en full hd pour le volcan et 330 fps pour opengl. La série Radeon hd 5700 produit 109 images par seconde en full hd sous opengl (vulkan ne prend pas en charge).

Utilisation des niveaux de mip

Lorsque vous accédez à des textures avec du bruit, vous pouvez prendre des données du niveau de mip zéro uniquement dans les tout premiers échantillons, puis plus les échantillons que nous faisons sont élevés, plus le niveau de mip peut être élevé.

Nuages ​​élevés

Pour simuler la présence de cirrus-altitude et de cirrocumulus dans Guerrilla Games lors de l'intégration, les derniers échantillons ne sont pas fabriqués à partir des textures 3D dont j'ai parlé, mais à partir d'une texture 2D spéciale.


Bruit

de courbure Plusieurs textures supplémentaires dans le bruit de courbure sont utilisées pour créer l'effet des nuages ​​de vent. Ces textures sont nécessaires pour déplacer les coordonnées d'origine.


Rayons divins


Ces rayons, captant les drames, sont réalisés en post-traitement. Tout d'abord, un éclairage brillant est dessiné autour du soleil, où il n'est pas bloqué par les nuages. Ensuite, ce rétro-éclairage doit être radialement décalé par rapport au soleil.


Vous devez maintenant appliquer un lissage radial.


En fait, il y a beaucoup plus d'améliorations et de subtilités, mais je ne les ai pas toutes vérifiées, donc je ne peux pas les dire avec confiance. Cependant, vous pouvez vous familiariser avec eux. Le plus fort je pense est la documentation cloud du moteur Frostbite.

Liens utiles


Guerrilla Games
d1z4o56rleaq4j.cloudfront.net/downloads/assets/Nubis-Authoring-Realtime-Volumetric-Cloudscapes-with-the-Decima-Engine-Final.pdf?mtime=20170807141817
killzone.dl.playstation.net/killzone/horizonzerodawn/presentations/Siggraph15_Schneider_Real-Time_Volumetric_Cloudscapes_of_Horizon_Zero_Dawn.pdf
www.youtube.com/watch?v=-d8qT5-1LOI

GPU Pro 7
vk.com/doc179245989_437393482?hash=a9af5f665eda4edf58&dl=806d4dbdac0f7a761c


www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/simulating-sky/simulating-colors-of-the-sky

Frostbite
media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf
www.shadertoy.com/view/XlBSRz

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


All Articles