Créer de l'eau pour le Web troisième partie

Bienvenue dans cette série en trois parties sur la création d’une eau stylisée dans PlayCanvas à l’aide de shaders de vertex. Dans la partie 2, nous avons couvert les lignes de flottabilité et de mousse. Dans cette dernière partie, nous allons appliquer la distorsion sous-marine comme effet post-traitement..

Effets de réfraction et de post-traitement

Notre objectif est de communiquer visuellement la réfraction de la lumière à travers l’eau. Nous avons déjà expliqué comment créer ce type de distorsion dans un fragment shader dans un précédent tutoriel pour une scène 2D. La seule différence est que nous devrons déterminer quelle partie de l'écran est sous l'eau et appliquer uniquement la distorsion à cet endroit.. 

Post-traitement

En général, un effet de post-traitement est tout ce qui est appliqué à la scène entière après son rendu, tel qu'une teinte de couleur ou un ancien effet d'écran CRT. Au lieu de rendre votre scène directement à l'écran, vous devez d'abord la restituer dans un tampon ou une texture, puis à l'écran, en passant par un shader personnalisé..

Dans PlayCanvas, vous pouvez créer un effet de post-traitement en créant un nouveau script. Appeler Refraction.js, et copiez ce modèle pour commencer avec:

// --------------- DÉFINITION POST-EFFET ------------------------ // pc.extend ( pc, function () // Constructor - Crée une instance de notre post-effet var RefractionPostEffect = function (graphicsDevice, vs, fs, tampon) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // c'est la définition du shader pour notre effet this.shader = new pc.Shader (graphicsDevice, attributs: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.buffer = tampon;; // Notre effet doit provenir de pc.PostEffect RefractionPostEffect = pc.inherits (RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Chaque effet de message doit implémenter le rendu méthode qui // définit les paramètres que le shader peut exiger et // rend également l'effet sur le rendu à l'écran: function (inputTarget, outputTarget, rect) var device = this.device; var scope = device.scope; // Set th L'entrée rend la cible au shader. Ceci est l'image rendue par notre caméra scope.resolve ("uColorBuffer"). SetValue (inputTarget.colorBuffer); // Dessine un quad plein écran sur la cible de sortie. Dans ce cas, la cible de sortie est l'écran. // Dessiner un quad plein écran exécutera le shader que nous avons défini ci-dessus pc.drawFullscreenQuad (device, outputTarget, this.vertexBuffer, this.shader, rect); ); return RefractionPostEffect: RefractionPostEffect;  ()); // --------------- DEFINITION DE SCRIPT ------------------------ // var Refraction = pc. createScript ('réfraction'); Refraction.attributes.add ('vs', type: 'asset', assetType: 'shader', titre: 'Vertex Shader'); Refraction.attributes.add ('fs', type: 'actif', assetType: 'shader', titre: 'Fragment Shader'); // initialise le code appelé une fois par entité Refraction.prototype.initialize = function () var effect = new pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource); // ajoute l'effet à la file d'attente postEffects de la caméra var queue = this.entity.camera.postEffects; queue.addEffect (effet); this.effect = effect; // Enregistrer les shaders actuels pour le rechargement à chaud this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ; Refraction.prototype.update = function () if (this.savedFS! = This.fs.resource || this.savedVS! = This.vs.resource) this.swap (this); ; Refraction.prototype.swap = function (old) this.entity.camera.postEffects.removeEffect (old.effect); this.initialize (); ;

Ceci est juste comme un script normal, mais nous définissons un RefractionPostEffect classe qui peut être appliquée à la caméra. Cela nécessite un rendu et un fragment shader. Les attributs sont déjà configurés, alors créons Refraction.frag avec ce contenu:

float highp de précision; uniform sampler2D uColorBuffer; variant vec2 vUv0; void main () vec4 color = texture2D (uColorBuffer, vUv0); gl_FragColor = couleur;  

Et Refraction.vert avec un vertex shader de base:

attribut vec2 aPosition; variant vec2 vUv0; void main (void) gl_Position = vec4 (aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1,0) * 0,5;  

Maintenant, attachez le Refraction.js script à la caméra et attribuer les shaders aux attributs appropriés. Lorsque vous lancez le jeu, vous devriez voir la scène exactement comme avant. Ceci est un effet de post vide qui restitue simplement le rendu de la scène. Pour vérifier que cela fonctionne, essayez de donner à la scène une teinte rouge..

