WebGL Essentials parte II

Questo articolo si baserà sul framework introdotto nella prima parte di questa miniserie, aggiungendo un importatore di modelli e una classe personalizzata per oggetti 3D. Verrai anche introdotto per l'animazione e i controlli. C'è molto da fare, quindi iniziamo!

Questo articolo fa molto affidamento sul primo articolo, quindi, se non lo hai ancora letto, dovresti iniziare da lì per primo.

Il modo in cui WebGL manipola gli oggetti nel mondo 3D è usando le formule matematiche conosciute come trasformazioni. Quindi, prima di iniziare a costruire la classe 3D, ti mostrerò alcuni dei diversi tipi di trasformazioni e in che modo vengono implementati.


trasformazioni

Ci sono tre trasformazioni di base quando si lavora con oggetti 3D.

  • In movimento
  • scalata
  • rotante

Ognuna di queste funzioni può essere eseguita sull'asse X, Y o Z, rendendo possibile la possibilità totale di nove trasformazioni di base. Tutti questi effetti influenzano la matrice di trasformazione 4x4 dell'oggetto 3D in modi diversi. Per eseguire trasformazioni multiple sullo stesso oggetto senza problemi sovrapposti, dobbiamo moltiplicare la trasformazione nella matrice dell'oggetto e non applicarla direttamente alla matrice dell'oggetto. Spostare è il più facile da fare, quindi iniziamo da lì.

Moving A.K.A. "Traduzione"

Spostare un oggetto 3D è una delle trasformazioni più facili che puoi fare, perché c'è un posto speciale nella matrice 4x4. Non c'è bisogno di matematica; basta mettere le coordinate X, Y e Z nella matrice e il tuo fatto. Se stai guardando la matrice 4x4, allora sono i primi tre numeri nella riga in basso. Inoltre, dovresti sapere che Z positiva è dietro la telecamera. Pertanto, un valore Z di -100 posiziona l'oggetto 100 unità verso l'interno sullo schermo. Lo compenseremo nel nostro codice.

Per eseguire trasformazioni multiple, non puoi semplicemente cambiare la matrice reale dell'oggetto; è necessario applicare la trasformazione a una nuova matrice vuota, nota come identità matrice e moltiplicarla con la matrice principale.

La moltiplicazione della matrice può essere un po 'complicata da comprendere, ma l'idea di base è che ogni colonna verticale viene moltiplicata per la riga orizzontale della seconda matrice. Ad esempio, il primo numero sarà la prima riga moltiplicata per la prima colonna dell'altro mastro. Il secondo numero nella nuova matrice sarebbe la prima riga moltiplicata per la seconda colonna della seconda matrice, e così via.

Il seguente frammento è il codice che ho scritto per moltiplicare due matrici in JavaScript. Aggiungi questo al tuo .js file che hai creato nella prima parte di questa serie:

funzione MH (A, B) var Sum = 0; per (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)]; 

Non penso che ciò richieda alcuna spiegazione, in quanto è solo la matematica necessaria per la moltiplicazione della matrice. Passiamo al ridimensionamento.

scalata

Scalare un modello è anche abbastanza semplice - è una semplice moltiplicazione. Devi moltiplicare i primi tre numeri diagonali indipendentemente dalla scala. Ancora una volta, l'ordine è X, Y e Z. Quindi, se vuoi ridimensionare il tuo oggetto per essere due volte più grande in tutti e tre gli assi, moltiplichi il primo, il sesto e l'undicesimo elemento dell'array per 2.

rotante

La rotazione è la trasformazione più difficile perché esiste un'equazione diversa per ciascuno dei tre assi. L'immagine seguente mostra le equazioni di rotazione per ciascun asse:

Non preoccuparti se questa immagine non ha senso per te; esamineremo presto l'implementazione di JavaScript.

È importante notare che è importante quale ordine esegui le trasformazioni; ordini diversi producono risultati diversi.

È importante notare che è importante quale ordine esegui le trasformazioni; ordini diversi producono risultati diversi. Se prima muovi il tuo oggetto e lo fai ruotare, WebGL muoverà il tuo oggetto come un pipistrello, invece di ruotare l'oggetto sul posto. Se prima ruoti e poi muovi il tuo oggetto, avrai un oggetto nella posizione specificata, ma dovrà affrontare la direzione che hai inserito. Questo perché le trasformazioni vengono eseguite attorno al punto di origine - 0,0,0 - nel mondo 3D. Non esiste un ordine giusto o sbagliato. Tutto dipende dall'effetto che stai cercando.

