IT knowledge base
CTRL+F per cercare la tua parola chiave

Impara OpenGL. Lezione 4.9 - Shader geometrico

OGL3

Shader geometrico

Tra le fasi dello shader vertice e frammentazione, esiste una fase opzionale per l'esecuzione dello shader geometrico. L'input per il geometry shader è un insieme di vertici che formano una delle primitive consentite in OpenGL (punti, triangoli, ...). Come risultato del suo lavoro, lo shader geometrico può trasformare questo insieme di vertici a sua discrezione prima di passarlo alla fase successiva dello shader. Allo stesso tempo, vale la pena notare la caratteristica più interessante dello shader geometrico: nel corso del suo lavoro, un insieme di vertici di input può essere trasformato per rappresentare una primitiva completamente diversa, e può anche generare vertici completamente nuovi basati su i dati di input, aumentando il numero totale di vertici.
Soddisfare
Parte 1. Inizio
  1. OpenGL
  2. Crea una finestra
  3. Ciao finestra
  4. Ciao triangolo
  5. Shader
  6. trame
  7. trasformazioni
  8. Sistemi di coordinate
  9. telecamera

Parte 2. Illuminazione di base
  1. Colori
  2. Nozioni di base sull'illuminazione
  3. Materiali (modifica)
  4. Mappe texture
  5. Fonti di luce
  6. Molteplici fonti di illuminazione

Parte 3. Caricamento di modelli 3D
  1. Libreria Assimp
  2. Classe poligono mesh
  3. Classe modello 3D

Parte 4. Funzionalità OpenGL avanzate
  1. Prova di profondità
  2. Prova dello stampino
  3. Miscelazione dei colori
  4. Ritaglio facce
  5. Buffer frame
  6. Carte cubiche
  7. Manipolazione avanzata dei dati
  8. GLSL avanzato
  9. Shader geometrico
  10. Istanza
  11. levigante

Parte 5. Illuminazione avanzata
  1. Illuminazione avanzata. Modello Blinn-Fong.
  2. Correzione gamma
  3. Mappe delle ombre
  4. Mappe ombre omnidirezionali
  5. Mappatura normale
  6. Mappatura della parallasse
  7. HDR
  8. fioritura
  9. Rendering differito
  10. SSAO

Parte 6. PBR
  1. Teoria
  2. Sorgenti luminose analitiche
  3. IBL. Irraggiamento diffuso.
  4. IBL. Irraggiamento speculare.

Non lo sfrutteremo per molto tempo e passeremo immediatamente all'esempio di uno shader geometrico:
#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;

void main() {    
    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); 
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
    EmitVertex();
    
    EndPrimitive();
}  
All'inizio del codice dello shader, è necessario specificare il tipo di primitiva, i cui dati provengono dalla fase del vertex shader. Questo viene fatto usando il layout dell'identificatore, che si trova prima della parola chiave in. Il tipo primitivo specificato nello specificatore può assumere uno dei seguenti valori, corrispondente al tipo della primitiva elaborata dal vertex shader:
  • punti : In uscita GL_POINTS (1).
  • righe : quando si emettono GL_LINES o GL_LINE_STRIP (2).
  • lines_adjacency : quando si emette GL_LINES_ADJACENCY o GL_LINE_STRIP_ADJACENCY (4).
  • triangoli : quando si emettono GL_TRIANGLES, GL_TRIANGLE_STRIP o GL_TRIANGLE_FAN (3).
  • triangolo_adiacenza : quando si emette GL_TRIANGLES_ADJACENCY o GL_TRIANGLE_STRIP_ADJACENCY (6).
