WebGL Essentials Partea a II-a

Acest articol se va baza pe cadrul introdus în prima parte a acestei mini-serii, adăugând un importator model și o clasă personalizată pentru obiecte 3D. Veți fi, de asemenea, prezentați animației și controalelor. Trebuie să trecem prin multe, deci să începem!

Acest articol se bazează foarte mult pe primul articol, deci, dacă nu l-ați citit încă, ar trebui să începeți mai întâi acolo.

Modul în care WebGL manipulează elementele din lumea 3D este prin utilizarea formulelor matematice cunoscute sub numele de transformări. Deci, înainte de a începe construirea clasei 3D, vă voi arăta câteva dintre diferitele transformări și modul în care sunt implementate.


transformări

Există trei transformări de bază atunci când lucrați cu obiecte 3D.

  • In miscare
  • scalarea
  • Rotirea

Fiecare dintre aceste funcții poate fi realizată pe axa X, Y sau Z, făcând astfel o posibilitate totală de nouă transformări de bază. Toate acestea afectează matricea de transformare 4x4 a obiectului 3D în moduri diferite. Pentru a efectua mai multe transformări pe același obiect fără probleme suprapuse, trebuie să înmulțim transformarea în matricea obiectului și să nu o aplicăm direct matricei obiectului. Mutarea este cea mai usor de facut, asa ca sa incepem acolo.

Mutarea lui A.K.A. "Traducere"

Mutarea unui obiect 3D este una dintre cele mai ușoare transformări pe care le puteți face, deoarece există un loc special în matricea 4x4 pentru el. Nu este nevoie de nici un fel de matematică; puneți coordonatele X, Y și Z în matrice și ați terminat. Dacă vă uitați la matricea 4x4, atunci sunt primele trei numere din rândul de jos. În plus, trebuie să știți că pozitivul Z este în spatele camerei. Prin urmare, o valoare Z de -100 plasează obiectul cu 100 de unități spre interior pe ecran. Vom compensa acest lucru în codul nostru.

Pentru a efectua mai multe transformări, nu puteți schimba pur și simplu matricea reală a obiectului; trebuie să aplicați transformarea la o matrice nouă, cunoscută sub numele de identitate matricea și multiplicați-o cu matricea principală.

Matricea de multiplicare poate fi un pic dificil de înțeles, însă ideea de bază este că fiecare coloană verticală este înmulțită cu rândul orizontal al celei de-a doua matrice. De exemplu, primul număr ar fi primul rând înmulțit cu prima coloană a celeilalte matrice. Al doilea număr din noua matrice ar fi primul rând înmulțit cu a doua coloană a celei de-a doua matrici și așa mai departe.

Fragmentul următor este codul pe care l-am scris pentru înmulțirea a două matrice în JavaScript. Adăugați-o la dvs. .js fișierul pe care l-ați făcut în prima parte a acestei serii:

funcția MH (A, B) var Sum = 0; pentru (var i = 0; i < A.length; i++)  Sum += A[i] * B[i];  return Sum;  function MultiplyMatrix(A, B)  var A1 = [A[0], A[1], A[2], A[3]]; var A2 = [A[4], A[5], A[6], A[7]]; var A3 = [A[8], A[9], A[10], A[11]]; var A4 = [A[12], A[13], A[14], A[15]]; var B1 = [B[0], B[4], B[8], B[12]]; var B2 = [B[1], B[5], B[9], B[13]]; var B3 = [B[2], B[6], B[10], B[14]]; var B4 = [B[3], B[7], B[11], B[15]]; return [ MH(A1, B1), MH(A1, B2), MH(A1, B3), MH(A1, B4), MH(A2, B1), MH(A2, B2), MH(A2, B3), MH(A2, B4), MH(A3, B1), MH(A3, B2), MH(A3, B3), MH(A3, B4), MH(A4, B1), MH(A4, B2), MH(A4, B3), MH(A4, B4)]; 

Nu cred că acest lucru necesită explicații, deoarece este doar matematica necesară pentru multiplicarea matricelor. Să mergem la scalare.

scalarea

Scalarea unui model este, de asemenea, destul de simplă - este o simplă multiplicare. Trebuie să înmulțiți primele trei numere diagonale, indiferent de scară. Încă o dată, ordinul este X, Y și Z. Deci, dacă doriți să vă scalați obiectul să fie de două ori mai mare în toate cele trei axe, veți înmulți elementele 1, 6 și 11 în matricele dvs. cu 2.

Rotirea