Potrebbe richiedere più di una trasformazione per creare animazioni avanzate. Ad esempio, se vuoi che una porta si apra sui suoi cardini, muovi la porta in modo che i suoi cardini si trovino sull'asse Y (cioè 0 su entrambi gli assi X e Z). Dovresti quindi ruotare sull'asse Y in modo che la porta si muova sui cardini. Infine, lo sposterai nuovamente nella posizione desiderata nella scena.

Questi tipi di animazioni sono un po 'più personalizzati per ogni situazione, quindi non ho intenzione di creare una funzione per questo. Tuttavia, eseguirò una funzione con l'ordine di base che è: ridimensionamento, rotazione e quindi spostamento. Ciò assicura che tutto sia nella posizione specificata e rivolto nella direzione giusta.

Ora che hai una conoscenza di base della matematica dietro a tutto questo e come funzionano le animazioni, creiamo un tipo di dati JavaScript per contenere i nostri oggetti 3D.


GL oggetti

Ricorda dalla prima parte di questa serie che hai bisogno di tre matrici per disegnare un oggetto 3D di base: l'array di vertici, l'array di triangoli e l'array di trame. Questa sarà la base del nostro tipo di dati. Abbiamo anche bisogno di variabili per le tre trasformazioni su ciascuno dei tre assi. Infine, abbiamo bisogno di una variabile per l'immagine della trama e per indicare se il modello ha terminato il caricamento.

Ecco la mia implementazione di un oggetto 3D in JavaScript:

function 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; // Aggiungi funzione di trasformazione qui

Ho aggiunto due variabili "pronte" separate: una per quando l'immagine è pronta e una per il modello. Quando l'immagine è pronta, preparerò il modello convertendo l'immagine in una trama WebGL e memorizzando i tre array in buffer WebGL. Ciò velocizzerà la nostra applicazione, come richiesto per il buffering dei dati in ogni ciclo di estrazione. Dal momento che convertiremo gli array in buffer, dobbiamo salvare il numero di triangoli in una variabile separata.

Ora, aggiungiamo la funzione che calcolerà la matrice di trasformazione dell'oggetto. Questa funzione prenderà tutte le variabili locali e le moltiplicherà nell'ordine che ho menzionato in precedenza (scala, rotazione e quindi traduzione). Puoi giocare con questo ordine per effetti diversi. Sostituisci il // Aggiungi la funzione di trasformazione qui commentare con il seguente codice:

this.GetTransforms = function () // Crea una matrice di identità vuota 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); // 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); // Rotating 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); // 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); // Moving 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); 

Poiché le formule di rotazione si sovrappongono, devono essere eseguite una alla volta. Questa funzione sostituisce il MakeTransform funzione dell'ultimo tutorial, quindi puoi rimuoverlo dal tuo script.


OBJ Importatore

Ora che abbiamo creato la nostra classe 3D, abbiamo bisogno di un modo per caricare i dati. Faremo un semplice importatore di modelli che convertirà .obj file nei dati necessari per creare uno dei nostri nuovi creati GLObject oggetti. Sto usando il .obj Formato del modello perché memorizza tutti i dati in una forma grezza e ha un'ottima documentazione su come memorizza le informazioni. Se il tuo programma di modellazione 3D non supporta l'esportazione in .obj, quindi puoi sempre creare un importatore per qualche altro formato di dati. .obj è un tipo di file 3D standard; quindi, non dovrebbe essere un problema. In alternativa puoi anche scaricare Blender, un'applicazione di modellazione 3D multipiattaforma gratuita che supporta l'esportazione in .obj

Nel .obj file, le prime due lettere di ogni riga ci dicono che tipo di dati contiene quella riga. "v"è per una linea" coordinate vertici ","vt"è per una linea" coordinate texture "e"f"è per la linea di mappatura. Con questa informazione, ho scritto la seguente funzione:

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

