Introduzione agli mesh

Paul Lewis

Introduzione

In precedenza ti ho introdotto Three.js. Se non avete letto che potrebbe essere interessato, questa è la base su cui parlerò in questo articolo.

Quello che voglio fare è parlare di Shaker. WebGL è fantastico e, come ho detto, Three.js (e altre librerie) svolge un ottimo lavoro nell'astrarre i problemi per te. Tuttavia, ci saranno casi in cui vorrai ottenere un effetto specifico o che vorrai scavare un po' più a fondo per capire come sono apparse sullo schermo i contenuti straordinari. Inoltre, se anche tu somiglia a me, potresti passare dagli elementi di base dell'ultimo tutorial a quelli più difficili. Lavoreremo partendo dal fatto che utilizzi Three.js, dal momento che ci aiuta a far lavorare molto lo strumento per capire come funziona. Direi anche che all'inizio spiegherò il contesto per gli analisti e che nell'ultima parte di questo tutorial ci addentreremo in aree leggermente più avanzate. Il motivo è che gli Shader sono insoliti a prima vista e devi spiegare un po'.

1. I nostri due Shader

WebGL non offre l'utilizzo della pipeline fissa, che è un modo abbreviato di dire che non offre alcun metodo per eseguire il rendering dei tuoi elementi da subito. Tuttavia, offre la pipeline programmabile, che è più potente ma anche più difficile da capire e utilizzare. In breve, con la pipeline programmabile si intende che, in qualità di programmatore, ti assumi la responsabilità di visualizzare i vertici e così via sullo schermo. Gli Shader fanno parte di questa pipeline e ne esistono di due tipi:

  1. Shader Vertex
  2. Shaker di frammenti

Entrambe le cose, di certo sarai d'accordo, significano assolutamente niente di per sé. Quello che dovresti sapere è che entrambi vengono eseguiti interamente sulla GPU della tua scheda grafica. Ciò significa che vogliamo scaricare tutto ciò che possiamo per loro, lasciando la nostra CPU per svolgere le altre operazioni. Una GPU moderna è fortemente ottimizzata per le funzioni richieste dagli Shader, perciò è fantastico poterla usare.

2. Shader per Vertex

Assumi una forma primitiva standard, come una sfera. È composta da vertici, giusto? Un Vertex Shaper è assegnato a ciascuno di questi vertici a sua volta e può stravolgersi. Spetta allo strumento Vertex Shaper ciò che fa effettivamente con ognuno, ma ha una responsabilità: a un certo punto deve impostare qualcosa chiamato gl_Position, un vettore a virgola mobile 4D, ovvero la posizione finale del vertice sullo schermo. Di per sé questo è un processo piuttosto interessante, perché in realtà stiamo parlando di come posizionare una posizione 3D (un vertice con x,y,z) su uno schermo 2D, o proiettarla. Fortunatamente, se usiamo qualcosa come Three.js avremo modo di impostare gl_Position un modo semplice senza che le cose diventino troppo pesanti.

3. Shader di frammenti

Quindi abbiamo l'oggetto con i suoi vertici e li abbiamo proiettati sullo schermo 2D, ma per quanto riguarda i colori che utilizziamo? Cosa sono le texture e l'illuminazione? Questo è esattamente ciò a cui serve lo strumento di shadowing dei frammenti. In modo molto simile al Vertex Shader, anche lo Shader dei frammenti ha un solo job da fare: deve impostare o scartare la variabile gl_FragColor, un altro vettore float 4D, che rappresenta il colore finale del nostro frammento. Ma cos'è un frammento? Pensa a tre vertici che formano un triangolo. Ogni pixel all'interno del triangolo deve essere disegnato. Un frammento è i dati forniti da quei tre vertici allo scopo di tracciare ogni pixel nel triangolo. Per questo motivo i frammenti ricevono valori interpolati dai vertici costitutivi. Se un vertice fosse di colore rosso e il vicino è blu, i valori del colore si varierebbero dal rosso al viola fino al blu.

4. Variabili Shader

