Shader d’écume semi-procédural


Bonjour à tous,

Comme d’hab, ça fait un moment que je n’ai pas posté de tutoriel technique, donc en voici enfin un! Aujourd’hui, on s’intéresse à la création de l’écume sur les rivages. Je détaillerai ici mon approche et les différents choix que j’ai faits pour créer ça:

20190114_finalMixing

Le rendu final de l’écume dans le jeu.

J’utilise Unity comme moteur de jeu, Blender pour la modélisation et le texturing, et Krita pour peindre les textures.

I – Problème

Mon jeu se déroule dans un monde recouvert d’océan; avoir de l’écume sur les rivages paraît donc assez indispensable. Mon plus gros soucis est que mon jeu est encore en cours de modification et je peux rajouter ou modifier des îles assez régulièrement. Donc, il me faut trouver une méthode simple et efficace, pour ne pas avoir à tout refaire à partir de zéro à chaque itération.

 

Il y a quelques temps, des amis m’ont conseillé la vidéo d’explication des effets graphiques de Rime, de Simon Trümpler. Il y a des techniques excellentes dans cette présentation. L’écume en particulier et très bien conçue et le rendu est vraiment impressionnant (pour qui cherche un visuel stylisé évidement).

Le seul problème que je vois dans cette méthode est le temps nécessaire pour créer les uv maps « seamless » (=répétables=sans bords=cycliques) pour les maillages d’écume. En effet, dans Blender, si l’on veut un maillage circulaire avec un texturing sans « cassure », il faut parfaitement aligner les bords de l’uv-map. C’est tout à fait acceptable pour un seul ou quelques objets, mais j’ai énormément d’îles et de rochers dans mon jeu, trop pour pouvoir utiliser cette méthode. En plus de ça, avec un uv mapping cyclique, il est difficile d’ajuster l’échelle des textures après coup, car on est limité à des multiples entiers. Je dois donc trouver une autre approche.

20190113_scalingUV

Processus de mise à l’échelle de l’uv map pour supprimer la cassure du texturing. Réussir un alignement parfait peut être un peu long si l’on a beaucoup d’objets à traiter..

II – Objectif

Idéalement, mon objectif est:

  • d’avoir une méthode facile de création d’écume dans Blender, avec aussi peu de clics que possible. Dans le meilleur des cas, j’aimerais n’avoir à me préoccuper que de la modélisation et pas de l’uv mapping.
  • d’avoir des vagues qui bougent sans un motif répété de manière trop évidente
  • d’avoir plusieurs paramètres pour modifier facilement la longueur d’onde des vagues, leur vitesse ou leur apparence
  • Graphiquement, comme j’utilise du deferred shading qui est souvent un peu complexe à combiner avec de la transparence, je veux une couleur pure pour l’écume avec des bords nets. J’utiliserai donc du clipping en fonction de la valeur de mes textures. Voici ce que j’aimerai avoir:

20190113_foamGoal2

III – Texture de base et coordonnées dans le repère monde

Comme d’habitude, mon approche est loin d’être parfaite et consiste en un compromis entre ce qui me tient à cœur et ce qui est techniquement atteignable.

Ma 1ère idée est d’utiliser des coordonnées de texturing automatiques, comme la position des sommets dans le repère monde. Ça marche déjà plutôt bien pour mon rendu de terrain (j’espère que je trouverai le temps d’en parler plus longuement un de ces jours), mais les coordonnées « monde » ne portent aucune information sur une direction privilégiée. Cela signifie que je ne peux pas utiliser une texture d’écume « classique » avec des lignes qui suivent le rivage (comme ci-dessous). L’effet pourrait effectivement marcher sur un côté de l’île mais pas de tous les côtés en même temps.

uvFoam 20190113_foamWorldCoordsUnity

Dans cette texture, il y a une direction principale forte (horizontale). L’image du rendu dans le jeu n’est pas très convaincante.

Si j’utilise du mapping automatique, il faut une texture relativement neutre/uniforme en terme de directions, par exemple:

uvFoamWorld 20190113_foamWorldCoordsUnityBetter

Une texture d’écume relativement simple sans direction privilégiée. Le rendu dans le jeu est un peu mieux……

OK, c’est mieux, mais encore un peu décevant. C’est le prix à payer avec une approche automatique; il va donc falloir ajouter autre chose.

IV – Vagues procédurales sur la coordonnée v

Si je veux des vagues le long du rivage, ou de l’écume qui se rapproche (ou s’éloigne) du rivage, il est nécessaire d’avoir une information supplémentaire sur la « direction » principale, et cette information doit être contenue dans le maillage (le mesh). J’en ai également besoin pour faire disparaître les vagues au fur et à mesure qu’elle s’éloignent. Cette direction change pour chaque triangle affiché, il faut donc la stocker au niveau des sommets.

