ShaderToy, che abbiamo usato nel precedente tutorial di questa serie, è ottimo per test ed esperimenti rapidi, ma è piuttosto limitato. Non è possibile controllare quali dati vengono inviati allo shader, ad esempio, tra le altre cose. Avere il proprio ambiente in cui è possibile eseguire gli ombreggiatori significa che è possibile eseguire effetti di fantasia di ogni tipo e applicarli ai propri progetti!
Utilizzeremo Three.js come framework per eseguire gli shader nel browser. WebGL è l'API Javascript che ci permetterà di eseguire il rendering degli shader; Three.js rende questo lavoro più semplice.
Se non ti interessa JavaScript o la piattaforma web, non ti preoccupare: non ci concentreremo sulle specifiche del rendering web (anche se ti piacerebbe saperne di più sul framework, guarda questo tutorial). Configurare gli shader nel browser è il modo più rapido per iniziare, ma familiarizzare con questo processo ti consentirà di configurare e utilizzare facilmente gli shader su qualsiasi piattaforma tu voglia.
Questa sezione ti guiderà attraverso la configurazione degli shader localmente. Puoi seguire senza dover scaricare nulla con questo CodePen precostruito:
Puoi fork e modificare questo su CodePen.Three.js è un framework JavaScript che si prende cura di un gran numero di codice boilerplate per WebGL che ci servirà per rendere i nostri shader. Il modo più semplice per iniziare è utilizzare una versione ospitata su un CDN.
Ecco un file HTML che puoi scaricare con una scena Threejs di base.
Prova a salvare il file su disco, quindi aprilo nel browser web. Dovresti vedere uno schermo nero. Non è molto eccitante, quindi proviamo ad aggiungere un cubo, giusto per assicurarci che tutto funzioni.
Per creare un cubo, dobbiamo definire la sua geometria e il suo materiale, e quindi aggiungerlo alla scena. Aggiungi questo snippet di codice sotto dove dice Aggiungi il tuo codice qui
:
var geometry = new THREE.BoxGeometry (1, 1, 1); var material = new THREE.MeshBasicMaterial (color: 0x00ff00); // Lo rendiamo verde var cube = new THREE.Mesh (geometry, material); // aggiungilo alla schermata scene.add (cubo); cube.position.z = -3; // Sposta il cubo indietro in modo che possiamo vederlo
Non entreremo troppo nel dettaglio di questo codice, poiché siamo più interessati alla parte dello shader. Ma se tutto andava bene, dovresti vedere un cubo verde al centro dello schermo:
Mentre ci siamo, facciamo ruotare. Il rendere
la funzione esegue ogni frame. Possiamo accedere alla rotazione del cubo attraverso cube.rotation.x
(o .y
o .z
). Prova ad incrementarlo, in modo che la funzione di rendering assomigli a questa:
function render () cube.rotation.y + = 0.02; requestAnimationFrame (render); renderer.render (scene, camera);
Sfida: Puoi farlo ruotare lungo un asse diverso? Che ne dici di due assi contemporaneamente?
Ora hai tutto pronto, aggiungiamo alcuni shader!
A questo punto, possiamo iniziare a pensare al processo di implementazione degli shader. Probabilmente ti troverai in una situazione simile a prescindere dalla piattaforma su cui prevedi di utilizzare gli shader: hai tutto pronto e hai le cose disegnate sullo schermo, ora come accedi alla GPU??
Stiamo usando JavaScript per costruire questa scena. In altre situazioni potresti usare C ++, o Lua o qualsiasi altra lingua. Gli shader, a prescindere, sono scritti in uno speciale Linguaggio di ombreggiatura. Il linguaggio di ombreggiatura di OpenGL è GLSL(ApertoGL Shading Language). Dal momento che stiamo usando WebGL, che è basato su OpenGL, allora GLSL è ciò che usiamo.
Quindi come e dove scriviamo il nostro codice GLSL? La regola generale è che vuoi caricare il tuo codice GLSL come stringa
. È quindi possibile inviarlo per essere analizzato ed eseguito dalla GPU.
In JavaScript, puoi farlo semplicemente lanciando tutto il tuo codice inline all'interno di una variabile in questo modo:
var shaderCode = "Tutto il codice dello shader qui;"
Funziona, ma dal momento che JavaScript non ha un modo per creare facilmente stringhe multilinea, questo non è molto conveniente per noi. La maggior parte delle persone tende a scrivere il codice shader in un file di testo e a dargli un'estensione .GLSL
o .Frag
(Corto per frammento shader), quindi caricare il file in.
Questo è valido, ma scriveremo il nostro codice shader all'interno di un nuovo tag and load it into the JavaScript from there, so that we can keep everything in one file for the purpose of this tutorial.
Create a new taginside the HTML that looks like this:
Gli diamo l'ID di fragShader
è così che possiamo accedervi in seguito. Il tipo Shader-code
è in realtà un tipo di script fittizio che non esiste. (Potresti inserire qualsiasi nome lì e funzionerebbe). Il motivo per cui lo facciamo è che il codice non viene eseguito e non viene visualizzato nell'HTML.
Ora inseriamo uno shader di base che restituisce semplicemente il bianco.
(I componenti di vec4
in questo caso corrisponde al valore rgba, come spiegato nel precedente tutorial.)
Infine, dobbiamo caricare questo codice. Possiamo farlo con una semplice riga JavaScript che trova l'elemento HTML e tira il testo interno:
var shaderCode = document.getElementById ("fragShader"). innerHTML;
Questo dovrebbe andare sotto il tuo codice cubo.
Ricorda: solo ciò che viene caricato come stringa verrà analizzato come codice GLSL valido (ovvero, void main () ...
. Il resto è solo un file HTML HTML.)
Il metodo per applicare lo shader potrebbe essere diverso a seconda della piattaforma che stai utilizzando e del modo in cui si interfaccia con la GPU. Non è mai un passaggio complicato, tuttavia, e una ricerca Google superficiale ci mostra come creare un oggetto e applicare shaders con Three.js.
Dobbiamo creare un materiale speciale e dargli il nostro codice shader. Creeremo un piano come nostro oggetto shader (ma potremmo anche usare il cubo). Questo è tutto quello che dobbiamo fare:
// Crea un oggetto per applicare gli shader a var material = new THREE.ShaderMaterial (fragmentShader: shaderCode) var geometry = new THREE.PlaneGeometry (10, 10); var sprite = new THREE.Mesh (geometria, materiale); scene.add (sprite); sprite.position.z = -1; // Spostalo indietro in modo che possiamo vederlo
A questo punto dovresti vedere uno schermo bianco:
Puoi fork e modificare questo su CodePen.Se cambi il codice nello shader con qualsiasi altro colore e lo aggiorni, dovresti vedere il nuovo colore!
Sfida: Puoi impostare una parte dello schermo in rosso e un'altra in blu? (Se sei bloccato, il prossimo passo dovrebbe darti un suggerimento!)
A questo punto, possiamo fare tutto ciò che vogliamo con il nostro shader, ma non c'è molto da noi può fare. Abbiamo solo la posizione dei pixel incorporata gl_FragCoord
con cui lavorare, e se ricordi, questo non è normalizzato. Abbiamo bisogno di avere almeno le dimensioni dello schermo.
Per inviare dati al nostro shader, dobbiamo inviarlo come ciò che viene chiamato a uniforme variabile. Per fare questo, creiamo un oggetto chiamato uniformi
e aggiungere le nostre variabili ad esso. Ecco la sintassi per l'invio della risoluzione:
var uniforms = ; uniforms.resolution = tipo: 'v2', valore: new THREE.Vector2 (window.innerWidth, window.innerHeight);Ogni variabile uniforme deve avere a
genere
e a valore
. In questo caso, è un vettore bidimensionale con larghezza e altezza della finestra come sue coordinate. La tabella seguente (tratta dai documenti Three.js) mostra tutti i tipi di dati che puoi inviare e i loro identificatori:Stringa di tipo uniforme | Tipo GLSL | Tipo JavaScript |
---|---|---|
'i', '1i' | int | Numero |
'f', '1f' | galleggiante | Numero |
'V2' | vec2 | THREE.Vector2 |
'V3' | vec3 | THREE.Vector3 |
'C' | vec3 | a tre colori |
'V4' | vec4 | THREE.Vector4 |
'M3' | Mat3 | THREE.Matrix3 |
'M4' | Mat4 | THREE.Matrix4 |
'T' | sampler2D | THREE.Texture |
'T' | samplerCube | THREE.CubeTexture |
ShaderMaterial
istanziatore per includerlo, come questo:var material = new THREE.ShaderMaterial (uniforms: uniforms, fragmentShader: shaderCode)
Non abbiamo ancora finito! Ora il nostro shader riceve questa variabile, dobbiamo fare qualcosa con esso. Creiamo una sfumatura nello stesso modo in cui facevamo nel tutorial precedente: normalizzando la nostra coordinata e usandola per creare il nostro valore di colore.
Modifica il tuo codice shader in modo che assomigli a questo:
risoluzione uniforme vec2; // Le variabili uniformi devono essere dichiarate qui prima void main () // Ora possiamo normalizzare la nostra coordinata vec2 pos = gl_FragCoord.xy / resolution.xy; // E crea un gradiente! gl_FragColor = vec4 (1.0, pos.x, pos.y, 1.0);
E dovresti vedere un gradiente gradevole!
Puoi fork e modificare questo su CodePen.Se sei un po 'confuso su come siamo riusciti a creare un gradiente così gradevole con solo due linee di codice shader, dai un'occhiata alla prima parte di questa serie di tutorial per un giro di profondità della logica alla base di questo.
Sfida: Puoi dividere lo schermo in 4 sezioni uguali con colori diversi? Qualcosa come questo:
È bello poter inviare dati al nostro shader, ma cosa succede se è necessario aggiornarlo? Ad esempio, se apri l'esempio precedente in una nuova scheda, quindi ridimensiona la finestra, la sfumatura non si aggiorna, perché continua a utilizzare le dimensioni iniziali dello schermo.
Per aggiornare le tue variabili, di solito devi semplicemente inviare nuovamente la variabile uniforme e la aggiornerà. Con Three.js, tuttavia, abbiamo solo bisogno di aggiornare il uniformi
oggetto nel nostro rendere
funzione: non è necessario inviarlo di nuovo allo shader.
Ecco come appare la nostra funzione di rendering dopo aver apportato tale modifica:
function render () cube.rotation.y + = 0.02; uniforms.resolution.value.x = window.innerWidth; uniforms.resolution.value.y = window.innerHeight; requestAnimationFrame (render); renderer.render (scene, camera);
Se apri il nuovo CodePen e ridimensiona la finestra, vedrai che i colori cambiano (anche se le dimensioni del viewport iniziale rimangono invariate). È più facile vederlo osservando i colori in ogni angolo per verificare che non cambino.
Nota: L'invio di dati alla GPU come questo è generalmente costoso. L'invio di una manciata di variabili per frame va bene, ma il framerate può davvero rallentare se stai inviando centinaia per frame. Potrebbe non sembrare uno scenario realistico, ma se hai qualche centinaia di oggetti sullo schermo e tutti hanno bisogno di avere un'illuminazione applicata ad essi, ad esempio, tutti con proprietà diverse, allora le cose possono rapidamente andare fuori controllo. Impareremo di più sull'ottimizzazione dei nostri shader negli articoli futuri!
Sfida: Puoi cambiare i colori nel tempo? (Se sei bloccato, guarda come l'abbiamo fatto nella prima parte di questa serie di tutorial.)
Indipendentemente dal modo in cui carichi le tue trame o in quale formato, le invierai allo shader nello stesso modo attraverso le piattaforme, come variabili uniformi.
Una breve nota sul caricamento dei file in JavaScript: puoi caricare immagini da un URL esterno senza troppi problemi (che è quello che faremo qui) ma se vuoi caricare un'immagine in locale, ti imbatterai in problemi di autorizzazione, perché JavaScript non può, e non dovrebbe, normalmente accedere ai file sul tuo sistema. Il modo più semplice per aggirare questo è avviare un server Python locale, che è più semplice di quanto possa sembrare.
Three.js ci fornisce una comoda piccola funzione per caricare un'immagine come trama:
THREE.ImageUtils.crossOrigin = "; // Consente di caricare un'immagine esterna var tex = THREE.ImageUtils.loadTexture (" https://tutsplus.github.io/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg ");
La prima riga deve essere impostata una sola volta. Puoi inserire qualsiasi URL per un'immagine lì.
Successivamente, vogliamo aggiungere la nostra texture al uniformi
oggetto.
uniforms.texture = tipo: 't', valore: tex;
Infine, vogliamo dichiarare la nostra variabile uniforme nel nostro codice shader e disegnarla nello stesso modo in cui facevamo nel precedente tutorial, con Texture2D
funzione:
risoluzione uniforme vec2; texture sampler2D uniforme; void main () vec2 pos = gl_FragCoord.xy / resolution.xy; gl_FragColor = texture2D (texture, pos);
E dovresti vedere alcune gustose gelatine, allungate sul nostro schermo:
Puoi fork e modificare questo su CodePen.(Questa immagine è un'immagine di prova standard nel campo della computer grafica, presa dall'Information Processing Institute della University of Southern California (da qui le iniziali IPI), sembra appropriato usarla come immagine di prova mentre si imparano gli shader grafici! )
Sfida: Riesci a rendere la trama passata dal colore pieno alla scala di grigi nel tempo? (Di nuovo, se sei bloccato, lo abbiamo fatto nella prima parte di questa serie.)
Non c'è nulla di speciale nell'aereo che abbiamo creato. Potremmo aver applicato tutto questo sul nostro cubo. In effetti, possiamo semplicemente cambiare la linea della geometria piana:
var geometry = new THREE.PlaneGeometry (10, 10);
a:
var geometry = new THREE.BoxGeometry (1, 1, 1);
Voila, jelly bean su un cubo:
Puoi fork e modificare questo su CodePen.Ora potresti star pensando, "Aspetta, non sembra una proiezione corretta di una trama su un cubo!". E avresti ragione se guardiamo indietro al nostro shader, vedremo che tutto ciò che abbiamo fatto è stato dire "mappare tutti i pixel di questa immagine sullo schermo". Il fatto che sia su un cubo significa che i pixel esterni vengono scartati.
Se volessi applicarlo in modo tale da sembrare che sia disegnato fisicamente sul cubo, ciò implicherebbe molto reinventare un motore 3D (che suona un po 'sciocco considerando che stiamo già utilizzando un motore 3D e possiamo solo chiedere a disegnare la trama su ciascun lato individualmente) .Questa serie di tutorial è più sull'uso degli shader per fare cose che non potremmo ottenere altrimenti, quindi non ci addentreremo in dettagli del genere. (Udacity ha un ottimo corso sui fondamenti della grafica 3D, se sei desideroso di saperne di più!)
A questo punto, dovresti essere in grado di fare tutto ciò che abbiamo fatto in ShaderToy, eccetto che ora hai la libertà di usare qualsiasi texture vuoi su qualsiasi forma tu voglia e, si spera, su qualunque piattaforma tu scelga.
Con questa libertà, ora possiamo fare qualcosa come impostare un sistema di illuminazione, con ombre ed evidenziazioni dall'aspetto realistico. Questo è il tema su cui si concentrerà la prossima parte, oltre a suggerimenti e tecniche per l'ottimizzazione degli shader!