Crearea de apă toon pentru Web Partea 2

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 1, am acoperit înființarea mediului și a apei. Această parte va acoperi aplicarea flotabilității obiectelor, adăugarea de linii de apă la suprafață și crearea liniilor de spumă cu tamponul de adâncime în jurul marginilor obiectelor care intersectează suprafața. 

Am făcut niște mici modificări scena mea pentru a face să pară puțin mai plăcută. Puteți personaliza scena dvs. oricum doriți, dar ceea ce am făcut a fost:

  • A fost adăugat farul și modelele de caracatiță.
  • Adăugat un plan de masă cu culoare # FFA457
  • A adăugat o culoare clară pentru camera foto # 6CC8FF.
  • A adăugat o culoare ambientală la fața locului # FFC480 (puteți găsi acest lucru în setările de scenă).

Mai jos este punctul meu de plecare acum.

Flotabilitate 

Cea mai simplă modalitate de a crea flotabilitatea este doar crearea unui script care va împinge obiectele în sus și în jos. Creați un script nou numit Buoyancy.js și setați inițiativa la:

Buoyancy.prototype.initialize = funcția () this.initialPosition = this.entity.getPosition () clone (); this.initialRotation = această.entity.getEulerAngles () clona (); // Timpul inițial este setat la o valoare aleatorie, astfel încât dacă // acest script este atașat la mai multe obiecte, acestea nu vor // toate să meargă în același mod this.time = Math.random () * 2 * Math.PI; ;

Acum, în actualizare, creștem timpul și rotim obiectul:

Buoyancy.prototype.update = funcția (dt) this.time + = 0.1; // Mutați obiectul în sus și în jos var pos = this.entity.getPosition () clone (); pos.y = this.initialPosition.y + Math.cos (this.time) * 0.07; this.entity.setPosition (pos.x, pos.y, pos.z); // Rotiți obiectul ușor var rot = this.entity.getEulerAngles () clone (); rot.x = this.initialRotation.x + Math.cos (acest.time * 0.25) * 1; rot.z = this.initialRotation.z + Math.sin (acest.time * 0.5) * 2; this.entity.setLocalEulerAngles (rot.x, rot.y, rot.z); ;

Aplicați acest script la barca dvs. și urmăriți-l să vă bateți în apă și în jos! Puteți aplica acest script pentru mai multe obiecte (inclusiv camera-încercați-o)!

Texturarea suprafeței

Chiar acum, singurul mod în care puteți vedea undele este să vă uitați la marginile suprafeței apei. Adăugarea unei texturi contribuie la creșterea vizibilității mișcării pe suprafață și este un mod ieftin de simulare a reflexiilor și a causelor.

Puteți încerca să găsiți niște texturi caustice sau să vă creați propriile. Iată unul pe care l-am atras în Gimp pe care îl puteți folosi în mod liber. Orice textură va funcționa atâta timp cât poate fi finisată perfect.

Odată ce ați găsit o textură care vă place, trageți-o în fereastra de activ a proiectului. Trebuie să facem referire la această textură în scriptul nostru Water.js, pentru a crea un atribut pentru acesta:

Water.attributes.add ('surfaceTexture', type: 'asset', assetType: 'texture', title: 'Textura suprafeței');

Și apoi alocați-o în editor:

Acum trebuie să-l transmitem shader-ului nostru. Mergi la Water.js și setați un nou parametru în CreateWaterMaterial funcţie:

material.setParameter ( 'uSurfaceTexture', this.surfaceTexture.resource);

Acum intrați Water.frag și să ne declare noua noastră uniformă:

eșantion unitar2D uSurfaceTexture;

Suntem aproape acolo. Pentru a face textura pe plan, trebuie să știm unde este fiecare pixel de-a lungul ochiurilor. Ceea ce înseamnă că trebuie să trecem niște date de la shader-ul de vârf la shader-ul fragmentului.

Variabile variabile

