Rendu d'eau

Nous avons voulu améliorer le rendu existant en matérialisant les surfaces d'eau que peuvent comporter un terrain. De la même manière que pour le texturage du terrain, nous avons utilisé des shaders Cg pour accomplir cette tâche.

Détection et matérialisation des lacs

Quand on parle de surfaces d'eau, surtout sur notre planète, on pense forcément aux océans et aux mers qui par rapport à un terrain sont généralement les zones de plus basse altitude et donc des zones facilement détectables. Cependant, pour ce projet, nous avons essentiellement travaillé sur la région des Alpes qui ne comporte ni mers ni océans mais plutôt des lacs qui sont à des altitudes différentes. Il devenait donc indispensable de pouvoir détecter sur le terrain les zones correspondantes aux lacs et de les matérialiser afin de pouvoir leurs appliquer les shaders correspondants.

Utilisation d'un masque pour la détection des lacs

Nous avons créé, à partir de la heightmap et d'un atlas, une image en noir et blanc de la taille de la heightmap utilisée et nous avons dessiné en noir les zones de lacs et en blanc les zones de terrains. Ensuite, nous avons utilisé un algorithme pour récupérer les contours de ce lac. Vous pouvez voir sur l'image ci-dessous le résultat de cette opération avec à gauche le masque de départ et à droite le résultat de la récupération des contours de ce lac :

masqueLacs
Masque du lac et récupération du contour

Création d'un format Lac

Une fois les contours du lac détectés, il suffit de récupérer les points noirs dans l'ordre qui convient et de les stocker dans un fichier .lac. Chaque lac a alors un fichier qui lui correspond. C'est ce dernier qui va permettre de créer les fonds marins et le polygone correspondant au lac lui-même.

Abaissement du terrain pour les fonds marins

Le fichier correspondant à un lac spécifie des points qui sont positionnés correctement par rapport à la heightmap utilisée. Pour baisser les fonds marins, il suffit donc de parcourir les points contenus dans le fichier et d'accéder pour chaque point à sa hauteur dans le maillage pour pouvoir la changer. On récupère la hauteur minimale de tous les points parcourus ce qui nous donne l'altitude du lac que l'on va ensuite créer. Le résultat de l'abaissement des fonds marins est illustré par la figure ci-dessous :

abaissementLacs
Abaissement des fonds marins

Création du polygône du lac

De la même manière que pour l'abaissement des fonds marins, on utilise le fichier .lac pour créer le polygône correspondant au lac. Les points sont donnés ligne par ligne et nous avons géré des lacs un peu particuliers puisque l'on impose qu'il n'y ait que deux points par ligne ce qui permet de créer un triangle strip directement en donnant la liste de points du fichier. Il faudrait évidemment retravailler l'algorithme de détection et la création de ce polygône si toutefois on avait des lacs qui ne présentent pas ces particularités. Vous pouvez voir ci dessous le résultat de la création du polygone du lac et son intégration dans le terrain une fois ce dernier abaissé :

lac
Ajout du polygone du lac

Calcul de la réflexion et animation des lacs

Propriétés visuelles des étendues d´eau

Maintenant que nous disposons d'un maillage qui sait où se trouvent les lacs, l'étape suivante consiste à donner à ces zones, pour l'instant bleues uniformes, la consistance d'une étendue d'eau. Mais à quoi reconnaît-on qu'une étendue a la consistance de l'eau ? Tout d'abord, l'environnement s'y reflète si on est proche de la surface. Ensuite, ce reflet est déformé par les ondulations de l'eau. Illustration par l'exemple :

Eau Miroir
Exemple de surface d'eau réflechissante

Modélisations de l´eau