Di conseguenza, qui sono elencati quasi tutti i tipi di primitivi che possono essere passati a una chiamata per rendere funzioni come glDrawArrays () . Se reso usando GL_TRIANGLES , allora il parametro dei triangoli dovrebbe essere specificato nell'identificatore. Il numero tra parentesi qui indica il numero minimo di vertici contenuti in una primitiva.
Inoltre, è necessario specificare anche il tipo della primitiva di output per questo shader. Di conseguenza, viene eseguito dall'identificatore della chiave per disporre la parola. In questo esempio, l'output sarà un line_strip con un massimo di due vertici.
Nel caso ve ne foste dimenticati: la primitiva Line Strip collega i punti del set, formando una linea continua tra loro, partendo da due punti del set. Ogni punto aggiuntivo superiore a due risulta in un ulteriore segmento di linea tracciato che si estende dal nuovo punto a quello precedente. Di seguito è riportata un'immagine di un segmento di cinque punti:
L'esempio presentato di uno shader può produrre solo segmenti retti separati, poiché impostiamo esplicitamente il numero massimo di vertici nella primitiva uguale a due.
Affinché lo shader possa fare qualcosa di utile, è necessario ricevere dati dall'output della fase precedente dello shader. GLSL fornisce una variabile incorporata gl_in , che può essere rappresentata con la seguente struttura:
in gl_Vertex
{
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[]; 
Pertanto, la variabile è simile ai blocchi di interfaccia discussi nell'ultima lezione e contiene diversi campi di cui siamo più interessati a gl_Position , che contiene il vettore di posizione dei vertici che è stato impostato come risultato dell'esecuzione del vertex shader.
Si noti che questa variabile è un array, poiché la maggior parte delle primitive contiene più di un vertice e lo stadio di shader geometrico riceve tutti i vertici della primitiva elaborata come input.
Dopo aver ricevuto i dati dei vertici dall'output del vertex shader, puoi iniziare a generare nuovi dati, cosa che viene eseguita utilizzando due funzioni speciali dello shader geometrico: EmitVertex () e EndPrimitive () . Nel tuo codice, dovresti generare almeno una primitiva dichiarata come output. Nel nostro esempio, dovrebbe essere emessa almeno una primitiva del tipo line_strip .

void main() {    
    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); 
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
    EmitVertex();
    
    EndPrimitive();
}    
Ogni chiamata a EmitVertex() aggiunge il valore corrente in gl_Position all'istanza primitiva corrente. Quando chiamiamo EndPrimitive() , tutti i vertici generati sono infine legati al tipo primitivo di output specificato. Ripetendo le chiamate a EndPrimitive() dopo una o più chiamate a EmitVertex(), puoi continuare a creare nuove istanze di primitive. In particolare, l'esempio genera due vertici ciascuno , spostato a una piccola distanza dalla posizione del vertice di input e quindi chiama EndPrimitive() , che forma una striscia di linea da questi due vertici generati, contenente due vertici.
Quindi, conoscendo (in teoria) come funziona lo shader geometrico, probabilmente hai già indovinato quale sarebbe l'effetto di questo esempio. Lo shader accetta primitive di punti come input e crea linee orizzontali sulla base di esse, dove il vertice di input si trova esattamente nel mezzo. L'output di un programma che utilizza uno shader di questo tipo è mostrato di seguito:
Non troppo impressionante, ma interessante, dato che abbiamo ottenuto questi risultati con un solo draw call:

 glDrawArrays(GL_POINTS, 0, 4);
Sebbene questo esempio sia semplice, dimostra un principio importante: la capacità di creare dinamicamente nuove forme utilizzando gli shader geometrici. Vedremo in seguito alcuni effetti più interessanti basati sugli shader geometrici, ma per ora esamineremo le basi con semplici shader.

Utilizzo di uno shader di geometria

Per dimostrare l'uso dello shader geometrico, utilizziamo un semplice programma che esegue il rendering di quattro punti che giacciono sul piano XoY in coordinate dispositivo normalizzate (NDC). Coordinate del punto:

 float points [] = {
-0.5f, 0.5f, // top-left
0.5f, 0.5f, // top-right
0.5f, -0.5f, // bottom-right
-0.5f, -0.5f // bottom-left
};
Il vertex shader è semplice: devi solo mappare i punti sul piano desiderato:

 #version 330 core
layout (location = 0) in vec2 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
}
Anche lo shader dei frammenti è banale e usa solo il colore hardcoded per i frammenti:

 #version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(0.0, 1.0, 0.0, 1.0);   
} 
Nel codice del programma, come al solito, creiamo VAO e VBO per i dati dei vertici e eseguiamo il rendering chiamando glDrawArrays () :

 shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4); 
Di conseguenza, lo schermo è completamente scuro e quattro punti verdi appena percettibili:
È un po' triste se abbiamo imparato così tanto solo per ottenere un'immagine così deprimente. Pertanto, interverremo urgentemente nella scena e diluiremo questa oscurità usando le capacità dello shader geometrico.
Ma prima, per scopi di addestramento, dovrai creare e capire come funziona uno shader di geometria end-to-end , che prende semplicemente i dati della primitiva di input e li invia all'output invariati:

 #version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;