Questa funzione accetta il nome di un modello e una funzione di callback. Il callback accetta quattro array: il vertice, il triangolo, la trama e le matrici normali. Non ho ancora coperto le normali, quindi puoi semplicemente ignorarle per ora. Li esaminerò nell'articolo di follow-up, quando discuteremo dell'illuminazione.

L'importatore inizia creando un XMLHttpRequest oggetto e definendo il suo onreadystatechange gestore di eventi. All'interno del gestore, dividiamo il file nelle sue linee e definiamo alcune variabili. .obj i file prima definiscono tutte le coordinate univoche e quindi definiscono il loro ordine. Questo è il motivo per cui ci sono due variabili per i vertici, le trame e le normali. La variabile contatore viene utilizzata per riempire l'array di triangoli perché .obj i file definiscono i triangoli in ordine.

Successivamente, dobbiamo passare attraverso ogni riga del file e verificare che tipo di linea è:

 for (var I in Script) var Line = Script [I]; // If 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 else if (Line.substring (0, 2) == "vt") var Row = Line.substring (3) .split (""); Textures.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]));  // Normals Line else if (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])); 

I primi tre tipi di linea sono abbastanza semplici; contengono una lista di coordinate uniche per i vertici, le trame e le normali. Tutto ciò che dobbiamo fare è spingere queste coordinate nei loro rispettivi array. L'ultimo tipo di linea è un po 'più complicato perché può contenere più cose. Potrebbe contenere solo vertici, o vertici e trame, o vertici, trame e normali. In quanto tale, dobbiamo verificare ciascuno di questi tre casi. Il seguente codice fa questo:

 // Mapping Line else if (Line.substring (0, 2) == "f") var Row = Line.substring (2) .split (""); for (var T in Row) // Rimuovi voci vuote if (Row [T]! = "") // Se questa è una voce a più valori if (Row [T] .indexOf ("/")! = -1) // Dividi i diversi valori var TC = Row [T] .split ("/"); // Increment Triangles Array Triangles.push (Counter); Contatore ++; // Inserisci Vertices var index = parseInt (TC [0]) - 1; VerticeMap.push (vertici [indice] .X); VerticeMap.push (vertici [indice] .Y); VerticeMap.push (vertici [indice] .Z); // Inserisci Textures index = parseInt (TC [1]) - 1; TextureMap.push (Textures [index] .X); TextureMap.push (Textures [index] .Y); // Se questa voce ha dati normali se (TC.length> 2) // Insert Normals index = parseInt (TC [2]) - 1; NormalMap.push (Normali [indice] .X); NormalMap.push (Normali [indice] .y); NormalMap.push (Normali [indice] .Z);  // Per le righe con solo vertici Triangles.push (Counter); // Increment Triangles Array Counter ++; var index = parseInt (Row [T]) - 1; VerticeMap.push (vertici [indice] .X); VerticeMap.push (vertici [indice] .Y); VerticeMap.push (vertici [indice] .Z); 

Questo codice è più lungo di quanto sia complicato. Anche se ho coperto lo scenario in cui il .obj il file contiene solo i dati del vertice, il nostro framework richiede vertici e coordinate di trama. Se una .obj il file contiene solo i dati del vertice, dovrai aggiungere manualmente i dati delle coordinate della trama ad esso.

Passiamo ora gli array alla funzione di callback e finiamo il LoadModel funzione:

  // Restituisce l'array CB (VerticeMap, Triangles, TextureMap, NormalMap);  Ajax.open ("GET", ModelName + ".obj", vero); Ajax.send (); 

Qualcosa che dovresti fare attenzione è che il nostro framework WebGL è abbastanza semplice e disegna solo modelli che sono fatti di triangoli. Potrebbe essere necessario modificare i modelli 3D di conseguenza. Fortunatamente, molte applicazioni 3D hanno una funzione o un plug-in per triangolare i tuoi modelli per te. Ho realizzato un semplice modello di una casa con le mie abilità di modellazione di base e lo includerò nei file di origine da utilizzare, se sei così inclinato.

