Denne artikkelen vil bygge videre på rammen som ble introdusert i del 1 av denne mini-serien, og tilført en modellimportør og en tilpasset klasse for 3D-objekter. Du vil også bli introdusert til animasjon og kontroller. Det er mye å gå gjennom, så la oss komme i gang!
Denne artikkelen er avhengig av den første artikkelen, så hvis du ikke har lest den ennå, bør du starte der først.
Måten WebGL manipulerer elementer i 3D-verdenen, er ved å bruke matematiske formler kjent som transformasjoner. Så, før vi begynner å bygge 3D-klassen, vil jeg vise deg noen av de forskjellige typer transformasjoner og hvordan de implementeres.
Det er tre grunnleggende transformasjoner når du arbeider med 3D-objekter.
Hver av disse funksjonene kan utføres på enten X-, Y- eller Z-aksen, noe som gir en total mulighet for ni grunnleggende transformasjoner. Alle disse påvirker 3D-objektets 4x4 transformasjonsmatrise på forskjellige måter. For å kunne utføre flere transformasjoner på samme objekt uten overlappende problemer, må vi formere transformasjonen i objektets matrise og ikke bruke den direkte på objektets matrise. Flytte er det enkleste å gjøre, så la oss starte der.
Flytte en 3D-objekt er en av de enkleste transformasjonene du kan gjøre, fordi det er et spesielt sted i 4x4-matrisen for det. Det er ikke behov for noen matte; bare sett X-, Y- og Z-koordinatene i matrisen og ferdig. Hvis du ser på 4x4-matrisen, er det de tre første tallene i den nederste raden. I tillegg bør du vite at positiv Z er bak kameraet. Derfor plasserer en Z-verdi på -100 objektet 100 enheter innover på skjermen. Vi vil kompensere for dette i vår kode.
For å utføre flere transformasjoner, kan du ikke bare endre objektets ekte matrise; du må bruke transformasjonen til en ny blank matriks, kjent som en identitet matrise, og formere den med hovedmatrisen.
Matriksmultiplikasjon kan være litt vanskelig å forstå, men den grunnleggende ideen er at hver vertikale kolonne blir multiplisert med den andre matriks horisontale rad. For eksempel vil det første nummeret være den første raden multiplisert med den andre matrisens første kolonne. Det andre nummeret i den nye matrisen vil være den første raden multiplisert med den andre matrisens andre kolonne, og så videre.
Følgende kode er kode jeg skrev for å multiplisere to matriser i JavaScript. Legg dette til din .js
fil du laget i første del av denne serien:
funksjon MH (A, B) var Sum = 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)];
Jeg tror ikke dette krever noen forklaring, for det er bare den nødvendige matematikken for matriksmultiplikasjon. La oss gå videre til skalering.
Skalering av en modell er også ganske enkel - det er enkelt multiplikasjon. Du må multiplisere de tre første diagonale tallene uansett omfanget. Enda en gang er rekkefølgen X, Y og Z. Så, hvis du vil skalere objektet ditt til å være to ganger større i alle tre aksene, vil du multiplisere det første, sjette og ellevte elementet i din rekkevidde med 2.
Rotering er den vanskeligste transformasjonen fordi det er en annen ligning for hver av de tre aksene. Følgende bilde viser rotasjonsligningene for hver akse:
Ikke bekymre deg om dette bildet ikke gir mening for deg; Vi vil snart se gjennom JavaScript-implementeringen.
Det er viktig å merke seg at det betyr noe i hvilken rekkefølge du utfører transformasjonene; forskjellige ordrer gir forskjellige resultater.
Det er viktig å merke seg at det betyr noe i hvilken rekkefølge du utfører transformasjonene; forskjellige ordrer gir forskjellige resultater. Hvis du først beveger objektet og roterer det, vil WebGL svinge objektet ditt som en flaggermus, i motsetning til at objektet roteres på plass. Hvis du roterer først og deretter flytter objektet, vil du ha et objekt på den angitte plasseringen, men det vil møte den retningen du skrev inn. Dette skyldes at transformasjonene utføres rundt opprinnelsespunktet - 0,0,0 - i 3D-verdenen. Det er ingen rett eller feil ordre. Alt avhenger av effekten du leter etter.
Det kan kreve mer enn en av hver transformasjon for å lage noen avanserte animasjoner. For eksempel hvis du vil at en dør skal svinge åpen på hengslene, beveger du døren slik at hengslene er på Y-aksen (dvs. 0 på både X- og Z-aksen). Du vil da rotere på Y-aksen, slik at døren svinger på hengslene. Til slutt vil du flytte den igjen til ønsket sted i din scene.
Disse typer animasjoner er litt mer skreddersydde for hver situasjon, så jeg skal ikke gjøre en funksjon for det. Jeg vil imidlertid gjøre en funksjon med den mest grunnleggende rekkefølgen som er: skalering, roterende og deretter flytte. Dette forsikrer alt er i den angitte plasseringen og vender den riktige veien.
Nå som du har en grunnleggende forståelse av matematikken bak alt dette og hvordan animasjoner fungerer, la oss lage en JavaScript-datatype for å holde våre 3D-objekter.
Husk fra den første delen av denne serien at du trenger tre arrays for å tegne et grunnleggende 3D-objekt: toppunktet, triangler-arrayet og strukturen array. Det vil være grunnlaget for vår datatype. Vi trenger også variabler for de tre transformasjonene på hver av de tre aksene. Endelig trenger vi en variabler for teksturbildet og for å angi om modellen er ferdig lastet.
Her er implementeringen av et 3D-objekt i JavaScript:
funksjon 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 = nytt bilde (); this.Image.onload = function () this.ReadyState = true; ; this.Image.src = ImageSrc; this.Ready = false; // Legg til transformasjonsfunksjonen her
Jeg har lagt til to separate "klare" variabler: en for når bildet er klart, og en for modellen. Når bildet er klart, vil jeg forberede modellen ved å konvertere bildet til en WebGL-tekstur og buffer de tre arrayene i WebGL buffere. Dette vil øke hastigheten på søknaden vår, som til hensikt å buffere dataene i hver tegningssyklus. Siden vi skal konvertere arrays til buffere, må vi lagre antall trekanter i en egen variabel.
La oss nå legge til funksjonen som beregner objektets transformasjonsmatrise. Denne funksjonen vil ta alle de lokale variablene og multiplisere dem i den rekkefølgen som jeg nevnte tidligere (skala, rotasjon og deretter oversettelse). Du kan leke med denne bestillingen for forskjellige effekter. Bytt ut // Legg til transformasjonsfunksjon her
kommentere med følgende kode:
this.GetTransforms = function () // Opprett en tom identitetsmatrise var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]; // Scaling 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); // Roterende 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); // Roterende Y Temp = [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); // Roterende 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); // Moving Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; Temp [12] = dette.Pos.X; Temp [13] = dette.Pos.Y; Temp [14] = this.Pos.Z * -1; returnere MultiplyMatrix (TMatrix, Temp);
Fordi rotasjonsformlene overlapper hverandre, må de utføres en om gangen. Denne funksjonen erstatter MakeTransform
Funksjon fra den siste opplæringen, slik at du kan fjerne den fra skriptet ditt.
Nå som vi har bygget vår 3D-klasse, trenger vi en måte å laste inn dataene på. Vi lager en enkel modellimportør som konverterer .obj
filer inn i de nødvendige dataene for å lage en av våre nyopprettede GLObject
objekter. Jeg bruker .obj
modellformat fordi det lagrer alle dataene i en rå form, og den har veldig god dokumentasjon for hvordan den lagrer informasjonen. Hvis ditt 3D-modelleringsprogram ikke støtter eksport til .obj
, da kan du alltid opprette en importør for et annet dataformat. .obj
er en standard 3D filtype; så det burde ikke være et problem. Alternativt kan du også laste ned Blender, en gratis tredimensjonal 3D-modelleringsprogrammer som støtter eksport til .obj
I .obj
filer, de to første bokstavene i hver linje forteller oss hva slags data linjen inneholder. "v
"er for en" vertex koordinater "linje,"vt
"er for en" tekstur koordinater "linje, og"f
"er for kartleggingslinjen. Med denne informasjonen skrev jeg følgende funksjon:
funksjon LoadModel (ModelName, CB) var Ajax = ny XMLHttpRequest (); Ajax.onreadystatechange = funksjon () if (Ajax.readyState == 4 && Ajax.status == 200) // Parse Model Data var Script = Ajax.responseText.split ("\ n"); var Vertices = []; var VerticeMap = []; var Triangles = []; Var Tekstur = []; var TextureMap = []; Var Normals = []; var NormalMap = []; var Counter = 0;
Denne funksjonen aksepterer navnet på en modell og en tilbakeringingsfunksjon. Tilbakeringingen aksepterer fire arrays: toppunktet, trekanten, tekstur og normale arrays. Jeg har ennå ikke dekket normaler, så du kan bare ignorere dem for nå. Jeg vil gå gjennom dem i oppfølgingsartikkelen når vi diskuterer belysning.
Importøren starter ved å opprette en XMLHttpRequest
objekt og definere dens onreadystatechange
hendelse handler. Inne i håndteringen deler vi filen i sine linjer og definerer noen få variabler. .obj
filer definerer først alle de unike koordinatene og definerer deretter deres rekkefølge. Derfor er det to variabler for toppene, teksturer og normaler. Tellervariabelen brukes til å fylle trianglesystemet fordi .obj
filer definerer trianglene i rekkefølge.
Deretter må vi gå gjennom hver linje i filen og sjekke hvilken type linje det er:
for (var jeg i script) var Line = Script [I]; // Hvis 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 ellers hvis (Line.substring (0, 2) == "vt") var Row = Line.substring (3) .split (""); Textures.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1])); // Normals Line ellers hvis (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ørste linjetyper er ganske enkle; de inneholder en liste over unike koordinater for toppene, teksturer og normaler. Alt vi trenger å gjøre er å skape disse koordinatene i deres respektive arrays. Den siste typen linje er litt mer komplisert fordi den kan inneholde flere ting. Det kan inneholde bare hjørner, eller hjørner og teksturer, eller topptekster, teksturer og normaler. Som sådan må vi sjekke for hver av disse tre sakene. Følgende kode gjør dette:
// Mapping Line ellers hvis (Line.substring (0, 2) == "f") var Row = Line.substring (2) .split (""); for (var T i rad) // Fjern tomme oppføringer hvis (Row [T]! = "") // Hvis dette er en multiværdig oppføring hvis (Row [T] .indexOf ("/")! = -1) // Split de forskjellige verdiene var TC = Row [T] .split ("/"); // Øk Trianglene Array Triangles.push (Counter); Counter ++; // Sett inn Vertices var index = parseInt (TC [0]) - 1; VerticeMap.push (Toppunkter [index] .X); VerticeMap.push (Toppunkter [index] .Y); VerticeMap.push (Toppunkter [index] .Z); // Sett inn Textures index = parseInt (TC [1]) - 1; TextureMap.push (Tekstur [index] .X); TextureMap.push (Tekstur [index] .Y); // Hvis denne oppføringen har normalsdata hvis (TC.length> 2) // Sett inn Normals index = parseInt (TC [2]) - 1; NormalMap.push (normaler [index] .x); NormalMap.push (normaler [index] .Y); NormalMap.push (normaler [index] .z); // For rader med bare vinkler ellers Triangles.push (Counter); // Økning Trianglene Array Counter ++; var index = parseInt (rad [T]) - 1; VerticeMap.push (Toppunkter [index] .X); VerticeMap.push (Toppunkter [index] .Y); VerticeMap.push (Toppunkter [index] .Z);
Denne koden er lengre enn den er komplisert. Selv om jeg dekket scenariet der .obj
filen inneholder bare vertexdata, vårt rammeverk krever vertikaler og teksturkoordinater. Hvis en .obj
filen inneholder bare vertex data, må du manuelt legge til tekstur koordinat data til den.
La oss nå passere arrays til tilbakekallingsfunksjonen og avslutte LoadModel
funksjon:
// Return Arrays CB (VerticeMap, Triangles, TextureMap, NormalMap); Ajax.open ("GET", ModelName + ".obj", true); Ajax.send ();
Noe du bør passe på er at vårt WebGL-rammeverk er ganske grunnleggende og bare tegner modeller som er laget av trekanter. Du må kanskje redigere 3D-modellene tilsvarende. Heldigvis har de fleste 3D-applikasjoner en funksjon eller plug-in for å triangulere modellene dine for deg. Jeg laget en enkel modell av et hus med mine grunnleggende modelleringsevner, og jeg vil inkludere den i kildefilene for deg å bruke, hvis du er så tilbøyelig.
La oss nå endre Tegne
Funksjon fra den siste opplæringen for å innlemme vår nye 3D-objektdatatype:
this.Draw = funksjon (Modell) if (Model.Image.ReadyState == true && Model.Ready == false) this.PrepareModel (Model); hvis (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); // Generer Perspektiv Matrix var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms (); // Sett spor 0 som den aktive Texturen this.GL.activeTexture (this.GL.TEXTURE0); // Legg i tekstur til minne this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Oppdater Tekstur Sampler i fragmentet skygger for å bruke spor 0 this.GL.uniform1i (this.GL.getUniformLocation (this.ShaderProgram, "uSampler"), 0); // Sett perspektiver og transformasjonsmatriser 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)); // Tegn Trianglene this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;
Den nye tegnefunksjonen kontrollerer først om modellen er utarbeidet for WebGL. Hvis tekstur er lastet, vil den forberede modellen til tegning. Vi kommer til PrepareModel
funksjon i et minutt. Hvis modellen er klar, kobler den bufferne til shaders og laster perspektivet og transformasjonsmatriser som det gjorde før. Den eneste virkelige forskjellen er at den nå tar alle dataene fra modellobjektet.
De PrepareModel
funksjonen konverterer bare tekstur- og datarammene til WebGL-kompatible variabler. Her er funksjonen; legg til det rett før tegnefunksjonen:
this.PrepareModel = funksjon (modell) Model.Image = this.LoadTexture (Model.Image); // Konverter Arrays til buffere var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, ny 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, ny 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, ny Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Buffer; Model.Ready = true; ;
Nå er rammene våre klare, og vi kan gå videre til HTML-siden.
Du kan slette alt som er inne i manus
merker som vi nå kan skrive koden mer konkret takket være vår nye GLObject
data-type.
Dette er den komplette JavaScript:
var GL; bygningen; funksjon Klar () GL = ny WebGL ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("House", funksjon (VerticeMap, Triangles, TextureMap) Building = new GLObject (VerticeMap, Triangles, TextureMap, "House.png"); Building.Pos.Z = 650; // Min modell var litt for stor Building.Scale.X = 0.5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // og Backwards Building.Rotation.Y = 180; setInterval (Update, 33);); funksjon Oppdatering () Building.Rotation.Y + = 0.2 GL.Draw (Building);
Vi laster en modell og forteller siden å oppdatere den på rundt tretti ganger per sekund. De Oppdater
funksjonen roterer modellen på Y-aksen, som oppnås ved å oppdatere objektets Y rotasjon
eiendom. Modellen min var litt for stor til WebGL-scenen, og det var bakover, så jeg trengte å utføre noen justeringer i koden.
Med mindre du gjør noen form for kinematisk WebGL-presentasjon, vil du sannsynligvis ønske å legge til noen kontroller. La oss se på hvordan vi kan legge til noen tastaturkontroller i applikasjonen vår.
Dette er egentlig ikke en WebGL-teknikk så mye som en innfødt JavaScript-funksjon, men det er praktisk for å kontrollere og posisjonere 3D-modellene dine. Alt du trenger å gjøre er å legge til en hendelseslytter til tastaturet keydown
eller keyup
hendelser og kontroller hvilken tast som ble trykket. Hver nøkkel har en spesiell kode, og en god måte å finne ut hvilken kode som tilsvarer nøkkelen er å logge nøkkelkoder til konsollen når hendelsen brenner. Så gå til området der jeg lastet inn modellen, og legg til følgende kode rett etter setInterval
linje:
document.onkeydown = handleKeyDown;
Dette vil stille inn funksjonen handleKeyDown
å håndtere keydown
begivenhet. Her er koden for handleKeyDown
funksjon:
funksjonshåndtakKeyDown (hendelse) // Du kan uncomment neste linje for å finne ut hver tastes kode //alert(event.keyCode); hvis (event.keyCode == 37) // Venstre piltast Building.Pos.X - = 4; ellers hvis (event.keyCode == 38) // Opp Pil Key Building.Pos.Y + = 4; annet hvis (event.keyCode == 39) // Høyre piltast Building.Pos.X + = 4; ellers hvis (event.keyCode == 40) // Ned-piltast Building.Pos.Y - = 4;
Alt denne funksjonen gjør er å oppdatere objektets egenskaper; WebGL-rammen tar vare på resten.
Vi er ikke ferdige! I den tredje og siste delen av denne mini-serien vil vi se gjennom ulike typer belysning, og hvordan du knytter det sammen med noen 2D ting!
Takk for at du leser, og som alltid, hvis du har noen spørsmål, vær så snill å legge igjen en kommentar nedenfor!