void main() {    
    gl_Position = gl_in[0].gl_Position; 
    EmitVertex();
    EndPrimitive();
}  
A questo punto, puoi già comprendere il codice dello shader senza richieste. Qui, generiamo semplicemente il vertice nella posizione ottenuta dal vertex shader, e quindi generiamo la stessa primitiva del punto.
Lo shader geometrico richiede la compilazione e il collegamento a un oggetto programma allo stesso modo dei vertex shader e dei frammenti. Tuttavia, questa volta l'oggetto shader viene creato con GL_GEOMETRY_SHADER come tipo di shader:

 geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);  
...
glAttachShader(program, geometryShader);
glLinkProgram(program); 
In effetti, il codice di compilazione è esattamente lo stesso di altri tipi di shader. Non dimenticare di controllare la compilazione e gli errori di collegamento!
Una volta eseguito, dovresti ottenere un'immagine familiare:
Abbiamo ottenuto la stessa cosa senza lo shader geometrico... Noioso! Ma, dal momento che i punti vengono ancora visualizzati, ci siamo almeno assicurati che il nostro shader funzioni e possiamo passare a qualcosa di più interessante.

Costruiamo case

Disegnare linee e punti semplici non è proprio quello che ci aspettavamo, quindi proviamo ad aggiungere un po' di creatività e disegnare case nei punti specificati dai vertici di input. Per fare ciò, dobbiamo cambiare il tipo della primitiva di output in triangolo_strip e disegnare tre triangoli: due per la base quadrata e uno per il tetto.
La primitiva Triangle Strip in OpenGL è un metodo più efficiente per disegnare triangoli, che richiede meno vertici di input. Dopo aver eseguito il rendering del primo triangolo, ogni vertice successivo crea un altro triangolo adiacente al precedente. Se nella striscia triangolare sono dati sei vertici, il risultato sarà la seguente sequenza di triangoli: (1,2,3), (2,3,4), (3,4,5) e (4,5, 6), che risulterà in quattro triangoli disegnati. Questa primitiva richiede di specificare almeno tre vertici per il rendering di successo. In generale, verranno visualizzati N-2 triangoli; avendo sei vertici abbiamo ottenuto 6-2 = 4 triangoli, che è illustrato di seguito:
Usando una striscia triangolare, puoi facilmente formare la forma della casa richiesta da soli tre triangoli adiacenti, impostandoli nell'ordine corretto. L'immagine seguente mostra l'ordine in cui è necessario visualizzare i vertici per ottenere l'aspetto del triangolo desiderato. Il punto blu mostra la posizione del vertice di input:
Shader geometria risultante:

 #version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;

void build_house(vec4 position)
{    
    gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);    // 1:bottom-left
    EmitVertex();   
    gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);    // 2:bottom-right
    EmitVertex();
    gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0);    // 3:top-left
    EmitVertex();
    gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0);    // 4:top-right
    EmitVertex();
    gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0);    // 5:top
    EmitVertex();
    EndPrimitive();
}

void main() {    
    build_house(gl_in[0].gl_Position);
}
Lo shader crea cinque vertici in posizioni sfalsate rispetto alla posizione del vertice di input, posizionandoli tutti in un'unica primitiva a strisce triangolari . Questa primitiva viene quindi inviata per la rasterizzazione e lo shader dei frammenti ne colora la superficie di verde. Otteniamo una serra per ogni punto di ingresso:
Qui puoi vedere che ogni casa è in realtà composta da tre triangoli, tutti basati su un singolo punto dati di input.
Ma qualcosa sembra ancora noioso! Proviamo a dipingere ciascuna delle case con il proprio colore. Per fare ciò, organizzeremo un altro attributo del vertice che memorizza le informazioni sul colore sul vertice. Il vertex shader legge il valore dell'attributo per il vertice e lo passa allo shader geometrico, che a sua volta fornirà il valore del colore allo shader frammento.
I dati dei vertici aggiornati hanno il seguente aspetto:

 float points [] = {
    -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, // top-left
     0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // top-right
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-right
    -0.5f, -0.5f, 1.0f, 1.0f, 0.0f // bottom-left
};
Successivamente, rifiniamo il codice del vertex shader per passare l'attributo color allo shader geometrico utilizzando un blocco di interfaccia:

 #version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out VS_OUT {
    vec3 color;
} vs_out;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    vs_out.color = aColor;
}  
Ovviamente, dobbiamo definire un blocco di interfaccia dello stesso tipo (ma con un nome diverso) nello shader geometrico:

 in VS_OUT {
    vec3 color;
} gs_in[];  
Poiché il geometry shader viene eseguito su interi insiemi di vertici di input, il suo parametro di input è sempre un array, anche nei casi in cui l'input è un singolo vertice.

