WebGL Essentials deel II

Dit artikel bouwt voort op het framework dat is geïntroduceerd in deel een van deze miniserie, door een modelimporter en een aangepaste klasse toe te voegen voor 3D-objecten. Je maakt ook kennis met animatie en bedieningselementen. Er is nog veel te doen, dus laten we aan de slag gaan!

Dit artikel is sterk afhankelijk van het eerste artikel, dus als u het nog niet hebt gelezen, moet u daar eerst beginnen.

De manier waarop WebGL items in de 3D-wereld manipuleert, is door wiskundige formules te gebruiken die bekend staan ​​als transformaties. Dus, voordat we beginnen met het bouwen van de 3D-klasse, zal ik je enkele van de verschillende soorten transformaties laten zien en hoe ze worden geïmplementeerd.


transformaties

Er zijn drie basistransformaties bij het werken met 3D-objecten.

  • In beweging
  • scaling
  • roterende

Elk van deze functies kan op de X-, Y- of Z-as worden uitgevoerd, wat een totale mogelijkheid van negen basistransformaties mogelijk maakt. Al deze hebben op verschillende manieren invloed op de 4x4-transformatiematrix van het 3D-object. Om meerdere transformaties op hetzelfde object uit te voeren zonder overlappende problemen, moeten we de transformatie in de matrix van het object vermenigvuldigen en niet direct op de matrix van het object toepassen. Verhuizen is het gemakkelijkst om te doen, dus laten we daar beginnen.

Verplaatsen A.K.A. "Vertaling"

Het verplaatsen van een 3D-object is een van de eenvoudigste transformaties die je kunt doen, omdat er een speciale plaats in de 4x4-matrix voor is. Rekenwerk is niet nodig; zet gewoon de X-, Y- en Z-coördinaten in de matrix en je bent klaar. Als je naar de 4x4-matrix kijkt, dan zijn het de eerste drie nummers in de onderste rij. Bovendien moet u weten dat positieve Z achter de camera staat. Daarom plaatst een Z-waarde van -100 het object 100 eenheden naar binnen op het scherm. We zullen dit compenseren in onze code.

Om meerdere transformaties uit te voeren, kunt u de werkelijke matrix van het object niet eenvoudig wijzigen; je moet de transformatie toepassen op een nieuwe lege matrix, bekend als een identiteit matrix en vermenigvuldig dit met de hoofdmatrix.

Matrixvermenigvuldiging kan een beetje lastig te begrijpen zijn, maar het basisidee is dat elke verticale kolom wordt vermenigvuldigd met de horizontale rij van de tweede matrix. Het eerste cijfer is bijvoorbeeld de eerste rij vermenigvuldigd met de eerste kolom van de andere matrix. Het tweede getal in de nieuwe matrix is ​​de eerste rij vermenigvuldigd met de tweede kolom van de andere matrix, enzovoort.

Het volgende fragment is de code die ik heb geschreven voor het vermenigvuldigen van twee matrices in JavaScript. Voeg dit toe aan uw .js bestand dat je in het eerste deel van deze serie hebt gemaakt:

functie MH (A, B) var Som = 0; for (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)]; 

Ik denk niet dat dit enige uitleg vereist, want het is gewoon de noodzakelijke wiskunde voor matrixvermenigvuldiging. Laten we verder gaan met schalen.

scaling

Het schalen van een model is ook vrij eenvoudig - het is eenvoudige vermenigvuldiging. Je moet de eerste drie diagonale getallen vermenigvuldigen met de schaal. Nogmaals, de volgorde is X, Y en Z. Dus, als je je object wilt schalen om twee keer groter te zijn in alle drie de assen, vermenigvuldig je de eerste, zesde en elfde elementen in je array met 2.

roterende

Roteren is de lastigste transformatie omdat er een andere vergelijking is voor elk van de drie assen. De volgende afbeelding toont de rotatievergelijkingen voor elke as:

Maak je geen zorgen als deze foto je niet begrijpt; we zullen de JavaScript-implementatie snel herzien.

