Rendu en temps réel de l'eau caustique

Dans cet article, je présenterai ma tentative de généralisation du calcul caustique en temps réel à l'aide de WebGL et ThreeJS. Le fait qu'il s'agisse d' une tentative est important, car trouver une solution qui fonctionne dans tous les cas et délivre 60 ips est difficile, voire impossible. Mais vous verrez qu'avec l'aide de ma méthode, vous pouvez obtenir des résultats assez décents.



Qu'est-ce que caustique?



Les caustiques sont des motifs lumineux qui se produisent lorsque la lumière est réfractée et réfléchie par une surface, dans notre cas, à la frontière de l'eau et de l'air.



Parce que la réflexion et la réfraction se produisent sur les ondes de l'eau, l'eau agit ici comme une lentille dynamique, créant ces motifs lumineux.





Dans cet article, nous nous concentrerons sur les caustiques causées par la réfraction de la lumière, ce qui se produit généralement sous l'eau.



Pour atteindre 60 ips stables, nous devons le calculer sur une carte graphique (GPU), nous ne calculerons donc que les caustiques avec des shaders écrits en GLSL.



Pour le calculer, nous avons besoin de:



  • calculer les rayons réfractés à la surface de l'eau (en GLSL c'est facile, car il y a une fonction intégrée pour cela )
  • calculer, à l'aide de l'algorithme d'intersection, les points où ces rayons entrent en collision avec l'environnement
  • calculer la brillance caustique en vérifiant les points de convergence des rayons




Démo bien connue de l'eau sur WebGL



J'ai toujours été étonné par cette démo d'Evan Wallace présentant des caustiques visuellement réalistes de l'eau sur WebGL: madebyevan.com/webgl-water





Je recommande de lire son article Medium , qui explique comment calculer les caustiques en temps réel en utilisant les fonctions de maillage avant léger et GLSL PD . Sa mise en œuvre est extrêmement rapide et a l'air très agréable, mais présente quelques inconvénients: elle ne fonctionne qu'avec une piscine cubique et une boule de billard sphérique . Si vous placez un requin sous l'eau, la démo ne fonctionnera pas: il est codé en dur dans les shaders qu'il y a une boule sphérique sous l'eau.



Il a placé une sphère sous l'eau car le calcul de l'intersection entre un rayon de lumière réfracté et une sphère est une tâche facile qui utilise des mathématiques très simples.



Tout cela est bon pour une démo, mais je voulais créer une solution plus générale. pour calculer les caustiques afin que tout maillage non structuré tel qu'un requin puisse se trouver dans la piscine.





Passons maintenant à ma technique. Pour cet article, je suppose que vous connaissez déjà les bases du rendu 3D avec rastérisation, et que vous savez comment le vertex shader et le fragment shader fonctionnent ensemble pour rendre les primitives (triangles) à l'écran.



Travailler avec des contraintes GLSL



Dans les shaders écrits en GLSL (OpenGL Shading Language), nous ne pouvons accéder qu'à une quantité limitée d'informations sur la scène, par exemple:



  • Attributs du sommet actuellement dessiné (position: vecteur 3D, normal: vecteur 3D, etc.). Nous pouvons transmettre nos attributs GPU, mais ils doivent être du type GLSL intégré.
  • Uniforme , c'est-à-dire des constantes pour tout le maillage actuellement rendu dans l'image courante. Il peut s'agir de textures, de matrice de projection de caméra, de direction d'éclairage, etc. Ils doivent avoir un type intégré: int, float, sampler2D pour les textures, vec2, vec3, vec4, mat3, mat4.


Cependant, il n'y a aucun moyen d'accéder aux maillages présents dans la scène.



C'est pourquoi la démo webgl-water ne peut être réalisée qu'avec une simple scène 3D. Il est plus facile de calculer l'intersection d'un rayon réfracté et d'une forme très simple qui peut être représentée en uniforme. Dans le cas d'une sphère, elle peut être spécifiée par la position (vecteur 3D) et le rayon (flottant), de sorte que ces informations peuvent être transmises aux shaders en utilisant l' uniformité , et le calcul des intersections nécessite des calculs très simples, facilement et rapidement effectués dans le shader.