In effetti, non abbiamo bisogno di usare i blocchi di interfaccia per passare i dati allo shader geometrico. Se il vertex shader ha passato il vettore con color come out vec3 vColor , allora potrebbe essere scritto in questo modo:


 in vec3 vColor[];


Tuttavia, nel caso generale, lavorare con i blocchi di interfaccia è molto più semplice, specialmente negli shader geometrici. In pratica, i parametri di input degli shader geometrici sono spesso rappresentati da dataset piuttosto grandi, e combinarli in un unico blocco di interfaccia, rappresentato da un array, è un passaggio abbastanza scontato.

Dovresti anche dichiarare una variabile di output che indirizza i dati del colore allo shader dei frammenti:

 out vec3 fColor;  
Poiché lo shader dei frammenti si aspetta un singolo valore di colore (interpolato), non ha senso inviare matrici di vettori di colore. Ecco perché fColor non è un array, ma un singolo vettore. Quando generiamo un vertice, ognuno di loro ricorderà l'ultimo valore che era nella variabile fColor per la loro chiamata allo shader frammento. Di conseguenza, per le nostre case, possiamo riempire fColor solo una volta con il colore ottenuto dalla fase del vertex shader per impostare il colore dell'intera casa:

 fColor = gs_in [0] .color; // gs_in [0] is used because we have only one vertex at the input
gl_Position = position + vec4 (-0.2, -0.2, 0.0, 0.0); // 1: bottom-left
EmitVertex ();
gl_Position = position + vec4 (0.2, -0.2, 0.0, 0.0); // 2: bottom-right
EmitVertex ();
gl_Position = position + vec4 (-0.2, 0.2, 0.0, 0.0); // 3: top-left
EmitVertex ();
gl_Position = position + vec4 (0.2, 0.2, 0.0, 0.0); // 4: top-right
EmitVertex ();
gl_Position = position + vec4 (0.0, 0.4, 0.0, 0.0); // 5: roof
EmitVertex ();
EndPrimitive ();
Di conseguenza, tutti i vertici generati memorizzeranno il valore del colore dalla variabile fColor , che corrisponde ai colori degli attributi dei vertici. Ora ogni casa è dipinta del proprio colore:
Aggiungiamo un po' di creatività e organizziamo un inverno virtuale cospargendo di neve i tetti delle case. Per fare ciò, assegneremo separatamente un colore bianco all'ultimo vertice:

 fColor = gs_in [0] .color;
gl_Position = position + vec4 (-0.2, -0.2, 0.0, 0.0); // 1: bottom-left
EmitVertex ();
gl_Position = position + vec4 (0.2, -0.2, 0.0, 0.0); // 2: bottom-right
EmitVertex ();
gl_Position = position + vec4 (-0.2, 0.2, 0.0, 0.0); // 3: top-left
EmitVertex ();
gl_Position = position + vec4 (0.2, 0.2, 0.0, 0.0); // 4: top-left
EmitVertex ();
gl_Position = position + vec4 (0.0, 0.4, 0.0, 0.0); // 5: roof
fColor = vec3 (1.0, 1.0, 1.0);
EmitVertex ();
EndPrimitive ();
Di conseguenza, abbiamo:
Puoi confrontare il codice della tua applicazione con l' esempio .
Penso che a questo punto ti sia già chiaro che gli shader di geometria forniscono ampie possibilità creative, anche con l'uso di semplici primitive. Poiché la geometria viene creata dinamicamente all'interno del core GPU ultraveloce, questo è molto più efficiente rispetto all'impostazione della stessa geometria utilizzando i buffer di vertice. Gli shader di geometria offrono ampie opportunità per ottimizzare il rendering di forme semplici e spesso ripetitive come i cubi per il rendering dei voxel o gli steli d'erba nelle scene all'aperto.

