Créez un shader d'eau de dessin animé pour le Web. 3e partie

image

Dans la deuxième partie, nous avons examiné les lignes de flottabilité et de mousse. Dans cette dernière partie, nous appliquons la distorsion sous-marine comme effet de post-traitement.

Effets de réfraction et de post-traitement


Notre objectif est de transmettre visuellement la réfraction de la lumière dans l'eau. Nous avons déjà expliqué comment créer ce type de distorsion dans un fragment shader pour une scène 2D. Ici, la seule différence est que nous devons comprendre quelle zone de l'écran est sous l'eau et lui appliquer uniquement de la distorsion.

Post-traitement


Dans le cas général, l'effet de post-traitement est tout effet appliqué à la scène entière après son rendu, par exemple des nuances de couleur ou l' effet d'un ancien écran CRT . Au lieu de rendre la scène directement à l'écran, nous la rendons d'abord dans le tampon ou la texture, puis, en passant la scène à travers notre shader, nous la rendons à l'écran.

Dans PlayCanvas, vous pouvez personnaliser cet effet de post-traitement en créant un nouveau script. Appelons-le Refraction.js et copions ce modèle dedans comme un blanc:

//---------------   ------------------------// pc.extend(pc, function () { //  -      var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //      this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.buffer = buffer; }; //      pc.PostEffect RefractionPostEffect = pc.inherits(RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //    ,    , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); return { RefractionPostEffect: RefractionPostEffect }; }()); //---------------  ------------------------// var Refraction = pc.createScript('refraction'); Refraction.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Refraction.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' }); //  initialize       Refraction.prototype.initialize = function() { var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource); //     postEffects var queue = this.entity.camera.postEffects; queue.addEffect(effect); this.effect = effect; //       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 similaire à un script standard, mais nous définissons une classe RefractionPostEffect qui peut être appliquée à la caméra. Pour le rendu, il a besoin de vertex et de fragment shaders. Les attributs sont déjà configurés, créons donc Refraction.frag avec le contenu suivant:

 precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; } 

Et Refraction.vert avec un vertex shader de base:

 attribute vec2 aPosition; varying vec2 vUv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5; } 

Attachez maintenant le script Refraction.js à la caméra et attribuez les attributs appropriés aux shaders. Lorsque vous démarrez le jeu, vous verrez la scène de la même manière qu'auparavant. Il s'agit d'un post-effet vide qui restitue simplement la scène. Pour nous assurer que cela fonctionne, essayons de donner à la scène une teinte rouge.

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


Shader de distorsion


Pour créer une distorsion animée, nous devons ajouter une variable de temps uniforme, nous allons donc la créer à l'intérieur de ce constructeur post-effet dans Refraction.js:

 var RefractionPostEffect = function (graphicsDevice, vs, fs) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); // >>>>>>>>>>>>>    this.time = 0; }; 

Maintenant à l'intérieur de la fonction de rendu, nous la passons à notre shader pour l'augmenter:

 RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //      , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); /// >>>>>>>>>>>>>>>>>>    uniform-  scope.resolve("uTime").setValue(this.time); this.time += 0.1; //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); 

Maintenant, nous pouvons utiliser le même code de shader du didacticiel sur la distorsion de l'eau, transformant notre shader de fragment complet en ce qui suit:

 precision highp float; uniform sampler2D uColorBuffer; uniform float uTime; varying 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(XY)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; } 

Si tout est fait correctement, alors l'image entière devrait ressembler à complètement sous l'eau.


Tâche 1: assurez-vous que la distorsion ne s'applique qu'au bas de l'écran.

Masques de caméra


Nous avons presque fini. Il nous reste à appliquer cet effet de distorsion à la partie sous-marine de l'écran. La façon la plus simple à laquelle j'ai pensé est de restituer la scène avec la surface de l'eau rendue en blanc uni, comme le montre la figure ci-dessous.


Il restituera la texture que nous utilisons comme masque. Ensuite, nous transférerons cette texture dans notre shader de réfraction, ce qui ne déformera le pixel de l'image finie que lorsque le pixel correspondant dans le masque sera blanc.

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

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

