În Ghidul meu pentru începători la Shaders m-am concentrat exclusiv pe shadere de fragmente, care este suficient pentru orice efect 2D și pentru fiecare exemplu ShaderToy. Dar există o întreagă categorie de tehnici care necesită shadere de vârfuri. Acest tutorial vă va ajuta să creați apă stilizată în formă de toy în timp ce introduceți shadere de vârfuri. Voi introduce, de asemenea, tamponul de adâncime și modul de utilizare a acestuia pentru a obține mai multe informații despre scenă și pentru a crea linii de spumă.
Iată cum ar trebui să arate efectul final. Puteți încerca o demonstrație live aici (mouse-ul stânga pe orbită, mouse-ul dreapta pentru a panorama, rotița de derulare pentru zoom).
Mai exact, acest efect este compus din:
Ceea ce îmi place în legătură cu acest efect este că atinge o mulțime de concepte diferite în grafica computerizată, așa că ne va permite să folosim idei din tutoriale din trecut, precum și să dezvoltăm tehnici pe care le putem folosi pentru o varietate de efecte viitoare.
Voi folosi PlayCanvas pentru aceasta doar pentru că are un IDE bazat pe web gratuit, dar totul ar trebui să fie aplicabil în orice mediu care rulează WebGL. Puteți găsi o versiune Three.js a codului sursă la sfârșit. Voi presupune că vă simțiți confortabil folosind shaderele de fragmente și navigând în interfața PlayCanvas. Puteți să vă spălați aici shaderele și să introduceți un intro la PlayCanvas aici.
Scopul acestei secțiuni este de a configura proiectul nostru PlayCanvas și de a plasa unele obiecte de mediu pentru a testa apa împotriva.
Dacă nu aveți deja un cont cu PlayCanvas, creați un cont și creați unul nou proiect necompletat. În mod implicit, ar trebui să aveți câteva obiecte, o cameră și o lumină în scenă.
Proiectul Google Poly este o resursă foarte bună pentru modelele 3D pentru web. Iată modelul de barcă pe care l-am folosit. Odată ce descărcați și dezarhivați acest lucru, ar trebui să găsiți a .obj
și a .png
fişier.
.png
fişier.Acum poți trage Tugboat.json în scena dvs. și ștergeți obiectele Box și Plane. Poți să scrii barca în sus dacă ar părea prea mică (am stabilit a mea la 50).
Puteți adăuga orice alte modele la scenă în același mod.
Pentru a configura o cameră cu orbită, vom copia un script din acest exemplu PlayCanvas. Accesați linkul respectiv și faceți clic pe Editor pentru a intra în proiect.
șoarece-input.js
și orbită-camera.js
de la acel proiect tutorial la fișierele cu același nume în propriul proiect.Sfat: Puteți crea dosare în fereastra de materiale pentru a păstra lucrurile organizate. Am pus aceste două scripturi de cameră sub Scripts / Camera /, modelul meu în Modele /, și materialul meu sub Materiale /.
Acum, când lansați jocul (butonul de redare din partea dreaptă sus a scenei), ar trebui să puteți vedea barca și orbita în jurul acesteia cu mouse-ul.
Scopul acestei secțiuni este de a genera o plasă subdivizată pentru a fi folosită ca suprafața apei noastre.
Pentru a genera suprafața apei, vom adapta un cod din acest tutorial de generare a terenurilor. Creați un nou fișier de script numit Water.js
. Editați acest script și creați o nouă funcție numită GeneratePlaneMesh
care arată astfel:
Water.prototype.GeneratePlaneMesh = funcție (opțiuni) // 1 - setați opțiunile implicite dacă nu sunt furnizate niciunul dacă opțiunile = subdiviziuni: 100, lățime: 10, înălțime: 10; // 2 - Generarea punctelor, a lui uv și a indiciilor var positions = []; var uvs = []; var indices = []; var rând, col; var normal; pentru (rând = 0; rând <= options.subdivisions; row++) for (col = 0; col <= options.subdivisions; col++) var position = new pc.Vec3((col * options.width) / options.subdivisions - (options.width / 2.0), 0, ((options.subdivisions - row) * options.height) / options.subdivisions - (options.height / 2.0)); positions.push(position.x, position.y, position.z); uvs.push(col / options.subdivisions, 1.0 - row / options.subdivisions); for (row = 0; row < options.subdivisions; row++) for (col = 0; col < options.subdivisions; col++) indices.push(col + row * (options.subdivisions + 1)); indices.push(col + 1 + row * (options.subdivisions + 1)); indices.push(col + 1 + (row + 1) * (options.subdivisions + 1)); indices.push(col + row * (options.subdivisions + 1)); indices.push(col + 1 + (row + 1) * (options.subdivisions + 1)); indices.push(col + (row + 1) * (options.subdivisions + 1)); // Compute the normals normals = pc.calculateNormals(positions, indices); // Make the actual model var node = new pc.GraphNode(); var material = new pc.StandardMaterial(); // Create the mesh var mesh = pc.createMesh(this.app.graphicsDevice, positions, normals: normals, uvs: uvs, indices: indices ); var meshInstance = new pc.MeshInstance(node, mesh, material); // Add it to this entity var model = new pc.Model(); model.graph = node; model.meshInstances.push(meshInstance); this.entity.addComponent('model'); this.entity.model.model = model; this.entity.model.castShadows = false; // We don't want the water surface itself to cast a shadow ;
Acum puteți suna acest lucru în inițializa
funcţie:
Water.prototype.initialize = function () this.GeneratePlaneMesh (subdiviziuni: 100, lățime: 10, înălțimea: 10); ;
Ar trebui să vezi doar un avion plat atunci când lansezi jocul acum. Dar acesta nu este doar un plan plat. Este o plasă compusă din mii de vârfuri. Ca provocare, încercați să verificați acest lucru (este o scuză bună să citiți codul pe care tocmai l-ați copiat).
Provocarea # 1: Deplasați coordonatele Y ale fiecărui vârf cu o sumă aleatorie pentru a obține planul să arate ceva asemănător imaginii de mai jos.
Scopul acestei secțiuni este de a conferi suprafeței apei un material particularizat și a crea valuri animate.
Pentru a obține efectele pe care le dorim, trebuie să stabilim un material personalizat. Majoritatea motoarelor 3D vor avea unele shadere predefinite pentru redarea obiectelor și o modalitate de a le suprascrie. Iată o bună referință pentru a face acest lucru în PlayCanvas.
Să creăm o nouă funcție numită CreateWaterMaterial
care definește un material nou cu un shader personalizat și îl returnează:
Water.prototype.CreateWaterMaterial = functie () // Crearea unui material nou blank var material = nou pc.Material (); // Un nume simplifică identificarea la depanarea materialului.name = "DynamicWater_Material"; // Creați definiția shaderului // setați dinamic precizia în funcție de dispozitiv. var gd = this.app.graphicsDevice; var fragmentShader = "precizie" + gd.precision + "float; \ n"; fragmentShader = fragmentShader + this.fs.resource; var vertexShader = acest.vs.resource; // O definiție de shader utilizată pentru a crea un shader nou. var shaderDefinition = atribute: aPoziție: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0, vshader: vertexShader, fshader: fragmentShader; // Creați shaderul din definiția this.shader = noul pc.Shader (gd, shaderDefinition); // Aplicați shader la acest material material.setShader (this.shader); materiale returnate; ;
Această funcție captează codul de vârf și fragmentul de shader de la atributele de script. Deci, să definim cele din partea de sus a fișierului (după pc.createScript
linia):
Water.attributes.add ('vs', type: 'asset', assetType: 'shader', titlu: 'Vertex Shader'); Water.attributes.add ('fs', type: 'asset', assetType: 'shader', titlu: 'Fragment Shader');
Acum putem crea aceste fișiere shader și le atașăm la scenariul nostru. Du-te înapoi la editor și creați două noi fișiere shader: Water.frag și Water.vert. Atașați aceste shadere în scenariul dvs. după cum se arată mai jos.
Dacă noile atribute nu apar în editor, faceți clic pe Analiza pentru a reîmprospăta scriptul.
Acum puneți acest shader de bază în Water.frag:
void principal (void) vec4 culoare = vec4 (0,0,0,0,1,0,0,5); gl_FragColor = culoare;
Și asta în Water.vert:
atribut vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void principal void gl_Position = matrice_viewProjecție * matrix_model * vec4 (aPoziție, 1.0);
În cele din urmă, reveniți la Water.js și să-l folosim pe noul nostru material personalizat în locul materialului standard. Deci, în loc de:
var material = nou pc.StandardMaterial ();
Do:
var materialul = this.CreateWaterMaterial ();
Acum, dacă lansați jocul, avionul ar trebui să fie acum albastru.
Până acum, tocmai am creat niște shadere false pe noul nostru material. Înainte de a ajunge la scrierea efectelor reale, ultimul lucru pe care vreau să-l instalez este reîncărcarea automată a codului.
Necomplementarea schimb
funcția în orice fișier script (cum ar fi Water.js) permite reîncărcarea la cald. Vom vedea cum să folosim acest lucru mai târziu pentru a menține statul chiar și atunci când actualizăm codul în timp real. Dar pentru moment dorim doar să reaplicăm shaderele după ce am detectat o schimbare. Shaders se compilează înainte de a fi difuzate în WebGL, așa că va trebui să recreăm materialul personalizat pentru a declanșa acest lucru.
Vom verifica dacă conținutul codului nostru shader a fost actualizat și, dacă este cazul, vom recrea materialul. Mai întâi, salvați shaderele curente în inițializa:
// inițializați codul numit o singură dată pe entitate Water.prototype.initialize = function () this.GeneratePlaneMesh (); // Salvați shaderele curente this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ;
Și în Actualizați, verificați dacă au existat modificări:
// actualizați codul numit fiecare cadru Water.prototype.update = function (dt) if (this.savedFS! = this.fs.resource || this.savedVS! = this.vs.resource) // Re-creați materialul astfel încât shaderele pot fi recompilate var newMaterial = this.CreateWaterMaterial (); // Aplicați-l la modelul var model = this.entity.model.model; model.meshInstances [0] .material = newMaterial; // Salvați noile shadere this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ;
Acum, pentru a confirma acest lucru, lansați jocul și schimbați culoarea planului în Water.frag la un albastru mai bun. Odată ce ați salvat fișierul, acesta ar trebui să se actualizeze fără a fi necesară reîmprospătarea sau relansarea! Aceasta a fost culoarea pe care am ales-o:
vec4 color = vec4 (0,0,0,7,1,0,0,5);
Pentru a crea valuri, trebuie să mutăm fiecare vârf în ochiul nostru în fiecare cadru. Sună ca și cum va fi foarte ineficient, dar fiecare vârf al fiecărui model se transformă deja pe fiecare cadru pe care îl facem. Acesta este ceea ce face shaderul de vârfuri.
Dacă te gândești la un shader de fragmente ca o funcție care rulează pe fiecare pixel, ia o poziție și apoi întoarce o culoare un shader de vârfuri este o funcție care rulează pe fiecare vârf, ocupă o poziție și returnează o poziție.
Valoarea implicită a shader-ului de vârfuri va lua poziția mondială a unui model dat și returnați ecranul. Scena noastră 3D este definită în termeni de x, y și z, dar monitorul dvs. este un plan plat bidimensional, deci proiectăm lumea 3D pe ecranul 2D. Această proiecție este ceea ce matricea de vizualizare, proiecție și model are grijă și este în afara scopului acestui tutorial, dar dacă doriți să aflați exact ce se întâmplă la acest pas, iată un ghid foarte bun.
Deci, această linie:
gl_Position = matrix_viewProjection * matrix_model * vec4 (aPoziție, 1.0);
ia o pozitie
ca poziție globală 3D a unui anumit vârf și care îl transformă în gl_Position
, care este poziția finală a ecranului 2D. Prefixul "a" pe aPosition înseamnă că această valoare este o valoare atribut. Amintiți-vă că a uniformăvariabila este o valoare pe care o putem defini pe CPU să treacă la un shader care păstrează aceeași valoare pentru toți pixelii / vârfurile. Valoarea unui atribut, pe de altă parte, vine de la mulțime definite pe CPU. Shader-ul de vârfuri se numește o singură dată pentru fiecare valoare din matricea respectivă.
Puteți vedea că aceste atribute sunt configurate în definiția shaderului pe care am creat-o în Water.js:
var shaderDefinition = atribute: aPoziție: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0, vshader: vertexShader, fshader: fragmentShader;
PlayCanvas are grijă de configurarea și trecerea unui șir de poziții de vârfuri pentru o pozitie
când vom trece acest enum, dar, în general, ați putea trece orice șir de date către shaderul de vârfuri.
Să presupunem că vrei să spargeți avionul prin înmulțirea tuturor X
valori pe jumătate. Ar trebui să te schimbi o pozitie
sau gl_Position
?
Sa incercam o pozitie
primul. Nu putem modifica direct un atribut, dar putem face o copie:
atribut vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void principală (void) vec3 pos = aPoziție; poz.x * = 0,5; gl_Position = matrix_viewProjection * matrix_model * vec4 (pos, 1.0);
Avionul ar trebui să pară mai dreptunghiular. Nimic ciudat acolo. Ce se întâmplă dacă încercăm în schimb să modificăm gl_Position
?
atribut vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void principală (void) vec3 pos = aPoziție; //pos.x * = 0,5; gl_Position = matrix_viewProjection * matrix_model * vec4 (pos, 1.0); gl_Position.x * = 0.5;
Ar putea arăta la fel până când începeți să rotiți camera. Modificăm coordonatele spațiului de pe ecran, ceea ce înseamnă că va arăta diferit în funcție de cum te uiți la el.
Deci, astfel puteți mișca vârfurile și este important să faceți această distincție între faptul că vă aflați în spațiu mondial sau pe ecran.
Provocarea # 2: Puteți mișca întreaga suprafață plană cu câteva unități în sus (de-a lungul axei Y) în shaderul de vârfuri fără a distorsiona forma?
Provocarea # 3: Am spus că gl_Position este 2D, dar gl_Position.z există. Puteți efectua anumite teste pentru a determina dacă această valoare afectează ceva și dacă da, la ce se utilizează?
Un ultim lucru de care avem nevoie înainte de a putea crea valuri în mișcare este o variabilă uniformă de utilizat ca timp. Declarați o uniformă în shaderul dvs. de vârfuri:
flotare uniformă uTime;
Apoi, pentru a trece asta la shaderul nostru, du-te înapoi Water.js și definiți o variabilă de timp în inițializare:
Water.prototype.initialize = function () this.time = 0; ///// Mai întâi definește timpul aici aici.GeneratePlaneMesh (); // Salvați shaderele curente this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ;
Acum, pentru a trece acest lucru la shader nostru, vom folosi material.setParameter
. Mai întâi setăm o valoare inițială la sfârșitul intervalului CreateWaterMaterial
funcţie:
// Creați shaderul din definiția this.shader = noul pc.Shader (gd, shaderDefinition); ///////////////////////////////////////////////////////////// this.material = material; // Salvați o referință la acest material //////////////// // Aplicați shader la acest material material.setShader (this.shader); materiale returnate;
Acum în Actualizați
puteți să creștem timpul și să accesăm materialul folosind referința pe care am creat-o pentru el:
this.time + = 0,1; this.material.setParameter ( 'uTime', this.time);
Ca o ultimă etapă, în funcția swap, copiați peste vechea valoare a timpului, astfel încât, chiar dacă modificați codul, va continua să crească fără a reseta la 0.
Water.prototype.swap = funcția (veche) this.time = old.time; ;
Acum totul este gata. Lansați jocul pentru a vă asigura că nu există erori. Acum hai să ne mutăm avionul cu o funcție de timp în Water.vert
:
poz.y + = cos (uTime)
Și avionul tău ar trebui să se deplaseze acum și în jos! Deoarece avem acum o funcție de swap, puteți actualiza și Water.js fără a fi nevoie să relansați. Încercați să creșteți sau să încetinească creșterea timpului pentru a confirma acest lucru.
Provocarea # 4: Puteți mișca vârfurile astfel încât să pară valul de mai jos?
Ca o sugestie, am vorbit în profunzime despre modalități diferite de a crea valuri aici. Asta a fost în 2D, dar se aplică aceeași matematică aici. Dacă doriți mai degrabă să aruncați o privire asupra soluției, iată ce e.
Scopul acestei secțiuni este de a face suprafața apelor translucide.
S-ar putea să fi observat că culoarea pe care o întoarcem în Water.frag are o valoare alfa de 0,5, dar suprafața este încă complet opacă. Transparența în multe privințe este încă o problemă deschisă în domeniul graficii pe calculator. O modalitate ieftină de ao realiza este folosirea amestecării.
În mod normal, când un pixel urmează să fie desenat, acesta verifică valoarea în tampon adâncime în funcție de valoarea adâncimii proprii (poziția sa de-a lungul axei Z) pentru a determina dacă suprascrie pixelul curent pe ecran sau se aruncă singur. Aceasta este ceea ce vă permite să faceți o scenă corect fără a fi nevoie să sortați obiecte înapoi în față.
Cu amestecare, în loc să aruncăm sau să suprascriem, putem combina culoarea pixelului deja desen (destinația) cu pixelul care urmează să fie extras (sursa). Puteți vedea aici toate funcțiile de amestecare disponibile în WebGL.
Pentru a face lucrul alfa așa cum ne așteptăm, vrem ca culoarea combinată a rezultatului să fie sursa înmulțită cu alfa plus destinația înmulțită cu o minus alfa. Cu alte cuvinte, dacă alfa este 0.4, culoarea finală ar trebui să fie:
finalColor = sursă * 0,4 + destinație * 0,6;
În PlayCanvas, opțiunea pc.BLEND_NORMAL face exact acest lucru.
Pentru a activa acest lucru, trebuie doar să setați proprietatea asupra materialului din interior CreateWaterMaterial
:
material.blendType = pc.BLEND_NORMAL;
Dacă lansați jocul acum, apa va fi translucidă! Dar nu este perfect. Apare o problemă dacă suprafața translucidă se suprapune cu ea însăși, după cum se arată mai jos.
Putem rezolva acest lucru folosind alfa la acoperire, care este o tehnică multi-prelevare de probe pentru a obține transparențăîn loc de amestecare:
//material.blendType = pc.BLEND_NORMAL; material.alphaToCoverage = adevărat;
Dar acest lucru este disponibil numai în WebGL 2. Pentru restul acestui tutorial, voi folosi blending-ul pentru al păstra simplu.
Până acum ne-am înființat mediul înconjurător și am creat suprafața translucidă a apei cu valuri animate de la shaderul nostru de vârfuri. A doua parte va acoperi aplicarea flotabilității pe obiecte, adăugarea de linii de apă la suprafață și crearea liniilor de spumă în jurul marginilor obiectelor care intersectează suprafața.
Partea finală va cuprinde aplicarea efectului de distorsiune post-proces sub apă și a unor idei despre locația următoare.
Puteți găsi proiectul finalizat găzduit aici PlayCanvas. Un port Three.js este de asemenea disponibil în acest depozit.