Ora modifichiamo il Disegnare funzione dall'ultimo tutorial per incorporare il nostro nuovo tipo di dati dell'oggetto 3D:

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); // Genera la matrice prospettica var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms (); // Imposta lo slot 0 come Texture attiva this.GL.activeTexture (this.GL.TEXTURE0); // Carica nella texture in memoria this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Aggiorna The Texture Sampler nel framment shader per usare lo slot 0 this.GL.uniform1i (this.GL.getUniformLocation (this.ShaderProgram, "uSampler"), 0); // Imposta le matrici Prospettiva e Trasformazione 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)); // Disegna i triangoli this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;

La nuova funzione di disegno controlla innanzitutto se il modello è stato preparato per WebGL. Se la trama è stata caricata, preparerà il modello per il disegno. Raggiungeremo il PrepareModel funzione in un minuto. Se il modello è pronto, collegherà i suoi buffer agli shader e caricherà le matrici di prospettiva e di trasformazione come prima. L'unica vera differenza è che ora prende tutti i dati dall'oggetto del modello.

Il PrepareModel funzione converte solo la trama e gli array di dati in variabili compatibili WebGL. Ecco la funzione; aggiungilo subito prima della funzione di disegno:

this.PrepareModel = function (Model) Model.Image = this.LoadTexture (Model.Image); // Converti array in buffer var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nuovo 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, nuovo 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, nuovo Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Buffer; Model.Ready = true; ;

Ora il nostro framework è pronto e possiamo passare alla pagina HTML.


La pagina HTML

Puoi cancellare tutto ciò che è dentro il copione tag come possiamo ora scrivere il codice in modo più conciso grazie al nostro nuovo GLObject tipo di dati.

Questo è il codice JavaScript completo:

var GL; var Building; funzione Pronto () GL = nuovo WebGL ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("House", function (VerticeMap, Triangles, TextureMap) Building = new GLObject (VerticeMap, Triangles, TextureMap, "House.png"); Building.Pos.Z = 650; // My Model Era un po 'troppo grande Building.Scale.X = 0.5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // And Backwards Building.Rotation.Y = 180; setInterval (Update, 33););  function Update () Building.Rotation.Y + = 0.2 GL.Draw (Building); 

Carichiamo un modello e comunichiamo alla pagina di aggiornarlo circa trenta volte al secondo. Il Aggiornare funzione ruota il modello sull'asse Y, che si ottiene aggiornando l'oggetto Y Rotazione proprietà. La mia modella era un po 'troppo grande per la scena WebGL ed era indietro, quindi avevo bisogno di eseguire alcune regolazioni nel codice.

A meno che non stiate facendo una specie di presentazione cinematografica WebGL, probabilmente vorrete aggiungere alcuni controlli. Diamo un'occhiata a come possiamo aggiungere alcuni controlli da tastiera alla nostra applicazione.


Controlli della tastiera

Questa non è una tecnica WebGL tanto quanto una funzione JavaScript nativa, ma è utile per controllare e posizionare i modelli 3D. Tutto quello che devi fare è aggiungere un listener di eventi alla tastiera keydown o keyup eventi e controllare quale tasto è stato premuto. Ogni chiave ha un codice speciale e un buon modo per scoprire quale codice corrisponde alla chiave è quello di registrare i codici chiave sulla console quando l'evento si attiva. Quindi vai nell'area in cui ho caricato il modello e aggiungi il seguente codice subito dopo setInterval linea:

document.onkeydown = handleKeyDown;

Questo imposterà la funzione handleKeyDown gestire il keydown evento. Ecco il codice per il handleKeyDown funzione:

function handleKeyDown (evento) // È possibile decommentare la riga successiva per trovare il codice di ogni chiave //alert(event.keyCode); if (event.keyCode == 37) // Left Arrow Key Building.Pos.X - = 4;  else if (event.keyCode == 38) // Up Arrow Key Building.Pos.Y + = 4;  else if (event.keyCode == 39) // Right Arrow Key Building.Pos.X + = 4;  else if (event.keyCode == 40) // Down Arrow Key Building.Pos.Y - = 4; 

Tutto ciò che fa è aggiornare le proprietà dell'oggetto; il framework WebGL si prende cura di tutto il resto.


Conclusione

Non abbiamo finito! Nella terza e ultima parte di questa mini-serie, esamineremo diversi tipi di illuminazione e come legare tutto con alcune cose 2D!

Grazie per aver letto e, come sempre, se avete qualche domanda, sentitevi liberi di lasciare un commento qui sotto!