Dans Refraction.frag, au lieu de simplement renvoyer la couleur, essayez de définir le composant rouge sur 1.0, ce qui devrait ressembler à l'image ci-dessous..

Distorsion Shader

Nous devons ajouter un uniforme temporel pour la distorsion animée, alors n'hésitez plus et créez-en un dans Refraction.js, à l'intérieur de ce constructeur pour l'effet post:

var RefractionPostEffect = fonction (graphicsDevice, vs, fs) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // c'est la définition du shader pour notre effet this.shader = new pc.Shader (graphicsDevice, attributs: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); // >>>>>>>>>>>>> Initialisez l'heure ici this.time = 0; ;

Maintenant, à l'intérieur de cette fonction de rendu, nous le passons à notre shader et l'incrémentons:

RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Chaque effet de publication doit implémenter la méthode de rendu // qui définit les paramètres que le shader peut exiger et // rend également l'effet sur le rendu à l'écran: function (inputTarget, outputTarget, rect) var device = this.device; var scope = device.scope; // définit la cible de rendu d'entrée sur le shader. C'est l'image rendue par notre caméra scope.resolve ("uColorBuffer"). setValue (inputTarget .colorBuffer); /// >>>>>>>>>>>>>>>>>> Passez l'uniforme de temps ici scope.resolve ("uTime"). setValue (this.time); this.time + = 0.1; // Dessine un quad en plein écran sur la cible en sortie. Dans ce cas, la cible en sortie est l’écran. // Dessiner un quad en plein écran exécute le shader défini ci-dessus pc.drawFullscreenQuad (device, outputTarget, this. vertexBuffer, this.shader, rect););

Maintenant, nous pouvons utiliser le même code de shader du didacticiel de distorsion de l'eau, donnant à notre fragment shader complet l'aspect suivant:

float highp de précision; uniform sampler2D uColorBuffer; uniforme float uTime; variant vec2 vUv0; void main () vec2 pos = vUv0; float X = pos.x * 15. + uTime * 0.5; float Y = pos.y * 15. + uTime * 0.5; pos.y + = cos (X + Y) * 0,01 * cos (Y); pos.x + = sin (X-Y) * 0,01 * sin (Y); vec4 color = texture2D (uColorBuffer, pos); gl_FragColor = couleur;  

Si tout a fonctionné, tout devrait maintenant ressembler à de l'eau, comme ci-dessous..

Défi n ° 1: appliquer la distorsion uniquement à la moitié inférieure de l'écran.

Masques de caméra

Nous y sommes presque. Il ne nous reste plus qu'à appliquer cet effet de distorsion uniquement sur la partie sous-marine de l'écran. Le moyen le plus simple que j’ai proposé de faire est de re-rendre la scène avec la surface de l’eau restituée en blanc, comme indiqué ci-dessous..

Cela serait rendu à une texture qui agirait comme un masque. Nous transmettrions ensuite cette texture à notre matériau de réfraction, qui ne déformerait un pixel de l'image finale que si le pixel correspondant du masque est blanc..

Ajoutons un attribut booléen à la surface de l’eau pour savoir s’il est utilisé comme masque. Ajoutez ceci à Water.js:

Water.attributes.add ('isMask', type: 'boolean', titre: "Is Mask?");

Nous pouvons ensuite le transmettre au shader avec material.setParameter ('isMask', this.isMask); comme d'habitude. Puis déclarez-le dans Water.frag et définissez la couleur en blanc si c'est vrai.

// Déclarer le nouvel uniforme en haut de l'uniforme bool isMask; // À la fin de la fonction principale, remplacez la couleur par le blanc // si le masque est vrai si (isMask) color = vec4 (1.0); 

Confirmez que cela fonctionne en basculant sur "Is Mask?" propriété dans l'éditeur et relancer le jeu. Il devrait sembler blanc, comme dans l'image précédente.

Maintenant, pour refaire le rendu de la scène, nous avons besoin d’une deuxième caméra. Créez une nouvelle caméra dans l'éditeur et appelez-la CameraMask. Dupliquez également l'entité Eau dans l'éditeur et appelez-la. Masque d'eau. Assurez-vous que le "Is Mask?" est faux pour l'entité Eau mais vrai pour le Masque d'eau.