A variabilvariabilă vă permite să transmiteți datele de la shader-ul de vârf la shader-ul fragmentului. Acesta este cel de-al treilea tip de variabilă specială pe care o puteți avea într-un shader (celelalte două fiind uniformăși atribut). Acesta este definit pentru fiecare vârf și este accesibil fiecărui pixel. Deoarece există mai mulți pixeli decât vârfurile, valoarea este interpolată între noduri (de aici vine numele "variabilă" - variază de la valorile pe care le dați).

Pentru a încerca acest lucru, declarați o nouă variabilă în Water.vert ca variantă:

variabile vec2 ScreenPosition;

Și apoi puneți-o gl_Position după ce a fost calculat:

ScreenPosition = gl_Position.xyz;

Acum du-te înapoi Water.frag și să declare aceeași variabilă. Nu există nici o modalitate de a obține o ieșire de depanare dintr-un shader, dar putem folosi culoarea pentru a vizualiza depanarea. Iată o modalitate de a face acest lucru:

eșantion unitar2D uSurfaceTexture; variabile vec3 ScreenPosition; void principal (void) vec4 culoare = vec4 (0,0,0,7,1,0,0,5); // Testarea noii culori variate variabile = vec4 (vec3 (ScreenPosition.x), 1.0); gl_FragColor = culoare; 

Avionul ar trebui să pară acum alb-negru, acolo unde este linia care le separă ScreenPosition.x este 0. Valorile culorilor variază de la 0 la 1, dar valorile din ScreenPosition poate fi în afara acestui interval. Ele se fixează automat, deci dacă vedeți negru, ar putea fi 0 sau negativ.

Ceea ce am făcut noi este trecerea poziției ecranului fiecărui vârf la fiecare pixel. Puteți vedea că linia care separă laturile alb-negru va fi întotdeauna în centrul ecranului, indiferent de locul în care suprafața este de fapt în lume.

Provocarea # 1: Creați o nouă variabilă variabilă pentru a trece poziția mondială în loc de poziția ecranului. Vizualizați-o în același mod ca și noi. Dacă culoarea nu se schimbă cu aparatul foto, atunci ați făcut acest lucru corect.

Folosind UV 

Semnalele UV sunt coordonatele 2D pentru fiecare vârf de-a lungul ochiului, normalizate de la 0 la 1. Acesta este exact ceea ce avem nevoie pentru a preleva textura pe planul corect și ar trebui să fie deja configurat din partea anterioară.

Declarați un atribut nou în Water.vert (acest nume vine din definiția shader în Water.js):

atributul vec2 aUv0;

Și tot ce trebuie să facem este să-l transmiteți fragmentului shader, așa că creați o variantă și setați-o la atributul:

// În Water.vert // Declarăm acest lucru împreună cu celelalte variabile ale noastre de la vc2 variabile vUv0; // ... // În jos în funcția principală, stocăm valoarea atributului // în variabilele astfel încât șahul frag să poată avea acces la acesta vUv0 = aUv0; 

Acum declarăm aceeași variație în fragmentul de shader. Pentru a verifica dacă funcționează, o putem vizualiza ca mai înainte, astfel încât Water.frag acum arată ca:

eșantion unitar2D uSurfaceTexture; variind vec2 vUv0; void principal (void) vec4 culoare = vec4 (0,0,0,7,1,0,0,5); // Confirmarea culorii UV = vec4 (vec3 (vUv0.x), 1.0); gl_FragColor = culoare; 

Și ar trebui să vedeți un gradient, confirmând că avem o valoare de 0 la un capăt și un la altul. Acum, pentru a ne proba textura, tot ce trebuie sa facem este:

culoare = textură2D (uSurfaceTexture, vUv0);

Și ar trebui să vedeți textura de pe suprafață:

Styling textura

În loc de a stabili textura ca noua culoare, să o combinăm cu albastrul pe care l-am avut:

eșantion unitar2D uSurfaceTexture; variind vec2 vUv0; void principal (void) vec4 culoare = vec4 (0,0,0,7,1,0,0,5); vec4 Linii de apă = textură2D (uSurfaceTexture, vUv0); color.rgba + = WaterLines.r; gl_FragColor = culoare; 

Acest lucru se datorează faptului că culoarea texturii este negru (0) peste tot, cu excepția liniilor de apă. Prin adăugarea acesteia, nu schimbăm culoarea albastră originală, cu excepția locurilor în care există linii, unde devine mai strălucitoare. 

Dar nu este singura modalitate de combinare a culorilor.

Provocarea # 2: puteți combina culorile într-un mod pentru a obține efectul subtil descris mai jos?

Mutarea texturii

Ca efect final, dorim ca liniile să se deplaseze de-a lungul suprafeței, astfel încât să nu pară atât de statice. Pentru a face acest lucru, folosim faptul că orice valoare dată texture2D funcția din afara domeniului 0 la 1 se va înfășura (astfel încât 1,5 și 2,5 devin 0,5). Așadar, putem să ne creștem poziția prin variabila uniformă a timpului pe care am stabilit-o deja și să înmulțim poziția fie pentru a mări, fie pentru a scădea densitatea liniilor din suprafața noastră, ceea ce face ca shaderul nostru fin de fragmente să arate astfel:

eșantion unitar2D uSurfaceTexture; flotare uniformă uTime; variind vec2 vUv0; void principal (void) vec4 culoare = vec4 (0,0,0,7,1,0,0,5); vec2 pos = vUv0; // Multiplicarea cu un număr mai mare de 1 determină textura // să se repete mai des pos = 2.0; // Deplasarea întregii texturi astfel încât să se deplaseze de-a lungul suprafeței pos.y + = uTime * 0,02; vec4 Linii de apă = textură2D (uSurfaceTexture, pos); color.rgba + = WaterLines.r; gl_FragColor = culoare; 

Linii de spumă și tampon de adâncime

Realizarea liniilor de spumă în jurul obiectelor din apă face mult mai ușor să vedem cum sunt scufundate obiectele și unde se taie suprafața. De asemenea, apa noastră pare mult mai credibilă. Pentru a face acest lucru, trebuie să găsim oarecum unde sunt marginile fiecărui obiect și să faceți acest lucru în mod eficient.

Trucul

Ceea ce vrem este să putem spune, având în vedere un pixel pe suprafața apei, fie că este aproape de un obiect. Dacă da, îl putem colora ca spumă. Nu există un mod simplu de a face acest lucru (de care știu). Deci, pentru a ne da seama de aceasta, vom folosi o tehnică utilă de rezolvare a problemelor: să vină cu un exemplu la care știm răspunsul și să vedem dacă îl putem generaliza. 

Luați în considerare punctul de vedere de mai jos.

Ce pixeli ar trebui să facă parte din spumă? Știm că ar trebui să pară așa:

Să ne gândim deci la doi pixeli specifici. Am marcat două cu stele de mai jos. Cel negru este în spumă. Cel roșu nu este. Cum le putem spune separat în interiorul unui shader?

Ceea ce știm este că, deși acești doi pixeli sunt aproape împreună în spațiul ecranului (ambele sunt redate chiar pe partea de sus a corpului farului), ele sunt, de fapt, foarte îndepărtate în spațiul mondial. Putem verifica acest lucru privindu-ne la aceeași scenă dintr-un unghi diferit, după cum se arată mai jos.

Observați că steaua roșie nu este pe partea de sus a corpului farului așa cum a apărut, dar este de fapt și steaua neagră. Le putem spune separat folosind distanța față de cameră, denumită în mod obișnuit "adâncime", unde o adâncime de 1 înseamnă că este foarte aproape de cameră și o adâncime de 0 înseamnă că este foarte departe. Dar nu este doar o problemă a distanței absolute a lumii, sau adâncimii, față de cameră. Este profunzimea comparativ cu pixelul din spate.

