Come menzionato nel capitolo Hello Triangle, gli shader sono piccoli programmi che poggiano sulla GPU. Questi programmi vengono eseguiti per ogni sezione specifica della pipeline grafica. In un senso di base, gli shader non sono altro che programmi che trasformano gli input in output. Gli shader sono anche programmi molto isolati in quanto non sono autorizzati a comunicare tra loro; l’unica comunicazione che hanno è tramite i loro input e output.
Nel capitolo precedente abbiamo brevemente toccato la superficie degli shader e come usarli correttamente. Ora spiegheremo gli shader, e in particolare il linguaggio di ombreggiatura OpenGL, in modo più generale.
GLSL
Gli shader sono scritti nel linguaggio simile a C GLSL. GLSL è su misura per l’uso con la grafica e contiene funzioni utili specificamente mirati alla manipolazione vettoriale e matrice.
Gli shader iniziano sempre con una dichiarazione di versione, seguita da un elenco di variabili di input e output, uniformi e la sua funzione principale. Il punto di ingresso di ogni shader è nella sua funzione principale in cui elaboriamo qualsiasi variabile di input e produciamo i risultati nelle sue variabili di output. Non preoccuparti se non sai cosa sono le uniformi, le raggiungeremo a breve.
Uno shader ha tipicamente la seguente struttura:
#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}
Quando parliamo specificamente del vertex shader, ogni variabile di input è anche nota come attributo vertex. C’è un numero massimo di attributi dei vertici che possiamo dichiarare limitati dall’hardware. OpenGL garantisce che ci sono sempre almeno 16 attributi di vertice a 4 componenti disponibili, ma alcuni hardware possono consentire di più che è possibile recuperare interrogando GL_MAX_VERTEX_ATTRIBS:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
Questo restituisce spesso il minimo di16
che dovrebbe essere più che sufficiente per la maggior parte degli scopi.
Tipi
GLSL ha, come qualsiasi altro linguaggio di programmazione, tipi di dati per specificare quale tipo di variabile vogliamo lavorare con. GLSL ha la maggior parte dei tipi di base predefiniti che conosciamo da linguaggi come C: int
float
double
uint
e bool
. GLSL dispone anche di due tipi di contenitore che useremo molto, vale a dire vectors
ematrices
. Discuteremo le matrici in un capitolo successivo.
Vettori
Un vettore in GLSL è un contenitore di 1,2,3 o 4 componenti per uno qualsiasi dei tipi di base appena menzionati. Possono assumere la seguente forma (n
rappresenta il numero di componenti):
-
vecn
: il vettore predefinito din
galleggia. -
bvecn
: un vettore din
booleani. -
ivecn
: un vettore din
interi. -
uvecn
: un vettore din
interi senza segno. -
dvecn
: un vettore din
componenti doppi.
La maggior parte del tempo useremo il vecn
di base poiché i float sono sufficienti per la maggior parte dei nostri scopi.
È possibile accedere ai componenti di un vettore tramite vec.x
dovex
è il primo componente del vettore. È possibile utilizzare .x
.y
.z
e .w
per accedere rispettivamente al primo, secondo, terzo e quarto componente. GLSL consente inoltre di utilizzare rgba
per i colori ostpq
per le coordinate delle texture, accedendo agli stessi componenti.
Il tipo di dati vettoriale consente una selezione di componenti interessante e flessibile chiamata swizzling. Swizzling ci permette di usare la sintassi come questa:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
È possibile utilizzare qualsiasi combinazione di un massimo di 4 lettere per creare un nuovo vettore (dello stesso tipo), come lungo come l’originale vettore di tali componenti; non è consentito l’accesso .z
componente di un vec2
per esempio. Possiamo anche passare vettori come argomenti a diverse chiamate di costruttori vettoriali, riducendo il numero di argomenti richiesti:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
I vettori sono quindi un tipo di dati flessibile che possiamo usare per tutti i tipi di input e output. In tutto il libro vedrai molti esempi di come possiamo gestire in modo creativo i vettori.
Ins and out
Gli shader sono piccoli programmi belli da soli, ma fanno parte di un tutto e per questo motivo vogliamo avere input e output sui singoli shader in modo da poter spostare le cose. GLSL ha definito le parole chiavein
eout
appositamente per questo scopo. Ogni shader può specificare input e output utilizzando tali parole chiave e ovunque una variabile di output corrisponda a una variabile di input della fase successiva dello shader. Lo shader vertice e frammento differiscono un po ‘ però.
Il vertex shader dovrebbe ricevere qualche forma di input altrimenti sarebbe piuttosto inefficace. Il vertex shader differisce nel suo input, in quanto riceve il suo input direttamente dai dati del vertice. Per definire come sono organizzati i dati dei vertici specifichiamo le variabili di input con i metadati di posizione in modo da poter configurare gli attributi dei vertici sulla CPU. Lo abbiamo visto nel capitolo precedente come layout (location = 0)
. Il vertex shader richiede quindi una specifica di layout extra per i suoi input in modo da poterlo collegare ai dati del vertice.
È anche possibile omettere l’identificatorelayout (location = 0)
e interrogare le posizioni degli attributi nel codice OpenGL tramite glGetAttribLocation, ma preferirei impostarle nel vertex shader. È più facile da capire e ti salva (e OpenGL) un po ‘ di lavoro.
L’altra eccezione è che lo shader di frammenti richiede una variabile di output di colore vec4
, poiché gli shader di frammenti devono generare un colore di output finale. Se non si riesce a specificare un colore di output nel fragment shader, l’output del buffer di colore per quei frammenti sarà indefinito (il che di solito significa che OpenGL li renderà neri o bianchi).
Quindi se vogliamo inviare dati da uno shader all’altro dovremmo dichiarare un output nello shader di invio e un input simile nello shader di ricezione. Quando i tipi e i nomi sono uguali su entrambi i lati OpenGL collegherà queste variabili insieme e quindi è possibile inviare dati tra gli shader (questo viene fatto quando si collega un oggetto programma). Per mostrare come funziona in pratica stiamo andando a modificare gli shader del capitolo precedente per lasciare che il vertex shader decidere il colore per lo shader frammento.
Vertex shader
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}
Fragment shader
#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;}
Si può vedere, abbiamo dichiarato un vertexColor variabile come un vec4
uscita che abbiamo impostato nel vertex shader e dichiariamo un simile vertexColor ingresso nel fragment shader. Poiché entrambi hanno lo stesso tipo e nome, il vertexColor nello shader frammento è collegato al vertexColor nel vertex shader. Poiché impostiamo il colore su un colore rosso scuro nel vertex shader, anche i frammenti risultanti dovrebbero essere rosso scuro. L’immagine seguente mostra l’output:
Eccoci! Siamo appena riusciti a inviare un valore dal vertex shader al fragment shader. Diamo spezia un po ‘ e vedere se siamo in grado di inviare un colore dalla nostra applicazione per lo shader frammento!
Uniformi
Le uniformi sono un altro modo per passare i dati dalla nostra applicazione sulla CPU agli shader sulla GPU. Le uniformi sono tuttavia leggermente diverse rispetto agli attributi dei vertici. Prima di tutto, le uniformi sono globali. Globale, il che significa che una variabile uniforme è unica per oggetto programma shader, e si può accedere da qualsiasi shader in qualsiasi fase del programma shader. In secondo luogo, qualunque cosa tu abbia impostato il valore uniforme, le uniformi manterranno i loro valori fino a quando non verranno ripristinati o aggiornati.
Per dichiarare un’uniforme in GLSL è sufficiente aggiungere la parola chiave uniform
a uno shader con un tipo e un nome. Da quel momento in poi possiamo usare la nuova uniforme dichiarata nello shader. Vediamo se questa volta possiamo impostare il colore del triangolo tramite un’uniforme:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
Abbiamo dichiarato un uniformevec4
ourColor nello shader frammento e impostare il colore di uscita del frammento al contenuto di questo valore uniforme. Poiché le uniformi sono variabili globali, possiamo definirle in qualsiasi fase dello shader che vorremmo, quindi non c’è bisogno di passare di nuovo attraverso il vertex shader per ottenere qualcosa nello shader del frammento. Non stiamo usando questa uniforme nel vertex shader quindi non c’è bisogno di definirla lì.
Se dichiari un’uniforme che non viene utilizzata da nessuna parte nel tuo codice GLSL, il compilatore rimuoverà silenziosamente la variabile dalla versione compilata che è la causa di diversi errori frustranti; tienilo a mente!
L’uniforme è attualmente vuota; non abbiamo ancora aggiunto alcun dato all’uniforme, quindi proviamo. Per prima cosa dobbiamo trovare l’indice/posizione dell’attributo uniform nel nostro shader. Una volta che abbiamo l’indice/posizione dell’uniforme, possiamo aggiornare i suoi valori. Invece di passare un singolo colore allo shader del frammento, ravviviamo le cose cambiando gradualmente colore nel tempo:
float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
In primo luogo, recuperiamo il tempo di esecuzione in secondi tramite glfwGetTime(). Quindi variiamo il colore nell’intervallo di0.0
1.0
usando la funzione sin e memorizziamo il risultato in greenValue.
Quindi interroghiamo la posizione dell’uniforme ourColor usando glGetUniformLocation. Forniamo il programma shader e il nome dell’uniforme (da cui vogliamo recuperare la posizione) alla funzione di query. Se glGetUniformLocation restituisce -1
, non è stato possibile trovare la posizione. Infine possiamo impostare il valore uniforme utilizzando la funzione glUniform4f. Si noti che trovare la posizione uniforme non richiede di utilizzare prima il programma shader, ma l’aggiornamento di una uniforme richiede di utilizzare prima il programma (chiamando glUseProgram), perché imposta l’uniforme sul programma shader attualmente attivo.
Poiché OpenGL è nel suo nucleo una libreria C non ha il supporto nativo per il sovraccarico di funzioni, quindi ovunque una funzione possa essere chiamata con tipi diversi OpenGL definisce nuove funzioni per ogni tipo richiesto; glUniform è un perfetto esempio di questo. La funzione richiede un postfix specifico per il tipo di uniforme che si desidera impostare. Alcuni dei postfix possibili sono:
-
f
: la funzione prevede unfloat
come valore. -
i
: la funzione prevede unint
come valore. -
ui
: la funzione prevede ununsigned int
come valore. -
3f
: la funzione prevede 3float
come valore. -
fv
: la funzione prevede unfloat
vettore / array come valore.
Ogni volta che vuoi configurare un’opzione di OpenGL scegli semplicemente la funzione sovraccaricata che corrisponde al tuo tipo. Nel nostro caso vogliamo impostare 4 float dell’uniforme individualmente in modo da passare i nostri dati tramite glUniform4f (si noti che avremmo anche potuto usare la versionefv
).
Ora che sappiamo come impostare i valori delle variabili uniformi, possiamo usarli per il rendering. Se vogliamo che il colore cambi gradualmente, vogliamo aggiornare questa uniforme ogni fotogramma, altrimenti il triangolo manterrebbe un singolo colore solido se lo impostiamo solo una volta. Quindi calcoliamo il greenValue e aggiorniamo l’uniforme ogni iterazione di rendering:
while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}
Il codice è un adattamento relativamente semplice del codice precedente. Questa volta, aggiorniamo un valore uniforme ogni fotogramma prima di disegnare il triangolo. Se aggiorni correttamente l’uniforme, dovresti vedere il colore del tuo triangolo cambiare gradualmente da verde a nero e tornare a verde.
Controlla il codice sorgente qui se sei bloccato.
Come puoi vedere, le uniformi sono uno strumento utile per impostare attributi che possono cambiare ogni fotogramma o per scambiare dati tra la tua applicazione e i tuoi shader, ma cosa succede se vogliamo impostare un colore per ogni vertice? In tal caso dovremmo dichiarare tutte le uniformi che abbiamo i vertici. Una soluzione migliore sarebbe quella di includere più dati negli attributi del vertice, che è quello che faremo ora.
Altri attributi!
Abbiamo visto nel capitolo precedente come possiamo riempire un VBO, configurare i puntatori di attributo vertex e memorizzarlo tutto in un VAO. Questa volta, vogliamo anche aggiungere dati di colore ai dati dei vertici. Stiamo per aggiungere i dati di colore come 3 float
s alla matrice vertici. Assegniamo rispettivamente un colore rosso, verde e blu a ciascuno degli angoli del nostro triangolo:
float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
Poiché ora abbiamo più dati da inviare al vertex shader, è necessario regolare il vertex shader per ricevere anche il nostro valore di colore come input dell’attributo vertex. Nota che abbiamo impostato la posizione del aColor attributo a 1 con il layout identificatore:
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data}
Dal momento che non usiamo più uniforme per il frammento di colore, ma ora uso il ourColor variabile di output, dovremo cambiare il fragment shader così:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
Perché abbiamo aggiunto un altro vertice attributo e aggiornato il VBO memoria dobbiamo ri-configurare il vertice attributo puntatori. I dati aggiornati nella memoria del VBO ora sembrano un po ‘ come questo:
Conoscere la corrente di layout, possiamo aggiornare il formato vertice con glVertexAttribPointer:
// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);
I primi argomenti di glVertexAttribPointer sono relativamente semplici. Questa volta stiamo configurando l’attributo vertex sulla posizione dell’attributo 1
. I valori di colore hanno una dimensione di3
float
s e non normalizziamo i valori.
Poiché ora abbiamo due attributi vertice dobbiamo ricalcolare il valore del passo. Per ottenere il valore dell’attributo successivo (ad esempio il successivo x
componente del vettore posizione) nell’array di dati dobbiamo spostare 6
float
s a destra, tre per i valori di posizione e tre per i valori di colore. Questo ci dà un valore stride di 6 volte la dimensione di un float
in byte (=24
byte).
Inoltre, questa volta dobbiamo specificare un offset. Per ogni vertice, l’attributo position vertex è il primo, quindi dichiariamo un offset di 0
. L’attributo di colore inizia dopo i dati di posizione, quindi l’offset è 3 * sizeof(float)
in byte (= 12
byte).
L’esecuzione dell’applicazione dovrebbe comportare la seguente immagine:
Controlla il codice sorgente qui se sei bloccato.
L’immagine potrebbe non essere esattamente quello che ci si aspetterebbe, dal momento che abbiamo fornito solo 3 colori, non l’enorme tavolozza di colori che stiamo vedendo in questo momento. Questo è tutto il risultato di qualcosa chiamato interpolazione di frammenti nello shader di frammenti. Quando si esegue il rendering di un triangolo, la fase di rasterizzazione di solito produce molti più frammenti rispetto ai vertici originariamente specificati. Il rasterizzatore determina quindi le posizioni di ciascuno di questi frammenti in base a dove risiedono sulla forma triangolare.
In base a queste posizioni, interpola tutte le variabili di input dello shader frammento. Diciamo ad esempio che abbiamo una linea in cui il punto superiore ha un colore verde e il punto inferiore un colore blu. Se lo shader frammento viene eseguito su un frammento che risiede attorno a una posizione in 70%
della linea, il suo attributo di input del colore risultante sarebbe quindi una combinazione lineare di verde e blu; per essere più precisi: 30%
blu e 70%
verde.
Questo è esattamente quello che è successo al triangolo. Abbiamo 3 vertici e quindi 3 colori, e a giudicare dai pixel del triangolo probabilmente contiene circa 50000 frammenti, dove lo shader del frammento interpola i colori tra quei pixel. Se guardi bene i colori vedrai che tutto ha un senso: dal rosso al blu arriva prima al viola e poi al blu. L’interpolazione dei frammenti viene applicata a tutti gli attributi di input dello shader dei frammenti.
La nostra classe di shader
Scrivere, compilare e gestire gli shader può essere piuttosto ingombrante. Come tocco finale sul soggetto shader stiamo andando a rendere la nostra vita un po ‘ più facile con la costruzione di una classe di shader che legge shader dal disco, compila e li collega, controlla gli errori ed è facile da usare. Questo ti dà anche un po ‘ un’idea di come possiamo incapsulare alcune delle conoscenze che abbiamo imparato finora in utili oggetti astratti.
Creeremo la classe shader interamente in un file di intestazione, principalmente per scopi di apprendimento e portabilità. Iniziamo aggiungendo gli include richiesti e definendo la struttura della classe:
#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif
Abbiamo usato diverse direttive del preprocessore nella parte superiore del file di intestazione. L’utilizzo di queste piccole righe di codice informa il compilatore di includere e compilare questo file di intestazione solo se non è stato ancora incluso, anche se più file includono l’intestazione dello shader. Ciò impedisce il collegamento di conflitti.
La classe shader contiene l’ID del programma shader. Il suo costruttore richiede i percorsi di file del codice sorgente del vertex e fragment shader rispettivamente che possiamo memorizzare su disco come semplici file di testo. Per aggiungere un po ‘di più aggiungiamo anche diverse funzioni di utilità per facilitare la nostra vita un po’: uso attiva il programma shader, e tutto pronto… le funzioni interrogano una posizione uniforme e ne impostano il valore.
Lettura da file
Stiamo usando filestream C++ per leggere il contenuto del file in diversistring
oggetti:
Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str();
Quindi dobbiamo compilare e collegare gli shader. Si noti che stiamo anche esaminando se la compilazione / collegamento non è riuscita e, in tal caso, stampare gli errori in fase di compilazione. Questo è estremamente utile durante il debug (alla fine avrai bisogno di quei log degli errori):
// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);
La funzione di utilizzo è semplice:
void use() { glUseProgram(ID);}
Allo stesso modo per una qualsiasi delle funzioni setter uniforme:
void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
E lì abbiamo, una classe shader completata. Utilizzando la classe shader, è abbastanza semplice; si crea un oggetto shader una volta e semplicemente iniziare ad usarlo:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
Qui abbiamo memorizzato il vertex e fragment shader codice sorgente in due file chiamato shader.vs
e shader.fs
. Sei libero di nominare i tuoi file shader come preferisci; Personalmente trovo le estensioni .vs
e .fs
abbastanza intuitive.
Puoi trovare il codice sorgente qui usando la nostra nuova classe shader creata. Si noti che è possibile fare clic sui percorsi dei file shader per trovare il codice sorgente degli shader.
Esercizi
- Regola il vertex shader in modo che il triangolo sia capovolto: soluzione.
- Specificare un offset orizzontale tramite un’uniforme e spostare il triangolo sul lato destro dello schermo nel vertex shader usando questo valore di offset: solution.
- Invia la posizione del vertice allo shader del frammento usando la parola chiave
out
e imposta il colore del frammento uguale a questa posizione del vertice (vedi come anche i valori della posizione del vertice sono interpolati attraverso il triangolo). Una volta che sei riuscito a farlo; prova a rispondere alla seguente domanda: perché il lato in basso a sinistra del nostro triangolo è nero?: soluzione.