Oggetti che esplodono

Disegnare case è, ovviamente, fantastico, ma non è qualcosa con cui dovremo lavorare spesso. Quindi aggiungiamo un po' di calore e andiamo direttamente a far esplodere i modelli 3D! Hmm, probabilmente non dovrai farlo troppo spesso, ma servirà come eccellente dimostrazione delle capacità degli shader geometrici.
Per far esplodere un oggetto, non intendiamo letteralmente distruggere i nostri preziosi vertici, ma il movimento di ciascun triangolo lungo la normale direzione nel tempo. Di conseguenza, questo effetto dà una parvenza di un'esplosione dell'oggetto, dividendolo in triangoli separati che si muovono nella direzione del loro vettore normale. Di seguito è riportato l'effetto applicato al modello di nanotuta:
La cosa fantastica è che l'uso di uno shader geometrico consente all'effetto di funzionare su qualsiasi oggetto, non importa quanto sia complesso.
Poiché dobbiamo spostare i triangoli lungo il vettore normale, dovremo prima calcolarlo. Nello specifico, dobbiamo trovare un vettore perpendicolare alla superficie del triangolo, con solo tre dei suoi vertici nelle nostre mani. Dal tutorial sulle trasformazioni, probabilmente ricorderai che un vettore perpendicolare agli altri due può essere ottenuto utilizzando un'operazione di prodotto incrociato . Se abbiamo trovato due vettori A e B parallelo alla superficie del triangolo, allora il vettore perpendicolare alla superficie sarebbe semplicemente il risultato del loro prodotto vettoriale. In realtà, il codice dello shader di geometria di seguito fa esattamente questo: calcola il vettore normale utilizzando i tre vertici del triangolo di input:

 vec3 GetNormal()
{
   vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
   vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
   return normalize(cross(a, b));
}  
Qui, per sottrazione, otteniamo due vettori a e b , paralleli alla superficie del triangolo. Sottraendo i vettori si ottiene un altro vettore, che è la differenza tra i due originali. Poiché tutti e tre i vertici giacciono nel piano del triangolo, la differenza di qualsiasi vettore che rappresenta i vertici del triangolo genera vettori paralleli alla superficie del triangolo. Prestare attenzione all'ordine dei parametri nell'esportazione della funzione cross() : se scambiassimo a e b , la direzione del vettore normale sarebbe opposta.
Ora che abbiamo un modo per trovare la normale, possiamo passare all'implementazione della funzione esplode () .La funzione prende un vettore normale e un vettore di posizione del vertice e restituisce la nuova posizione del vertice, spostata lungo la normale:

 vec4 explode(vec4 position, vec3 normal)
{
    float magnitude = 2.0;
    vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; 
    return position + vec4(direction, 0.0);
} 
Il codice è piuttosto semplice. La funzione sin() dipende dalla variabile temporale associata all'ora corrente e restituisce periodicamente valori nell'intervallo [-1., 1.]. Poiché non siamo interessati all'effetto di esplosione verso l'interno (implosione), limitiamo i valori di sin() all'intervallo [0., 1.]. Successivamente, il valore ottenuto e la costante di controllo utilizzati per scalare la grandezza del vettore normale nel calcolo della direzione del vettore di direzione finale . Questo vettore viene aggiunto all'input della posizione del vertice per ottenere una nuova posizione di offset.
Di seguito è riportato il codice completo dello shader geometrico dell'effetto esplosione quando si utilizza il codice di rendering dei file del modello 3D dal tutorial corrispondente :

 #version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;

in VS_OUT {
    vec2 texCoords;
} gs_in[];

out vec2 TexCoords; 

uniform float time;

vec4 explode(vec4 position, vec3 normal) { ... }

vec3 GetNormal() { ... }

void main() {    
    vec3 normal = GetNormal();

    gl_Position = explode(gl_in[0].gl_Position, normal);
    TexCoords = gs_in[0].texCoords;
    EmitVertex();
    gl_Position = explode(gl_in[1].gl_Position, normal);
    TexCoords = gs_in[1].texCoords;
    EmitVertex();
    gl_Position = explode(gl_in[2].gl_Position, normal);
    TexCoords = gs_in[2].texCoords;
    EmitVertex();
    EndPrimitive();
}
Nota che prima di generare ogni vertice, passiamo le coordinate della trama corrispondenti.
Inoltre, non dimenticare di impostare un valore per l'uniforme temporale nel codice cliente:

 shader.setFloat("time", glfwGetTime());  