Voyons maintenant comment se modélise ce phénomène naturel lorsqu'on projette de le représenter sur ordinateur. Pour modéliser l'eau avec des shaders, il y a deux approches qui sont très répandues :

  1. La première consiste à utiliser des volumes de particules qui s'entrechoquent, de manière à recréer l'illusion d'un volume de fluide. On obtient ainsi une simulation de fluide proche du modèle naturel. Elle présente l'inconvénient d'être gourmande en ressources et rend difficile un rendu en temps réel d'étendues un tant soit peu vastes (y compris lorsqu'on choisit d'utiliser des particules plus grosses afin d'avoir moins de calculs à faire). Elle paraît néanmoins appropriée si le rendu de l'eau est l'objectif principal du projet. Dans le cadre de notre projet, où l'eau n'est qu'un élément parmi tant d'autres suffisamment coûteux, nous ne pouvions pas nous permettre un tel luxe.
  2. La deuxième technique utilise un plan réfléchissant, ne s'appuyant ainsi que sur les propriétés optiques de l'eau pour en donner l'illusion. C'est cette option que nous avons choisi afin de préserver notre framerate. Bien sûr, cela implique quelques inconvénients inhérents au mode de représentation choisi : si l'angle d'incidence est trop rasant, on voit clairement que l'eau n'a pas de relief.

Fort de ces observations, nous avons travaillé sur deux aspects différents :

  1. réaliser le plan réfléchissant ;
  2. animer la surface de reflets spéculaires mouvants.

Réalisation du plan refléchissant - Plan A

L'idée est la suivante : dans la partie OpenGL de notre programme, on décompose notre rendu en deux passes.

Première passe

On se place sous l'eau et on fait un rendu vers texture. Cela signifie que ce qui est observé depuis cette position est stocké dans une texture.

planA
plan A
Deuxième passe

On fait le rendu de la scène du point de vue de l'observateur en sa position normale. En texturant la surface avec la texture obtenue précédemment, on obtient le reflet de la scène sur l'étendue de l'eau.

Cette technique est appropriée, en théorie, pour les grandes étendues tels que les mers et océans. En grande partie parce que pour ces étendues, on ne prend pas la peine de modéliser le fond marin. Il se trouve que nous modélisons des lacs et non des océans. Notre maillage est encore présent et il nous empêche de générer le rayon réfléchi.

planA rate
Pourquoi le plan A n'est pas bon

Réalisation du plan refléchissant - Plan B

Il nous a donc fallu trouver un autre moyen d'avoir une surface réfléchissante. L'idée trouvée est la suivante : se placer au centre du lac et prendre six vues de notre environnement (haut, bas, gauche, droit, devant, derrière) pour recréer un cube d'environnement, ou cube map. Voilà à quoi ressemble une vue :

Vue
Une vue de notre environnement

Voilà à quoi ressemblent ces vues déroulées :

Vues deroulees
Les vues déroulées

À partir de ces six vues qu'on va stocker comme un cube d'environnement, on pourra calculer la réflexion (éventuellement la réfraction mais ce n'est pas à l'ordre du jour). C'est une technique très répandue dans la programmation de shaders.

Animation de la surface

Dans la réalité l'eau ne se contente pas de refléter son environnement, elle ondule également. C'est pourquoi si l'on se contente de la réflexion on aura au final un rendu plus proche de celui d'un miroir que de celui d'une surface d'eau. Pour avoir un rendu réaliste il faut donc, en plus, simuler les ondulations à la surface de l'eau. Pour cela nous avons envisagé différentes méthodes et nous avons retenue celle qui nous semblait être le meilleur compromis entre complexité et qualité de rendu.

Dans cette partie nous allons vous présenter, dans un premier temps, succinctement, les techniques de rendu des ondulations que nous avons envisagées puis, dans un deuxième temps, nous détaillerons plus précisément celle que nous avons choisi d'implémenter.

Méthodes existantes

La première méthode à avoir été envisagée consiste à modifier la hauteur des vertices puis à recalculer leur normale afin de créer un mouvement ondulatoire sur l'ensemble du maillage puis à calculer la réflexion à l'aide d'une cubemap. Elle s'effectue en trois passes de programmes Cg. Durant la première, la force appliquée a chaque vertex est calculée à partie de sa hauteur, passée en paramètre au programme Cg par le biais d'une texture. Le résultat est sauvegardé dans une couleur. Enfin on récupère l'affichage du framebuffer (qui représente les forces appliquées à chaque vertex) qui est rendu dans une texture. La deuxième passe permet de calculer la vélocité de chaque vertex à partir de la texture de force obtenue précédemment. Le résultat est récupèré comme avant (via l'affichage du framebuffer et sauvegardé dans une texture).

Enfin durant la dernière passe on calcule la nouvelle hauteur des vertices grâce à la texture de vélocité. Puis il faut également recalculer les normales de chaque vertex. Ensuite la réflexion se calcule classiquement à l'aide d'une cubemap d'environnement. Cependant nous pensions alors implémenter le calcul de réflexion en déplaçant la caméra, comme c'était prévu au départ (cf. plan A). Cela s'adaptait mal à cette méthode, c'est pourquoi nous avons songé à utiliser la méthode qui suit.

La deuxième méthode est assez proche de la première (bien que plus ancienne l'article date de janvier 1999 contre 2004 pour le premier), elle consiste aussi à modifier la hauteur des vertices pour créer un mouvement ondulatoire (bien que le site source implémente l'animation du maillage en C++, nous aurions procédé comme dans la méthode précédente). Par contre au lieu de calculer la réflexion à l'aide d'une cubemap, on colore les pixels en fonction de la position de leur voisins, afin de créer un éclairement artificiel. Le résultat ainsi obtenu peut être combiné avec une texture de fonds quelconque, en l'occurrence dans notre cas la texture issue du calcul de la réflexion.

L'avantage de ces techniques est qu'elles donnent, surtout la première, un rendu très réaliste. Cependant l'animation du maillage présente deux inconvénients à nos yeux. D'une part elle est complexe car elle nécessite de passer à un vertex des informations sur ses voisins (hauteur...), ce qui en Cg est compliqué (il faut ruser en les passant dans des textures ou dans des couleurs par exemple). D'autre part elle est coûteuse, car il faut recalculer les hauteurs et les normales à chaque affichage.

Méthode implémentée

La méthode que nous avons choisi d'implémenter simule des ondulations sur une surface plane. Elle consiste à jouer sur la couleur pour donner une impression de mouvement. Pour cela on déplace une texture de bruit que l'on combine avec la réflexion. Tout le traitement s'effectue dans le shader de fragment.

Nous utilisons une texture de bruit (appelée Noise dans le shader de fragment), en niveaux de gris comme dans la figure ci-dessous. Il est important que cette texture ait en plus la propriété d'être morcelable mais nous y reviendrons plus loin.

bruit
Texture de bruit

Pour créer l'effet d'ondulation l'idée est de dupliquer la texture de bruit puis de déplacer indépendamment les deux textures résultantes. Le traitement étant fait dans le shader de fragment, le travail se fait au niveau du pixel. On modifie une première fois les coordonnées du pixel courant, puis à partir de celles-ci on récupère la couleur de la texture de bruit, que l'on stocke dans une première variable. On réitère l'opération mais en modifiant différemment les coordonnées du pixel courant (afin que les deux textures dupliquées ne bougent pas de la même façon), puis on stocke la couleur dans une deuxième variable. Pour modifier les coordonnées nous utilisons des fonctions périodiques, en l'occurrence ici sinus et cosinus, que nous faisons varier à l'aide d'un paramètre t, modifié dans le .cpp à chaque ré-affichage, et que l'on passe en paramètre au shader de fragment.

Ensuite nous combinons les deux couleurs obtenues précédemment avec celle issue de la réflexion afin de la perturber. Cela nous donne un effet de mouvement, cependant il manque encore des effets spéculaires afin de rendre le tout plus réaliste. Pour faire cela nous avons recours à un gradient comme sur la figure ci-dessous, qui nous permet à la fois d'accentuer les couleurs claires afin qu'elles tendent vers le blanc, pour simuler les tâches spéculaires, et d'ajouter un peu de bleu à la couleur de réflexion pour lui donner un côté aquatique. Ci-dessous un petit schéma explicatif :

animEau
Schéma récapitulatif

Ce traitement n'est pas appliqué directement à toute la surface d'eau, il est répété plusieurs fois afin de couvrir son ensemble. C'est pourquoi le fait que la texture de bruit soit morcelable est important, cela permet de ne pas voir de cassures entre les différents plaquages d'animation. Ci-après le rendu final :

renduEau
Rendu final