WebGL Essentials Del II

Denna artikel kommer att bygga vidare på det ramverk som introducerades i del 1 av denna mini-serie, tillägg av en modellimportör och en anpassad klass för 3D-objekt. Du kommer också att presenteras för animering och kontroller. Det finns mycket att gå igenom, så låt oss börja!

Denna artikel bygger starkt på den första artikeln, så om du inte har läst det ännu borde du börja där först.

Det sätt som WebGL manipulerar objekt i 3D-världen är genom att använda matematiska formler som kallas transformationer. Så innan vi börjar bygga 3D-klassen kommer jag att visa dig några av de olika typerna av omvandlingar och hur de implementeras.


transformationer

Det finns tre grundläggande omvandlingar när man arbetar med 3D-objekt.

  • Rör på sig
  • skalning
  • Roterande

Var och en av dessa funktioner kan utföras på antingen X-, Y- eller Z-axeln, vilket ger en total möjlighet till nio grundläggande omvandlingar. Alla dessa påverkar 3D-objektets 4x4-transformationsmatris på olika sätt. För att kunna utföra flera transformationer på samma objekt utan överlappande problem måste vi multiplicera omvandlingen till objektets matris och inte applicera den direkt på objektets matris. Att flytta är det enklaste att göra, så låt oss börja där.

Flytta A.K.A. "Översättning"

Att flytta ett 3D-objekt är en av de enklaste omvandlingar du kan göra, eftersom det finns en speciell plats i 4x4-matrisen för den. Det finns inget behov av någon matte; sätt bara X-, Y- och Z-koordinaterna i matrisen och din färdiga. Om du tittar på 4x4-matrisen är det de tre första siffrorna i den nedre raden. Dessutom borde du veta att positiv Z är bakom kameran. Därför placerar ett Z-värde på -100 objektet 100 enheter inåt på skärmen. Vi kommer att kompensera för detta i vår kod.

För att kunna utföra flera omvandlingar kan du inte bara ändra objektets verkliga matris; du måste tillämpa omvandlingen till en ny blankmatris, känd som en identitet matris och multiplicera den med huvudmatrisen.

Matrixmultiplicering kan vara lite knepig att förstå, men den grundläggande tanken är att varje vertikal kolumn multipliceras med den andra matrisens horisontella rad. Till exempel skulle det första numret vara den första raden multiplicerad med den andra matrisens första kolumn. Det andra numret i den nya matrisen skulle vara den första raden multiplicerad med den andra matrisens andra kolumn och så vidare.

Följande kod är kod som jag skrev för att multiplicera två matriser i JavaScript. Lägg till detta i din .js fil som du gjorde i den första delen av denna serie:

funktion MH (A, B) var Sum = 0; för (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)]; 

Jag tror inte att detta kräver någon förklaring, eftersom det bara är den nödvändiga matematiken för matrismultiplicering. Låt oss gå vidare till skalning.

skalning

Att skala en modell är också ganska enkel - det är enkelt att multiplicera. Du måste multiplicera de tre första diagonala talen oavsett skalaen. Återigen är ordningen X, Y och Z. Så om du vill skala ditt objekt till att vara två gånger större i alla tre axlarna, skulle du multiplicera det första, sjätte och elfte elementet i din matris med 2.

Roterande

Roterande är den svåraste transformationen eftersom det finns en annan ekvation för var och en av de tre axlarna. Följande bild visar rotationsekvationerna för varje axel:

Oroa dig inte om den här bilden inte är meningsfull för dig. Vi granskar JavaScript-implementeringen snart.

Det är viktigt att notera att det spelar roll vilken ordning du utför transformationerna. olika order ger olika resultat.

Det är viktigt att notera att det spelar roll vilken ordning du utför transformationerna. olika order ger olika resultat. Om du först flyttar ditt objekt och roterar det, kommer WebGL att svänga ditt objekt runt som en fladdermus, i stället för att rotera objektet på plats. Om du roterar först och sedan flyttar objektet kommer du att ha ett objekt på den angivna platsen, men den kommer att möta den riktning du angav. Detta beror på att transformationerna utförs runt ursprungspunkten - 0,0,0 - i 3D-världen. Det finns ingen rätt eller fel ordning. Det beror allt på effekten du letar efter.

Det kan kräva mer än en av varje omvandling för att göra några avancerade animeringar. Till exempel om du vill att en dörr ska svänga öppen på gångjärnen flyttar du dörren så att gångjärnen ligger på Y-axeln (dvs 0 på både X- och Z-axeln). Du roterar sedan på Y-axeln så att dörren svänger på gångjärnen. Slutligen skulle du flytta den igen till önskad plats i din scen.

Dessa typer av animeringar är lite mer skräddarsydda för varje situation, så jag kommer inte att göra en funktion för den. Jag kommer dock att göra en funktion med den mest grundläggande ordningen som är: skalning, rotering och flyttning. Detta försäkrar att allt är i den angivna platsen och på rätt sätt.