Quando parli delle variabili, puoi fare tre dichiarazioni: Uniforms, Attributes e Varyings. Quando ne ho sentito nominare, ero molto confuso perché non corrispondevano a nessun'altra cosa con cui avevo mai lavorato. Tuttavia, ecco come puoi immaginare di utilizzarli:

  1. Le Uniformi vengono inviate a entrambi i Vertex Shaker e i _____________tan_lover. Un buon esempio potrebbe essere la posizione di una luce.

  2. Gli attributi sono valori applicati ai singoli vertici. Gli attributi sono disponibili solo per Vertex Shader. Ogni vertice ha un colore diverso. Gli attributi hanno una relazione one-to-one con i vertici.

  3. Le variabili variazioni sono dichiarate nello sharting vertex che vogliamo condividere con lo strumento di shadowing dei frammenti. A questo scopo, ci assicuriamo di dichiarare una variabile variabile dello stesso tipo e dello stesso nome sia in Vertex Shader che in fragment Shader. Un uso classico di questa funzionalità è la normalità di un vertice, poiché può essere usata per i calcoli dell'illuminazione.

Più avanti li utilizzeremo tutti e tre i tipi per farti un'idea di come vengono applicati.

Dopo aver parlato dei vertex shabbyer e dei frammentatori e dei tipi di variabili da gestire, vale la pena esaminare gli ombreggiatori più semplici che possiamo creare.

5. Il mondo Bonjourno

Ecco, quindi, l'esempio di Hello World di Vertex Shader:

/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}   

ed ecco lo stesso per lo strumento di shadowing dei frammenti:

/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}

Ma non è troppo complicato, vero?

In Vertex Shadr ci manda un paio di uniformi da Three.js. Queste due uniformi sono matrici 4D, chiamate Matrice di visualizzazione modello e Matrice di proiezione. Non hai disperatamente bisogno di sapere esattamente come funzionano, anche se è sempre meglio capire come le cose fanno quello che fanno, se puoi. In breve, rappresentano il modo in cui la posizione 3D del vertice viene effettivamente proiettata nella posizione 2D finale sullo schermo.

Li ho omessi dallo snippet riportato sopra perché Three.js li aggiunge all'inizio del codice dello strumento di Shader, quindi non dovrete preoccuparvi di farlo. In realtà, aggiunge molto di più, ad esempio dati chiari, colori dei vertici e valori normali dei vertici. Se lo facessi senza Three.js, dovrai creare e impostare tutte le uniformi e gli attributi personalmente. Storia vera.

6. Utilizzo di un materiale meshShader

Ok, quindi abbiamo impostato uno Shader, ma come lo usiamo con Three.js? Ho scoperto che è terribilmente facile. È piuttosto simile a questo:

/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader:   $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});

Da qui, Three.js compilerà ed eseguirà i tuoishar attaccati alla rete mesh a cui assegni il materiale. Non sarà molto più facile di così. Probabilmente sì, ma stiamo parlando dell'esecuzione 3D nel browser, quindi immagino che ci sia una certa complessità.

Possiamo aggiungere altre due proprietà al nostro MeshShaderMaterial: uniformi e attributi. Possono entrambi prendere vettori, numeri interi o numeri in virgola mobile, ma, come ho detto prima, le uniformi sono le stesse per l'intero frame, ovvero per tutti i vertici, quindi tendono a essere valori singoli. Gli attributi, invece, sono variabili per vertice, pertanto devono essere un array. Deve esserci una relazione uno a uno tra il numero di valori nell'array di attributi e il numero di vertici nel mesh.

7. Passaggi successivi

Ora passiamo un po' di tempo ad aggiungere un loop di animazione, gli attributi di vertice e un'uniforme. Aggiungeremo anche una variabile variabile in modo che lo Shaker di vertice possa inviare alcuni dati allo strumento di shadowing dei frammenti. Il risultato è che la nostra sfera rosa sembrerà illuminata dall'alto e di lato e pulserà. Può sembrare inopportuno, ma speriamo che ti consenta di comprendere bene i tre tipi di variabili e la loro relazione tra loro e con la geometria di base.

8. Una luce finta

Aggiorniamo la colorazione in modo che non sia un oggetto di colore piatto. Potremmo dare un'occhiata a come Three.js gestisce l'illuminazione, ma come sicuramente saprai, è più complesso di quello di cui abbiamo bisogno in questo momento. Dovresti dare un'occhiata ai fantastici smoother che fanno parte di Three.js e anche quelli del recente incredibile progetto WebGL di Chris Milk e Google, Roma. Torniamo ai nostri passatempi. Aggiorneremo il nostro Vertex Shader per fornire ogni vertice normale allo Shader Fragment. Per farlo, utilizziamo i seguenti strumenti:

// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;

void main() {

// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}

e in Fragment Shader imposteremo lo stesso nome di variabile e utilizzeremo il prodotto scalare del vertice normale con un vettore che rappresenta una luce che illumina la sfera dall'alto e a destra. Il risultato finale è un effetto simile a una luce direzionale in un pacchetto 3D.

// same name and type as VS
varying vec3 vNormal;

void main() {

// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
    
// ensure it's normalized
light = normalize(light);

// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));

// feed into our frag colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

Il motivo per cui funziona il prodotto scalare è che, dati due vettori, esce con un numero che indica quanto sono "simili" i due vettori. Con i vettori normalizzati, se puntano esattamente nella stessa direzione, il valore è 1. Se puntano in direzioni opposte, il valore è -1. Quello che facciamo è prendere questo numero e applicarlo all'illuminazione. Quindi un vertice in alto a destra avrà un valore vicino o uguale a 1, ovvero completamente illuminato, mentre un vertice sul lato avrà un valore vicino a 0 e arrotondato alla parte posteriore sarebbe -1. Blocchiamo il valore a 0 per qualsiasi cosa negativa, ma quando inserisci i numeri si ottiene l'illuminazione di base che vediamo.

Passaggi successivi Sarebbe bello provare a modificare la posizione del vertice.

9. Attributi

Quello che vorrei fare ora è collegare un numero casuale a ogni vertice tramite un attributo. Utilizzeremo questo numero per spingere il vertice in uscita secondo il normale. Il risultato finale sarà una sorta di strana palla a punta che cambierà ogni volta che aggiorni la pagina. Non sarà ancora animata (cosa succede dopo), ma alcuni aggiornamenti di pagina ti mostreranno che è stata casuale.

Iniziamo aggiungendo l'attributo a Vertex Shader:

attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position + 
                    normal * 
                    vec3(displacement);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

Che aspetto ha?

Non molto diverso, in realtà! Questo perché l'attributo non è stato configurato in MeshShaderMaterial, quindi di fatto lo shader utilizza un valore pari a zero. In questo momento è un po' come un segnaposto. Tra un secondo aggiungeremo l'attributo MeshShaderMaterial in JavaScript e Three.js le collegherà automaticamente.

È importante anche assegnare la posizione aggiornata a una nuova variabile vec3 perché l'attributo originale, come tutti gli attributi, è di sola lettura.

10. Aggiornamento di MeshShaderMaterial

Iniziamo subito ad aggiornare il nostro MeshShaderMaterial con l'attributo necessario per lo spostamento. Ricorda: gli attributi sono valori per vertice, quindi abbiamo bisogno di un valore per vertice nella nostra sfera. come illustrato di seguito:

var attributes = {
displacement: {
    type: 'f', // a float
    value: [] // an empty array
}
};

// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}

Ora vediamo una sfera angolata, ma l'aspetto fantastico è che tutto lo spostamento avviene sulla GPU.

11. Animazione di That Sucker

Dovremmo assolutamente animare. Come procediamo? Ci sono due cose che dobbiamo mettere in atto:

  1. Un'uniforme per animare lo spostamento da applicare a ogni frame. Possiamo usare il seno o il coseno, poiché vanno da -1 a 1
  2. Un loop di animazione in JS

Aggiungeremo l'uniforme sia a MeshShaderMaterial sia a Vertex Shader. Iniziamo con Vertex Shader:

uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position + 
                    normal * 
                    vec3(displacement *
                        amplitude);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

Successivamente, aggiorneremo il materiale MeshShader:

// add a uniform for the amplitude
var uniforms = {
amplitude: {
    type: 'f', // a float
    value: 0
}
};

// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms:       uniforms,
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

Per il momento abbiamo finito. Ma sembrerebbe di aver fatto un passo indietro. Ciò è dovuto principalmente al fatto che il valore dell'ampiezza è 0 e, poiché lo moltiplichiamo per lo spostamento, non notiamo alcun cambiamento. Inoltre, non abbiamo configurato il loop dell'animazione, quindi non vediamo mai il cambiamento da 0 a nessun altro.

Nel nostro codice JavaScript, ora dobbiamo racchiudere la chiamata di rendering in una funzione, quindi usare requestAnimationFrame per chiamarla. Qui dobbiamo anche aggiornare il valore dell'uniforme.

var frame = 0;
function update() {

// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;

renderer.render(scene, camera);

// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);

12. Conclusione

e il gioco è fatto. Ora puoi vedere che si anima in modo strano (e leggermente intrigante).

C'è molto altro che possiamo trattare sugli ombreggiatori, ma spero che questa introduzione ti sia stata utile. Ora dovresti essere in grado di capire gli ombrelli quando li vedi e avere la sicurezza di creare in modo personale alcuni incredibili.