Priviți înapoi la prima vedere. Să presupunem că corpul farului are o valoare de adâncime de 0,5. Adâncimea stelei negre ar fi foarte aproape de 0,5. Deci ea și pixelul din spatele ei au valori de adâncime similare. Steaua roșie, pe de altă parte, ar avea o profunzime mult mai mare, pentru că ar fi mai aproape de cameră, să zicem 0,7. Și totuși, pixelul din spatele lui, încă pe far, are o valoare de adâncime de 0,5, deci există o diferență mai mare acolo.

Acesta este trucul. Atunci când adâncimea pixelului de pe suprafața apei este suficient de apropiată de adâncimea pixelului este trasată deasupra, suntem aproape de marginea ceva, și o putem face ca spumă. 

Așadar, avem nevoie de mai multe informații decât cele disponibile în orice pixel dat. Trebuie să cunoaștem adâncimea pixelului pe care urmează să fie desenată. Aici intră tamponul de adâncime.

Buffer-ul de adâncime

Vă puteți gândi la un tampon sau la un framebuffer, doar ca o destinație de redare în afara ecranului sau o textură. Ați dori să renunțați la ecran atunci când încercați să citiți date înapoi, o tehnică care utilizează acest efect de fum.

Tamponul de adâncime este o țintă specială de randare care conține informații despre valorile adâncimii la fiecare pixel. Amintiți-vă că valoarea în gl_Position calculată în shader-ul de vârf a fost o valoare a spațiului ecranului, dar a avut și o a treia coordonată, o valoare Z. Această valoare Z este utilizată pentru a calcula adâncimea care este scrisă la tamponul de adâncime. 

Scopul tamponului de adâncime este de a atrage scena în mod corect, fără a fi nevoie să sortați obiecte înapoi în față. Fiecare pixel care urmează să fie desenat mai întâi consultă tamponul de adâncime. Dacă valoarea adâncimii este mai mare decât valoarea din tampon, este extrasă și valoarea proprie suprascrie cea din tampon. În caz contrar, este aruncat (pentru că înseamnă un alt obiect în fața acestuia).

De fapt, puteți opri scrierea la tampon de adâncime pentru a vedea cum ar arăta lucrurile fără ea. Puteți încerca acest lucru în Water.js:

material.depthTest = false;

Veți vedea cum va apărea întotdeauna apa deasupra, chiar dacă se află în spatele obiectelor opace.

Vizualizarea buffer-ului de adâncime

Să adăugăm o modalitate de a vizualiza tamponul de adâncime în scopul depanării. Creați un script nou numit DepthVisualize.js. Atașați-le la camera dvs. foto. 

Tot ce trebuie să facem pentru a avea acces la tamponul de adâncime în PlayCanvas este de a spune:

this.entity.camera.camera.requestDepthMap (); 

Aceasta va injecta automat o uniformă în toate shaderele pe care le putem folosi declarând ca:

uniform sampler2D uDepthMap;

Mai jos este un exemplu de script care cere harta de adâncime și o face pe partea de sus a scenei noastre. Este programată pentru reîncărcare la cald. 