Nu när du har en grundläggande förståelse för matematiken bakom allt detta och hur animationer fungerar, låt oss skapa en JavaScript-datatyp för att hålla våra 3D-objekt.


GL Objekt

Kom ihåg från den första delen av den här serien att du behöver tre arrays för att rita ett grundläggande 3D-objekt: toppunkterna, trianglarna och textureringsrutan. Det kommer att ligga till grund för vår datatyp. Vi behöver också variabler för de tre omvandlingarna på var och en av de tre axlarna. Slutligen behöver vi en variabler för texturbilden och ange huruvida modellen har slutfört laddning.

Här är min implementering av ett 3D-objekt i JavaScript:

funktion GLObject (VertexArr, TriangleArr, TextureArr, ImageSrc) this.Pos = X: 0, Y: 0, Z: 0; this.Scale = X: 1.0, Y: 1.0, Z: 1.0; this.Rotation = X: 0, Y: 0, Z: 0; this.Vertices = VertexArr; this.Triangles = TriangleArr; this.TriangleCount = TriangleArr.length; this.TextureMap = TextureArr; this.Image = ny bild (); this.Image.onload = function () this.ReadyState = true; ; this.Image.src = ImageSrc; this.Ready = false; // Lägg till transformationsfunktion här

Jag har lagt till två separata "redo" variabler: en för när bilden är klar och en för modellen. När bilden är klar kommer jag att förbereda modellen genom att konvertera bilden till en WebGL-textur och buffra de tre matriserna till WebGL-buffertar. Detta kommer att påskynda vår applikation, så att den buffrar data i varje dragcykel. Eftersom vi ska konvertera arrayerna till buffertar, måste vi spara antalet trianglar i en separat variabel.

Låt oss nu lägga till den funktion som kommer att beräkna objektets transformationsmatris. Den här funktionen tar alla lokala variabler och multiplicerar dem i den ordning som jag nämnde tidigare (skala, rotation och översättning sedan). Du kan leka med denna beställning för olika effekter. Ersätt // Lägg till transformationsfunktion här kommentera med följande kod:

this.GetTransforms = function () // Skapa en blank identitetsmatris var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]; // Skalning var Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; Temp [0] * = this.Scale.X; Temp [5] * = this.Scale.Y; Temp [10] * = this.Scale.Z; TMatrix = MultiplyMatrix (TMatrix, Temp); // Rotering X Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var X = this.Rotation.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); // Roterande Y Temp = [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var Y = this.Rotation.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); // Rotering Z Temp = [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var Z = this.Rotation.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); // Flytt Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; Temp [12] = this.Pos.X; Temp [13] = this.Pos.Y; Temp [14] = this.Pos.Z * -1; returnera MultiplyMatrix (TMatrix, Temp); 

Eftersom rotationsformlerna överlappar varandra måste de utföras en i taget. Denna funktion ersätter MakeTransform funktionen från den sista handledningen så att du kan ta bort den från ditt skript.


OBJ Importör

Nu när vi har byggt 3D-klassen behöver vi ett sätt att ladda data. Vi gör en enkel modellimportör som konverterar .obj filer till nödvändiga data för att göra en av våra nyskapade GLObject objekt. Jag använder .obj modellformat eftersom det lagrar alla data i en rå form, och den har mycket bra dokumentation om hur det lagrar informationen. Om ditt 3D-modelleringsprogram inte stöder export till .obj, då kan du alltid skapa en importör för något annat dataformat. .obj är en vanlig 3D-filtyp; så det borde inte vara ett problem. Alternativt kan du också ladda ner Blender, en gratis 3D-modelleringsprogram för 3D-modeller som stöder export till .obj

I .obj filer, de två första bokstäverna i varje rad berättar vilken typ av data den innehåller. "v"är för en" vertex koordinater "linje,"vt"är för en" texturkoordinater "linje och"f"är för kartläggningslinjen. Med den här informationen skrev jag följande funktion:

funktion LoadModel (ModelName, CB) var Ajax = ny XMLHttpRequest (); Ajax.onreadystatechange = funktion () if (Ajax.readyState == 4 && Ajax.status == 200) // Parse Model Data var Script = Ajax.responseText.split ("\ n"); Var Vertices = []; var VerticeMap = []; var Triangles = []; Var Texturer = []; var TextureMap = []; var normaler = []; var NormalMap = []; var Counter = 0;

Den här funktionen accepterar namnet på en modell och en återuppringningsfunktion. Återuppringningen accepterar fyra arrays: toppunktet, triangeln, texturen och normala arrays. Jag har ännu inte täckt normaler, så du kan bara ignorera dem för nu. Jag kommer att gå igenom dem i uppföljningsartikeln när vi diskuterar belysning.