Ensuite, comme d'habitude, nous pouvons le passer au shader en utilisant material.setParameter('isMask',this.isMask); . Déclarez-le ensuite dans Water.frag et coloriez le pixel en blanc si l'attribut est vrai.

 //    uniform uniform bool isMask; //      ,    //    true if(isMask){ color = vec4(1.0); } 

Assurez-vous que cela fonctionne en activant la propriété «Is Mask?». dans l'éditeur et redémarrer le jeu. Il devrait être blanc, comme dans l'image ci-dessus.

Maintenant, pour restituer 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 . Nous dupliquons également l'entité Water dans l'éditeur et nommons le WaterMask en double. Assurez-vous que l'entité "L'eau est un masque?" est faux et WaterMask est vrai.

Pour commander le rendu d'une nouvelle caméra sur une texture plutôt que sur un écran, créez un nouveau script CameraMask.js et attachez-le à la nouvelle caméra. Nous créons un RenderTarget pour capturer la sortie de cette caméra:

 //  initialize       CameraMask.prototype.initialize = function() { //  512x512x24-      var colorBuffer = new pc.Texture(this.app.graphicsDevice, { width: 512, height: 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, { depth: true }); this.entity.camera.renderTarget = renderTarget; }; 

Maintenant, après avoir lancé l'application, vous verrez que cette caméra ne s'affiche plus à l'écran. Nous pouvons obtenir la sortie de son rendu cible dans Refraction.js comme suit:

 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); // ... //     ,    }; 

Notez que je passe cette texture de masque comme argument au constructeur post-effet. Nous devons créer un lien vers celui-ci dans notre constructeur, il ressemblera à ceci:

 ////       var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.time = 0; //// <<<<<<<<<<<<<    this.buffer = buffer; }; 

Enfin, dans la fonction de rendu, nous passons le tampon à notre shader:

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

Maintenant, pour m'assurer que tout cela fonctionne, je vous le laisse comme tâche.

Tâche 2: restituez le uMaskBuffer à l'écran pour vous assurer qu'il s'agit bien de la sortie de la deuxième caméra.

Les éléments suivants doivent être pris en compte: le rendu cible est configuré dans l'initialisation du script CameraMask.js et il doit être prêt au moment de l'appel de Refraction.js. Si les scripts fonctionnent différemment, nous obtenons une erreur. Pour vous assurer qu'ils fonctionnent dans le bon ordre, faites glisser CameraMask en haut de la liste d'entités dans l'éditeur, comme indiqué ci-dessous.


La deuxième caméra doit toujours avoir la même vue que la caméra d'origine, nous devons donc toujours suivre la position et la rotation du script CameraMask.js dans la mise à jour:

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

Dans l'initialisation, définissez CameraToFollow :

 this.CameraToFollow = this.app.root.findByName('Camera'); 

Masques d'écrêtage


Les deux caméras rendent maintenant la même chose. Nous voulons que la caméra masque rende tout sauf l'eau réelle, et la vraie caméra rende tout sauf l'eau masque.

Pour ce faire, nous pouvons utiliser le masque d'écrêtage de la caméra. Il fonctionne de manière similaire aux masques anti-collision . Un objet sera découpé (c'est-à-dire non rendu) si le résultat d'un AND du bit entre son masque et le masque de la caméra est 1.

Supposons que Water a le bit 2, WaterMask a le bit 3. Tous les bits sauf 3 doivent être définis pour une caméra réelle et tous les bits sauf 2 pour une caméra masque. La façon la plus simple de dire «tous les bits sauf N» est la suivante façon:

 ~(1 << N) >>> 0 

En savoir plus sur les opérations au niveau du bit ici .

Pour configurer les masques d'écrêtage de caméra, nous pouvons insérer ce qui suit au bas de l'initialisation du script CameraMask.js :

  //   ,  2 this.entity.camera.camera.cullingMask &= ~(1 << 2) >>> 0; //   ,  3 this.CameraToFollow.camera.camera.cullingMask &= ~(1 << 3) >>> 0; //      ,   : // console.log((this.CameraToFollow.camera.camera.cullingMask >>> 0).toString(2)); 