Il risultato è una scena con un modello che periodicamente esplode e ritorna al suo stato originale. L'esempio è banale, ma si presta bene a un uso approfondito degli shader geometrici.
Il codice risultante può essere confrontato con un esempio .

Visualizzazione di vettori normali

Questa volta cercheremo di implementare qualcosa di veramente utile nella pratica utilizzando uno shader geometrico: visualizzare i vettori normali della geometria renderizzata. Quando implementi algoritmi di illuminazione, ti imbatterai inevitabilmente in strani risultati e problemi visivi, la cui causa sarà difficile da determinare. Uno degli errori più comuni quando si lavora con l'illuminazione è l'impostazione di normali errate, come se fossero dovuti a errori nel caricamento dei dati dei vertici, errori nell'impostazione del formato degli attributi dei vertici o semplicemente errori di conversione direttamente negli shader. Sarebbe bello avere uno strumento per determinare la correttezza delle normali fornite. Il rendering normale è uno di questi strumenti e gli shader geometrici sono creati solo per implementarlo.
L'idea è semplice: prima, renderizziamo la scena nel modo consueto senza il geometry shader abilitato, quindi eseguiamo un secondo passaggio, ma visualizzando solo le normali generate dal geometry shader. Lo shader prenderà una primitiva triangolare come input e creerà tre segmenti di linea nella direzione del vettore normale nella posizione di ciascun vertice. In pseudocodice sembra qualcosa del genere:

 shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();
Questa volta il geometry shader utilizzerà le normali fornite come attributo del vertice invece di calcolarlo al volo. Lo shader di geometria prende come input il vettore di posizione nello spazio di una clip (spazio clip), quindi dovremmo convertire il vettore normale nello stesso spazio. Ma prima di fare ciò, dobbiamo trasformare i vettori normali usando una matrice normale - in questo modo terremo conto della scala e delle rotazioni (date dalle matrici della vista e del modello). Tutto questo viene fatto nel vertex shader:

 #version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out VS_OUT {
    vec3 normal;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0); 
    mat3 normalMatrix = mat3(transpose(inverse(view * model)));
    vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * aNormal, 0.0)));
}
Il vettore normale convertito in clip space viene passato allo stadio successivo dello shader tramite il blocco dell'interfaccia. Lo shader geometrico legge gli attributi del vertice (posizione e vettore normale) ed emette un segmento di linea nella direzione normale alla posizione di ciascun vertice:

 #version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;

in VS_OUT {
    vec3 normal;
} gs_in [];

const float MAGNITUDE = 0.4;

void GenerateLine (int index)
{
    gl_Position = gl_in [index] .gl_Position;
    EmitVertex ();
    gl_Position = gl_in [index] .gl_Position + vec4 (gs_in [index] .normal, 0.0) * MAGNITUDE;
    EmitVertex ();
    EndPrimitive ();
}

void main ()
{
    GenerateLine (0); // normal vector for the first vertex
    GenerateLine (1); // ... for the second
    GenerateLine (2); // ... for the third
}
Penso che al momento il codice sia autoesplicativo. Noterò solo che il vettore normale viene ridimensionato utilizzando la costante MAGNITUDE , che consente di limitare la lunghezza del segmento visualizzato (altrimenti sarebbe leggermente grande).
Poiché l'output delle normali è principalmente a scopo di debug, possono essere emesse semplicemente come linee dello stesso colore utilizzando uno shader di frammenti:

 #version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}  
Di conseguenza, la combinazione di rendering del modello utilizzando uno shader normale e re-rendering utilizzando uno shader normale di rendering fresco darà questa immagine:
A parte il fatto che la nanotuta ora sembra più un uomo peloso nelle presine della cucina, siamo stati in grado di ottenere un metodo molto conveniente per determinare la correttezza dei vettori normali per i modelli utilizzati nella scena. Bene, visto che si tratta di questo, ora è chiaro che qualcosa di simile viene utilizzato negli shader che implementano l'effetto pelliccia.
Il codice sorgente per questo esempio può essere trovato qui .
PS : abbiamo una telegramma-conferenza per coordinare i trasferimenti. Se hai un serio desiderio di aiutare con la traduzione, allora sei il benvenuto!