Het is belangrijk op te merken dat het van belang is in welke volgorde u de transformaties uitvoert; verschillende orders produceren verschillende resultaten.

Het is belangrijk op te merken dat het van belang is in welke volgorde u de transformaties uitvoert; verschillende orders produceren verschillende resultaten. Als u eerst uw object verplaatst en vervolgens roteert, zwaait WebGL uw object rond als een vleermuis, in plaats van het object op zijn plaats te draaien. Als u eerst roteert en vervolgens uw object verplaatst, heeft u een object op de opgegeven locatie, maar dit staat in de richting die u hebt ingevoerd. Dit komt omdat de transformaties worden uitgevoerd rond het oorsprongspunt - 0,0,0 - in de 3D-wereld. Er is geen goede of foute bestelling. Het hangt allemaal af van het effect dat u zoekt.

Het kan meer dan één van elke transformaties vereisen om een ​​aantal geavanceerde animaties te maken. Als u bijvoorbeeld wilt dat een deur opengaat op de scharnieren, verplaatst u de deur zodat de scharnieren zich op de Y-as bevinden (dwz 0 op zowel de X- als de Z-as). Je zou dan op de Y-as draaien, zodat de deur op zijn scharnieren kan slingeren. Ten slotte zou u het opnieuw naar de gewenste locatie in uw scène verplaatsen.

Dit soort animaties is een beetje meer op maat gemaakt voor elke situatie, dus ik ga er geen functie voor maken. Ik zal echter wel een functie uitvoeren met de meest elementaire volgorde: verschalen, draaien en dan bewegen. Dit zorgt ervoor dat alles op de opgegeven locatie staat en op de juiste manier wordt weergegeven.

Nu je een basiskennis hebt van de wiskunde achter dit alles en hoe animaties werken, laten we dan een JavaScript-gegevenstype maken om onze 3D-objecten te bevatten.


GL-objecten

Onthoud uit het eerste deel van deze serie dat u drie arrays nodig hebt om een ​​eenvoudig 3D-object te tekenen: de array met vertices, de array met driehoeken en de matrix met texturen. Dat zal de basis zijn van ons gegevenstype. We hebben ook variabelen nodig voor de drie transformaties op elk van de drie assen. Ten slotte hebben we een variabele nodig voor de texture-afbeelding en om aan te geven of het model klaar is met laden.

Hier is mijn implementatie van een 3D-object in JavaScript:

functie 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 = new Image (); this.Image.onload = function () this.ReadyState = true; ; this.Image.src = ImageSrc; this.Ready = false; // Transformatiefunctie toevoegen hier

Ik heb twee afzonderlijke "klaar" -variabelen toegevoegd: een voor wanneer de afbeelding gereed is en een voor het model. Wanneer de afbeelding gereed is, zal ik het model voorbereiden door de afbeelding in een WebGL-structuur te converteren en de drie arrays in WebGL-buffers te bufferen. Dit zal onze applicatie versnellen, omdat deze bestemd is voor het bufferen van de gegevens in elke trekkingscyclus. Omdat we de arrays in buffers zullen omzetten, moeten we het aantal driehoeken opslaan in een afzonderlijke variabele.

Laten we nu de functie toevoegen die de transformatiematrix van het object zal berekenen. Deze functie neemt alle lokale variabelen en vermenigvuldigt ze in de volgorde die ik eerder heb genoemd (schaal, rotatie en vervolgens vertaling). Je kunt met deze bestelling spelen voor verschillende effecten. Vervang de // Transformatiefunctie toevoegen Hier reageer met de volgende code:

this.GetTransforms = function () // Maak een lege identiteitsmatrix var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]; // Schaling 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); // Rotating 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); // Y-temp rotatie = [1, 0, 0, 0, 0, 1, 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); // Rotating Z Temp = [1, 0, 0, 0, 0, 1, 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); // Bewegende 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; return MultiplyMatrix (TMatrix, Temp); 