Importören börjar med att skapa en XMLHttpRequest objekt och definiera dess onreadystatechange händelsehanterare. Inne i handlaren delar vi upp filen i sina rader och definierar några variabler. .obj filer definierar först alla unika koordinater och definierar sedan deras order. Därför finns det två variabler för toppunkter, texturer och normaler. Räknaren variabel används för att fylla i trianglarna array eftersom .obj filer definierar trianglarna i ordning.

Därefter måste vi gå igenom varje rad i filen och kontrollera vilken typ av linje det är:

 för (var jag i Script) var Line = Script [I]; // Om Vertice Line if (Line.substring (0, 2) == "v") var Row = Line.substring (2) .split (""); Vertices.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]), Z: parseFloat (Row [2]));  // Texture Line annars om (Line.substring (0, 2) == "vt") var Row = Line.substring (3) .split (""); Textures.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]));  // Normals Line annars om (Line.substring (0, 2) == "vn") var Row = Line.substring (3) .split (""); Normals.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]), Z: parseFloat (Row [2])); 

De tre första linjetyperna är ganska enkla; de innehåller en lista med unika koordinater för punkter, texturer och normaler. Allt vi behöver göra är att driva dessa koordinater i sina respektive arrays. Den sista typen av linje är lite mer komplicerad eftersom den kan innehålla flera saker. Det kan innehålla bara hörn, eller toppunkter och texturer, eller toppunkter, texturer och normaler. Som sådan måste vi kolla för var och en av dessa tre fall. Följande kod gör det här:

 // Mapping Line annars om (Line.substring (0, 2) == "f") var Row = Line.substring (2) .split (""); för var T i rad) // Ta bort tomma inmatningar om (rad [T]! = "") // Om det här är en multivärdet post om (Row [T] .indexOf ("/")! = -1) // Dela de olika värdena var TC = Row [T] .split ("/"); // Ökning Trianglarna Array Triangles.push (Counter); Räknare ++; // Sätt in Vertices var index = parseInt (TC [0]) - 1; VerticeMap.push (Hörn [index] .X); VerticeMap.push (Hörn [index] .Y); VerticeMap.push (Hörn [index] .Z); // Sätt in Textures index = parseInt (TC [1]) - 1; TextureMap.push (texturer [index] .X); TextureMap.push (texturer [index] .Y); // Om denna post har normaldata om (TC.length> 2) // Insert Normals index = parseInt (TC [2]) - 1; NormalMap.push (Normala [index] .x); NormalMap.push (Normala [index] .y); NormalMap.push (Normala [index] .Z);  // För rader med bara kryssningar annars Triangles.push (Counter); // Ökning Trianglarna Array Counter ++; var index = parseInt (rad [T]) - 1; VerticeMap.push (Hörn [index] .X); VerticeMap.push (Hörn [index] .Y); VerticeMap.push (Hörn [index] .Z); 

Koden är längre än den är komplicerad. Även om jag täckte scenariot där .obj filen innehåller bara vertexdata, vår ram kräver vertikaler och texturkoordinater. Om en .obj filen innehåller bara vertexdata, måste du manuellt lägga till textkoordinatdata till den.

Låt oss nu passera arraysna till återuppringningsfunktionen och avsluta LoadModel fungera:

  // Återgå Array CB (VerticeMap, Triangles, TextureMap, NormalMap);  Ajax.open ("GET", ModelName + ".obj", true); Ajax.send (); 

Något du bör se upp för är att vår WebGL-ram är ganska grundläggande och drar bara modeller som är gjorda av trianglar. Du kanske måste redigera dina 3D-modeller i enlighet med detta. Lyckligtvis har de flesta 3D-applikationer en funktion eller plug-in för att triangulera dina modeller för dig. Jag gjorde en enkel modell av ett hus med mina grundläggande modelleringskunskaper, och jag kommer att inkludera den i källfilerna för dig att använda, om du är så benägen.

Låt oss nu ändra Dra funktion från den sista handledningen för att införliva vår nya 3D-objektdatatyp:

this.Draw = function (Model) if (Model.Image.ReadyState == true && Model.Ready == false) this.PrepareModel (Model);  om (Model.Ready) this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Vertices); this.GL.vertexAttribPointer (this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.TextureMap); this.GL.vertexAttribPointer (this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0); this.GL.bindBuffer (this.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles); // Generera perspektivmatrisen var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms (); // Ställ in 0 som den aktiva Texturen this.GL.activeTexture (this.GL.TEXTURE0); // Ladda i texturen till minnet this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Uppdatera Textur Sampler i fragmentet skuggare för att använda plats 0 this.GL.uniform1i (this.GL.getUniformLocation (this.ShaderProgram, "uSampler"), 0); // Ange perspektiv och transformationsmatriser var pmatrix = this.GL.getUniformLocation (this.ShaderProgram, "PerspectiveMatrix"); this.GL.uniformMatrix4fv (pmatrix, false, new Float32Array (PerspectiveMatrix)); var tmatrix = this.GL.getUniformLocation (this.ShaderProgram, "TransformationMatrix"); this.GL.uniformMatrix4fv (tmatrix, false, new Float32Array (TransformMatrix)); // Rita trianglarna this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;

