De cele mai multe ori, folosirea tehnicilor grafice convenționale este cea mai bună cale de a merge. Uneori, totuși, experimentarea și creativitatea la nivelele fundamentale ale unui efect pot fi benefice pentru stilul jocului, făcându-l să iasă mai mult. În acest tutorial vă voi arăta cum să creați un râu de lavă 2D animat folosind curbele Bézier, geometria personalizată cu textură și shaderele vârfurilor.
Notă: Deși acest tutorial este scris folosind AS3 și Flash, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.
Faceți clic pe semnul Plus pentru a deschide mai multe opțiuni: puteți regla grosimea și viteza râului și trageți punctele de control și punctele de poziție în jur.
Fără Flash? Consultați în schimb videoclipul YouTube:
Implementarea demo de mai sus utilizează AS3 și Flash cu Starling Framework pentru redarea accelerată a GPU și biblioteca Feathers pentru elementele UI. În scena noastră inițială vom plasa o imagine de bază și o imagine de rock în prim plan. Mai târziu vom adăuga un râu, introducându-l între cele două straturi.
Râurile sunt formate din procese naturale complexe de interacțiune între o masă fluidă și solul de sub ea. Ar fi impractic să faci o simulare fizică corectă pentru un joc. Vrem doar să obținem reprezentarea vizuală corectă și, pentru a face acest lucru, vom folosi un model simplificat al unui râu.
Modelarea râului ca o curbă este una dintre soluțiile pe care le putem folosi, ceea ce ne permite să avem un control bun și să obținem un aspect meandering. Am ales să folosesc curbele quadratice Bézier pentru a păstra lucrurile simple.
Curbele Bézier sunt curbe parametrice utilizate adesea în grafica computerizată; în curbe bivoliene Bézier, curba trece prin două puncte specificate, iar forma sa este determinată de al treilea punct, care este denumit de obicei un punct de control.
După cum se arată mai sus, curba trece prin punctele de poziție în timp ce punctul de control gestionează cursul pe care îl are. De exemplu, punerea punctului de control direct între punctele de poziție definește o linie dreaptă, în timp ce alte valori pentru punctul de control "atrag" curba să se apropie de acel punct.
Acest tip de curbă este definit folosind următoarea formulă matematică:
[latex] \ Large B (t) = (1 - t) ^ 2 P_0 + (2t - 2t ^ 2) C + t ^ 2 P_1 [/ latex]La t = 0 suntem la începutul curbei noastre; la t = 1 suntem la sfârșit.
Din punct de vedere tehnic, vom folosi mai multe curbe Bézier unde sfârșitul unuia este începutul celuilalt, formând un lanț.
Acum trebuie să rezolvăm problema de a afișa râul nostru. Curbele nu au o grosime, deci vom construi o geometrică primitivă în jurul ei.
Mai întâi avem nevoie de o modalitate de a lua curba și ao transforma în segmente de linie. Pentru a face acest lucru, luăm punctele noastre și le conectăm la definiția matematică a curbei. Lucrul perfect despre acest lucru este că putem adăuga cu ușurință un parametru pentru a controla calitatea acestei operații.
Iată codul pentru generarea punctelor din definiția curbei:
// Calculați punctul din expresia quadratică Bezier funcția privată quadraticBezier (P0: Punct, P1: Punct, C: Punct, t: Număr): Punct var x = (2 - 2 * t) * t * Cx + t * t * P1.x; var y = (1 - t) * (1 - t) * P0.y + (2 - 2 * t) * t * C.y + t * t * P1.y; returnați un nou punct (x, y);
Iată cum puteți converti curba la segmente de linie:
// Aceasta este o metodă care utilizează o listă de noduri // Fiecare nod este definit ca: position, control funcția publică convertToPoints (quality: Number = 10): Vector. puncte var: Vector. = Vector nou (); var precizie: Număr = 1 / calitate; // Treceți prin toate nodurile pentru a genera segmente de linie pentru (var i: int = 0; i < _nodes.length - 1; i++) var current:CurveNode = _nodes[i]; var next:CurveNode = _nodes[i + 1]; // Sample Bezier curve between two nodes // Number of steps is determined by quality parameter for (var step:Number = 0; step < 1; step += precision) var newPoint:Point = quadraticBezier(current.position, next.position, current.control, step); points.push(newPoint); return points;
Acum putem lua o curbă arbitrară și o putem transforma într-un număr particularizat de segmente de linie - cu cât sunt mai multe segmente, cu atât este mai mare calitatea:
Pentru a ajunge la geometrie, vom genera două curbe noi pe baza celei originale. Poziția lor și punctele de control vor fi mutate de o valoare normală a vectorului de offset, pe care o putem considera drept grosime. Prima curbă va fi mutată în direcția negativă, în timp ce a doua este mutată în direcția pozitivă.
Vom folosi acum funcția definită mai devreme pentru a crea segmente de linie din curbe. Aceasta va forma o limită în jurul curbei inițiale.
Cum facem acest lucru în cod? Va trebui să calculam normalele pentru pozițiile și punctele de control, să le multiplicăm prin offset și să le adăugăm la valorile originale. Pentru punctele de poziție trebuie să interpola normale formate de linii către punctele de control adiacente.
// Iterați prin toate punctele pentru (var i: int = 0; i < _nodes.length; i++) var normal:Point; var surface:Point; // Normal formed by position points if (i == 0) // First point - take normal from first line segment normal = lineNormal(_nodes[i].position, _nodes[i].control); surface = lineNormal(_nodes[i].position, _nodes[i + 1].position); else if (i + 1 == _nodes.length) // Last point - take normal from last line segment normal = lineNormal(_nodes[i - 1].control, _nodes[i].position); surface = lineNormal(_nodes[i - 1].position, _nodes[i].position); else // Middle point - take 2 normals from segments // adjecent to the point, and interpolate them normal = lineNormal(_nodes[i].position, _nodes[i].control); normal = normal.add( lineSegmentNormal(_nodes[i - 1].control, _nodes[i].position)); normal.normalize(1); // This causes a slight visual issue for thicker rivers // It can be avoided by adding more nodes surface = lineNormal(_nodes[i].position, _nodes[i + 1].position); // Add offsets to the original node, forming a new one. nodesWithOffset.add( _nodes[i].position.x + normal.x * offset, _nodes[i].position.y + normal.y * offset, _nodes[i].control.x + surfaceNormal.x * offset, _nodes[i].control.y + surfaceNormal.y * offset );
Puteți vedea deja că putem folosi aceste puncte pentru a defini mici poligoane cu patru fețe - "quads". Implementarea noastră utilizează un Starling DisplayObject personalizat, care oferă datele noastre geometrice direct pe unitatea de procesare grafică.
O problemă, în funcție de implementare, este că nu putem trimite quad-uri direct; în schimb, trebuie să trimitem triunghiuri. Dar este destul de ușor să alegeți două triunghiuri folosind patru puncte:
Rezultat:
Stilul geometric curat este distractiv și ar putea fi chiar un stil bun pentru unele jocuri experimentale. Dar, pentru ca râul nostru să arate cu adevărat bine, am putea face și câteva detalii. Folosirea unei texturi este o idee bună. Ceea ce ne conduce la problema de a fi afișată pe geometria personalizată creată mai devreme.
Va trebui să adăugăm informații suplimentare la nodurile noastre; singurele poziții nu vor mai face. Fiecare vârf poate stoca parametri adiționali în funcție de preferințele noastre și pentru a sprijini cartografierea texturii, va trebui să definim coordonatele texturii.
Coordonatele texturilor sunt în spațiul texturii și valorile pixelilor de pe hartă ale imaginii în pozițiile lumii de vârfuri. Pentru fiecare pixel care apare pe ecran, se calculează coordonatele de textură interpolate și se utilizează valorile pixelilor de căutare pentru pozițiile din textura. Valorile 0 și 1 din spațiul texturii corespund marginilor texturii; dacă valorile părăsesc acest interval, avem câteva opțiuni:
Cei care știu puțin despre cartografierea texturilor sunt cu siguranță conștienți de posibilele complexități ale tehnicii. Am vesti bune pentru tine! Acest mod de reprezentare a râurilor este ușor de cartografiat într-o textura.
Din laturi, înălțimea texturii este mapată în întregime, în timp ce lungimea râului este segmentată în bucăți mai mici ale spațiului texturii, dimensionate corespunzător la lățimea texturii.
Acum, pentru ao pune în aplicare în cod:
// _texture este o distanță de tip Starling tex var: Number = 0; // Iterați prin toate punctele pentru (var i: int = 0; i < _points.length; i++) if (i > 0) // Distanța în textură spațiu pentru distanța curentă a segmentului de linie + = Punct.distanță (lastPoint, _points [i]) / _texture.width; // Alocați coordonatele texturii geometriei _vertexData.setTexCoords (vertexId ++, distance, 0); _vertexData.setTexCoords (vertexId ++, distanța, 1);
Acum arată mai mult ca un râu:
Râul nostru pare acum mai mult ca unul real, cu o singură excepție: rămâne în picioare!
Bine, deci trebuie să o animăm. Primul lucru pe care vă puteți gândi este să utilizați animație foaie sprite. Și asta ar putea funcționa, dar pentru a păstra mai multă flexibilitate și pentru a salva puțin memoria texturii, vom face ceva mai interesant.
În loc de a schimba textura, putem schimba felul în care texturile se potrivesc geometriei. Facem asta prin schimbarea coordonatelor texturilor pentru nodurile noastre. Acest lucru va funcționa numai pentru texturile care pot fi imprimabile cu setarea de mapare repeta
.
O modalitate ușoară de a implementa acest lucru este schimbarea coordonatelor de textura de pe CPU și trimiterea rezultatelor către GPU-ul fiecărui cadru. De obicei, aceasta este o modalitate bună de a începe o implementare a acestui tip de tehnică, deoarece depanarea este mult mai ușoară. Cu toate acestea, ne vom îndrepta direct în cel mai bun mod în care putem realiza acest lucru: animarea coordonatelor de textură folosind shader-urile de vârf.
Din experiență pot spune că oamenii sunt uneori intimidați de shaders, probabil datorită legăturii lor cu efectele grafice avansate ale jocurilor blockbuster. Adevărul este că conceptul din spatele lor este extrem de simplu și dacă poți scrie un program, poți scrie un shader - toate acestea sunt, mici programe care rulează pe GPU. Vom folosi un shader de vârf pentru a ne anima râul, există și alte tipuri de shadere, dar putem face fără ele.
După cum sugerează și numele, șuturile vertex procesează nodurile. Ei rulează pentru fiecare vârf și iau ca atribute de vârf de intrare: poziția, coordonatele de textură și culoarea.
Scopul nostru este de a compensa valoarea X a coordonatelor texturii fluviului pentru a simula fluxul. Păstrăm un contor de debit și îl creștem în fiecare cadru în funcție de delta timpului. Putem specifica un parametru suplimentar pentru viteza animatiei. Valoarea offset ar trebui să fie transferată la shader ca o valoare uniformă (constantă), o modalitate de a furniza programului shader mai multe informații decât doar nodurile. Această valoare este de obicei un vector cu patru componente; vom folosi componenta X pentru a stoca valoarea, în timp ce setați Y, Z și W la 0.
// Texture offset la indexul 5, pe care îl vom referi mai târziu în contextul shader.setProgramConstantsFromVector (Context3DProgramType.VERTEX, 5, new [-_textureOffset, 0, 0, 0], 1);
Această implementare utilizează limbajul shadow AGAL. Poate fi un pic greu de înțeles, deoarece este o adunare ca limba. Puteți afla mai multe despre el aici.
Vertex Shader:
m4 op, va0, vc0 // Calculați poziția lumii vertex mul v0, va1, vc4 // Calculați culoarea vertexului // Adăugați coordonatele texturii vertexului (va2) și constanta decalajului texturii (vc5): add v1, va2, vc5
Animație în acțiune:
Suntem destul de mult făcuți, cu excepția faptului că râul nostru pare încă nefiresc. Tăierea tăiată între fundal și râu este un adevărat ochi. Pentru a rezolva acest lucru, puteți folosi un strat suplimentar al râului, puțin mai gros, și o textură specială, care ar suprapune malurile râurilor și ar acoperi tranziția urâtă.
Și din moment ce demo-ul reprezintă râu de lavă topită, nu putem merge fără o strălucire! Faceți un alt exemplu de geometrie a râului, folosind acum o textură strălucitoare și setați modul de amestecare pentru a "adăuga". Pentru mai multă distracție, adăugați o animație netedă a valorii strălucitoare alfa.
Demo final:
Desigur, puteți face mult mai mult decât râurile folosind acest tip de efect. Am văzut-o folosită pentru efecte particulare fantomă, cascade sau chiar pentru animarea lanțurilor. Există o mulțime de spațiu pentru îmbunătățirea ulterioară, performanța înțeleaptă versiunea finală de sus poate fi făcută folosind un apel egal dacă texturile sunt îmbinate cu un atlas. Râurile lungi ar trebui împărțite în mai multe părți și aruncate. O extensie majoră ar fi punerea în aplicare a forfetării nodurilor curbe pentru a permite mai multe căi fluviale și, la rândul lor, a simula bifurcația.
Folosesc această tehnică în ultimul nostru joc și sunt foarte mulțumit de ceea ce putem face cu ea. Îl folosim pentru râuri și drumuri (fără animație, evident). Mă gândesc să folosesc un efect similar pentru lacuri.
Sper că ți-am dat niște idei despre cum să gândești în afara tehnicilor grafice obișnuite, cum ar fi utilizarea de foi de sprite sau seturi de plăci pentru a realiza efecte de genul asta. Este nevoie de un pic mai mult de lucru, un pic de matematica, și unele cunoștințe de programare GPU, dar în schimb veți obține o flexibilitate mult mai mare.