On dirait que je n’ai pas le choix, je vais quand même devoir faire un uv mapping basique pour stocker cette « direction » du rivage. Mais je n’ai besoin que d’une seule coordonnée de texture; et je choisis arbitrairement v (voir image ci-dessous). L’idée est d’avoir v=0 sur la ligne d’arête extérieure (du côté de l’océan), v=1 sur la ligne centrale (du côté du rivage) et v=2 à l’intérieur de l’île.

20190113_foamVMapping2

Le mapping que j’ai choisi: ici, je ne me préoccupe pas du problème de « coutûre » (en bas du mesh). Seul le mapping sur v m’importe.

Grâce à cette coordonnée, je vais pouvoir construire le mouvement des vagues. L’approche la plus simple pour créer ces vagues et d’utilise un sinus de v. Pour le faire bouger, j’y ajoute un offset dépendant du temps et de la vitesse des vagues.

20190116_sinus 20190113_movingSinus

sinus(fréquence*v + vitesse*temps) ; clippé à 0

Gardons à l’esprit que pour avoir des contours nets, je « clippe » (cache) chaque « fragment » (pixel) en dessous d’un certain seuil.

20190114_movingSinusLoweredThresholdChange

Changer le seuil de clipping génère des vagues plus ou moins grosses

20190114_sinusGlobalFrequencyChangePrinciple

Changer la fréquence génère plus ou moins de vagues

Si je veux que l’écume disparaisse progressivement dans la mer, il me suffit d’ajouter la coordonnée v au sinus, comme ceci:

20190116_sinusPlusV 20190113_movingSinusLowered

sinus(fréquence*v + vitesse*temps)+v ; clippé au bon seuil s. Les vagues deviennent de plus en plus fines au fur et à mesure qu’elles avancent dans la mer

Maintenant, on peut s’amuser à rajouter du bruit sur ce shader de base, pour le rendre un peu plus « naturel ». Comme base de bruit, j’utilise une simple texture répétable faite dans GIMP grâce au filtre « solid noise »:

tileable2DWave

Bruit répétable fait avec GIMP.

A partir de ça, on peut altérer la vitesse, la fréquence ou tout autre paramètre du sinus. Bruiter la fréquence est particulièrement intéressant puisque ça permet de rajouter localement plus ou moins de vagues, comme ceci:

20190113_movingSinusLoweredFrequencyChange

Ajoutons un bruit (en coordonnées monde) à la fréquence: sinus((fréquence+bruit(xz))*v + vitesse*temps)+v; où xz sont les coordonnées monde horizontales. Certaines zones se retrouvent ainsi avec plus de vagues que d’autres.

Ces vagues procédurales ont l’air d’être une bonne base, mais je trouve qu’il manque quelques « connections » entre les différentes lignes, et, si je veux les rajouter, je dois exploiter les coordonnées « monde ». Je pourrai aussi utiliser un bruit pour les générer, mais j’ai envie d’essayer une approche un peu plus « contrôlée » en y superposant une texture d’écume peinte à la main.

20190114_movingSinusLoweredPlusBaseTex

Mélange des vagues procédurales (à base de sinus) et de texture peinte à la main.

Mais si j’utilise une texture binaire, je n’ai aucun contrôle sur son atténuation progressive. Je vais donc utiliser des textures de « transformation en distance » (« distance transform » en anglais).

V – Texture « Distance transform »

Une image de transformée en distance est habituellement créée à partir d’un motif noir et blanc. Chaque pixel de l’image « distance transform » représente la distance à un pixel blanc dans le motif original, comme ceci:

20190114_distanceTransformExplained

Bon, en fait, j’utilise une transformée en distance inversée ici, car c’est plus pratique pour l’étape de clipping, mais vous saisissez l’idée. Et oui, j’ai utilisé un motif différent de celui du début de cet article.

Les images de transformée en distance sont juste une manière de voir les données. On peut aussi les interpréter comme des « cartes de hauteur » (même si la signification est un peu différente, mais ça aide à saisir le concept). L’idée de base est d’avoir une image qui incorpore une forme d’ « évolution » / « animation » du motif initial.

Les images de transformée en distance sont généralement créée au travers de filtres, mais généralement ces filtres ne gèrent pas très bien (voire pas du tout) les images répétables. Une approche pour créer ce genre d’images consiste à les peindre directement. Bien sûr elles sont rarement exactes mathématiquement dans ce cas là, mais en contrepartie, on peut créer des animations plus variées/originales. Une méthode que j’ai fini par adopter dans Krita (le logiciel de peinture numérique que j’utilise) est la suivante:

  • Utiliser le mode de peinture « Build-up » (par opposition à « Wash » par défaut), pour que chaque coup de brosse puisse s’intersecter lui même correctement
  • Utiliser le mode de fusion de brosse « Greater », pour peindre uniquement les valeurs plus grandes que celles déjà existantes (c’est d’ailleurs la vraie manière de fusionner 2 images de transformée en distance)
  • Utiliser une brosse aux bords flous avec une pente linéaire de 100% à 0%, pour exploiter la plus grande plage de valeurs possibles. Cela permet d’avoir les animations les plus lisses possibles. En fait, le profil de la brosse agit directement comme les paramètres « ease in » et « ease out » d’une animation..
  • Pas de sensibilité à la pression (mais c’est plutôt un choix personnel)
  • Activer l’option « Wrap Around » qui permet de créer des textures répétables (je peins beaucoup de textures dans Krita, et c’est clairement mon option favorite).