var DepthVisualize = pc.createScript ('depthVisualize'); // inițializați codul numit o dată pe entitate DepthVisualize.prototype.initialize = function () this.entity.camera.camera.requestDepthMap (); this.antiCacheCount = 0; // Pentru a împiedica motorul să cacheze shader-ul nostru astfel încât să îl putem actualiza. ; DepthVisualize.prototype.SetupDepthViz = funcție () var device = this.app.graphicsDevice; var chunks = pc.shaderChunks; this.fs = "; this.fs + = 'variază vec2 vUv0;'; this.fs + = 'sampler uniformă uDepthMap;'; this.fs + ="; this.fs + = 'float unpackFloat (vec4 rgbaDepth) '; this.fs + = 'const vec4 bitShift = vec4 (1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0);'; this.fs + = 'adâncimea flotorului = punct (rgbaDepth, bitShift);'; this.fs + = 'adâncimea de întoarcere;'; this.fs + = ''; Acest lucru este incomplet si incomplet, iar acest lucru este incomplet si incomplet. gl_FragColor = vec4 (vec3 (adâncime), 1.0); '; this.fs + =' '; this.shader = chunks.createShaderFromCode (dispozitiv, chunks.fullscreenQuadVS, this.fs, "renderDepth" + this.antiCacheCount); this.antiCacheCount ++; / / Noi creăm manual un apel de remiză pentru a reda harta de adâncime peste toate lucrurile this.command = pc.Command nou (pc.LAYER_FX, pc.BLEND_NONE, function () pc.drawQuadWithShader (dispozitiv, null, this.shader); .bind (aceasta)); this.command.isDepthViz = true; // marcați-o astfel încât să o putem elimina mai târziu this.app.scene.drawCalls.push (this.command); ; // actualizați codul numit fiecare cadru DepthVisualize.prototype.update = function (dt) ; // metoda swap numită script-reloading // moșteni starea scriptului aici DepthVisualize.prototype.swap = function (old) this .antiCacheCount = old.antiCacheCount; // Eliminați adâncimea apel apel draw (var i = 0; i

Încercați să copiați conținutul și să comentați / dezactivați linia this.app.scene.drawCalls.push (this.command); pentru a comuta redarea în profunzime. Ar trebui să arate ceva asemănător imaginii de mai jos.

Provocarea # 3: Suprafața de apă nu este trasă în tampon de adâncime. Motorul PlayCanvas face acest lucru în mod intenționat. Îți poți da seama de ce? Ce e special cu materialul de apă? Altfel, bazându-ne pe regulile noastre de verificare a adâncimii, ce s-ar întâmpla dacă pixelii de apă au scris la tamponul de adâncime?

Sugestie: Există o linie pe care o puteți modifica în Water.js care va determina scrierea apei în tamponul de adâncime.

Un alt lucru de observat este faptul că înmulțesc valoarea adâncimii cu 30 în shaderul încorporat în funcția de inițializare. Acest lucru este doar pentru a putea vedea în mod clar, deoarece altfel gama de valori sunt prea mici pentru a vedea ca nuante de culoare.

Punerea în aplicare a trucului

Motorul PlayCanvas include o mulțime de funcții de ajutor pentru a lucra cu valori de adâncime, dar la momentul redactării nu sunt lansate în producție, așa că noi le vom pune singuri pe noi înșine.

Definiți următoarele uniforme la Water.frag:

// Aceste uniforme sunt toate injectate automat de PlayCanvas uniform sampler2D uDepthMap; uniformă vec4 uScreenSize; uniform mat4 matrix_view; // Trebuie să ne stabilim acest lucru în mod uniform vec4 camera_params;

Definiți aceste funcții de ajutor peste funcția principală:

#fdef GL2 float linearizeDepth (float z) z = z * 2,0 - 1,0; retur 1.0 / (camera_params.z * z + camera_params.w);  #else #ifndef UNPACKFLOAT #define UNPACKFLOAT float unpackFloat (vec4 rgbaDepth) const vec4 bitShift = vec4 (1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0); punct de întoarcere (rgbaDepth, bitShift);  #endif #endif float getLinearScreenDepth (vec2 uv) #ifdef GL2 întoarcere linearizeDepth (texture2D (uDepthMap, uv) .r) * camera_params.y; # nu se întoarce unpackFloat (texture2D (uDepthMap, uv)) * camera_params.y; #endif float getLinearDepth (vec3 pos) retur - (matrix_view * vec4 (pos, 1.0)); z;  float getLinearScreenDepth () vec2 uv = gl_FragCoord.xy * uScreenSize.zw; returna getLinearScreenDepth (uv); 

Transmiteți câteva informații despre aparatul foto la umbra Water.js. Puneți acest lucru în cazul în care treci alte uniforme, cum ar fi uTime:

