În ciuda notorietății lor, crearea de nivele de apă este o tradiție onorată în istoria jocurilor video, fie că este vorba de a scutura mecanica jocului, fie doar pentru că apa este atât de frumoasă încât să te uiți. Există mai multe moduri de a produce o senzație subacvatică, de la simplă vizualizare (cum ar fi tentația ecranului albastru) la mecanică (cum ar fi mișcarea lentă și greutatea slabă).
Vom examina denaturarea ca o modalitate de a comunica vizual prezența apei (imaginați-vă că vă aflați la marginea unei piscine și căutați pe lucrurile din interior - acesta este efectul pe care vrem să îl recreăm). Puteți verifica o demonstrație a aspectului final aici pe CodePen.
Voi folosi Shadertoy pe tot parcursul tutorialului pentru a putea urmări în browser-ul dvs. Voi încerca să-l păstrați destul de agnostic pe platformă, astfel încât să puteți implementa ceea ce învățați aici în orice mediu care să accepte shaderele grafice. În final, vă voi oferi câteva sfaturi de implementare, precum și codul JavaScript pe care l-am folosit pentru implementarea exemplului de mai sus cu biblioteca de jocuri Phaser.
Ar putea părea puțin cam complicat, dar efectul în sine este doar câteva linii de cod! Nu e nimic mai mult decât efecte diferite de deplasare combinate. Vom începe de la zero și vom vedea exact ce înseamnă asta.
Treceți la Shadertoy și creați un nou shader. Înainte de a putea aplica orice distorsiune, trebuie să facem o imagine. Știm din tutorialele anterioare că trebuie doar să selectăm o imagine într-unul din canalele de jos ale paginii și să o mapăm pe ecran cu texture2D:
vec2 uv = fragCoord.xy / iResolution.xy; // Obțineți poziția normalizată a pixelului curent fragColor = texture2D (iChannel0, uv); // Obțineți culoarea actuală a pixelilor în textură și setați-o pe culoarea pe ecran
Iată ce am ales:
Acum, ceea ce se întâmplă dacă nu este doar redarea pixelului în poziție uv
, noi facem pixelul la uv + vec2 (0.1,0.0)
?
Este întotdeauna mai ușor să gândiți ce se întâmplă pe un singur pixel când lucrați cu shadere. Având în vedere orice poziție pe ecran, în loc să desenezi culoarea originală în textură, ea va desena culoarea unui pixel în dreapta. Asta înseamnă că, din punct de vedere vizual, totul se schimbă stânga. Incearca-l!
În mod implicit, Shadertoy stabilește modul de înfășurare pe toate texturile repeta. Deci, dacă încercați să eșantionați un pixel din partea dreaptă a pixelului din dreapta, acesta se va înfășura pur și simplu în jur. Aici am schimbat-o clemă (pe care o puteți face de la pictograma roată din cutia unde ați selectat textura).
Provocare: Puteți face întreaga imagine să meargă încet spre dreapta? Ce zici de mutarea înainte și înapoi? Dar într-un cerc?
Sugestie: Shadertoy vă oferă o variabilă de timp de funcționare numită iGlobalTime.
Mutarea unei imagini întregi nu este foarte interesantă și nu necesită o putere foarte paralelă a GPU-ului. Dacă în loc să deplasăm fiecare poziție cu o sumă fixă (cum ar fi 0,1), am deplasat diferiți pixeli în funcție de sume diferite?
Avem nevoie de o variabilă care este oarecum unică pentru fiecare pixel. Orice variabilă pe care o declarați sau o uniformă pe care o introduceți nu va varia între pixeli. Din fericire, avem deja ceva care variază în felul următor: propriul pixel X și y. Incearca asta:
vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Mutati y cu pixelul curent x fragColor = texture2D (iChannel0, uv);
Suntem vertical offset fiecare pixel prin valoarea lui x. Pixelii din stânga vor primi cel mai mic decalaj (0), în timp ce cel mai din dreapta va obține decalajul maxim (1).
Acum avem o valoare care variază în întreaga imagine de la 0 la 1. Folosim acest lucru pentru a împinge pixelii în jos, așa că obținem această înclinație. Acum, pentru următoarea provocare!
Provocare: Puteți folosi acest lucru pentru a crea un val? (După cum se prezintă mai jos)
Sugestie: Variabila dvs. de offset merge de la 0 la 1. Vrei să meargă periodic de la -1 la 1. Funcția cosinus / sine este o alegere perfectă pentru asta.
Dacă ați dat seama de efectul de undă, încercați să o faceți să se răsucească înainte și înapoi prin înmulțirea cu variabila noastră de timp! Iată încercarea mea de până acum:
vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25.) * 0,06 * cos (iGlobalTime); fragColor = textură2D (iChannel0, uv);
Înmulțesc uv.x de un număr mare (25) pentru a controla frecvența undei. Apoi am scalat-o în jos prin înmulțirea cu 0,06, deci aceasta este amplitudinea maximă. În cele din urmă, mă înmulțesc cu cosinusul vremii, ca să-l răsturnăm periodic înainte și înapoi.
Notă: Dacă doriți cu adevărat să confirmați că distorsiunea noastră urmărește un val sinusoidal, modificați valoarea de la 0,06 la 1,0 și urmăriți-o la maxim!
Provocare: Poți să-ți dai seama cum să-l faci mai rapid?
Sugestie: Este același concept pe care l-am folosit pentru a mări frecvența spațială a undelor.
În timp ce sunteți la el, un alt lucru pe care îl puteți încerca este să aplicați același lucru pentru uv.x de asemenea, așa că distorsionează atât pe x cât și pe y (și poate comutați cosul pentru păcat).
Acum asta este chircit într-o mișcare de undă, dar ceva e oprit. Nu este chiar cum se comporta apa ...
Apa trebuie să arate ca și cum ar curge. Ceea ce avem acum este doar merge înainte și înapoi. Să examinăm din nou ecuația noastră:
Frecvența noastră nu se schimbă, ceea ce este bine pentru moment, dar nu vrem să ne schimbăm amplitudinea. Vrem ca valul să rămână în aceeași formă, dar să mișcare pe ecran.
Pentru a vedea unde în ecuația noastră vrem să compensăm, gândiți-vă la ce determină unde începe și se termină valul. uv.x este variabila dependentă în acest sens. Oriunde uv.x este pi / 2, nu va exista nici o deplasare (deoarece cos (pi / 2) = 0), și unde uv.x este în jur pi / 2, care va fi deplasarea maximă.
Să ne întoarcem puțin ecuația:
Acum amplitudinea și frecvența sunt fixe, iar singurul lucru care variază va fi poziția valului în sine. Cu acest pic de teorie în afara drumului, timp pentru o provocare!
Provocare: Implementați această nouă ecuație și optimizați coeficienții pentru a obține o mișcare frumos ondulată.
Iată codul meu pentru ceea ce am ajuns până acum:
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 = textură2D (iChannel0, uv);
Acum, aceasta este în esență inima efectului. Cu toate acestea, putem păstra lucrurile tweaking pentru a face să pară chiar mai bine. De exemplu, nu există niciun motiv pentru care trebuie să modificați valul doar cu coordonatele x sau y. Puteți schimba ambele, deci variază în diagonală! Iată un exemplu:
flotare X = uv.x * 25. + iGlobalTime; flotați Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01; uv.x + = sin (X-Y) * 0,01;
Părea un pic repetitiv, așa că am schimbat al doilea cos pentru un păcat pentru a rezolva asta. În timp ce suntem la el, putem încerca, de asemenea, să modificăm amplitudinea un pic:
flotare X = uv.x * 25. + iGlobalTime; flotați Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01 * cos (Y); uv.x + = sin (X-Y) * 0,01 * sin (Y);
Și este cam la fel de mult ca și cum am ajuns, dar puteți oricând să combinați și să combinați mai multe funcții pentru a obține rezultate diferite!
Ultimul lucru pe care vreau să-l menționez în umbra este că, în majoritatea cazurilor, probabil că va trebui să aplicați efectul doar unei părți a ecranului în locul întregului lucru. O modalitate ușoară de a face asta este trecerea într-o mască. Aceasta ar fi o imagine care să indice care zone ale ecranului ar trebui să fie afectate. Acelea care sunt transparente (sau alb) nu pot fi afectate, iar pixelii opaci (sau negri) pot avea efectul complet.
În Shadertoy, nu poți încărca imagini arbitrare, dar poți să le faci unui tampon separat și să treci asta ca o textură. Aici este un link Shadertoy unde aplic efectul de mai sus doar în partea de jos a ecranului.
Masca pe care o introduceți nu trebuie să fie o imagine statică. Poate fi un lucru complet dinamic; atâta timp cât îl puteți reda în timp real și îl puteți transfera la umbra, apa dvs. se poate mișca sau curge pe tot ecranul fără probleme.
Am folosit Phaser.js pentru a implementa acest shader. Puteți verifica sursa în acest Live CodePen sau puteți descărca o copie locală din acest depozit.
Puteți vedea cum trec manual imaginile ca uniforme și trebuie să actualizez și eu variabila temporală.
Cea mai mare detaliu de implementare pe care trebuie să o gândiți este ce să aplicați acest shader. În exemplul Shadertoy și în exemplul meu JavaScript, am o singură imagine în lume. Într-un joc, probabil că veți avea mult mai mult.
Phaser vă permite să aplicați shadere obiectelor individuale, dar puteți, de asemenea, să le aplicați la obiectul mondial, ceea ce este mult mai eficient. În mod similar, ar putea fi o idee bună pe o altă platformă să redați toate obiectele pe un tampon și să treci prin umbra de apă, în loc să o aplicați fiecărui obiect individual. În felul acesta funcționează ca un efect post-procesare.
Sper că trecând prin a compune acest shader de la zero, ați dat o bună perspectivă asupra modului în care sunt construite multe efecte complexe prin întinderea tuturor acestor mici deplasări!
Ca o provocare finală, iată un fel de shader de apă care se bazează pe același tip de idei de deplasare pe care le-am văzut. Puteți încerca să o despărțiți, să desfaceți straturile și să aflați ce face fiecare piesă!