Rotirea este cea mai slabă transformare deoarece există o ecuație diferită pentru fiecare dintre cele trei axe. Următoarea imagine prezintă ecuațiile de rotație pentru fiecare axă:

Nu vă faceți griji dacă această imagine nu are sens pentru voi; vom examina implementarea JavaScript în curând.

Este important să rețineți că este important modul în care efectuați transformările; comenzile diferite produc rezultate diferite.

Este important să rețineți că este important modul în care efectuați transformările; comenzile diferite produc rezultate diferite. Dacă vă mutați mai întâi obiectul și apoi îl rotiți, WebGL vă va schimba obiectul ca o bâtă, spre deosebire de rotirea obiectului în loc. Dacă rotiți mai întâi și apoi mutați obiectul, veți avea un obiect în locația specificată, dar se va confrunta cu direcția pe care ați introdus-o. Acest lucru se datorează transformărilor efectuate în jurul punctului de origine - 0,0,0 - în lumea 3D. Nu există nici o ordine corectă sau greșită. Totul depinde de efectul pe care îl căutați.

Ar putea fi nevoie de mai mult de una din transformări pentru a face unele animații avansate. De exemplu, dacă doriți ca o ușă să se deschidă pe balamale, ați mișca ușa astfel încât balamalele să fie pe axa Y (adică 0 pe ambele axe X și Z). Apoi, veți roti pe axa Y, astfel încât ușa se va balansa pe balamale. În sfârșit, îl mutați din nou în locația dorită în scenă.

Aceste tipuri de animații sunt puțin mai personalizate pentru fiecare situație, așa că nu voi face o funcție pentru asta. Cu toate acestea, voi face o funcție cu ordinea de bază care este: scalarea, rotirea și apoi mișcarea. Acest lucru asigură că totul se află în locația specificată și se îndreaptă în direcția corectă.

Acum, că aveți o înțelegere de bază a matematică în spatele tuturor acestor lucruri și a modului în care funcționează animațiile, să creăm un tip de date JavaScript pentru a ține obiectele 3D.


Obiecte GL

Amintiți-vă din prima parte a acestei serii că aveți nevoie de trei tablouri pentru a desena un obiect 3D de bază: matricea vârfurilor, matricea triunghiurilor și matricea texturilor. Acesta va fi baza tipului nostru de date. De asemenea, avem nevoie de variabile pentru cele trei transformări pe fiecare dintre cele trei axe. În cele din urmă, avem nevoie de o variabilă pentru imaginea texturii și de a indica dacă modelul a terminat încărcarea.

Iată implementarea unui obiect 3D în JavaScript:

funcția GLObject (VertexArr, TriangleArr, TextureArr, ImageSrc) this.Pos = X: 0, Y: 0, Z: 0; acest Scalare = X: 1.0, Y: 1.0, Z: 1.0; această rotire = X: 0, Y: 0, Z: 0; this.Vertices = VertexArr; this.Triangles = TriangleArr; this.TriangleCount = TriangleArr.length; this.TextureMap = TextureArr; this.Image = imagine nouă (); this.Image.onload = funcția () this.ReadyState = true; ; this.Image.src = ImageSrc; this.Ready = false; // Adăugați funcția Transformare aici

Am adăugat două variabile separate "gata": una pentru când imaginea este gata și una pentru model. Când imaginea este gata, voi pregăti modelul transformând imaginea într-o textură WebGL și tamponând cele trei tablouri în tampoane WebGL. Acest lucru va accelera aplicația noastră, așa cum se recomandă buffering-ul datelor în fiecare ciclu de tragere. Deoarece vom transforma matricele în buffere, trebuie să salvăm numărul de triunghiuri într-o variabilă separată.

Acum, să adăugăm funcția care va calcula matricea de transformare a obiectului. Această funcție va lua toate variabilele locale și le va multiplica în ordinea pe care am menționat-o mai devreme (scală, rotație și apoi traducere). Puteți juca cu această comandă pentru diferite efecte. Inlocuieste // Adăugați funcția Transformare aici comentează cu următorul cod:

this.GetTransforms = function () // Crearea unei Matrici Identitate Blank var TMatrix = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]; // Scaling var Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1]; Temp [0] * = această scară.X; Temp [5] * = această scară.Y; Temp [10] * = această scară.SZ; TMatrix = MultiplyMatrix (TMatrix, Temp); // Rotire X Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]; var X = această rotire.X * (Math.PI / 180.0); Temp [5] = Math.cos (X); Temp [6] = Math.sin (X); Temp [9] = -1 * Math.sin (X); Temp [10] = Math.cos (X); TMatrix = MultiplyMatrix (TMatrix, Temp); // Temp Y rotativ = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var Y = această rotire.Y * (Math.PI / 180.0); Temp [0] = Math.cos (Y); Temp [2] = -1 * Math.sin (Y); Temp [8] = Math.sin (Y); Temp [10] = Math.cos (Y); TMatrix = MultiplyMatrix (TMatrix, Temp); // Rotire Z Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]; var Z = această rotație.Z * (Math.PI / 180.0); Temp [0] = Math.cos (Z); Temp [1] = Math.sin (Z); Temp [4] = -1 * Math.sin (Z); Temp [5] = Math.cos (Z); TMatrix = MultiplyMatrix (TMatrix, Temp); // Mutarea templului = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]; Temp [12] = this.Pos.X; Temp [13] = this.Pos.Y; Temp [14] = this.Pos.Z * -1; returnați MultiplyMatrix (TMatrix, Temp); 

Deoarece formulele de rotație se suprapun unul pe celălalt, trebuie să fie efectuate unul câte unul. Această funcție înlocuiește funcția MakeTransform funcția de la ultimul tutorial, astfel încât să îl puteți elimina din scriptul dvs..


OBJ Importer

Acum că avem clasa 3D construită, avem nevoie de o modalitate de încărcare a datelor. Vom face un model simplu de importator care va converti .obj fișierele în datele necesare pentru a face una dintre noile noastre create GLObject obiecte. Eu folosesc .obj deoarece stochează toate datele într-o formă brută și are o documentație foarte bună despre modul în care stochează informațiile. Dacă programul dvs. de modelare 3D nu acceptă exportul către .obj, atunci puteți crea întotdeauna un importator pentru un alt format de date. .obj este un tip de fișier tip 3D; deci nu ar trebui să fie o problemă. Alternativ, puteți să descărcați și Blender, o aplicație gratuită de modelare 3D pe platforme, care nu suportă exportul .obj

În .obj fișierele, primele două litere ale fiecărei linii ne indică ce fel de date conține linia respectivă. "v"este pentru o linie" coordonate de vârf ","vt"este pentru o linie" coordonate textură "și"f"este pentru linia de cartografiere Cu aceste informații, am scris următoarea funcție:

funcția LoadModel (ModelName, CB) var Ajax = noul XMLHttpRequest (); Ajax.onreadystatechange = funcția () if (Ajax.readyState == 4 && Ajax.status == 200) // Parse Model Data var var script = Ajax.responseText.split ("\ n"); var Vertices = []; var VerticeMap = []; var Triunghiuri = []; var Textures = []; var TextureMap = []; var Normals = []; var NormalMap = []; var Counter = 0;

Această funcție acceptă numele unui model și a unei funcții de apel invers. Callback-ul acceptă patru matrice: vârful, triunghiul, textura și matricele normale. Nu am acoperit încă normale, așa că puteți să le ignorați pentru moment. Voi trece prin ele în articolul următor, când discutăm despre iluminare.

Importatorul începe prin crearea unui XMLHttpRequest obiect și definirea lui onreadystatechange organizatorul evenimentului. În interiorul handlerului, am împărțit fișierul în linii și definim câteva variabile. .obj fișierele definesc mai întâi toate coordonatele unice și apoi definesc ordinea lor. Acesta este motivul pentru care există două variabile pentru vârfuri, texturi și normale. Variabila contor este utilizată pentru a umple matricea triunghiurilor deoarece .obj fișierele definesc triunghiurile în ordine.

Apoi, trebuie să trecem prin fiecare linie a fișierului și să verificăm ce fel de linie este:

 pentru (var I în script) var Line = Script [I]; // Daca linia Vertice daca (Line.substring (0, 2) == "v") var Row = Line.substring (2) .split (""); Vertex.push (X: parseFloat (rândul [0]), Y: parseFloat (rândul [1]), Z: parseFloat (rândul [2]));  // Linia de textură altceva dacă (Line.substring (0, 2) == "vt") var Row = Linia de subliniere (3) .split (""); Texturi.push (X: parseFloat (rândul [0]), Y: parseFloat (rândul [1]));  // Normals Linia altceva dacă (Line.substring (0, 2) == "vn") var Row = Linie.substring (3) .split (""); Normals.push (X: parseFloat (rândul [0]), Y: parseFloat (rândul [1]), Z: parseFloat (rândul [2])); 