dacă (! this.camera) this.camera = this.app.root.findByName ("Camera") camera;  var camera = this.camera; var n = camera.nearClip; var f = camera.farClip; var camera_paramelor = [1 / f, f, (1-f / n) / 2, (1 + f / n) / 2]; material.setParametru ("camera_params", camera_params);

În cele din urmă, avem nevoie de poziția mondială pentru fiecare pixel în shaderul nostru frag. Trebuie să obținem asta de la shaderul de vârfuri. Deci, definiți un variabil în Water.frag:

variind vec3 WorldPosition;

Definiți aceeași variație în Water.vert. Apoi, setați-l la poziția distorsionată în shaderul vârfului, astfel încât codul complet să arate ca:

atribut vec3 aPosition; atributul vec2 aUv0; variind vec2 vUv0; variind vec3 WorldPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; flotare uniformă uTime; void principal (void) vUv0 = aUv0; vec3 pos = aPoziție; poz.y + = cos (poz.z * 5.0 + uTime) * 0.1 * sin (pos.x * 5.0 + uTime); gl_Position = matrix_viewProjection * matrix_model * vec4 (pos, 1.0); WorldPosition = pos;  

De fapt, punerea în aplicare a trucului

Acum suntem gata să implementăm tehnica descrisă la începutul acestei secțiuni. Vrem să comparăm adâncimea pixelului la care suntem la adâncimea pixelului din spatele lui. Pixelul la care suntem vine din poziția mondială, iar pixelul din spate vine din poziția ecranului. Prindeți aceste două adâncimi:

flotați worldDepth = getLinearDepth (WorldPosition); float screenDepth = getLinearScreenDepth ();
Provocarea # 4: Una dintre aceste valori nu va fi niciodată mai mare decât cealaltă (presupunând că depthTest = true). Puteți deduce care?

Știm că spuma va fi acolo unde distanța dintre aceste două valori este mică. Deci, să facem diferența la fiecare pixel. Puneți acest lucru în partea de jos a shader-ului dvs. (și asigurați-vă că scriptul de vizualizare a adâncimii din secțiunea anterioară este dezactivat):

culoare = vec4 (vec3 (screenDepth - worldDepth), 1.0); gl_FragColor = culoare;

Care ar trebui să arate astfel:

Care corectă marginile oricărui obiect imersat în apă în timp real! Puteți, bineînțeles, să scalați această diferență pe care o facem pentru a face spuma să fie mai groasă / mai subțire.

Există acum multe moduri în care puteți combina această ieșire cu culoarea suprafeței apei pentru a obține linii de spumă frumoase. Ați putea să o păstrați ca pe un gradient, să o utilizați pentru a preleva dintr-o altă textura sau să o setați la o anumită culoare dacă diferența este mai mică sau egală cu un anumit prag.

Aspectul meu preferat este setarea la o culoare similară cu cea a liniilor statice de apă, astfel încât funcția mea finală principală arata astfel:

void principal (void) vec4 culoare = vec4 (0,0,0,7,1,0,0,5); vec2 pos = vUv0 * 2.0; poz.y + = uTime * 0,02; vec4 Linii de apă = textură2D (uSurfaceTexture, pos); color.rgba + = WaterLines.r * 0.1; flotați worldDepth = getLinearDepth (WorldPosition); float screenDepth = getLinearScreenDepth (); float foamLine = clemă ((screenDepth - worldDepth), 0.0,1.0); în cazul în care (foamLine < 0.7) color.rgba += 0.2;  gl_FragColor = color; 

rezumat

Am creat flotabilitate pe obiecte plutitoare în apă, am dat suprafața noastră o textură în mișcare pentru a simula caustica și am văzut cum am putea folosi tamponul de adâncime pentru a crea linii dinamice de spumă.

Pentru a termina acest lucru, următoarea și ultima parte va introduce efecte ulterioare procesului și cum să le folosiți pentru a crea efectul de distorsiune subacvatică.

Cod sursa

Puteți găsi proiectul finalizat găzduit aici PlayCanvas. Un port Three.js este de asemenea disponibil în acest depozit.