Voilà ce que ça donne en cours d’utilisation:

20190114_drawingDistanceTransform

Peinture directe d’une image de transformée en distance. Le motif en croix au niveau de l’intersection est typique d’une image transformée en distance.

 

Bien sûr, ceci est une manière assez « mathématique » d’aborder la chose. On peut évidemment utiliser une brosse douce par défaut et obtenir quelque chose de tout à fait correct (et sûrement plus naturel), mais il faut toujours garder à l’esprit que l’on « peint l’animation » (je ne pensais pas qu’on pouvait dire ça…)

VI – Mélange des textures et réglages

Maintenant, j’ai un peu plus de contrôle sur ma texture faite à la main:

20190114_movingFoamThreshold2

tex(xz); clippé à un certain seuil s; Maintenant, je peux choisir la largeur des vagues en changeant le seuil de clipping.

De plus, j’ai utilisé quelques trucs pour rendre le visuel un peu plus intéressant:

  • Ajouter la coordonnée v (pour avoir une écume plus large près de la côte et plus fine côté océan), comme pour le sinus
  • Un paramètre de vitesse vitesseTexture pour faire bouger la texture doucement
  • Un peu de bruit animé en coordonnées monde pour créer un effet de déformation

20190114_finalHandMadeFoam

tex(xz + bruit(xz+vitesseBruit*temps)+vitesseTexture*temps)+v; clippé à s

Et finalement, je peux mélanger la texture procédurale et la texture peinte. Après pas mal de réglages et de test, on aboutit à ça:

20190114_finalMixing

Le résultat final mélangeant texture peinte et texture procédurale:
ratioSinus * [sinus((fréquence+bruit(xz))*v + vitesse*temps)+v] +
ratioTex * [tex(xz + bruit(xz+vitesseBruit*temps)+vitesseTexture*temps)+v];
clippé
à s.

Enfin, j’ai également ajouté quelques paramètres pour contrôler l’amplitude et la fréquence des bruits. Donc chaque bruit(xz…) est en réalité amplitudeBruit*bruit(fréquenceBruit*xz…). Cela permet un meilleur réglage des déformations. J’ai aussi remplacé le simple « +v » par la fonction affine « +a*v+b » plus générique.

VII – Création dans Blender

Au final, je dois toujours faire un uv mapping (enfin plutôt un v mapping). Mais la bonne nouvelle, c’est que je ne dois le faire qu’UNE SEULE FOIS pour tout le jeu. En effet, pour chaque nouvelle île (ou objet immergé), il me suffit de copier le modèle de base qui a déjà un uv mapping correct, et je n’ai qu’à modifier le maillage:

  • Si je bouge des sommets, le mapping sera toujours correct.
  • Si je supprime une ligne d’arêtes, le mapping des voisins n’est pas affecté.
  • Si je rajoute une ligne d’arêtes, l’uv mapping de ces nouveaux sommets sera automatiquement interpolé depuis les voisins, ce qui donnera une coordonnée v correcte.

Ça n’aurait pas été le cas si j’avais aussi utilisé la coordonnée u via un uv mapping plus classique, car le fait de bouger les sommets aurait étiré la texture associée (et j’aurais eu le problème des « coutures » sur chaque nouvelle île).

VIII – Pour aller plus loin…

On pourrait travailler quelques pistes pour améliorer tout ça:

  • Empiler 2 couches (ou +) de textures faites à la main, avec différentes vitesses et fréquence pour créer un motif global encore plus unique partout
  • Bruiter la vitesse du sinus pour avoir des variations locales de flux
  • Bruiter le seuil ou la fonction affine (« +v »/« +a*v+b ») pour qu’il y ait de l’écume sur une zone plus ou moins avancée dans la mer
  • Bruiter les coordonnées monde pour encore plus de déformations
  • Utiliser un sinus sur l’offset de temps pour faire en sorte que les vagues aillent et viennent dans les 2 sens (à l’heure actuelle, elles vont juste vers l’océan)

Au passage, je n’ai pas du tout parlé d’optimisation ici, il y aurait surement un gros travail à faire (notamment sur les textures de bruit qui pourraient exploiter moins de canaux que le classique RGB).

En tout cas, cette expérience aura été très intéressante pour moi, et même si le résultat n’est pas exceptionnel, j’ai vraiment apprécié de mélanger une approche mathématique avec des considérations plus artistiques.

Merci d’avoir lu!

Paix!

 

Subscribe by email to get all the news!

Support me on Patreon

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *