Creazione di shader con Babylon.js

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Gli shader sono un concetto chiave se vuoi liberare la potenza pura della tua GPU. Ti aiuterò a capire come funzionano e persino a sperimentare il loro potere interiore in un modo semplice, grazie a Babylon.js . Prima di sperimentare, dobbiamo vedere come funzionano le cose internamente. Quando hai a che fare con il 3D con accelerazione hardware, dovrai fare i conti con due CPU: la CPU principale e la GPU. La GPU è una specie di CPU estremamente specializzata.

Gli shader sono un concetto chiave se vuoi liberare la potenza pura della tua GPU. Ti aiuterò a capire come funzionano e persino a sperimentare il loro potere interiore in un modo semplice, grazie a Babylon.js.

Come funziona?

Prima di sperimentare, dobbiamo vedere come funzionano le cose internamente.

Quando hai a che fare con il 3D con accelerazione hardware, dovrai fare i conti con due CPU: la CPU principale e la GPU. La GPU è una specie di CPU estremamente specializzata.

Ulteriori letture su SmashingMag:

  • Costruire un gioco WebGL multipiattaforma con Babylon.js
  • Utilizzo dell'API Gamepad nei giochi Web
  • Introduzione alla modellazione poligonale e Three.js
  • Come creare una drum machine a 8 bit reattiva
Altro dopo il salto! Continua a leggere sotto ↓

La GPU è una macchina a stati configurata utilizzando la CPU. Ad esempio, la CPU configurerà la GPU per il rendering di linee anziché triangoli; definirà se la trasparenza è attiva; e così via.

Una volta impostati tutti gli stati, la CPU può definire cosa renderizzare: la geometria.

La geometria è composta da:

  • un elenco di punti che sono chiamati vertici e memorizzati in un array chiamato vertex buffer,
  • un elenco di indici che definiscono le facce (o triangoli) archiviati in un array denominato index buffer.

Il passaggio finale per la CPU è definire come eseguire il rendering della geometria; per questo compito, la CPU definirà gli shader nella GPU. Gli shader sono pezzi di codice che la GPU eseguirà per ciascuno dei vertici e pixel di cui deve eseguire il rendering. (Un vertice - o vertici quando ce ne sono molti - è un "punto" in 3D).

Esistono due tipi di shader: vertex shader e pixel (o frammento).

Pipeline grafica

Prima di approfondire gli shader, facciamo un passo indietro. Per eseguire il rendering dei pixel, la GPU prenderà la geometria definita dalla CPU e farà quanto segue:

  • Usando il buffer dell'indice, vengono raccolti tre vertici per definire un triangolo.
  • Il buffer dell'indice contiene un elenco di indici di vertice. Ciò significa che ogni voce nel buffer dell'indice è il numero di un vertice nel buffer dei vertici.
  • Questo è davvero utile per evitare la duplicazione dei vertici.

Ad esempio, il seguente buffer di indice è un elenco di due facce: [1 2 3 1 3 4]. La prima faccia contiene il vertice 1, il vertice 2 e il vertice 3. La seconda faccia contiene il vertice 1, il vertice 3 e il vertice 4. Quindi, ci sono quattro vertici in questa geometria:

(Visualizza versione grande)

Il vertex shader viene applicato a ciascun vertice del triangolo. L'obiettivo principale del vertex shader è produrre un pixel per ogni vertice (la proiezione sullo schermo 2D del vertice 3D):

(Visualizza versione grande)

Utilizzando questi tre pixel (che definiscono un triangolo 2D sullo schermo), la GPU interpolerà tutti i valori associati al pixel (almeno le loro posizioni) e il pixel shader verrà applicato a ogni pixel incluso nel triangolo 2D in modo da genera un colore per ogni pixel:

(Visualizza versione grande)

Questo processo viene eseguito per ogni volto definito dal buffer dell'indice.

Ovviamente, data la sua natura parallela, la GPU è in grado di elaborare questo passaggio per molti volti contemporaneamente e ottenere prestazioni davvero buone.

GLSL

Abbiamo appena visto che per renderizzare i triangoli, la GPU ha bisogno di due shader: il vertex shader e il pixel shader. Questi shader sono scritti in un linguaggio chiamato Graphics Library Shader Language (GLSL). Sembra C.

Ecco un esempio di un comune vertex shader:

 precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

Vertex Shader Struttura

Un vertex shader contiene quanto segue:

  • Attributi . Un attributo definisce una parte di un vertice. Per impostazione predefinita, un vertice dovrebbe contenere almeno una posizione (a vector3:x, y, z ). Tuttavia, come sviluppatore, puoi decidere di aggiungere ulteriori informazioni. Ad esempio, nel precedente shader, c'è un vector2 chiamato uv (cioè coordinate di texture che consentono di applicare una texture 2D a un oggetto 3D).
  • Uniformi . Un'uniforme è una variabile utilizzata dallo shader e definita dalla CPU. L'unica uniforme che abbiamo qui è una matrice usata per proiettare la posizione del vertice (x, y, z) sullo schermo (x, y).
  • Variante . Le variabili variabili sono valori creati dal vertex shader e trasmessi al pixel shader. Qui, il vertex shader trasmetterà un vUV (una semplice copia di uv ) al pixel shader. Ciò significa che qui viene definito un pixel con una posizione e coordinate della trama. Questi valori verranno interpolati dalla GPU e utilizzati dal pixel shader.
  • Principale . La funzione denominata main è il codice eseguito dalla GPU per ogni vertice e deve almeno produrre un valore per gl_position (la posizione del vertice corrente sullo schermo).

Possiamo vedere nel nostro esempio che il vertex shader è piuttosto semplice. Genera una variabile di sistema (che inizia con gl_ ) denominata gl_position per definire la posizione del pixel associato e imposta una variabile variabile chiamata vUV .

Il voodoo dietro le matrici

Il problema del nostro shader è che abbiamo una matrice denominata worldViewProjection e usiamo questa matrice per proiettare la posizione del vertice sulla variabile gl_position . È fantastico, ma come otteniamo il valore di questa matrice? È un'uniforme, quindi dobbiamo definirla lato CPU (usando JavaScript).

Questa è una delle parti complesse del fare 3D. Devi capire la matematica complessa (o dovrai usare un motore 3D come Babylon.js, che vedremo più avanti).

La matrice worldViewProjection è la combinazione di tre diverse matrici:

(Visualizza versione grande)

L'utilizzo della matrice risultante ci consente di trasformare i vertici 3D in pixel 2D, tenendo conto del punto di vista e di tutto ciò che riguarda la posizione, la scala e la rotazione dell'oggetto corrente.

Questa è la tua responsabilità come sviluppatore 3D: creare e mantenere aggiornata questa matrice.

Torniamo agli Shader

Una volta eseguito il vertex shader su ogni vertice (tre volte, quindi), avremo tre pixel con la corretta gl_position e un valore vUV . La GPU interpolerà questi valori su ogni pixel contenuto nel triangolo prodotto con questi pixel.

Quindi, per ogni pixel, eseguirà il pixel shader:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }

Struttura dello Shader Pixel (o Frammento).

La struttura di un pixel shader è simile a quella di un vertex shader:

  • Variante . Le variabili variabili sono valori creati dal vertex shader e trasmessi al pixel shader. Qui, il pixel shader riceverà un valore vUV dal vertex shader.
  • Uniformi . Un'uniforme è una variabile utilizzata dallo shader e definita dalla CPU. L'unica uniforme che abbiamo qui è un campionatore, che è uno strumento utilizzato per leggere i colori delle texture.
  • Principale . La funzione denominata main è il codice eseguito dalla GPU per ogni pixel e che deve almeno produrre un valore per gl_FragColor (ovvero il colore del pixel corrente).

Questo pixel shader è abbastanza semplice: legge il colore dalla trama usando le coordinate della trama dal vertex shader (che, a sua volta, lo ottiene dal vertice).

Il problema è che quando vengono sviluppati gli shader, sei solo a metà strada, perché devi quindi fare i conti con molto codice WebGL. In effetti, WebGL è davvero potente ma anche di basso livello e devi fare tutto da solo, dalla creazione dei buffer alla definizione delle strutture dei vertici. Devi anche fare tutta la matematica, impostare tutti gli stati, gestire il caricamento delle texture e così via.

Troppo difficile? BABYLON.ShaderMaterial per il salvataggio

So cosa stai pensando: "Gli shader sono davvero fantastici, ma non voglio preoccuparmi dell'impianto idraulico interno di WebGL o anche della matematica".

E hai ragione! Questa è una domanda perfettamente legittima, ed è proprio per questo che ho creato Babylon.js!

Per utilizzare Babylon.js, hai prima bisogno di una semplice pagina web:

 <!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Normal varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; } </script> <script type="application/fragmentShader"> precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); } </script> <script src="index.js"></script> <style> html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; margin: 0px; overflow: hidden; } #renderCanvas { width: 100%; height: 100%; touch-action: none; -ms-touch-action: none; } </style> </head> <body> <canvas></canvas> </body> </html>

Noterai che gli shader sono definiti dai tag <script> . Con Babylon.js puoi anche definirli in file separati (file .fx ).

  • Fonte Babylon.js
  • Archivio GitHub

Infine, il codice JavaScript principale è questo:

 "use strict"; document.addEventListener("DOMContentLoaded", startGame, false); function startGame() { if (BABYLON.Engine.isSupported()) { var canvas = document.getElementById("renderCanvas"); var engine = new BABYLON.Engine(canvas, false); var scene = new BABYLON.Scene(engine); var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas); // Creating sphere var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene); var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] }); amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene)); sphere.material = amigaMaterial; engine.runRenderLoop(function () { sphere.rotation.y += 0.05; scene.render(); }); } };

Puoi vedere che uso BABYLON.ShaderMaterial per sbarazzarmi dell'onere di compilare, collegare e gestire gli shader.

Quando crei BABYLON.ShaderMaterial , devi specificare l'elemento DOM utilizzato per memorizzare gli shader o il nome di base dei file in cui si trovano gli shader. Se scegli di utilizzare i file, devi creare un file per ogni shader e utilizzare il seguente modello: basename.vertex.fx e basename.fragment.fx . Quindi, dovrai creare il materiale in questo modo:

 var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });

Devi anche specificare i nomi degli attributi e delle uniformi che utilizzi.

Quindi, puoi impostare direttamente i valori delle tue uniformi e campionatori usando setTexture , setFloat , setFloats , setColor3 , setColor4 , setVector2 , setVector3 , setVector4 , setMatrix functions.

Abbastanza semplice, vero?

E ti ricordi la precedente matrice worldViewProjection , utilizzando Babylon.js e BABYLON.ShaderMaterial . Semplicemente non devi preoccuparti di questo! BABYLON.ShaderMaterial lo calcolerà automaticamente per te perché lo dichiarerai nell'elenco delle divise.

BABYLON.ShaderMaterial può anche gestire per te le seguenti matrici:

  • world ,
  • view ,
  • projection ,
  • worldView ,
  • worldViewProjection .

Non c'è più bisogno di matematica. Ad esempio, ogni volta che esegui sphere.rotation.y += 0.05 , la matrice world della sfera verrà generata per te e trasmessa alla GPU.

Guarda tu stesso il risultato dal vivo.

Crea il tuo shader (CYOS)

Ora, ingrandiamo e creiamo una pagina in cui puoi creare dinamicamente i tuoi shader e vedere immediatamente il risultato. Questa pagina utilizzerà lo stesso codice di cui abbiamo discusso in precedenza e utilizzerà l'oggetto BABYLON.ShaderMaterial per compilare ed eseguire gli shader che creerai.

Ho usato l'editor di codice ACE per Create Your Own Shader (CYOS). È un incredibile editor di codice, con l'evidenziazione della sintassi. Sentiti libero di dargli un'occhiata.

Usando la prima casella combinata, sarai in grado di selezionare gli shader predefiniti. Vedremo ognuno di loro subito dopo.

Puoi anche cambiare la mesh (cioè l'oggetto 3D) usata per visualizzare in anteprima i tuoi shader usando la seconda casella combinata.

Il pulsante di compilazione viene utilizzato per creare un nuovo BABYLON.ShaderMaterial dai tuoi shader. Il codice utilizzato da questo pulsante è il seguente:

 // Compile shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "normal", "uv"], uniforms: ["world", "worldView", "worldViewProjection"] }); var refTexture = new BABYLON.Texture("ref.jpg", scene); refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; var amigaTexture = new BABYLON.Texture("amiga.jpg", scene); shaderMaterial.setTexture("textureSampler", amigaTexture); shaderMaterial.setTexture("refSampler", refTexture); shaderMaterial.setFloat("time", 0); shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero()); shaderMaterial.backFaceCulling = false; mesh.material = shaderMaterial;

Incredibilmente semplice, vero? Il materiale è pronto per inviarti tre matrici precalcolate ( world , worldView e worldViewProjection ). I vertici arriveranno con le coordinate di posizione, normali e texture. Anche due trame sono già caricate per te:

amiga.jpg (Visualizza versione grande)
ref.jpg (Visualizza versione grande)

Infine, il renderLoop è dove aggiorno due comode uniformi:

  • Uno si chiama time e riceve delle animazioni divertenti.
  • L'altro è chiamato cameraPosition , che ottiene la posizione della telecamera nei tuoi shader (utile per le equazioni di illuminazione).

 engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });

Shader di base

Iniziamo con il primissimo shader definito in CYOS: lo shader di base.

Conosciamo già questo shader. Calcola gl_position e usa le coordinate della trama per recuperare un colore per ogni pixel.

Per calcolare la posizione dei pixel, abbiamo solo bisogno della matrice worldViewProjection e della posizione del vertice:

 precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

Le coordinate della trama ( uv ) vengono trasmesse non modificate al pixel shader.

Si noti che è necessario aggiungere il precision mediump float sulla prima riga sia per il vertex che per il pixel shader perché Chrome lo richiede. Specifica che, per prestazioni migliori, non utilizziamo valori mobili di precisione completa.

Il pixel shader è ancora più semplice, perché dobbiamo solo usare le coordinate della trama e recuperare un colore della trama:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }

In precedenza abbiamo visto che l'uniforme di textureSampler è riempita con la texture amiga . Quindi, il risultato è il seguente:

(Visualizza versione grande)

Shader in bianco e nero

Continuiamo con un nuovo shader: lo shader bianco e nero. L'obiettivo di questo shader è utilizzare il precedente ma con una modalità di rendering solo in bianco e nero.

Per fare ciò, possiamo mantenere lo stesso vertex shader. Il pixel shader verrà leggermente modificato.

La prima opzione che abbiamo è prendere un solo componente, come quello verde:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }

Come puoi vedere, invece di usare .rgb (questa operazione è chiamata swizzle), abbiamo usato .ggg .

Ma se vogliamo un effetto bianco e nero davvero accurato, calcolare la luminanza (che tiene conto di tutti i componenti) sarebbe meglio:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11)); gl_FragColor = vec4(luminance, luminance, luminance, 1.0); }

L'operazione dot (o prodotto dot ) viene calcolata in questo modo: result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z .

Quindi, nel nostro caso, luminance = r * 0.3 + g * 0.59 + b * 0.11 . (Questi valori si basano sul fatto che l'occhio umano è più sensibile al verde.)

Suona bene, vero?

(Visualizza versione grande)

Shader per l'ombreggiatura cellulare

Passiamo a uno shader più complesso: lo shader cell-shading.

Questo ci richiederà di ottenere la normale del vertice e la posizione del vertice nel pixel shader. Quindi, il vertex shader sarà simile a questo:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 world; uniform mat4 worldViewProjection; // Varying varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vPositionW = vec3(world * vec4(position, 1.0)); vNormalW = normalize(vec3(world * vec4(normal, 0.0))); vUV = uv; }

Tieni presente che utilizziamo anche la matrice del mondo perché posizione e normale vengono memorizzate senza alcuna trasformazione e dobbiamo applicare la matrice del mondo per tenere conto della rotazione dell'oggetto.

Il pixel shader è il seguente:

 precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { float ToonThresholds[4]; ToonThresholds[0] = 0.95; ToonThresholds[1] = 0.5; ToonThresholds[2] = 0.2; ToonThresholds[3] = 0.03; float ToonBrightnessLevels[5]; ToonBrightnessLevels[0] = 1.0; ToonBrightnessLevels[1] = 0.8; ToonBrightnessLevels[2] = 0.6; ToonBrightnessLevels[3] = 0.35; ToonBrightnessLevels[4] = 0.2; vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); vec3 color = texture2D(textureSampler, vUV).rgb; if (ndl > ToonThresholds[0]) { color *= ToonBrightnessLevels[0]; } else if (ndl > ToonThresholds[1]) { color *= ToonBrightnessLevels[1]; } else if (ndl > ToonThresholds[2]) { color *= ToonBrightnessLevels[2]; } else if (ndl > ToonThresholds[3]) { color *= ToonBrightnessLevels[3]; } else { color *= ToonBrightnessLevels[4]; } gl_FragColor = vec4(color, 1.); }

L'obiettivo di questo shader è simulare la luce e invece di calcolare l'ombreggiatura uniforme, applicheremo la luce in base a soglie di luminosità specifiche. Ad esempio, se l'intensità della luce è compresa tra 1 (massimo) e 0,95, il colore dell'oggetto (prelevato dalla trama) verrebbe applicato direttamente. Se l'intensità è compresa tra 0,95 e 0,5, il colore sarebbe attenuato di un fattore 0,8. E così via.

Ci sono principalmente quattro passaggi in questo shader.

Innanzitutto, dichiariamo le soglie e le costanti di livello.

Quindi, calcoliamo l'illuminazione usando l'equazione di Phong (considereremo che la luce non si muove):

 vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));

L'intensità della luce per pixel dipende dall'angolo tra la direzione normale e quella della luce.

Quindi, otteniamo il colore della trama per il pixel.

Infine, controlliamo la soglia e applichiamo il livello al colore.

Il risultato sembra un oggetto cartone animato:

(Visualizza versione grande)

Phong Shader

Abbiamo usato una parte dell'equazione di Phong nello shader precedente. Usiamolo completamente ora.

Il vertex shader è chiaramente semplice qui perché tutto verrà fatto nel pixel shader:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vUV = uv; vPosition = position; vNormal = normal; }

Secondo l'equazione, dobbiamo calcolare le parti "diffusa" e "speculare" usando la direzione della luce e la normale al vertice:

 precision highp float; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; // Uniforms uniform mat4 world; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 vLightPosition = vec3(0, 20, 10); // World values vec3 vPositionW = vec3(world * vec4(vPosition, 1.0)); vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0))); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); vec3 color = texture2D(textureSampler, vUV).rgb; // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); // Specular vec3 angleW = normalize(viewDirectionW + lightVectorW); float specComp = max(0., dot(vNormalW, angleW)); specComp = pow(specComp, max(1., 64.)) * 2.; gl_FragColor = vec4(color * ndl + vec3(specComp), 1.); }

Abbiamo già utilizzato la parte diffusa nello shader precedente, quindi qui non ci resta che aggiungere la parte speculare. Puoi trovare maggiori informazioni sull'ombreggiatura Phong su Wikipedia.

Il risultato della nostra sfera:

(Visualizza versione grande)

Elimina lo shader

Per lo shader di scarto, vorrei introdurre un nuovo concetto: la parola chiave di discard .

Questo shader elimina ogni pixel non rosso e crea l'illusione di un oggetto scavato.

Il vertex shader è lo stesso utilizzato dallo shader di base:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

Il pixel shader su un lato dovrà testare il colore e utilizzare lo scarto quando, ad esempio, la componente verde è troppo alta:

 precision highp float; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { vec3 color = texture2D(textureSampler, vUV).rgb; if (color.g > 0.5) { discard; } gl_FragColor = vec4(color, 1.); }

Il risultato è un po' divertente:

(Visualizza versione grande)

Ombreggiatore d'onda

Abbiamo giocato molto con i pixel shader, ma voglio anche farti sapere che possiamo fare molte cose con i vertex shader.

Per lo shader wave, riutilizzeremo lo shader pixel Phong.

Il vertex shader utilizzerà il time denominato uniforme per ottenere alcuni valori animati. Usando questa uniforme, lo shader genererà un'onda con le posizioni dei vertici:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; uniform float time; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec3 v = position; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }

Un seno viene applicato a position.y e il risultato è il seguente:

(Visualizza versione grande)

Mappatura dell'ambiente sferico

Questo è stato in gran parte ispirato dall'articolo "Creating a Spherical Reflection/Environment Mapping Shader". Ti farò leggere quell'eccellente articolo e giocare con lo shader associato.

(Visualizza versione grande)

Shader di Fresnel

Vorrei concludere questo articolo con il mio preferito: lo shader di Fresnel.

Questo shader viene utilizzato per applicare un'intensità diversa in base all'angolo tra la direzione della vista e la normale del vertice.

Il vertex shader è lo stesso utilizzato dal cell-shading shader e possiamo facilmente calcolare il termine di Fresnel nel nostro pixel shader (perché abbiamo la posizione normale e della telecamera, che può essere utilizzata per valutare la direzione della vista):

 precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 color = vec3(1., 1., 1.); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Fresnel float fresnelTerm = dot(viewDirectionW, vNormalW); fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.); gl_FragColor = vec4(color * fresnelTerm, 1.); } 
(Visualizza versione grande)

Il tuo Shader?

Ora sei più preparato a creare il tuo shader. Sentiti libero di pubblicare sul forum Babylon.js per condividere i tuoi esperimenti!

Se vuoi andare oltre, ecco alcuni link utili:

  • Babylon.js, sito ufficiale
  • Babylon.js, repository GitHub
  • Forum Babylon.js, sviluppatori di giochi HTML5
  • Crea il tuo shader (CYOS), Babylon.js
  • Linguaggio di ombreggiatura OpenGL,” Wikipedia
  • Linguaggio di ombreggiatura OpenGL, documentazione