Omdat de rotatieformules elkaar overlappen, moeten ze één voor één worden uitgevoerd. Deze functie vervangt de MakeTransform functie van de laatste zelfstudie, zodat je deze uit je script kunt verwijderen.


OBJ-importeur

Nu we onze 3D-klasse hebben laten bouwen, hebben we een manier nodig om de gegevens te laden. We zullen een eenvoudige modelimporteur maken die zal converteren .obj bestanden in de benodigde gegevens om een ​​nieuw aangemaakt bestand te maken GLObject voorwerpen. Ik gebruik de .obj modelformaat omdat het alle gegevens in een onbewerkte vorm opslaat, en het heeft zeer goede documentatie over hoe het de informatie opslaat. Als uw 3D-modelleringsprogramma exporteren naar niet ondersteunt .obj, dan kunt u altijd een importeur voor een ander gegevensformaat creëren. .obj is een standaard 3D-bestandstype; dus, het zou geen probleem moeten zijn. Je kunt ook Blender downloaden, een gratis platformoverschrijdende 3D-modelleringsapplicatie die exporteren naar ondersteunt .obj

In .obj bestanden, de eerste twee letters van elke regel vertellen ons wat voor soort gegevens die regel bevat. "v"is voor een regel" vertex-coördinaten ","vt"is voor een regel met" textuurcoördinaten "en"f"is voor de mapping-lijn. Met deze informatie schreef ik de volgende functie:

functie LoadModel (ModelName, CB) var Ajax = new XMLHttpRequest (); Ajax.onreadystatechange = function () if (Ajax.readyState == 4 && Ajax.status == 200) // Parse Model Data var Script = Ajax.responseText.split ("\ n"); var Vertices = []; var VerticeMap = []; var Triangles = []; var Textures = []; var TextureMap = []; var Normals = []; var NormalMap = []; var Counter = 0;

Deze functie accepteert de naam van een model en een callback-functie. De callback accepteert vier arrays: de vertex, driehoek, textuur en normale arrays. Ik heb nog geen normalen besproken, dus je kunt ze nu gewoon negeren. Ik zal ze doornemen in het vervolgartikel, wanneer we het hebben over verlichting.

De importeur begint met het maken van een XMLHttpRequest object en het definiëren ervan onreadystatechange gebeurtenishandler. In de handler splitsen we het bestand in zijn lijnen en definiëren een paar variabelen. .obj bestanden definiëren eerst alle unieke coördinaten en definiëren vervolgens hun volgorde. Dat is de reden waarom er twee variabelen zijn voor de hoekpunten, texturen en normalen. De tellervariabele wordt gebruikt om de driehoekenarray in te vullen omdat .obj bestanden definiëren de driehoeken in volgorde.

Vervolgens moeten we elke regel van het bestand doorlopen en controleren wat voor soort lijn het is:

 for (var I in Script) var Line = Script [I]; // Als Vertice Line if (Line.substring (0, 2) == "v") var Row = Line.substring (2) .split (""); Vertices.push (X: parseFloat (rij [0]), Y: parseFloat (rij [1]), Z: parseFloat (rij [2]));  // Texture Line else if (Line.substring (0, 2) == "vt") var Row = Line.substring (3) .split (""); Textures.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]));  // Normals Lijn anders if (Line.substring (0, 2) == "vn") var Row = Line.substring (3) .split (""); Normals.push (X: parseFloat (rij [0]), Y: parseFloat (rij [1]), Z: parseFloat (rij [2])); 