Pour indiquer à la nouvelle caméra de rendre une texture à la place de l’écran, créez un nouveau script appelé CameraMask.js et fixez-le à la nouvelle caméra. Nous créons un RenderTarget pour capturer la sortie de cette caméra comme ceci:

// initialise le code appelé une fois par entité CameraMask.prototype.initialize = function () // Crée une cible de rendu 512x512x24 bits avec un tampon de profondeur var colorBuffer = new pc.Texture (this.app.graphicsDevice, width: 512, hauteur: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = new pc.RenderTarget (this.app.graphicsDevice, colorBuffer, profondeur: true); this.entity.camera.renderTarget = renderTarget; ;

Maintenant, si vous lancez, vous verrez que cette caméra ne rend plus le rendu à l'écran. Nous pouvons saisir la sortie de sa cible de rendu dans Refraction.js comme ça:

Refraction.prototype.initialize = function () var cameraMask = this.app.root.findByName ('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = new pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer); //… // Le reste de cette fonction est le même qu'avant;

Notez que je passe cette texture de masque en tant qu’argument au constructeur post-effet. Nous devons créer une référence à celle-ci dans notre constructeur, ainsi:

//// Ajout d'un argument supplémentaire sur la ligne ci-dessous var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // c'est la définition du shader pour notre effet this.shader = new pc.Shader (graphicsDevice, attributs: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.time = 0; //// <<<<<<<<<<<<< Saving the buffer here this.buffer = buffer; ;

Enfin, dans la fonction de rendu, transmettez le tampon à notre shader avec:

scope.resolve ("uMaskBuffer"). setValue (this.buffer); 

Maintenant, pour vérifier que tout fonctionne, je vais laisser cela comme un défi.

Défi n ° 2: restituer uMaskBuffer à l'écran pour confirmer qu'il s'agit bien de la sortie de la deuxième caméra..

Une chose à prendre en compte est que la cible de rendu est définie dans l'initialisation de CameraMask.js et qu'elle doit être prête au moment où Refraction.js est appelé. Si les scripts fonctionnent en sens inverse, vous obtiendrez une erreur. Pour vous assurer qu'ils fonctionnent dans le bon ordre, faites glisser le CameraMask en haut de la liste des entités dans l'éditeur, comme indiqué ci-dessous..

La deuxième caméra doit toujours regarder la même vue que la photo d'origine. Faisons-le donc toujours suivre sa position et sa rotation dans la mise à jour de CameraMask.js:

CameraMask.prototype.update = fonction (dt) var pos = this.CameraToFollow.getPosition (); var rot = this.CameraToFollow.getRotation (); this.entity.setPosition (pos.x, pos.y, pos.z); this.entity.setRotation (rot); ;

Et définir CaméraSuivre dans l'initialisation:

this.CameraToFollow = this.app.root.findByName ('Caméra');

Masques d'abattage

Les deux caméras rendent actuellement la même chose. Nous voulons que la caméra du masque rende tout, sauf la vraie eau, et que la vraie caméra rende tout, sauf le masque de l'eau.

Pour ce faire, nous pouvons utiliser le masque de bit de sélection de la caméra. Cela fonctionne de la même manière que les masques de collision, si vous en avez déjà utilisés. Un objet sera sélectionné (non rendu) si le résultat d'un bitwise ET entre son masque et le masque de la caméra est 1.

Supposons que le bit 2 de l’eau soit défini et le bit 3 de WaterMask. Ensuite, la caméra réelle doit avoir tous les bits configurés, à l’exception de 3, et la caméra masque doit être configurée à tous les bits sauf le 2. Un moyen simple de dire "tous les bits sauf N" est à faire:

~ (1 << N) >>> 0

Vous pouvez en savoir plus sur les opérateurs au niveau des bits ici.

Pour mettre en place les masques de sélection de caméra, nous pouvons mettre cela à l’intérieur. CameraMask.js 's initialise en bas:

 // Définit tous les bits sauf 2 this.entity.camera.camera.cullingMask & = ~ (1 << 2) >>> 0; // Définit tous les bits sauf 3 this.CameraToFollow.camera.camera.cullingMask & = ~ (1 << 3) >>> 0; // Si vous souhaitez imprimer ce masque binaire, essayez: // console.log ((this.CameraToFollow.camera.camera.cullingMask >>> 0) .toString (2));

Dans Water.js, définissez le masque du maillage Eau sur le bit 2 et sa version du masque sur le bit 3:

// Placez ceci au bas de l'initialisation de Water.js // Définissez les masques de sélection var bit = this.isMask? 3: 2; meshInstance.mask = 0; meshInstance.mask | = (1 << bit);

Maintenant, une vue aura l'eau normale et l'autre une eau blanche solide. La moitié gauche de l'image ci-dessous correspond à la vue de la caméra d'origine et la moitié droite à la caméra du masque..

Appliquer le masque

Une dernière étape maintenant! Nous savons que les zones sous l'eau sont marquées par des pixels blancs. Nous devons juste vérifier si nous ne sommes pas à un pixel blanc et, le cas échéant, désactiver la distorsion Refraction.frag:

// Vérifier la position d'origine ainsi que la nouvelle position déformée vec4 maskColor = texture2D (uMaskBuffer, pos); vec4 maskColor2 = texture2D (uMaskBuffer, vUv0); // nous ne sommes pas à un pixel blanc? if (maskColor! = vec4 (1.0) || maskColor2! = vec4 (1.0)) // Remettre à la position d'origine pos = vUv0; 

Et ça devrait le faire!

Une chose à noter est que puisque la texture du masque est initialisée au lancement, si vous redimensionnez la fenêtre au moment de l'exécution, elle ne correspondra plus à la taille de l'écran..

Anti crénelage

En tant qu'étape facultative de nettoyage, vous avez peut-être remarqué que les contours de la scène ont maintenant un aspect un peu net. En effet, lorsque nous avons appliqué notre effet post, nous avons perdu l'anti-aliasing. 

Nous pouvons appliquer un anti-alias supplémentaire en plus de notre effet comme un autre effet post. Heureusement, il y en a une disponible dans le magasin PlayCanvas que nous pouvons simplement utiliser. Accédez à la page des ressources de script, cliquez sur le gros bouton de téléchargement vert et choisissez votre projet dans la liste qui apparaît. Le script apparaîtra à la racine de la fenêtre de votre actif en tant que posteffect-fxaa.js. Attachez simplement ceci à l'entité Caméra et votre scène devrait être un peu plus jolie.! 

Dernières pensées

Si vous avez réussi jusque-là, donnez-vous une tape dans le dos! Nous avons couvert beaucoup de techniques dans cette série. Vous devriez maintenant être à l'aise avec les vertex shaders, le rendu des textures, l'application d'effets de post-traitement, le tri sélectif des objets, l'utilisation du tampon de profondeur et l'utilisation de la fusion et de la transparence. Même si nous l’implémentions dans PlayCanvas, ce sont tous des concepts graphiques généraux que vous trouverez sous une forme quelconque sur la plate-forme dans laquelle vous vous retrouvez..

Toutes ces techniques sont également applicables à divers autres effets. Dans cette présentation sur l'art d'Abzu, une application particulièrement intéressante que j'ai trouvée des vertex shaders explique comment ils ont utilisé des vertex shaders pour animer efficacement des dizaines de milliers de poissons à l'écran..

Vous devriez maintenant aussi avoir un bel effet d’eau que vous pouvez appliquer à vos jeux! Vous pouvez facilement le personnaliser maintenant que vous avez rassemblé tous les détails vous-même. Il reste encore beaucoup à faire avec l’eau (je n’ai même pas mentionné le moindre reflet). Voici quelques idées.

Ondes à base de bruit

Au lieu d'animer simplement les vagues avec une combinaison de sinus et de cosinus, vous pouvez échantillonner une texture de bruit pour rendre les vagues un peu plus naturelles et imprévisibles..

Sentiers dynamiques en mousse

Au lieu de lignes d'eau complètement statiques sur la surface, vous pouvez dessiner sur cette texture lorsque les objets bougent, pour créer une traînée de mousse dynamique. Il y a beaucoup de façons de le faire, alors ça pourrait être son propre projet.

Code source

Vous pouvez trouver le projet PlayCanvas hébergé terminé ici. Un port Three.js est également disponible dans ce référentiel.