Maintenant, dans Water.js, nous allons définir le bit 2 du masque du maillage Water, et sa version de masque sur le bit 3:

 //      initialize  Water.js //    var bit = this.isMask ? 3 : 2; meshInstance.mask = 0; meshInstance.mask |= (1 << bit); 

Maintenant, une espèce sera avec de l'eau claire et la seconde avec de l'eau blanche solide. L'image de gauche montre la vue de la caméra d'origine et à droite la vue de la caméra de masque.


Application de masque


Et maintenant la dernière étape! Nous savons que les zones sous-marines sont marquées de pixels blancs. Nous avons juste besoin de vérifier si nous sommes dans un pixel blanc, et sinon, désactivez la distorsion dans Refraction.frag :

 //   ,      vec4 maskColor = texture2D(uMaskBuffer, pos); vec4 maskColor2 = texture2D(uMaskBuffer, vUv0); //     ? if(maskColor != vec4(1.0) || maskColor2 != vec4(1.0)){ //      pos = vUv0; } 

Et cela devrait résoudre notre problème!

Il convient également de noter que, puisque la texture du masque est initialisée au démarrage, lorsque vous redimensionnez la fenêtre au moment de l'exécution, elle ne correspondra plus à la taille de l'écran.

Lissage


Vous remarquerez peut-être que les bords de la scène sont maintenant un peu nets. Cela est arrivé car après avoir appliqué le post-effet, nous avons perdu le lissage.

Nous pouvons appliquer un lissage supplémentaire au-dessus de notre effet comme un autre post-effet. Heureusement, il existe une autre variable dans le magasin PlayCanvas que nous pouvons utiliser. Accédez à la page des actifs de script , cliquez sur le gros bouton de téléchargement vert et sélectionnez votre projet dans la liste qui apparaît. Le script apparaîtra à la racine de la fenêtre Actifs sous le nom posteffect-fxaa.js . Attachez-le simplement à l'entité Appareil photo et votre scène commencera à paraître beaucoup mieux!

Réflexions en conclusion


Si vous arrivez ici, vous pouvez vous féliciter! Dans ce didacticiel, nous avons couvert plusieurs techniques. Vous devez maintenant vous sentir en confiance lorsque vous travaillez avec des vertex shaders, le rendu dans les textures, l'application d'effets de post-traitement, l'écrêtage sélectif d'objets, l'utilisation du tampon de profondeur et le travail avec le mélange et la transparence. Bien que nous ayons implémenté tout cela dans PlayCanvas, vous pouvez rencontrer tous ces concepts généraux de l'infographie sous une forme ou une autre sur n'importe quelle plate-forme.

Toutes ces techniques sont également applicables à de nombreux autres effets. Une application particulièrement intéressante trouvée pour les vertex shaders, que j'ai trouvée dans le rapport sur le graphique Abzu , où les développeurs expliquent comment ils ont utilisé les vertex shaders pour animer efficacement des dizaines de milliers de poissons à l'écran.

Maintenant, vous avez un bel effet d'eau que vous pouvez appliquer dans vos jeux! Vous pouvez le personnaliser et ajouter vos propres détails. Beaucoup plus peut être fait avec de l'eau (je n'ai même pas mentionné de types de reflets). Voici quelques idées.

Ondes sonores


Au lieu d'animer simplement les vagues avec une combinaison de cosinus et de sinus, vous pouvez échantillonner la texture de sorte que les vagues semblent un peu plus naturelles et moins prévisibles.

Traces dynamiques de mousse


Au lieu de lignes d'eau complètement statiques à la surface, vous pouvez dessiner dans la texture lorsque vous déplacez des objets pour créer des traces dynamiques de mousse. Cela peut être fait de différentes manières, de sorte que cette tâche en soi peut devenir un projet.

Code source


Le projet PlayCanvas terminé peut être trouvé ici . Notre référentiel possède également un port de projet sous Three.js .

Source: https://habr.com/ru/post/fr417091/


All Articles