De eerste drie lijntypen zijn redelijk eenvoudig; ze bevatten een lijst met unieke coördinaten voor de hoekpunten, texturen en normalen. Het enige wat we moeten doen is deze coördinaten in hun respectievelijke arrays te duwen. De laatste soort lijn is iets gecompliceerder omdat deze meerdere dingen kan bevatten. Het kan alleen hoekpunten, hoekpunten en structuren, of hoekpunten, structuren en normalen bevatten. Als zodanig moeten we voor elk van deze drie gevallen controleren. De volgende code doet dit:

 // Mapping Line else if (Line.substring (0, 2) == "f") var Row = Line.substring (2) .split (""); for (var T in rij) // Lege invoer verwijderen als (rij [T]! = "") // Als dit een invoer met meerdere waarden is als (rij [T] .indexOf ("/")! = -1) // Splits de verschillende waarden var TC = Rij [T] .split ("/"); // Verhoog de driehoeken Array Triangles.push (Counter); Counter ++; // Voeg de Vertices var index = parseInt (TC [0]) - 1 in; VerticeMap.push (hoekpunten [index] .X); VerticeMap.push (hoekpunten [index] .Y); VerticeMap.push (hoekpunten [index] .Z); // Voeg de Textures-index in = parseInt (TC [1]) - 1; TextureMap.push (Structuren [index] .X); TextureMap.push (Structuren [index] .Y); // Als dit item gegevens over de norm heeft als (TC.length> 2) // Norma's index invoegen = parseren (TC [2]) - 1; NormalMap.push (Normalen [index] .X); NormalMap.push (Normalen [index] .Y); NormalMap.push (Normalen [index] .Z);  // Voor rijen met alleen hoekpunten anders Triangles.push (Counter); // Verhoog de array-teller voor driehoeken ++; var index = parseInt (rij [T]) - 1; VerticeMap.push (hoekpunten [index] .X); VerticeMap.push (hoekpunten [index] .Y); VerticeMap.push (hoekpunten [index] .Z); 

Deze code is langer dan gecompliceerd. Hoewel ik het scenario behandelde waar het .obj bestand bevat alleen hoekpuntgegevens, ons kader vereist hoekpunten en textuurcoördinaten. Als een .obj bestand bevat alleen vertex-gegevens, u moet de textuurcoördinaatgegevens er handmatig aan toevoegen.

Laten we nu de arrays doorgeven aan de callback-functie en eindigen met de LoadModel functie:

  // Return The Arrays CB (VerticeMap, Triangles, TextureMap, NormalMap);  Ajax.open ("GET", ModelName + ".obj", true); Ajax.send (); 

Waar u op moet letten, is dat ons WebGL-framework vrij eenvoudig is en alleen modellen tekent die zijn gemaakt van driehoeken. Mogelijk moet u uw 3D-modellen dienovereenkomstig bewerken. Gelukkig hebben de meeste 3D-toepassingen een functie of plug-in om uw modellen voor u te trianguleren. Ik heb een eenvoudig model van een huis gemaakt met mijn basisvaardigheden voor modelleren, en ik zal het opnemen in de bronbestanden die je kunt gebruiken, als je zo geneigd bent.

Laten we nu het Trek functie van de laatste zelfstudie om ons nieuwe 3D-objectgegevenstype te integreren:

this.Draw = function (Model) if (Model.Image.ReadyState == true && Model.Ready == false) this.PrepareModel (Model);  if (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); // Genereer de Perspectiefmatrix var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms (); // Stel slot 0 in als de actieve structuur this.GL.activeTexture (this.GL.TEXTURE0); // Laad in de textuur in het geheugen this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Update The Texture Sampler in de fragmentshader om slot 0 this.GL.uniform1i (this.GL.getUniformClassificatie (this.ShaderProgram, "uSampler"), 0) te gebruiken; // Stel de perspectieven en transformatiematrices var pmatrix = this.GL.getUniformClassificatie (this.ShaderProgram, "PerspectiveMatrix"); this.GL.uniformMatrix4fv (pmatrix, false, nieuwe Float32Array (PerspectiveMatrix)); var tmatrix = this.GL.getUniformClassificatie (this.ShaderProgram, "TransformationMatrix"); this.GL.uniformMatrix4fv (tmatrix, false, nieuwe Float32Array (TransformMatrix)); // Teken de driehoeken this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;

De nieuwe tekenfunctie controleert eerst of het model is voorbereid voor WebGL. Als de textuur is geladen, bereidt deze het model voor op tekenen. We komen bij de PrepareModel functie in een minuut. Als het model gereed is, verbindt het zijn buffers met de shaders en laadt het de perspectief- en transformatiematrices zoals eerder. Het enige echte verschil is dat het nu alle gegevens van het modelobject inneemt.