Den nya teckningsfunktionen kontrollerar först om modellen har utarbetats för WebGL. Om texturen har laddats, kommer den att förbereda modellen för ritning. Vi kommer till PrepareModel fungera på en minut. Om modellen är klar, kommer den att ansluta buffertarna till shadersna och ladda perspektivet och transformationsmatriserna som det gjorde tidigare. Den enda verkliga skillnaden är att den nu tar alla data från modellobjektet.

De PrepareModel funktionen omvandlar bara textur- och datarrayerna till WebGL-kompatibla variabler. Här är funktionen; lägg till det strax innan teckningsfunktionen:

this.PrepareModel = function (Model) Model.Image = this.LoadTexture (Model.Image); // Konvertera arrays till buffertar var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, buffert); this.GL.bufferData (this.GL.ARRAY_BUFFER, nya Float32Array (Model.Vertices), this.GL.STATIC_DRAW); Model.Vertices = Buffert; Buffert = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ELEMENT_ARRAY_BUFFER, buffert); this.GL.bufferData (this.GL.ELEMENT_ARRAY_BUFFER, ny Uint16Array (Model.Triangles), this.GL.STATIC_DRAW); Model.Triangles = Buffert; Buffert = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, buffert); this.GL.bufferData (this.GL.ARRAY_BUFFER, ny Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Buffert; Model.Ready = true; ;

Nu är vår ram klar och vi kan gå vidare till HTML-sidan.


HTML-sidan

Du kan radera allt som är inne i manus taggar som vi nu kan skriva koden mer kortfattat tack vare vårt nya GLObject data typ.

Detta är det fullständiga JavaScript:

var GL; var Byggnad; funktion Klar () GL = ny WebGL ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("House", funktionen (VerticeMap, Triangles, TextureMap) Building = new GLObject (VerticeMap, Triangles, TextureMap, "House.png"); Building.Pos.Z = 650; // Min modell var lite för stor Building.Scale.X = 0.5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // och Backwards Building.Rotation.Y = 180; setInterval (Uppdatering, 33););  funktion Uppdatering () Building.Rotation.Y + = 0.2 GL.Draw (Building); 

Vi laddar en modell och berätta för sidan om att uppdatera den ungefär trettio gånger per sekund. De Uppdatering funktionen roterar modellen på Y-axeln, vilket uppnås genom att uppdatera objektets Y Rotation fast egendom. Min modell var lite för stor för WebGL-scenen och det var bakåt, så jag behövde göra några justeringar i kod.

Om du inte gör någon form av filmisk WebGL-presentation, kommer du förmodligen att vilja lägga till några kontroller. Låt oss titta på hur vi kan lägga till några tangentbordskontroller till vår applikation.


Tangentbordskontroller

Det här är inte en WebGL-teknik lika mycket som en inbyggd JavaScript-funktion, men det är praktiskt att styra och positionera dina 3D-modeller. Allt du behöver göra är att lägga till en händelse lyssnare på tangentbordet nyckel ner eller keyUp händelser och kontrollera vilken knapp som trycktes. Varje nyckel har en särskild kod och ett bra sätt att ta reda på vilken kod som motsvarar nyckeln är att logga in nyckelkoderna till konsolen när händelsen brinner. Så gå till det område där jag laddade modellen och lägg till följande kod strax efter setInterval linje:

document.onkeydown = handleKeyDown;

Detta ställer in funktionen handleKeyDown att hantera nyckel ner händelse. Här är koden för handleKeyDown fungera:

funktion handtagKeyDown (händelse) // Du kan uncomment nästa rad för att ta reda på varje nyckel kod //alert(event.keyCode); om (event.keyCode == 37) // Vänsterpil Key Building.Pos.X - = 4;  annars om (event.keyCode == 38) // Upp Pil Key Building.Pos.Y + = 4;  annars om (event.keyCode == 39) // Högerpil Key Building.Pos.X + = 4;  annars om (event.keyCode == 40) // Nedåtpilen Key Building.Pos.Y - = 4; 

All denna funktion gör att uppdatera objektets egenskaper; WebGL-ramen tar hand om resten.


Slutsats

Vi är inte färdiga! I den tredje och sista delen av denna mini-serie kommer vi att granska olika typer av belysning och hur man binder allt med några 2D-saker!

Tack för att du läste och, som alltid, om du har några frågor, var god att lämna en kommentar nedan!