Certaines techniques de lancer de rayons effectuées dans les shaders rendent les maillages dans des textures, mais en 2020 cette solution n'est pas applicable pour le rendu en temps réel sur WebGL. Il faut se rappeler que pour obtenir un résultat décent, il faut calculer 60 images par seconde avec beaucoup de rayons. Si nous calculons les caustiques en utilisant 256x256 = 65536 rayons, alors chaque seconde, nous devons faire une quantité significative de calculs d'intersection (qui dépend également du nombre de mailles dans la scène).



Nous devons trouver un moyen de représenter l'environnement sous-marin de manière uniforme et calculer l'intersection tout en maintenant une vitesse suffisante.



Créer une carte d'environnement



Lorsque le calcul des ombres dynamiques est nécessaire, le mappage des ombres est une technique bien connue . Il est souvent utilisé dans les jeux vidéo, a une belle apparence et est rapide à exécuter.



Le shadow mapping est une technique à deux passes:



  • Tout d'abord, la scène 3D est rendue en termes de source lumineuse. Cette texture ne contient pas les couleurs des fragments, mais la profondeur des fragments (la distance entre la source lumineuse et le fragment). Cette texture s'appelle une carte d'ombre.
  • La carte d'ombre est ensuite utilisée lors du rendu de la scène 3D. Lors du dessin d'un fragment à l'écran, on sait s'il y a un autre fragment entre la source lumineuse et le fragment actuel. Si tel est le cas, nous savons que le fragment actuel est dans l'ombre et nous devons le dessiner un peu plus sombre.


Vous pouvez en savoir plus sur le shadow mapping dans cet excellent tutoriel OpenGL: www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping .



Vous pouvez également regarder un exemple interactif sur ThreeJS (appuyez sur T pour afficher le shadow map dans le coin inférieur gauche): threejs.org/examples/?q=shadowm#webgl_shadowmap .



Dans la plupart des cas, cette technique fonctionne bien. Il peut fonctionner avec n'importe quel maillage non structuré de la scène.



Au début, j'ai pensé que je pourrais utiliser une approche similaire pour les caustiques de l'eau, c'est-à-dire d'abord rendre l'environnement sous-marin en une texture, puis utiliser cette texture pour calculer l'intersection entre les rayons et l'environnement.... Au lieu de rendre uniquement les profondeurs des fragments, je rend également la position des fragments dans la carte d'environnement.



Voici le résultat de la création d'une carte d'environnement:





Env map: la position XYZ est stockée dans les canaux RVB, la profondeur dans le canal alpha



Comment calculer l'intersection d'un rayon et d'un environnement



Maintenant que j'ai une carte de l'environnement sous-marin, je dois calculer l'intersection entre les rayons réfractés et l'environnement.



L'algorithme fonctionne comme suit:



  • Étape 1: commencez au point d'intersection entre le rayon de lumière et la surface de l'eau
  • Étape 2: calcul de la réfraction à l' aide de la fonction de réfraction
  • Étape 3: aller de la position actuelle dans la direction du rayon réfracté, un pixel dans la texture de la carte d'environnement.
  • Étape 4: Comparez la profondeur d'ambiance enregistrée (stockée dans le pixel actuel de la texture d'ambiance) avec la profondeur actuelle. Si la profondeur de l'environnement est supérieure à la profondeur actuelle, nous devons passer à autre chose, donc nous appliquons à nouveau l' étape 3 . Si la profondeur de l'environnement est inférieure à la profondeur actuelle, cela signifie que le rayon est entré en collision avec l'environnement à la position lue dans la texture de l'environnement et nous avons trouvé une intersection avec l'environnement.




La profondeur actuelle est inférieure à la profondeur de l'environnement: il faut avancer