Primele trei tipuri de linii sunt destul de simple; acestea conțin o listă de coordonate unice pentru vârfuri, texturi și normale. Tot ce trebuie să facem este să împingem aceste coordonate în matricele lor respective. Ultimul tip de linie este un pic mai complicat deoarece poate conține mai multe lucruri. Ar putea să conțină doar noduri sau noduri și texturi sau noduri, texturi și normale. Ca atare, trebuie să verificăm fiecare dintre aceste trei cazuri. Următorul cod face acest lucru:

 // Linia de Mapare altceva dacă (Line.substring (0, 2) == "f") var Row = Linia.substring (2) .split (""); pentru (var T în Row) // Eliminați intrările Blank dacă (Row [T]! = "") // Dacă aceasta este o intrare cu mai multe valori dacă (Row [T] .indexOf ("/" -1) // Împărțiți diferitele valori var TC = Row [T] .split ("/"); // Creșterea triunghiurilor Triunghiuri Triunghiuri (Counter); Counter ++; // Introduceți vertexele var index = parseInt (TC [0]) - 1; VerticeMap.push (Nodurile [index] .X); VerticeMap.push (Nodurile [index] .y); VerticeMap.push (Nodurile [index] .Z); // Introduceți Textures index = parseInt (TC [1]) - 1; TextureMap.push (Texturile [index] .X); TextureMap.push (Texturile [index] .y); // Dacă această intrare are date normale dacă (TC.length> 2) // Insert Normals index = parseInt (TC [2]) - 1; NormalMap.push (Normale [index] .x); NormalMap.push (Normale [index] .y); NormalMap.push (Normale [index] .Z);  // Pentru rânduri cu doar noduri Triangles.push (Counter); // Creșterea contorului de triunghiuri din array ++; var index = parseInt (Rând [T]) - 1; VerticeMap.push (Nodurile [index] .X); VerticeMap.push (Nodurile [index] .y); VerticeMap.push (Nodurile [index] .Z); 

Acest cod este mai lung decât este complicat. Deși am abordat scenariul în care .obj fișierul conține numai date de vârf, cadrul nostru necesită noduri și coordonate de textură. În cazul în care un .obj fișierul conține numai date de vârf, va trebui să adăugați manual datele de coordonate ale texturii.

Să trecem acum matricele la funcția de apel invers și să terminăm LoadModel funcţie:

  // Intoarce Arrays CB (VerticeMap, Triunghiuri, TextureMap, NormalMap);  Ajax.open ("GET", ModelName + ".obj", adevărat); Ajax.send (); 

Ceva la care ar trebui să aveți grijă este că cadrul nostru WebGL este destul de fundamental și atrage numai modele care sunt realizate din triunghiuri. Este posibil să fie necesar să editați modelele 3D în consecință. Din fericire, majoritatea aplicațiilor 3D au o funcție sau plug-in pentru a triangula modelele pentru dvs. Am făcut un model simplu al unei case cu abilitățile mele de modelare de bază și o voi include în fișierele sursă pe care să le folosești, dacă ești atât de înclinat.

Acum, să modificăm A desena din ultimul tutorial pentru a încorpora noul tip de date obiect 3D:

this.Draw = funcție (Model) if (Model.Image.ReadyState == true && Model.Ready == false) this.PrepareModel (Model);  dacă (Model.Ready) this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Vertices); this.GL.vertexAttribPointer (acest.VertexPosition, 3, this.GL.FLOAT, false, 0, 0); this.GL.bindBuffer (acest.GL.ARRAY_BUFFER, Model.TextureMap); this.GL.vertexAttribPointer (acest.VertexTexture, 2, this.GL.FLOAT, false, 0, 0); this.GL.bindBuffer (acest.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles); // Generați Perspectiva Matrix var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms (); // Setați slotul 0 ca textura activă this.GL.activeTexture (this.GL.TEXTURE0); // Încărcați în textură în memorie this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Actualizați proba de textură în shaderul de fragmente pentru a utiliza slotul 0 this.GL.uniform1i (acest.GL.getUniformLocation (acest.ShaderProgram, "uSampler"), 0); // Setați matricea de perspectivă și transformare var pmatrix = this.GL.getUniformLocation (acest.ShaderProgram, "PerspectiveMatrix"); this.GL.uniformMatrix4fv (pmatrix, false, noul Float32Array (PerspectiveMatrix)); var tmatrix = acest.GL.getUniformLocation (acest.ShaderProgram, "TransformationMatrix"); this.GL.uniformMatrix4fv (tmatrix, false, noul Float32Array (TransformMatrix)); // Desenați triunghiurile this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;

