Bine ați venit în această serie de trei părți privind crearea de apă stilizată în canal PlayCanvas folosind shader-urile de vârf. În partea 2 am acoperit linii de plutire și spumă. În această ultimă parte, vom aplica distorsiunea subacvatică ca efect post-proces.
Scopul nostru este de a comunica vizual refracția luminii prin apă. Am acoperit deja modul de a crea acest tip de distorsiune într-un shader de fragment într-un tutorial anterior pentru o scenă 2D. Singura diferență aici este că va trebui să dăm seama care zonă a ecranului este sub apă și aplică doar distorsiunea acolo.
În general, un efect post-proces este orice aplicație pentru întreaga scenă după ce este redată, cum ar fi o nuanță colorată sau un efect vechi al ecranului CRT. În loc să redenumezi scena direct pe ecran, mai întâi redați-l într-un buffer sau textură și apoi redați-l pe ecran, trecând printr-un shader personalizat.
În PlayCanvas, puteți seta un efect post-proces prin crearea unui nou script. Sună-l Refraction.js, și copiați acest șablon pentru a începe cu:
// --------------- DEFINIȚIE PO EFFECT ------------------------ // pc.extend ( pc, function () // Constructor - Creeaza o instanta a efectului nostru post var RefractionPostEffect = functie (graficDevice, vs, fs, buffer) var fragmentShader = "precision" + graphicsDevice.precision + "float; = fragmentShader + fs; // aceasta este definiția shader pentru efectul nostru this.shader = nou pc.Shader (graphicsDevice, atribute: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); = tampon;; // Efectul nostru trebuie să decurgă din pc.PostEffect RefractionPostEffect = pc.inherits (RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Fiecare efect post trebuie să implementeze renderul metodă care // stabilește parametrii pe care shader-ul ar putea să le solicite și // face, de asemenea, efectul pe render rendering: funcția (inputTarget, outputTarget, rect) var device = this.device; var scope = device.scope; lea Intrarea e face ca țintă să fie umbra. Aceasta este imaginea redată de la camera noastră. () // Desenați un quad pe ecran complet pe țintă de ieșire. În acest caz, ținta de ieșire este ecranul. // Desenarea unui quad pe ecran complet va rula shader-ul definit mai sus pc.drawFullscreenQuad (device, outputTarget, this.vertexBuffer, this.shader, rect); ); returnează RefractionPostEffect: RefractionPostEffect; ()); // --------------- DEFINIȚIE SCRIPTĂ ------------------------ // var Refracție = pc. createScript ( 'refracție'); Refraction.attributes.add ('vs', type: 'asset', assetType: 'shader', titlu: 'Vertex Shader'); Refraction.attributes.add ('fs', type: 'asset', assetType: 'shader', titlu: 'Fragment Shader'); // inițializați codul numit o singură dată pe entitate Refraction.prototype.initialize = function () var effect = new pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource); // adăugați efectul la coada postEffects a camerei var queue = this.entity.camera.postEffects; queue.addEffect (efect); this.effect = efect; // Salvați shaderele curente pentru reîncărcarea la cald this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ; Refraction.prototype.update = funcția () if (this.savedFS! = This.fs.resource || this.savedVS! = This.vs.resource) this.swap (aceasta); ; Refraction.prototype.swap = funcția (veche) this.entity.camera.postEffects.removeEffect (old.effect); this.initialize (); ;
Acesta este la fel ca un script normal, dar definim a RefractionPostEffect
care pot fi aplicate camerei. Acest lucru are nevoie de un vârf și un fragment de shader pentru a face. Atributele sunt deja configurate, așa că să creăm Refraction.frag cu acest conținut:
precizie highp float; uniform sampler2D uColorBuffer; variind vec2 vUv0; void principală () vec4 color = texture2D (uColorBuffer, vUv0); gl_FragColor = culoare;
Și Refraction.vert cu un shader de vârf de bază:
atribut vec2 aPosition; variind vec2 vUv0; void principal (void) gl_Pozi = vec4 (aPoziție, 0.0, 1.0); vUv0 = (aPosition.xy + 1,0) * 0,5;
Acum atașați Refraction.js script-ul pentru camera foto, și atribuiți shadere la atributele corespunzătoare. Când lansezi jocul, ar trebui să vezi scena exact așa cum a fost înainte. Acesta este un efect de post gol care pur și simplu redirecționează scena. Pentru a verifica dacă aceasta funcționează, încercați să dați scenei o nuanță roșie.
În Refraction.frag, în loc să readuceți pur și simplu culoarea, încercați să setați componenta roșie la 1.0, care ar trebui să arate ca imaginea de mai jos.
Trebuie să adăugăm o uniformă de timp pentru distorsiunea animată, deci mergeți mai departe și creați unul în Refraction.js, în interiorul acestui constructor pentru efectul de post:
var RefractionPostEffect = funcția (graficDevice, vs, fs) var fragmentShader = "precizie" + graficDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // aceasta este definiția shader pentru efectul nostru this.shader = nou pc.Shader (graficDevice, atribute: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); // >>>>>>>>>>>>>> Inițializați timpul aici here.time = 0; ;
Acum, în interiorul acestei funcții de randare, îl transmitem către shader-ul nostru și îl incretimăm:
RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Fiecare efect post trebuie să implementeze metoda rendering care // stabilește parametrii pe care shader-ul ar putea să le impună și // redă efectul pe funcția rendering: screen (inputTarget, outputTarget, rect) var device = this.device; var scope = device.scope; // Setați țintă de redare de intrare la shader. Aceasta este imaginea redată de la camera.resolve ("uColorBuffer") setValue (inputTarget .colorBuffer); /// >>>>>>>>>>>>>>>>> Parcurge uniforma de timp aici scope.resolve ("uTime") setValue (this.time); this.time + = 0.1; / / Desenați un quad pe ecran complet pe țintă de ieșire.În acest caz, țintă de ieșire este ecranul.// Desenarea unui quad pe ecran complet va rula shader-ul definit mai sus pc.drawFullscreenQuad (device, outputTarget, this. vertexBuffer, this.shader, rect););
Acum putem folosi același cod de shader din tutorialul de denaturare a apei, făcând ca umbrele noastre de fragmente să arate astfel:
precizie highp float; uniform sampler2D uColorBuffer; flotare uniformă uTime; variind vec2 vUv0; void principal () vec2 pos = vUv0; float X = poz.x * 15. + uTime * 0.5; float Y = poz.y * 15. + uTime * 0.5; poz.y + = cos (X + Y) * 0,01 * cos (Y); pos.x + = sin (X-Y) * 0,01 * sin (Y); vec4 culoare = textură2D (uColorBuffer, pos); gl_FragColor = culoare;
Dacă totul a ieșit, totul ar trebui să pară acum ca și cum ar fi subacvatice, așa cum se arată mai jos.
Provocarea # 1: Faceți distorsiunea numai în partea de jos a ecranului.
Suntem aproape acolo. Tot ce trebuie să facem acum este să aplicăm acest efect de distorsiune doar pe partea subacvatică a ecranului. Cea mai simplă metodă pe care am realizat-o este de a reda scena cu suprafața de apă redată ca o albă solidă, după cum se arată mai jos.
Aceasta ar fi făcută unei texturi care ar acționa ca o mască. Apoi, trecem această textura la shaderul nostru de refracție, care ar distorsiona doar un pixel în imaginea finală dacă pixelul corespunzător din mască este alb.
Să adăugăm un atribut boolean pe suprafața apei pentru a ști dacă este folosit ca o mască. Adăugați acest la Water.js:
Water.attributes.add ('isMask', type: 'boolean', titlu: 'Is Mask?');
Putem apoi să-l transmitem shader-ului material.setParameter ( 'isMask', this.isMask);
ca de obicei. Apoi declasați-o în Water.frag și setați culoarea albă dacă este adevărată.
// Declare noua uniformă la vârful uniform de bool isMask; // La sfârșitul funcției principale, suprascrie culoarea albă // dacă masca este adevărată dacă (isMask) color = vec4 (1.0);
Confirmați că acest lucru funcționează prin trecerea butonului "Is Mask?" proprietatea în editor și relansarea jocului. Ar trebui să pară alb, ca în imaginea anterioară.
Acum, pentru a reda scenă, avem nevoie de oa doua cameră. Creați o nouă cameră în editor și sunați-o CameraMask. Duplicați entitatea Apă și în editor și apelați-o WaterMask. Asigurați-vă că este "Masca?" este falsă pentru entitatea Apă, dar este adevărată pentru WaterMask.
Pentru a le spune noii camere să redea textura în locul ecranului, creați un script nou numit CameraMask.js și atașați-o la noua cameră. Creați un RenderTarget pentru a capta ieșirea acestui aparat astfel:
// initializeaza codul numit o singura data pe entitate CameraMask.prototype.initialize = function () // Creeaza o tinta de redare 512x512x24-bit cu un tampon de adancime var colorBuffer = nou pc.Texture (this.app.graphicsDevice, width: 512, înălțime: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = nou pc.RenderTarget (acest.app.graphicsDevice, colorBuffer, depth: true); this.entity.camera.renderTarget = renderTarget; ;
Acum, dacă lansați, veți vedea că această cameră nu mai este afișată pe ecran. Putem capta rezultatul țintei sale de redare în Refraction.js asa:
Refraction.prototype.initialize = funcție () var cameraMask = this.app.root.findByName ("CameraMask"); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var efect = nou pc.RefractionPostEffect (acest.app.graficeDevice, acest.vs.resource, this.fs.resource, maskBuffer); // ... // Restul acestei funcții este același ca înainte;
Observați că trec această textura de mască ca argument pentru constructorul efectului post. Trebuie să creați o referință la acesta în constructorul nostru, astfel încât să pară:
//// Adăugarea unui argument suplimentar pe linia de mai jos var RefractionPostEffect = funcția (graficDevice, vs, fs, buffer) var fragmentShader = "precizie" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // aceasta este definiția shader pentru efectul nostru this.shader = nou pc.Shader (graficDevice, atribute: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.time = 0; //// <<<<<<<<<<<<< Saving the buffer here this.buffer = buffer; ;
În cele din urmă, în funcția de randare, treci tamponul la shader cu:
scope.resolve ( "uMaskBuffer") SetValue (this.buffer.);
Acum, pentru a verifica dacă toate acestea funcționează, voi lăsa asta ca pe o provocare.
Provocarea # 2: Faceți uMaskBuffer pe ecran pentru a confirma că este ieșirea celei de-a doua camere.
Unul dintre lucrurile pe care trebuie să le știm este faptul că obiectivul de redare este configurat în inițiativa CameraMask.js și că trebuie să fie gata de momentul în care se numește Refraction.js. Dacă script-urile rulează invers, veți primi o eroare. Pentru a vă asigura că rulează în ordinea corectă, glisați CameraMask în partea de sus a listei de entități din editor, după cum se arată mai jos.
Camera a doua ar trebui să privească întotdeauna aceeași vizualizare ca și cea originală, deci să o facem întotdeauna să-și urmeze poziția și rotația în actualizarea CameraMask.js:
CameraMask.prototype.update = funcția (dt) var pos = aceasta.CameraToFollow.getPosition (); var rot = aceasta.CameraToFollow.getRotare (); this.entity.setPosition (pos.x, pos.y, pos.z); this.entity.setRotation (rot); ;
Și definiți CameraToFollow
în inițializare:
this.CameraToFollow = this.app.root.findByName ("Camera");
Ambele camere oferă simultan același lucru. Vrem ca camera de mască să facă totul, cu excepția apei reale, și vrem ca camera reală să facă totul, cu excepția apei de mască.
Pentru a face acest lucru, putem folosi masca de biberon a camerei. Acest lucru funcționează similar cu măștile de coliziune dacă le-ați folosit vreodată. Un obiect va fi cedat (nu redat) dacă rezultatul unui bitar ȘI
între masca sa și masca camerei este 1.
Să presupunem că Apa va avea setul de biți 2, iar WaterMask va avea un bit 3. Apoi, camera reală trebuie să aibă toate setările de biți, cu excepția celor 3, iar camera de mascare trebuie să aibă toate setările, cu excepția 2. O modalitate ușoară de a spune "toți biții cu excepția lui N" trebuie să facă:
~ (1 << N) >>> 0
Puteți citi mai multe despre operatorii de biți aici.
Pentru a configura măștile de închidere a camerelor, putem pune acest lucru înăuntru CameraMask.js's inițializați în partea de jos:
// Setați toți biții, cu excepția 2 this.entity.camera.camera.cullingMask & = ~ (1 << 2) >>> 0; // Setați toți biți, cu excepția pentru 3 this.CameraToFollow.camera.camera.cullingMask & = ~ (1 << 3) >>> 0; // Dacă doriți să imprimați această mască bit, încercați: // console.log ((this.CameraToFollow.camera.camera.cullingMask >>> 0) .toString (2));
Acum, în Water.js, setați masca ochiurilor de plasă pe bitul 2 și versiunea de mască a acestuia pe bitul 3:
// Puneți acest lucru în partea de jos a inițializării Water.js // Setați măștile de deșert var bit = this.isMask? 3: 2; meshInstance.mask = 0; meshInstance.mask | = (1 << bit);
Acum, o vedere va avea apa normală, iar cealaltă va avea apa albă solidă. Jumătatea stângă a imaginii de mai jos este vizualizarea de la camera originală, iar jumătatea dreaptă este din camera de mascare.
Un ultim pas! Știm că zonele sub apă sunt marcate cu pixeli albi. Trebuie doar să verificăm dacă nu suntem la un pixel alb și, dacă da, opriți distorsiunea Refraction.frag:
// Verificați poziția originală, precum și noua poziție distorsionată vec4 maskColor = texture2D (uMaskBuffer, pos); vec4 maskColor2 = textură2D (uMaskBuffer, vUv0); // Nu suntem la un pixel alb? dacă (maskColor! = vec4 (1.0) || maskColor2! = vec4 (1.0)) // Reveniți la poziția inițială pos = vUv0;
Și ar trebui să o facă!
Un lucru de observat este faptul că, din moment ce textura pentru mască este inițializată la lansare, dacă redimensionați fereastra în timpul rulării, nu va mai corespunde dimensiunii ecranului.
Ca o opțiune de curățare opțională, este posibil să fi observat că marginile scenei arată acum puțin ascuțite. Acest lucru se datorează faptului că, atunci când am aplicat efectul nostru post, am pierdut anti-aliasing.
Putem aplica un anti-alias suplimentar pe lângă efectul nostru ca un alt efect post. Din fericire, există unul disponibil în magazinul PlayCanvas pe care îl putem folosi. Accesați pagina de activare a scriptului, faceți clic pe butonul verde mare de descărcare și alegeți proiectul din lista care apare. Scriptul va apărea în rădăcina ferestrei dvs. de materiale ca posteffect-fxaa.js. Doar atașați acest lucru entității camerei și scena dvs. ar trebui să arate puțin mai plăcută!
Dacă ați reușit acest lucru, dați-vă un pic pe spate! Am acoperit o mulțime de tehnici în această serie. Ar trebui să vă simțiți confortabil cu shaderele de vârf, redarea la texturi, aplicarea efectelor postprocesare, culegerea selectivă a obiectelor, utilizarea bufferului de adâncime și lucrul cu amestecarea și transparența. Chiar dacă am implementat acest lucru în PlayCanvas, acestea sunt toate conceptele grafice generale pe care le veți găsi într-o anumită formă pe orice platformă veți ajunge.
Toate aceste tehnici sunt de asemenea aplicabile la o varietate de alte efecte. O aplicație deosebit de interesantă pe care am descoperit-o cu privire la shaderele de vârf se află în această discuție despre arta lui Abzu, unde explică modul în care foloseau shaderele de vârfuri pentru a anima în mod eficient zeci de mii de pești pe ecran.
Acum ar trebui să ai și un efect de apă frumos pe care îl poți aplica la jocurile tale! Ați putea personaliza cu ușurință acum că ați pus împreună fiecare detaliu. Există încă mult mai multe lucruri pe care le puteți face cu apă (nici măcar nu am menționat vreun fel de reflecție). Mai jos sunt câteva idei.
În loc să animați pur și simplu valurile cu o combinație de sine și cosine, puteți încerca o textură de zgomot pentru a face valurile să pară ceva mai naturale și mai imprevizibile.
În loc de linii de apă complet statice de pe suprafață, puteți trage pe acea textura atunci când obiectele se mișcă, pentru a crea o pistă dinamică din spumă. Există multe modalități de a face acest lucru, astfel încât acesta ar putea fi propriul proiect.
Puteți găsi proiectul finalizat găzduit aici PlayCanvas. Un port Three.js este de asemenea disponibil în acest depozit.