La profondeur actuelle est supérieure à la profondeur environnante: nous avons trouvé l'intersection



Texture caustique



Après avoir trouvé l'intersection, nous pouvons calculer la luminance caustique (et la texture de luminance caustique) en utilisant la technique décrite par Evan Wallace dans son article . La texture résultante ressemble à ceci:





Texture de luminance caustique (notez que l'effet caustique est moins important sur le requin car il est plus proche de la surface de l'eau, ce qui réduit la convergence des rayons lumineux)



Cette texture contient des informations sur l'intensité lumineuse pour chaque point de l'espace 3D. Lors du rendu de la scène terminée, nous pouvons lire cette intensité lumineuse à partir de la texture caustique et obtenir le résultat suivant:







Une implémentation de cette technique peut être trouvée dans le référentiel Github: github.com/martinRenou/threejs-caustics . Donnez-lui une étoile si vous l'avez aimé!



Si vous voulez voir les résultats du calcul des caustiques, vous pouvez lancer la démo: martinrenou.github.io/threejs-caustics .



À propos de cet algorithme d'intersection



Cette décision dépend fortement de la résolution de la texture de l'environnement . Plus la texture est grande, meilleure est la précision de l'algorithme, mais plus il faut de temps pour trouver une solution (avant de la trouver, vous devez compter et comparer plus de pixels).



De plus, la lecture de la texture dans les shaders est acceptable tant que vous ne le faites pas trop de fois; ici, nous créons une boucle qui continue de lire les nouveaux pixels de la texture, ce qui n'est pas recommandé.



De plus, les boucles while ne sont pas autorisées dans WebGL.(et pour cause), nous devons donc implémenter un algorithme dans une boucle for qui peut être étendue par le compilateur. Cela signifie que nous avons besoin d'une condition de terminaison de boucle connue au moment de la compilation, généralement la valeur «d'itération maximale», qui nous oblige à arrêter de chercher une solution si nous ne l'avons pas trouvée dans le nombre maximum de tentatives. Cette limitation conduit à des résultats caustiques incorrects si la réfraction est trop importante.



Notre technique n'est pas aussi rapide que la méthode simplifiée suggérée par Evan Wallace, mais elle est beaucoup plus flexible que l'approche de traçage de rayons à part entière et peut également être utilisée pour le rendu en temps réel. Cependant, la vitesse dépend toujours de certaines conditions - la direction de la lumière, la luminosité des réfractions et la résolution de la texture ambiante.



Clôture de la revue de démonstration



Dans cet article, nous avons examiné le calcul de la caustique de l'eau, mais d'autres techniques ont été utilisées dans la démo.



Lors du rendu de la surface de l'eau, nous avons utilisé une texture skybox et des cartes cubiques pour obtenir des reflets. Nous avons également appliqué la réfraction à la surface de l'eau en utilisant une simple réfraction de l'espace écran (voir cet article sur les réflexions et réfractions de l'espace écran), cette technique est physiquement incorrecte, mais visuellement convaincante et rapide. Nous avons également ajouté une aberration chromatique pour plus de réalisme.



Nous avons d'autres idées pour améliorer encore la méthodologie, notamment:



  • Aberration chromatique sur les caustiques: Nous appliquons maintenant l'aberration chromatique à la surface de l'eau, mais cet effet devrait également être visible sur les caustiques sous-marines.
  • Diffusion de lumière dans le volume d'eau.
  • Comme Martin Gerard et Alan Wolf l'ont conseillé sur Twitter , nous pouvons améliorer les performances avec des cartes d'environnement hiérarchiques (qui seront utilisées comme quatre arbres pour trouver des intersections). Ils ont également conseillé de rendre les cartes d'environnement en termes de rayons réfractés (en supposant qu'ils soient parfaitement plats), ce qui rendra les performances indépendantes de l'angle d'incidence de l'éclairage.


Remerciements



Ce travail de visualisation réaliste et en temps réel de l'eau a été réalisé chez QuantStack et financé par ERDC .



All Articles