Malgré leur notoriété, la création de niveaux d’eau est une tradition séculaire de l’histoire des jeux vidéo, qu’il s’agisse de bouleverser les mécanismes de jeu ou tout simplement parce que l’eau est si belle à regarder. Il existe différentes façons de créer une sensation sous-marine, allant de visuels simples (comme la teinte bleue à l'écran) à la mécanique (comme un mouvement lent et une gravité faible)..
Nous allons examiner la distorsion comme un moyen de communiquer visuellement la présence d'eau (imaginez que vous vous tenez au bord d'une piscine et que vous observez l'intérieur, c'est le genre d'effet que nous voulons recréer). Vous pouvez consulter une démonstration du look final ici sur CodePen.
Je vais utiliser Shadertoy tout au long du didacticiel pour que vous puissiez suivre directement dans votre navigateur. Je vais essayer de rester assez agnostique sur la plate-forme pour que vous puissiez mettre en œuvre ce que vous avez appris ici sur tout environnement prenant en charge les shaders graphiques. À la fin, je donnerai quelques astuces pour l'implémentation ainsi que le code JavaScript que j'ai utilisé pour implémenter l'exemple ci-dessus avec la bibliothèque de jeux Phaser..
Cela peut paraître un peu compliqué, mais l’effet lui-même n’est que quelques lignes de code! Ce n'est rien de plus que différents effets de déplacement combinés ensemble. Nous allons commencer à partir de zéro et voir exactement ce que cela signifie.
Rendez-vous sur Shadertoy et créez un nouveau shader. Avant de pouvoir appliquer une distorsion, nous devons restituer une image. Les didacticiels précédents nous ont appris qu'il suffit de sélectionner une image dans l'un des canaux du bas de la page et de la mapper à l'écran avec texture2D:
vec2 uv = fragCoord.xy / iResolution.xy; // Obtenir la position normalisée du pixel actuel fragColor = texture2D (iChannel0, uv); // Récupère la couleur du pixel actuel dans la texture et la définit à la couleur à l'écran
Voici ce que j'ai choisi:
Maintenant que se passe-t-il si au lieu de simplement rendre le pixel à la position uv
, nous rendons le pixel en uv + vec2 (0.1.0.0)
?
Il est toujours plus facile de penser à ce qui se passe sur un seul pixel lorsque vous travaillez avec des shaders. Quelle que soit la position de l’écran, au lieu de dessiner la couleur d’origine dans la texture, elle dessine la couleur d’un pixel à sa droite. Cela signifie que, visuellement, tout est déplacé la gauche. L'essayer!
Par défaut, Shadertoy définit le mode habillage de toutes les textures sur répéter. Donc, si vous essayez d’échantillonner un pixel à droite du pixel le plus à droite, il se retournera tout simplement. Ici, je l'ai changé pour serrer (que vous pouvez faire à partir de l'icône d'engrenage dans la case où vous avez sélectionné la texture).
Défi: Pouvez-vous déplacer l’image entière lentement vers la droite? Pourquoi ne pas aller et venir? Qu'en est-il en cercle?
Astuce: Shadertoy vous donne une variable de temps d'exécution appelée iGlobalTime.
Déplacer une image entière n’est pas très excitant et n’exige pas la puissance hautement parallèle du GPU. Et si au lieu de déplacer chaque position d'une quantité fixe (telle que 0,1), nous avons déplacé différents pixels de différentes quantités?
Nous avons besoin d'une variable qui est en quelque sorte unique pour chaque pixel. Toute variable que vous déclarez ou uniforme que vous transmettez ne variera pas entre les pixels. Heureusement, nous avons déjà quelque chose qui varie comme ceci: le pixel X et y. Essaye ça:
vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Déplace le y de x pixel du pixel en cours = texture2D (iChannel0, uv);
Nous décalons verticalement chaque pixel de sa valeur x. Les pixels les plus à gauche auront le décalage le plus faible (0), tandis que les pixels les plus à droite auront le décalage maximum (1).
Nous avons maintenant une valeur qui varie dans l’image de 0 à 1. Nous l’utilisons pour réduire les pixels, nous obtenons donc cette inclinaison. Maintenant pour ton prochain défi!
Défi: Pouvez-vous utiliser cela pour créer une vague? (Comme sur la photo ci-dessous)
Astuce: votre variable offset va de 0 à 1. Vous voulez qu'elle passe périodiquement de -1 à 1. La fonction cosinus / sinus est un choix parfait pour cela.
Si vous avez compris l'effet de vague, essayez de le faire bouger en le multipliant par notre variable de temps! Voici ma tentative jusqu'ici:
vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25.) * 0,06 * cos (iGlobalTime); fragColor = texture2D (iChannel0, uv);
Je multiplie uv.x par un grand nombre (25) pour contrôler la fréquence de l’onde. Je l'ai ensuite réduit en le multipliant par 0,06, ce qui correspond à l'amplitude maximale. Enfin, je multiplie par le cosinus du temps, pour le faire basculer périodiquement.
Remarque: si vous voulez vraiment confirmer que notre distorsion suit une onde sinusoïdale, remplacez-la par 0,06 et par 1,0 et observez-la au maximum.!
Défi: Pouvez-vous comprendre comment le faire bouger plus rapidement?
Astuce: C'est le même concept que nous avons utilisé pour augmenter la fréquence de la vague spatialement.
Pendant que vous y êtes, vous pouvez également essayer d’appliquer la même chose pour uv.x aussi, donc il est déformant à la fois sur le x et le y (et peut-être changer le cos pour le péché).
Maintenant ça est remuer dans un mouvement de vague, mais quelque chose ne va pas. Ce n'est pas tout à fait comme ça que l'eau se comporte…
L'eau doit avoir l'air de couler. Ce que nous avons en ce moment, c'est juste des allers-retours. Examinons à nouveau notre équation:
Notre fréquence ne change pas, ce qui est bon pour le moment, mais nous ne voulons pas non plus que notre amplitude change. Nous voulons que la vague garde la même forme, mais bouge toi à travers l'écran.
Pour voir où dans notre équation nous voulons compenser, réfléchissez à ce qui détermine le début et la fin de la vague. uv.x est la variable dépendante dans ce sens. Partout où uv.x est pi / 2, il n'y aura pas de déplacement (puisque cos (pi / 2) = 0), et où uv.x est d'environ pi / 2, ce sera le déplacement maximum.
Modifions un peu notre équation:
Maintenant, notre amplitude et notre fréquence sont fixes, et la seule chose qui varie est la position de l’onde elle-même. Avec ce peu de théorie à l'écart, le temps d'un défi!
Défi: implémenter cette nouvelle équation et ajuster les coefficients pour obtenir un joli mouvement ondulé.
Voici mon code pour ce que nous avons jusqu'à présent:
vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25. + iGlobalTime) * 0,01; uv.x + = cos (uv.y * 25. + iGlobalTime) * 0,01; fragColor = texture2D (iChannel0, uv);
Maintenant, c’est essentiellement le cœur de l’effet. Cependant, nous pouvons continuer à peaufiner les choses pour que le résultat soit encore meilleur. Par exemple, il n’ya aucune raison pour que vous modifiiez l’onde par la seule coordonnée x ou y. Vous pouvez changer les deux, cela varie donc en diagonale! Voici un exemple:
float X = uv.x * 25. + iGlobalTime; float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01; uv.x + = sin (X-Y) * 0,01;
Cela semblait un peu répétitif alors j'ai changé le second cos pour un péché pour réparer ça. Pendant que nous y sommes, nous pouvons aussi essayer de varier légèrement l’amplitude:
float X = uv.x * 25. + iGlobalTime; float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01 * cos (Y); uv.x + = sin (X-Y) * 0,01 * sin (Y);
Et c'est à peu près tout ce que j'ai obtenu, mais vous pouvez toujours composer et combiner davantage de fonctions pour obtenir des résultats différents!
La dernière chose que je veux mentionner dans le shader est que dans la plupart des cas, vous devrez probablement appliquer l'effet à une partie de l'écran plutôt qu'à la totalité. Un moyen facile de le faire est de passer un masque. Ce serait une image qui mappe les zones de l'écran qui devraient être affectées. Ceux qui sont transparents (ou blancs) peuvent ne pas être affectés et les pixels opaques (ou noirs) peuvent produire l’effet complet..
Dans Shadertoy, vous ne pouvez pas télécharger d'images arbitraires, mais vous pouvez effectuer le rendu dans un tampon séparé et le transmettre sous forme de texture. Voici un lien Shadertoy où j'applique l'effet ci-dessus à la moitié inférieure de l'écran.
Le masque que vous passez n'a pas besoin d'être une image statique. Cela peut être une chose complètement dynamique. tant que vous pouvez la restituer en temps réel et la transmettre au shader, votre eau peut se déplacer ou couler de manière transparente sur tout l'écran..
J'ai utilisé Phaser.js pour implémenter ce shader. Vous pouvez consulter la source dans ce CodePen en direct ou télécharger une copie locale de ce référentiel..
Vous pouvez voir comment je passe les images manuellement sous forme d'uniformes et je dois également mettre à jour la variable de temps moi-même..
Le plus grand détail d'implémentation auquel il faut réfléchir est celui auquel appliquer ce shader. Dans l'exemple Shadertoy et dans mon exemple JavaScript, je n'ai qu'une image au monde. Dans un jeu, vous allez probablement en avoir beaucoup plus.
Phaser vous permet d'appliquer des shaders à des objets individuels, mais vous pouvez également l'appliquer à l'objet world, ce qui est beaucoup plus efficace. De même, sur une autre plate-forme, il pourrait être judicieux de rendre tous vos objets dans un tampon et de le passer à travers le shader de l'eau, au lieu de l'appliquer à chaque objet individuel. De cette façon, il fonctionne comme un effet de post-traitement.
J'espère que la composition de ce shader à partir de zéro vous a donné une bonne idée de la manière dont beaucoup d'effets complexes sont construits en superposant tous ces différents petits déplacements.!
En guise de dernier défi, voici une sorte de nuage d’ondes à l’eau qui repose sur les mêmes idées de déplacement que nous avons déjà vues. Vous pouvez essayer de la démonter, déplier les couches et comprendre ce que fait chaque pièce.!