Bentornati a questa terza e ultima puntata nella nostra mini serie di WebGL Essentials. In questa lezione, daremo un'occhiata all'illuminazione e aggiungendo oggetti 2D alla scena. Ci sono molte nuove informazioni qui, quindi tuffiamoci dentro!
L'illuminazione può essere l'aspetto più tecnico e difficile di un'applicazione 3D da comprendere. Una solida comprensione dell'illuminazione è assolutamente essenziale.
Prima di entrare nei diversi tipi di tecniche di luce e codice, è importante sapere come funziona la luce nel mondo reale. Ogni fonte di luce (es: una lampadina, il sole, ecc.) Genera particelle chiamate fotoni. Questi fotoni rimbalzano intorno agli oggetti fino a quando alla fine non entrano nei nostri occhi. I nostri occhi convertono i fotoni per produrre una "immagine" visiva. Questo è il modo in cui vediamo. Anche la luce è additiva, il che significa che un oggetto con più colore è più luminoso di un oggetto senza colore (nero). Il nero è la completa assenza di colore, mentre il bianco contiene tutti i colori. Questa è una distinzione importante quando si lavora con luci molto luminose o "sovra-saturi".
La luminosità è solo un principio che ha più stati. La riflessione, ad esempio, può avere una varietà di livelli diversi. Un oggetto, come uno specchio, può essere completamente riflettente, mentre altri oggetti possono avere una superficie opaca. La trasparenza determina come gli oggetti piegano la luce e causano rifrazione; un oggetto può essere completamente trasparente mentre altri possono essere opachi (o qualsiasi altro livello intermedio).
L'elenco continua, ma penso che tu possa già vedere che la luce non è semplice.
Se volevi anche una piccola scena per simulare la luce reale, funzionerebbe a qualcosa come 4 fotogrammi all'ora, e questo è su un computer ad alta potenza. Per aggirare questo problema, i programmatori usano trucchi e tecniche per simulare l'illuminazione semi-realistica a un frame rate ragionevole. Devi trovare una qualche forma di compromesso tra realismo e velocità. Diamo un'occhiata ad alcune di queste tecniche.
Prima di iniziare a elaborare tecniche diverse, vorrei darti un piccolo disclaimer. Vi sono molte polemiche sui nomi esatti delle diverse tecniche di illuminazione e diverse persone vi daranno spiegazioni diverse su cosa sia "Ray Casting" o "Light Mapping". Quindi, prima di iniziare a ricevere la posta dell'odio, vorrei dire che userò i nomi che ho imparato; alcune persone potrebbero non essere d'accordo sui miei titoli esatti. In ogni caso, la cosa importante da sapere è quali sono le diverse tecniche. Quindi, senza ulteriori indugi, iniziamo.
Devi trovare una qualche forma di compromesso tra realismo e velocità.
Il ray tracing è una delle tecniche di illuminazione più realistiche, ma è anche uno dei più costosi. Il ray tracing emula la luce reale; emette "fotoni" o "raggi" dalla fonte di luce e li rimbalza. Nella maggior parte delle implementazioni di ray tracing, i raggi provengono dalla "camera" e rimbalzano sulla scena nella direzione opposta. Questa tecnica viene solitamente utilizzata in film o scene che possono essere renderizzati in anticipo. Questo non vuol dire che non si possa usare il ray tracing in un'applicazione in tempo reale, ma farlo in questo modo ti costringe ad attenuare altre cose nella scena. Ad esempio, potresti dover ridurre la quantità di "rimbalzi" che i raggi dovrebbero eseguire, oppure puoi assicurarti che non ci siano oggetti con superfici riflettenti o rifrangenti. Il ray tracing può anche essere un'opzione percorribile se la tua applicazione ha pochissime luci e oggetti.
Se hai un'applicazione in tempo reale, potresti essere in grado di precompilare parti della scena.
Se le luci della tua applicazione non si muovono o si muovono solo in una piccola area alla volta, puoi precompilare l'illuminazione con un algoritmo di ray tracing molto avanzato e ricalcolare una piccola area attorno alla sorgente di luce in movimento. Ad esempio, se stai realizzando un gioco in cui le luci non si muovono, puoi precompilare il mondo con tutte le luci e gli effetti desiderati. Quindi, puoi semplicemente aggiungere un'ombra attorno al tuo personaggio quando si muove. Questo produce un aspetto di alta qualità con una quantità minima di elaborazione.
Il ray casting è molto simile al ray tracing, ma i "photons" non rimbalzano sugli oggetti o interagiscono con materiali diversi. In un'applicazione tipica, si inizia fondamentalmente con una scena scura e quindi si disegnano delle linee dalla fonte di luce. Qualunque cosa accenda la luce è accesa; tutto il resto rimane oscuro. Questa tecnica è significativamente più veloce del ray tracing pur continuando a darti un effetto ombra realistico. Ma il problema con ray casting è la sua restrizione; non hai molto spazio per lavorare quando cerchi di aggiungere effetti come i riflessi. Di solito, devi trovare una sorta di compromesso tra ray casting e ray tracing, bilanciamento tra velocità ed effetti visivi.
Il problema principale con entrambe queste tecniche è che WebGL non ti dà accesso a nessun vertice tranne quello attualmente attivo.
Ciò significa che devi eseguire tutto sulla CPU (come indicato nella scheda grafica), oppure hai creato un secondo shader che calcola tutta l'illuminazione e memorizza le informazioni in una trama finta. Dovresti quindi decomprimere i dati della trama nelle informazioni di illuminazione e mapparli ai vertici. Quindi, in pratica, la versione corrente di WebGL non è molto adatta per questo. Non sto dicendo che non si possa fare, sto solo dicendo che WebGL non ti aiuterà.
Il ray tracing può anche essere un'opzione percorribile se la tua applicazione ha pochissime luci e oggetti.
Una migliore alternativa a ray casting in WebGL è chiamata shadow mapping. Ti dà lo stesso effetto di ray casting, ma usa un approccio diverso. Shadow mapping non risolverà tutti i tuoi problemi, ma WebGL è semi-ottimizzato per questo. Si può pensare ad una specie di trucco, ma la mappatura delle ombre viene utilizzata nelle applicazioni reali per PC e console.
Allora, che cosa chiedi?
Devi capire come WebGL rende le sue scene per rispondere a questa domanda. WebGL sposta tutti i vertici nel vertex shader, che calcola le coordinate finali per ciascun vertice dopo l'applicazione delle trasformazioni. Quindi, per risparmiare tempo, WebGL elimina i vertici nascosti dietro altri oggetti e disegna solo gli oggetti essenziali. Se ricordi come funziona il ray casting, proietta solo i raggi luminosi sugli oggetti visibili. Quindi impostiamo la "camera" della nostra scena sulle coordinate della sorgente luminosa e la puntiamo nella direzione in cui vogliamo che la luce sia rivolta. Quindi, WebGL rimuove automaticamente tutti i vertici che non sono in vista della luce. Possiamo quindi salvare questi dati e usarli quando rendiamo la scena per sapere quale dei vertici sono illuminati.
Questa tecnica suona bene sulla carta ma ha alcuni aspetti negativi:
Tutte queste tecniche richiedono una discreta quantità di modifiche con WebGL. Ma ti mostrerò una tecnica molto basilare per produrre una luce diffusa per dare un po 'di personalità ai tuoi oggetti. Non chiamerei la luce realistica, ma fornisce la definizione degli oggetti. Questa tecnica utilizza la matrice delle normali dell'oggetto per calcolare l'angolo della luce rispetto alla superficie dell'oggetto. È veloce, efficiente e non richiede alcun hacking con WebGL. Iniziamo.
Iniziamo aggiornando gli shader per incorporare l'illuminazione. Abbiamo bisogno di aggiungere un valore booleano che determini se l'oggetto debba essere illuminato o no. Quindi, abbiamo bisogno del vertice normale effettivo e lo trasformiamo in modo che si allinea con il modello. Infine, dobbiamo creare una variabile per passare il risultato finale allo shader del frammento. Questo è il nuovo vertex shader:
Se non usiamo le luci, passiamo semplicemente un vertice vuoto allo shader del frammento e il suo colore rimane lo stesso. Quando le luci sono accese, calcoliamo l'angolo tra la direzione della luce e la superficie dell'oggetto usando la funzione punto sul normale e moltiplichiamo il risultato per il colore della luce come una sorta di maschera da sovrapporre all'oggetto.
Foto di superfici normali di Oleg Alexandrov.
Questo funziona perché le normali sono già perpendicolari alla superficie dell'oggetto, e la funzione punto ci dà un numero basato sull'angolo della luce rispetto al normale. Se il normale e la luce sono quasi paralleli, la funzione punto restituisce un numero positivo, ovvero la luce è rivolta verso la superficie. Quando il normale e la luce sono perpendicolari, la superficie è parallela alla luce e la funzione restituisce zero. Qualunque cosa più alta di 90 gradi tra la luce e la normale produce un numero negativo, ma la filtriamo con la funzione "max zero".
Ora lascia che ti mostri lo shader del frammento:
Questo shader è praticamente lo stesso delle precedenti parti della serie. L'unica differenza è che moltiplichiamo il colore della trama per il livello di luce. Questo illumina o scurisce le diverse parti dell'oggetto, dandogli una certa profondità.
Questo è tutto per gli shader, ora andiamo al WebGL.js
file e modifica le nostre due classi.
Iniziamo con GLObject
classe. Abbiamo bisogno di aggiungere una variabile per l'array di normali. Ecco qual è la parte superiore del tuo GLObject
dovrebbe ora apparire come:
funzione GLObject (VertexArr, TriangleArr, TextureArr, ImageSrc, NormalsArr) 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; // Array per contenere i dati normali this.Normals = NormalsArr; // Il resto di GLObject continua qui
Questo codice è abbastanza semplice. Ora torniamo al file HTML e aggiungiamo l'array di normali al nostro oggetto.
Nel Pronto()
funzione in cui carichiamo il nostro modello 3D, dobbiamo aggiungere il parametro per l'array di normali. Un array vuoto significa che il modello non contiene dati normali e dovremo disegnare l'oggetto senza luce. Nel caso in cui la matrice normale contenga dati, la passeremo semplicemente su GLObject
oggetto.
Abbiamo anche bisogno di aggiornare il WebGL
classe. Dobbiamo collegare le variabili agli shader subito dopo aver caricato gli shader. Aggiungiamo il vertice normale; il tuo codice dovrebbe ora apparire come questo:
// Collega l'attributo Posizione vertice da Shader this.VertexPosition = this.GL.getAttribLocation (this.ShaderProgram, "VertexPosition"); this.GL.enableVertexAttribArray (this.VertexPosition); // Collega l'attributo Coordinate Texture da Shader this.VertexTexture = this.GL.getAttribLocation (this.ShaderProgram, "TextureCoord"); this.GL.enableVertexAttribArray (this.VertexTexture); // Questo è il nuovo attributo di array Normals this.VertexNormal = this.GL.getAttribLocation (this.ShaderProgram, "VertexNormal"); this.GL.enableVertexAttribArray (this.VertexNormal);
Successivamente, aggiorniamo il PrepareModel ()
funzione e aggiungere un po 'di codice per bufferizzare i dati normali quando è disponibile. Aggiungi il nuovo codice subito prima del Model.Ready
affermazione in fondo:
if (false! == Model.Normals) Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nuovo Float32Array (Model.Normals), this.GL.STATIC_DRAW); Model.Normals = Buffer; Model.Ready = true;
Ultimo ma non meno importante, aggiorna il reale Disegnare
funzione per incorporare tutti questi cambiamenti. C'è un paio di cambiamenti qui, quindi portami con me. Ho intenzione di andare pezzo per pezzo attraverso l'intera funzione:
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);
Fino a qui è lo stesso di prima. Ora arriva la parte normale:
// Check For Normal if (false! == Model.Normals) // Connetti il buffer delle normali allo shader this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Normals); this.GL.vertexAttribPointer (this.VertexNormal, 3, this.GL.FLOAT, false, 0, 0); // Comunica allo shader di utilizzare var var UseLights = this.GL.getUniformLocation (this.ShaderProgram, "UseLights"); this.GL.uniform1i (UseLights, true); else // Anche se il nostro oggetto non ha dati normali, dobbiamo ancora passare qualcosa // Quindi passo i Vertices invece this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Vertices); this.GL.vertexAttribPointer (this.VertexNormal, 3, this.GL.FLOAT, false, 0, 0); // Comunica allo shader di utilizzare var var UseLights = this.GL.getUniformLocation (this.ShaderProgram, "UseLights"); this.GL.uniform1i (UseLights, false);
Controlliamo per vedere se il modello ha dati normali. In tal caso, collega il buffer e imposta il valore booleano. In caso contrario, lo shader ha ancora bisogno di dati o ti darà un errore. Così, invece, ho passato il buffer dei vertici e impostato il UseLight
booleano a falso
. Si potrebbe aggirare questo usando più shader, ma ho pensato che sarebbe stato più semplice per quello che stiamo cercando di fare.
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 ();
Di nuovo questa parte della funzione è sempre la stessa.
var NormalsMatrix = MatrixTranspose (InverseMatrix (TransformMatrix));
Qui calcoliamo la matrice di trasformazione delle normali. Discuterò il MatrixTranspose ()
e InverseMatrix ()
funzioni in un minuto. Per calcolare la matrice di trasformazione per l'array di normali, devi trasporre la matrice inversa della matrice di trasformazione regolare dell'oggetto. Maggiori informazioni su questo più tardi.
// 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)); var nmatrix = this.GL.getUniformLocation (this.ShaderProgram, "NormalTransformation"); this.GL.uniformMatrix4fv (nmatrix, false, new Float32Array (NormalsMatrix)); // Disegna i triangoli this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;
Puoi facilmente visualizzare la fonte di qualsiasi applicazione WebGL per saperne di più.
Questo è il resto del Disegnare()
funzione. È quasi la stessa di prima, ma c'è il codice aggiunto che collega la matrice normale agli shader. Ora, torniamo a quelle due funzioni che ho usato per ottenere la matrice di trasformazione delle normali.
Il InverseMatrix ()
la funzione accetta una matrice e restituisce la sua matrice inversa. Una matrice inversa è una matrice che, moltiplicata per la matrice originale, restituisce una matrice di identità. Diamo un'occhiata ad un esempio base di algebra per chiarirlo. L'inverso del numero 4 è 1/4 perché quando 1/4 x 4 = 1
. L'equivalente "uno" nelle matrici è una matrice di identità. quindi, il InverseMatrix ()
la funzione restituisce la matrice di identità per l'argomento. Ecco questa funzione:
function InverseMatrix (A) var s0 = A [0] * A [5] - A [4] * A [1]; var s1 = A [0] * A [6] - A [4] * A [2]; var s2 = A [0] * A [7] - A [4] * A [3]; var s3 = A [1] * A [6] - A [5] * A [2]; var s4 = A [1] * A [7] - A [5] * A [3]; var s5 = A [2] * A [7] - A [6] * A [3]; var c5 = A [10] * A [15] - A [14] * A [11]; var c4 = A [9] * A [15] - A [13] * A [11]; var c3 = A [9] * A [14] - A [13] * A [10]; var c2 = A [8] * A [15] - A [12] * A [11]; var c1 = A [8] * A [14] - A [12] * A [10]; var c0 = A [8] * A [13] - A [12] * A [9]; var invdet = 1.0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); var B = []; B [0] = (A [5] * c5 - A [6] * c4 + A [7] * c3) * invdet; B [1] = (-A [1] * c5 + A [2] * c4 - A [3] * c3) * invdet; B [2] = (A [13] * s5 - A [14] * s4 + A [15] * s3) * invdet; B [3] = (-A [9] * s5 + A [10] * s4 - A [11] * s3) * invdet; B [4] = (-A [4] * c5 + A [6] * c2 - A [7] * c1) * invdet; B [5] = (A [0] * c5 - A [2] * c2 + A [3] * c1) * invdet; B [6] = (-A [12] * s5 + A [14] * s2 - A [15] * s1) * invdet; B [7] = (A [8] * s5 - A [10] * s2 + A [11] * s1) * invdet; B [8] = (A [4] * c4 - A [5] * c2 + A [7] * c0) * invdet; B [9] = (-A [0] * c4 + A [1] * c2 - A [3] * c0) * invdet; B [10] = (A [12] * s4 - A [13] * s2 + A [15] * s0) * invdet; B [11] = (-A [8] * s4 + A [9] * s2 - A [11] * s0) * invdet; B [12] = (-A [4] * c3 + A [5] * c1 - A [6] * c0) * invdet; B [13] = (A [0] * c3 - A [1] * c1 + A [2] * c0) * invdet; B [14] = (-A [12] * s3 + A [13] * s1 - A [14] * s0) * invdet; B [15] = (A [8] * s3 - A [9] * s1 + A [10] * s0) * invdet; ritorno B;
Questa funzione è piuttosto complicata e, a dire il vero, non capisco appieno perché funzioni la matematica. Ma ho già spiegato il succo di ciò sopra. Non sono venuto con questa funzione; è stato scritto in ActionScript da Robin Hilliard.
La prossima funzione, MatrixTranspose ()
, è molto più semplice da capire. Restituisce la versione "trasposta" della sua matrice di input. In breve, ruota semplicemente la matrice su un lato. Ecco il codice:
function MatrixTranspose (A) return [A [0], A [4], A [8], A [12], A [1], A [5], A [9], A [13], A [ 2], A [6], A [10], A [14], A [3], A [7], A [11], A [15]];
Invece di andare in righe orizzontali (cioè A [0], A [1], A [2] ...) questa funzione scende verticalmente (A [0], A [4], A [8] ...).
Sei a posto dopo aver aggiunto queste due funzioni al tuo WebGL.js
file e qualsiasi modello che contenga i dati normali dovrebbe essere ombreggiato. Puoi giocare con la direzione e il colore della luce nel vertex shader per ottenere effetti diversi.
C'è un ultimo argomento che desidero trattare, e cioè aggiungere contenuti 2D alla nostra scena. L'aggiunta di componenti 2D su una scena 3D può avere molti vantaggi. Ad esempio, può essere utilizzato per visualizzare le informazioni sulle coordinate, una mini mappa, le istruzioni per la tua app e l'elenco può continuare. Questo processo non è così semplice come potresti pensare, quindi controlliamolo.
HTML non ti consente di utilizzare l'API WebGL e l'API 2D dalla stessa area di disegno.
Potresti pensare: "Perché non usare solo l'API 2D HTML5 integrata nel canvas?" Bene, il problema è che HTML non ti consente di utilizzare l'API WebGL e l'API 2D dallo stesso canvas. Una volta assegnato il contesto della tela a WebGL, non è possibile utilizzarlo con l'API 2D. HTML5 restituisce semplicemente nullo
quando si tenta di ottenere il contesto 2D. Allora come fai ad aggirare questo? Bene, ti darò due opzioni.
2.5D, per coloro che non sono consapevoli, è quando si mettono oggetti 2D (oggetti senza profondità) in una scena 3D. L'aggiunta di testo a una scena è un esempio di 2.5D. Puoi prendere il testo da un'immagine e applicarlo come una trama ad un piano 3D, oppure puoi ottenere un modello 3D per il testo e renderlo sul tuo schermo.
I vantaggi di questo approccio sono che non hai bisogno di due tele, e sarebbe più veloce disegnare se hai usato solo forme semplici nella tua applicazione.
Ma per fare cose come il testo, devi avere le foto di tutto ciò che vuoi scrivere, o un modello 3D per ogni lettera (un po 'esagerato, secondo me).
L'alternativa è creare una seconda tela e sovrapporla sopra la tela 3D. Preferisco questo approccio perché sembra meglio attrezzato per disegnare contenuti 2D. Non inizierò a creare un nuovo framework 2D, ma creeremo un semplice esempio in cui mostriamo le coordinate del modello insieme alla sua rotazione corrente. Aggiungiamo una seconda tela al file HTML subito dopo il canvas WebGL. Ecco la nuova tela insieme a quella attuale:
Ho anche aggiunto alcuni CSS incorporati per sovrapporre la seconda tela sopra alla prima. Il passo successivo è creare una variabile per la tela 2D e ottenere il suo contesto. Lo farò nel Pronto()
funzione. Il tuo codice aggiornato dovrebbe apparire in questo modo:
var GL; var Building; var Canvas2D; function Ready () // Dichiarazione del gl e funzione di caricamento del modello Here Canvas2D = document.getElementById ("2DCanvas"). getContext ("2d"); Canvas2D.fillStyle = "# 000";
In alto, puoi vedere che ho aggiunto una variabile globale per la tela 2D. Quindi, ho aggiunto due righe alla fine del Pronto()
funzione. La prima nuova riga ottiene il contesto 2D e la seconda nuova riga imposta il colore sul nero.
L'ultimo passo è disegnare il testo all'interno del Aggiornare()
funzione:
function Update () Building.Rotation.Y + = 0.3 // Cancella il Canvas dal precedente draw Canvas2D.clearRect (0, 0, 600, 400); // Titolo Text Canvas2D.font = "25px sans-serif"; Canvas2D.fillText ("Edificio", 20, 30); // Proprietà dell'oggetto Canvas2D.font = "16px sans-serif"; Canvas2D.fillText ("X:" + Building.Pos.X, 20, 55); Canvas2D.fillText ("Y:" + Building.Pos.Y, 20, 75); Canvas2D.fillText ("Z:" + Building.Pos.Z, 20, 95); Canvas2D.fillText ("Rotazione:" + Math.floor (Building.Rotation.Y), 20, 115); GL.GL.clear (16384 | 256); GL.Draw (costruzione);
Iniziamo ruotando il modello sul suo asse Y e quindi cancelliamo la tela 2D di qualsiasi contenuto precedente. Successivamente, impostiamo la dimensione del carattere e disegniamo del testo per ciascun asse. Il fillText ()
il metodo accetta tre parametri: il testo da disegnare, la coordinata x e la coordinata y.
La semplicità parla da sé. Questo potrebbe essere stato un po 'eccessivo per disegnare un testo semplice; avresti potuto facilmente scrivere il testo in una posizione o
elemento. Ma se stai facendo qualcosa come disegnare forme, sprite, una barra della salute, ecc, allora questa è probabilmente la tua migliore opzione.
Nell'ambito degli ultimi tre tutorial, abbiamo creato un motore 3D piuttosto carino, sebbene di base. Nonostante la sua natura primitiva, ti dà una solida base su cui lavorare. Andando avanti, suggerisco di guardare altri framework come three.js o glge per avere un'idea di ciò che è possibile. Inoltre, WebGL viene eseguito nel browser e puoi facilmente visualizzare l'origine di qualsiasi applicazione WebGL per saperne di più.
Spero ti sia piaciuta questa serie di tutorial e, come sempre, lascia i tuoi commenti e domande nella sezione commenti qui sotto.