Noua funcție de tragere verifică mai întâi dacă modelul a fost pregătit pentru WebGL. Dacă textura sa încărcat, va pregăti modelul pentru desen. Vom ajunge la PrepareModel funcționează într-un minut. Dacă modelul este gata, acesta va conecta tampoanele la shadere și va încărca matricele de perspectivă și transformare ca în trecut. Singura diferență reală este că acum ia toate datele din obiectul model.

PrepareModel Funcția doar convertește textura și matricele de date în variabile compatibile WebGL. Iată funcția; adăugați-l chiar înainte de funcția de tragere:

this.PrepareModel = funcție (model) Model.Image = this.LoadTexture (Model.Image); // Convertiți Arrays în buffere var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (acest.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (acest.GL.ARRAY_BUFFER, noul Float32Array (Model.Vertices), this.GL.STATIC_DRAW); Model.Vertices = Buffer; Tampon = this.GL.createBuffer (); this.GL.bindBuffer (acest.GL.ELEMENT_ARRAY_BUFFER, Buffer); this.GL.bufferData (acest.GL.ELEMENT_ARRAY_BUFFER, noul Uint16Array (Model.Triangles), this.GL.STATIC_DRAW); Model.Triangles = Tampon; Tampon = this.GL.createBuffer (); this.GL.bindBuffer (acest.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (acest.GL.ARRAY_BUFFER, noul Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Tampon; Model.Ready = adevărat; ;

Acum, cadrul nostru este gata și putem trece la pagina HTML.


Pagina HTML

Puteți șterge tot ce există în interiorul lui scenariu așa cum putem scrie acum codul mai concis datorită noului nostru GLObject tip de date.

Acesta este JavaScript complet:

var GL; var Building; funcția Ready () GL = WebGL nou ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("Casa", functie (VerticeMap, Triunghiuri, TextureMap) Constructie = GLObject nou (VerticeMap, Triunghiuri, TextureMap, House.png); Building.Scale.X = 0.5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // Și înapoi Building.Rotation.Y = 180; setInterval (Update, 33););  funcția Actualizare () Building.Rotation.Y + = 0.2 GL.Draw (Building); 

Încărcăm un model și îi spunem paginii să o actualizeze la aproximativ treizeci de ori pe secundă. Actualizați funcția rotește modelul pe axa Y, ceea ce se realizează prin actualizarea obiectului Y Rotație proprietate. Modelul meu a fost un pic prea mare pentru scena WebGL și a fost înapoi, așa că a trebuit să fac unele ajustări în cod.

Dacă nu faceți o prezentare cinematografică WebGL, probabil că veți dori să adăugați niște controale. Să ne uităm la modul în care putem adăuga niște controale de la tastatură la cererea noastră.


Controlul tastaturii

Aceasta nu este o tehnică WebGL la fel de mult ca o caracteristică JavaScript nativă, dar este utilă pentru controlul și poziționarea modelelor 3D. Tot ce trebuie să faceți este să adăugați un ascultător al evenimentului la tastatură Tasta în jos sau keyup evenimente și verificați ce tastă a fost apăsată. Fiecare cheie are un cod special și o modalitate bună de a afla ce cod corespunde cheii este de a înregistra codurile de chei la consola când evenimentul se declanșează. Deci, du-te la zona în care am încărcat modelul, și adăugați următorul cod imediat după setInterval linia:

document.onkeydown = handleKeyDown;

Aceasta va seta funcția handleKeyDown să se ocupe de Tasta în jos eveniment. Iată codul pentru handleKeyDown funcţie:

funcția handleKeyDown (eveniment) // Puteți dezmembra următoarea linie pentru a afla codul fiecărei chei //alert(event.keyCode); dacă (event.keyCode == 37) // Arrow Key Building.Pos.X - = 4;  altceva dacă (event.keyCode == 38) // Arrow Up Building Key.Pos.Y + = 4;  altfel dacă (event.keyCode == 39) // Arrow Key Building.Pos.X + = 4;  altfel dacă (event.keyCode == 40) // Down Arrow Key Building.Pos.Y - = 4; 

Toate aceste funcții sunt actualizarea proprietăților obiectului; cadrul WebGL se ocupă de restul.


Concluzie

Nu am terminat! În cea de-a treia și ultima parte a acestei mini-serii, vom examina diferite tipuri de iluminat și cum să le legăm totul cu niște lucruri 2D!

Vă mulțumim că ați citit și, ca de obicei, dacă aveți întrebări, nu ezitați să lăsați un comentariu mai jos!

Cod