De PrepareModel function converteert de textuur- en gegevensarrays gewoon naar WebGL-compatibele variabelen. Hier is de functie; voeg het toe vlak voor de draw-functie:

this.PrepareModel = function (Model) Model.Image = this.LoadTexture (Model.Image); // Converteer arrays naar buffers var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nieuwe Float32Array (Model.Vertices), this.GL.STATIC_DRAW); Model.Vertices = Buffer; Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ELEMENT_ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ELEMENT_ARRAY_BUFFER, nieuwe Uint16Array (Model.Triangles), this.GL.STATIC_DRAW); Model.Triangles = Buffer; Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nieuwe Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Buffer; Model.Ready = true; ;

Nu is ons framework klaar en kunnen we doorgaan naar de HTML-pagina.


De HTML-pagina

Je kunt alles wissen dat zich in de script tags omdat we de code nu bondiger kunnen schrijven dankzij ons nieuwe GLObject data type.

Dit is het volledige JavaScript:

var GL; var gebouw; function Ready () GL = new WebGL ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("House", functie (VerticeMap, Triangles, TextureMap) Gebouw = nieuwe GLObject (VerticeMap, Triangles, TextureMap, "House.png"); Building.Pos.Z = 650; // Mijn model Was een beetje te groot Building.Scale.X = 0,5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // And Backwards Building.Rotation.Y = 180; setInterval (Update, 33););  functie Update () Building.Rotation.Y + = 0.2 GL.Draw (Building); 

We laden een model en vertellen de pagina om het ongeveer dertig keer per seconde bij te werken. De Bijwerken functie roteert het model op de Y-as, wat wordt bereikt door de Y van het object bij te werken omwenteling eigendom. Mijn model was een beetje te groot voor de WebGL-scène en het was achterstevoren, dus moest ik wat aanpassingen in de code uitvoeren.

Tenzij je een soort filmische WebGL-presentatie maakt, wil je waarschijnlijk wat besturingselementen toevoegen. Laten we eens kijken naar hoe we enkele toetsenbordbedieningen aan onze applicatie kunnen toevoegen.


Toetsenbordbesturing

Dit is niet zozeer een WebGL-techniek als een native JavaScript-functie, maar het is handig voor het besturen en positioneren van uw 3D-modellen. Het enige dat u hoeft te doen is een gebeurtenislistener aan het toetsenbord toevoegen toets neer of keyup gebeurtenissen en controleer welke toets is ingedrukt. Elke sleutel heeft een speciale code en een goede manier om erachter te komen welke code overeenkomt met de sleutel is om de sleutelcodes te loggen op de console wanneer de gebeurtenis wordt geactiveerd. Dus ga naar het gebied waar ik het model heb geladen, en voeg de volgende code direct na de setInterval lijn:

document.onkeydown = handleKeyDown;

Hiermee wordt de functie ingesteld handleKeyDown omgaan met de toets neer evenement. Hier is de code voor de handleKeyDown functie:

function handleKeyDown (event) // U kunt de volgende regel verwijderen om achter de code van de toets te komen //alert(event.keyCode); if (event.keyCode == 37) // Linker pijltjestoets Building.Pos.X - = 4;  else if (event.keyCode == 38) // Pijl-omhoog-omhoog bouwen .Pos.Y + = 4;  else if (event.keyCode == 39) // Rechter pijltoets Gebouw.Pos.X + = 4;  else if (event.keyCode == 40) // Pijltjestoets omlaag Gebouw.Pos.Y - = 4; 

Al deze functie doet het bijwerken van de eigenschappen van het object; het WebGL-framework zorgt voor de rest.


Conclusie

We zijn niet klaar! In het derde en laatste deel van deze miniserie zullen we verschillende soorten verlichting bekijken en hoe je alles kunt verbinden met een aantal 2D-dingen!

Bedankt voor het lezen en, zoals altijd, als je vragen hebt, kun je hieronder een reactie achterlaten!