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

Impara OpenGL. Lezione 4.8 - GLSL avanzato

OGL3

GLSL avanzato

Questo tutorial non ti mostrerà i nuovi strumenti avanzati che miglioreranno notevolmente la qualità visiva di una scena. In questo tutorial, esamineremo aspetti più o meno interessanti di GLSL e toccheremo alcuni buoni trucchi che possono aiutarti nei tuoi sforzi. Fondamentalmente, conoscenze e strumenti per semplificarti la vita durante la creazione di applicazioni OpenGL in combinazione con GLSL.
Discuteremo alcune interessanti variabili integrate , nuovi approcci all'I/O dello shader e uno strumento molto utile: l'oggetto buffer uniforme .
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

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

Variabili GLSL integrate

Gli shader sono autosufficienti, se abbiamo bisogno di dati da qualsiasi altra fonte, dovremo passarli allo shader. Abbiamo imparato a farlo usando attributi di vertice, uniformi e campionatori. Tuttavia, ci sono molte altre variabili definite in GLSL con il prefisso gl_ , che ci offre ulteriori opportunità per leggere e/o scrivere dati. Abbiamo già visto due rappresentanti dei vettori risultanti: la gl_Position del vertex shader e la gl_FragCoord del frammento shader.
Discuteremo alcune delle interessanti variabili integrate sia di input che di output in GLSL e spiegheremo come sono utili. Nota che non discuteremo di tutte le variabili integrate in GLSL, quindi se vuoi vedere tutte le variabili integrate, puoi farlo nella pagina OpenGL corrispondente.

Variabili del Vertex shader

Abbiamo già lavorato con la variabile gl_Position , che è il vettore di output del vertex shader, che imposta il vettore di posizione nello spazio di ritaglio. L'impostazione di gl_Position è un prerequisito per visualizzare qualcosa sullo schermo. Niente di nuovo per noi.

gl_PointSize

Una delle primitive elaborate che possiamo scegliere è GL_POINTS . In questo caso, ogni vertice è una primitiva e viene trattato come un punto. Puoi anche impostare la dimensione dei punti elaborati usando la funzione glPointSize . Ma possiamo anche cambiare questo valore tramite lo shader.
Nella variabile float di output gl_PointSize , dichiarata in GLSL, puoi impostare l'altezza e la larghezza dei punti in pixel. Descrivendo la dimensione in punti in un vertex shader, puoi influenzare questo valore per ogni vertice.
Per impostazione predefinita, il ridimensionamento dei punti nel vertex shader è disabilitato, ma se lo desideri, puoi impostare il flag OpenGL GL_PROGRAM_POINT_SIZE :
glEnable(GL_PROGRAM_POINT_SIZE);
Un semplice esempio di ridimensionamento di un punto è l'impostazione della dimensione di un punto sul valore della componente z dello spazio di ritaglio, che a sua volta è uguale alla distanza dal vertice all'osservatore. La dimensione del punto aumenterà man mano che ci allontaniamo dall'alto.
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    gl_PointSize = gl_Position.z;    
}  
Di conseguenza, più siamo lontani dai punti, più grandi saranno visualizzati.
point_size
Il ridimensionamento del punto per ogni vertice sarà utile per tecniche come la generazione di particelle.

gl_VertexID

Le variabili gl_Position e gl_PointSize sono variabili di output , poiché i loro valori vengono letti come output dello stadio di frammentazione del frammento e scrivendo possiamo influenzarli. Il vertex shader fornisce anche una variabile di input di sola lettura gl_VertexID .
La variabile intera gl_VertexID contiene l'ID del vertice da disegnare. Durante il rendering dell'indice (con glDrawElements ) questa variabile contiene l'indice corrente del vertice che viene visualizzato. Quando si esegue il rendering senza indici (tramite glDrawArrays ), la variabile contiene il numero di vertici elaborati fino a quel momento dalla chiamata al rendering.
Sebbene non sia molto necessario ora, è utile conoscere la presenza di tale variabile.

Frammento variabili shader

All'interno dello shader dei frammenti, abbiamo anche accesso ad alcune curiose variabili. GLSL ci fornisce due variabili di input gl_FragCoord e gl_FrontFacing .

gl_FragCoord

Abbiamo già visto gl_FragCoord un paio di volte mentre discutevamo del controllo di profondità. Il componente z del vettore gl_FragCoord è uguale alla profondità del particolare frammento. Tuttavia, possiamo anche usare i componenti x e y per alcuni effetti.
I componenti x e y di gl_FragCoord sono le coordinate del frammento nel sistema di coordinate della finestra, provenienti dall'angolo inferiore sinistro della finestra. Abbiamo specificato la dimensione della finestra come 800x600 usando glViewport , quindi le coordinate del frammento nel sistema di coordinate della finestra saranno comprese tra 0-800 in x e nell'intervallo 0-600 in y.
Utilizzando uno shader di frammenti, possiamo calcolare vari valori di colore in base alle coordinate dello schermo del frammento. Un uso comune della variabile gl_FragCoord è confrontare il risultato del calcolo visibile di diversi frammenti, come di solito si fa nelle demo tecniche. Ad esempio, possiamo dividere lo schermo in due parti visualizzando una parte nella metà sinistra dello schermo e un'altra parte nella metà destra dello schermo. Di seguito è riportato un esempio di uno shader di frammenti che emette colori diversi in base alle coordinate dello schermo.
void main()
{             
    if(gl_FragCoord.x < 400)
        FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    else
        FragColor = vec4(0.0, 1.0, 0.0, 1.0);        
}  
Perché la larghezza della finestra è 800, in caso di impostazione delle coordinate pixel inferiori a 400, dovrebbe essere sul lato sinistro dello schermo, e questo ci dà un oggetto di un colore diverso.
fragcoord
Ora possiamo calcolare due risultati completamente diversi nello shader dei frammenti e visualizzarli ciascuno sulla propria parte dello schermo. Questo è ottimo per testare varie meccaniche di illuminazione.

gl_FrontFacing

Un'altra curiosa variabile nello shader dei frammenti è gl_FrontFacing . Nel tutorial di ritaglio della faccia - abbiamo detto che OpenGL può determinare se una faccia è una faccia nell'ordine di attraversamento dei vertici. Se non usiamo il ritaglio del viso (attivando il flag GL_FACE_CULL ), la variabile gl_FrontFacing ci dice se il frammento corrente è frontale o non frontale. Ad esempio, possiamo calcolare diversi colori per la parte anteriore.
La variabile booleana gl_FrontFacing è impostata su true se il frammento è sulla faccia anteriore, altrimenti è false . Ad esempio, possiamo creare un cubo con diverse trame all'interno e all'esterno.
#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D frontTexture;
uniform sampler2D backTexture;

void main()
{             
    if(gl_FrontFacing)
        FragColor = texture(frontTexture, TexCoords);
    else
        FragColor = texture(backTexture, TexCoords);
} 
Se guardiamo all'interno del contenitore, vedremo che lì viene utilizzata una trama diversa.
sul davanti
Nota che se abiliti il ​​ritaglio dei bordi, non vedrai alcun bordo all'interno del contenitore, quindi l'uso di gl_FronFacing sarà inutile.

gl_FragDepth

gl_FragCoord è una variabile di input che ci permette di leggere le coordinate nel sistema di coordinate della finestra e ottenere il valore di profondità del frammento corrente, ma questa variabile è di sola lettura . Non possiamo cambiare le coordinate del frammento nel sistema di coordinate della finestra, ma possiamo impostare il valore di profondità del frammento. GLSL ci fornisce una variabile di output - gl_FragDepth , usando la quale possiamo impostare il valore della profondità del frammento all'interno dello shader.
L'impostazione del valore di profondità è semplice: devi solo scrivere un valore float compreso tra 0.0 e 1.0 nella variabile gl_FragDepth .
gl_FragDepth = 0.0; // now the depth value of this fragment is equal to zero
Se lo shader non scrive un valore su gl_FragDepth , allora il valore per questa variabile sarà preso automaticamente da gl_FragCoord.z .
Tuttavia, impostare il valore di profondità da soli ha un notevole svantaggio, perché OpenGL disabilita tutti i primi controlli di profondità (come discusso nel test di profondità ) non appena appare una voce gl_FragDepth nello shader dei frammenti. Ciò è dovuto al fatto che OpenGL non può sapere quale valore di profondità avrà il frammento prima di avviare lo shader del frammento, poiché lo shader dei frammenti potrebbe cambiare completamente questo valore.
Quando si scrive su gl_FragDepth , vale la pena considerare il potenziale degrado delle prestazioni. Tuttavia, a partire dalla versione OpenGL 4.2, possiamo trovare una variabile di compromesso pereobyavlyaya gl_FragDepth all'inizio dello shader dei frammenti con la profondità della condizione.
layout (depth_<condition>) out float gl_FragDepth;
Il parametro condition può assumere i seguenti valori:
Condizione Descrizione
qualunque Valore predefinito. Il controllo della profondità anticipato è disabilitato - perderai prestazioni
maggiore Puoi solo impostare il valore di profondità maggiore di gl_FragCoord.z
Di meno Puoi solo impostare il valore di profondità inferiore a gl_FragCoord.z
invariato In gl_FragDepth scrivi un valore uguale a gl_FragCoord.z
Specificando maggiore o minore come condizione di profondità, OpenGL può presumere che si scriveranno solo valori maggiori o minori dei valori di profondità del frammento. In questo scenario, OpenGL può ancora eseguire un test anticipato del valore di profondità se il valore è inferiore al valore di profondità del frammento.
Nell'esempio seguente, stiamo aumentando il valore della profondità nello shader dei frammenti, ma vogliamo anche mantenere il controllo anticipato della profondità nello shader dei frammenti.
#version 420 core // note the OpenGL version
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;

void main ()
{
    FragColor = vec4 (1.0);
    gl_FragDepth = gl_FragCoord.z ​​+ 0.1;
}
Questa proprietà è disponibile solo in OpenGL 4.2 e versioni successive.

Blocchi di interfaccia

Fino ad ora, ogni volta che volevamo passare i dati da un vertex shader a un frammento shader, dichiaravamo diverse variabili di input/output corrispondenti. Questo è il modo più semplice per trasferire dati da uno shader a un altro. Man mano che le applicazioni crescono in complessità, potresti voler passare più di poche variabili, che possono includere array e/o strutture.
Per aiutarci a organizzare le variabili, GLSL fornisce cose come i blocchi di interfaccia che ci consentono di raggruppare le variabili. Dichiarare tali blocchi di interfaccia è molto simile a dichiarare una struttura , tranne per l'uso delle parole chiave in e out basate sull'uso del blocco (input o output).
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

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

out VS_OUT
{
    vec2 TexCoords;
} vs_out;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    vs_out.TexCoords = aTexCoords;
}  
Qui abbiamo dichiarato il blocco dell'interfaccia vs_out che raggruppa tutte le variabili di output insieme, che verranno inviate allo shader successivo. Questo è un esempio banale, ma immagina come può aiutarti a organizzare il tuo I/O negli shader. Questo sarà utile anche nel prossimo tutorial sugli shader di geometria quando è necessario combinare gli shader I/O in array.
Quindi dobbiamo dichiarare il blocco dell'interfaccia di input nel prossimo shader - frammento. Il nome del blocco ( VS_OUT) dovrebbe essere lo stesso, ma il nome dell'istanza ( vs_out , utilizzato nel vertex shader) può essere qualsiasi cosa, l'importante è evitare confusione nei nomi (ad esempio, nominare l'istanza contenente l'input vs_out ).
#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
} 
Poiché i nomi dei blocchi di interfaccia sono gli stessi, i loro I/O corrispondenti vengono raggruppati insieme. Questa è un'altra proprietà utile che ti aiuta a organizzare il tuo codice e si rivela utile mentre navighi tra fasi specifiche come uno shader geometrico.

Tampone uniforme

Usiamo OpenGL da un po' di tempo ormai e abbiamo imparato alcuni buoni trucchi, ma abbiamo anche riscontrato alcuni inconvenienti. Ad esempio, quando usiamo costantemente più di uno shader, dobbiamo impostare variabili uniformi, mentre la maggior parte di esse è la stessa in ogni shader: perché dobbiamo farlo di nuovo?
OpenGL ci fornisce uno strumento chiamato buffer uniforme che ci consente di dichiarare un insieme di variabili uniformi globali che rimangono le stesse in ogni shader. Quando si utilizza un buffer uniforme, è necessario impostare solo una volta le variabili uniformi desiderate. Ma dobbiamo ancora occuparci delle variabili uniche per un particolare shader. Tuttavia, dobbiamo sudare un po' per impostare l'oggetto buffer uniforme.
Poiché il buffer uniforme è un buffer come qualsiasi altro buffer, possiamo crearlo tramite la funzione glGenBuffers , associarlo al target GL_UNIFORMS_BUFFER e inserire lì tutti i dati necessari per le variabili uniformi. Ci sono alcune regole per inserire i dati in un buffer uniforme - su questo torneremo più avanti. Per prima cosa, inseriremo la nostra proiezione e le matrici di visualizzazione in un cosiddetto riquadro uniforme nel vertex shader.
#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};

uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
Nella maggior parte dei nostri esempi, impostiamo le matrici di proiezione e vista in ogni iterazione di rendering per ogni shader utilizzato. Questo è un esempio perfetto per dimostrare l'utilità di un buffer uniforme, perché ora dobbiamo solo chiedere loro una volta. Abbiamo dichiarato e denominato il blocco uniforme - Matrices , che memorizza due matrici 4x4. È possibile accedere direttamente alle variabili in un blocco senza specificare un prefisso di blocco. Quindi mettiamo i valori di queste matrici in un buffer da qualche parte nel codice e ogni shader che dichiara questo blocco uniforme ha accesso alle matrici.
Probabilmente ora ti starai chiedendo cosa significhi std140 . Dice che il blocco uniforme usa un metodo speciale per collocare il suo contenuto in memoria; questa espressione specifica il layout (layout) del blocco uniforme.

Markup blocco uniforme

Il contenuto di un blocco uniforme viene archiviato in un oggetto buffer, che essenzialmente non è altro che un blocco di memoria riservato. Poiché questo pezzo di memoria non contiene informazioni sul tipo di dati che memorizza, dobbiamo dire a OpenGL quale pezzo di memoria corrisponde a ciascuna delle variabili uniformi nello shader.
Immagina il seguente blocco uniforme in uno shader:
layout (std140) uniform ExampleBlock
{
    float value;
    vec3  vector;
    mat4  matrix;
    float values[3];
    bool  boolean;
    int   integer;
};  
Vogliamo conoscere la dimensione (in byte) e l'offset (dall'inizio del blocco) per ciascuna di queste variabili in modo da poterle inserire nel buffer nell'ordine appropriato. La dimensione di ogni elemento è definita esplicitamente in OpenGL ed è direttamente correlata ai tipi C++; vettori e matrici sono grandi array di numeri in virgola mobile. Ciò che OpenGL non definisce esplicitamente è lo spazio tra le variabili. Ciò consente all'hardware di allocare le variabili come meglio crede. Ad esempio, alcune istanze posizionano vec3 accanto a float . Non tutti possono gestirlo e quindi allineare vec3 a un array di quattro float prima di aggiungere i float . Una struttura meravigliosa, ma scomoda per noi.
GLSL utilizza quello che viene chiamato markup condiviso (layout) per impostazione predefinita per la memoria buffer uniforme. Il markup condiviso viene chiamato perché gli offset definiti dall'hardware sono condivisi da più programmi. Con il markup condiviso, GLSL ha il diritto di spostare le variabili uniformi per l'ottimizzazione, a condizione che l'ordine delle variabili non cambi. Perché non sappiamo a quale offset ogni variabile uniforme, non sappiamo come riempire esattamente il nostro buffer uniforme. Possiamo interrogare queste informazioni con funzioni come glGetUniformIndices , ma questo va oltre lo scopo di questo tutorial.
Sebbene il markup condiviso ci fornisca un po' di ottimizzazione della memoria, dobbiamo richiedere un offset da ogni variabile uniforme, il che risulta essere un sacco di lavoro. Tuttavia, è pratica comune non utilizzare il markup condiviso, ma utilizzare il markup std140 . Std140 imposta esplicitamente il layout di memoria per ogni tipo di variabile, impostando l'offset corrispondente secondo regole speciali. Perché confusione è esplicitamente indicata, possiamo trovare manualmente gli offset per ogni variabile.
Ogni variabile ha un allineamento di base pari alla quantità di memoria occupata dalla variabile (inclusi i byte di riempimento) all'interno del blocco uniforme - il valore di questo allineamento di base è calcolato secondo le regole di markup std140 . Quindi, per ogni variabile, calcoliamo l' offset di byte allineato dall'inizio del blocco. L'offset di byte allineato della variabile deve essere un multiplo dell'allineamento di base.
Puoi trovare le regole esatte di markup nella specifica OpenGL Uniform Buffer - qui . Ma elencheremo le regole generali di seguito. Ogni tipo di variabile in GLSL, come int , float e bool, è definito come quattro byte, ogni oggetto da quattro byte è indicato come N.
Un tipo Regola di layout (markup)
Scalare ( int , bool ) Ogni tipo scalare ha un allineamento di base di N
Vettore 2N o 4N. Ciò significa che vec3 ha un allineamento di base di 4N.
Matrice di vettori o scalari Ogni elemento ha un allineamento di base uguale all'allineamento vec4
matrici Memorizzati come grandi array di colonne vettoriali, dove ogni vettore ha un allineamento di base vec4
Struttura Uguale alla dimensione calcolata di tutti gli elementi, secondo la regola precedente, ma imbottito a un multiplo di vec4
Come la maggior parte delle specifiche OpenGL, è più facile da capire con un esempio. Esamineremo il blocco uniforme presentato in precedenza, ExampleBlock, e calcoleremo l'offset allineato di ciascun membro utilizzando il markup std140 .
layout (std140) uniform ExampleBlock
{
                     // base alignment // aligned offset
    float value; // 4 // 0
    vec3 vector; // 16 // 16 (must be a multiple of 16, so we replace 4 with 16)
    mat4 matrix; // 16 // 32 (column 0)
                     // 16 // 48 (column 1)
                     // 16 // 64 (column 2)
                     // 16 // 80 (column 3)
    float values ​​[3]; // 16 // 96 (values ​​[0])
                     // 16 // 112 (values ​​[1])
                     // 16 // 128 (values ​​[2])
    bool boolean; // 4 // 144
    int integer; // 4 // 148
};
Come esercizio, prova a calcolare tu stesso il valore di offset e confrontalo con questa tabella. Con i valori di offset calcolati, basati sulle regole di markup std140, possiamo riempire il buffer con i dati ad ogni offset usando funzioni come glBufferSubData . Std140 non è il più efficiente, ma ci garantisce che la memoria di markup rimane la stessa per ogni programma che dichiara questo blocco uniforme.
Aggiungendo il layout dell'espressione (std140) prima della definizione del blocco uniforme, diciamo a OpenGL che il blocco utilizza il markup std140. Ci sono altri due schemi di posizionamento che possiamo usare che richiedono di richiedere ogni offset prima di riempire il buffer. Abbiamo già visto il markup diviso in azione e il markup rimanente è condensato . Quando si utilizza il markup condensato, non vi è alcuna garanzia che il markup rimanga lo stesso tra i programmi (non condiviso), poiché ciò consente al compilatore di ottimizzare le variabili uniformi eliminando variabili uniformi separate, il che può portare a differenze in diversi shader.

Utilizzo di buffer uniformi

Abbiamo discusso della definizione di unità uniformi negli shader e della specifica dell'allocazione della memoria, ma non abbiamo ancora discusso come usarle.
La prima cosa di cui abbiamo bisogno è un buffer uniforme, che è già stato fatto con glGenBuffers . Dopo aver creato un oggetto buffer, lo leghiamo a GL_UNIFORM_BUFFER e allochiamo la quantità di memoria richiesta chiamando glBufferData .
unsigned int uboExampleBlock;
glGenBuffers (1, & uboExampleBlock);
glBindBuffer (GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData (GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // allocate 150 bytes of memory
glBindBuffer (GL_UNIFORM_BUFFER, 0);
Nel caso in cui desideriamo aggiornare o inserire dati nel buffer, ci colleghiamo a uboExampleBlock e utilizziamo glBufferSubData per aggiornare la memoria. Abbiamo solo bisogno di aggiornare questo buffer una volta e tutti gli shader che utilizzano questo buffer utilizzeranno i dati aggiornati. Ma come fa OpenGL a sapere quali buffer uniformi corrispondono a quali blocchi uniformi?
Nel contesto di OpenGL, ci sono un certo numero di punti di ancoraggio che definiscono dove possiamo associare il buffer uniforme. Avendo creato un buffer uniforme, lo colleghiamo a uno dei punti di ancoraggio e colleghiamo anche il blocco uniforme allo stesso punto, infatti, legandoli insieme. Lo schema seguente illustra questo:
punti_vincolanti
Come puoi vedere, possiamo associare più buffer uniformi a diversi punti di ancoraggio. Poiché lo shader A e lo shader B hanno un blocco uniforme connesso allo stesso punto di ancoraggio 0, l'informazione uboMatrices nei blocchi uniformi diventa loro comune; Richiede che questi shader siano definiti le stesse Matrici di blocchi uniformi .
Per associare il blocco uniforme al punto di ancoraggio, è necessario chiamare glUnifomBlockBinding , che accetta l'identificatore dell'oggetto shader come primo argomento, l'indice di blocco uniforme come secondo e il punto di ancoraggio (dove stiamo legando) come terzo. Indice blocco uniforme : l'indice della posizione di un blocco specifico nello shader. Queste informazioni possono essere ottenute chiamando glGetUnifromBlocIndex , che accetta l'identificatore dell'oggetto shader e il nome del blocco uniforme come argomenti. Possiamo legare il blocco Uniform Lights mostrato nella Figura 3 al punto di ancoraggio 2 come segue.
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);
Nota che dovremo ripeterlo per ogni shader.

A partire da OpenGL 4.2, è diventato possibile archiviare punti di ancoraggio dei blocchi uniformi nello shader aggiungendo esplicitamente un identificatore di layout aggiuntivo, che ci evita di dover chiamare glGetUniformBlockIndex e glUniformBlockBinding . Nell'esempio seguente, stiamo collegando esplicitamente il punto di ancoraggio e il blocco Uniforme di luci.

layout(std140, binding = 2) uniform Lights { ... };
Quindi dobbiamo legare il buffer uniforme allo stesso punto di ancoraggio usando glBindBufferBase o glBindBufferRange .
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// или
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
La funzione glBindBufferBase prevede un identificatore di destinazione di associazione del buffer, un indice del punto di ancoraggio e un buffer uniforme, come parametri. Questa funzione collega uboExampleBlock e il punto di ancoraggio 2 e da quel punto in poi il punto 2 collega entrambi gli oggetti. Puoi anche usare glBindBufferRange , che accetta ulteriori parametri di offset e dimensione: con questo approccio, puoi associare solo l'intervallo specificato del buffer uniforme al punto di ancoraggio. Usando glBindBufferRange , puoi associare più blocchi uniformi a un singolo buffer uniforme.
Ora che tutto è impostato, possiamo iniziare ad aggiungere dati al buffer uniforme. Potremmo aggiungere tutti i dati come un singolo array o aggiornare parti del buffer quando ne abbiamo bisogno usando glBufferSubData . Per aggiornare la variabile uniforme con booleano, potremmo aggiornare il buffer uniforme in questo modo:
glBindBuffer (GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // boolean variables in GLSL are represented as four bytes, so we store them as an integer variable
glBufferSubData (GL_UNIFORM_BUFFER, 144, 4, & b);
glBindBuffer (GL_UNIFORM_BUFFER, 0);
La stessa operazione viene eseguita con tutte le variabili uniformi all'interno del blocco uniforme, ma con argomenti diversi.

Un semplice esempio

Dimostriamo i veri vantaggi di un buffer uniforme. Se guardi le sezioni precedenti del codice, abbiamo sempre usato 3 matrici: proiezione, vista e modello. Di tutte queste matrici, spesso cambia solo la matrice del modello. Se disponiamo di più shader che utilizzano un insieme delle stesse matrici, è probabile che trarremo vantaggio dall'utilizzo di un oggetto buffer uniforme.
Memorizzeremo le matrici di proiezione e vista in un blocco uniforme chiamato Matrici . Non memorizzeremo qui la matrice del modello, poiché cambia abbastanza spesso negli shader, non otterremmo molto beneficio da tali azioni.
#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};
uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
Qui non succede nulla di speciale, a parte l'utilizzo del layout std 140. Nel nostro esempio, disegneremo 4 cubi, utilizzando uno shader personale per ogni cubo. Tutti e quattro utilizzeranno lo stesso vertex shader, ma diversi shader di frammenti che emettono il proprio colore.
Per prima cosa, posizioniamo il blocco uniforme del vertex shader nel punto di ancoraggio 0. Nota che dobbiamo farlo per ogni shader.
unsigned int uniformBlockIndexRed    = glGetUniformBlockIndex(shaderRed.ID, "Matrices");
unsigned int uniformBlockIndexGreen  = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");
unsigned int uniformBlockIndexBlue   = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");
unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");  
  
glUniformBlockBinding(shaderRed.ID,    uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID,  uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID,   uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);
Successivamente, creiamo un buffer uniforme e leghiamo anche il buffer al punto 0.
unsigned int uboMatrices
glGenBuffers(1, &uboMatrices);
  
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
  
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
Innanzitutto, allochiamo memoria sufficiente per il nostro buffer, che è il doppio di glm :: mat4 . La dimensione della matrice GLM corrisponde esattamente alla dimensione della matrice GLSL mat4 . Quindi concateniamo un intervallo specifico del buffer, nel nostro caso l'intero buffer, per ancorare il punto 0.
Ora non resta che riempire il buffer. Se il parametro dell'angolo di visione della matrice di proiezione viene reso invariato (diciamo addio alla possibilità di zoom), la matrice può essere determinata solo una volta, il che significa che è sufficiente copiarla nel buffer una volta. Poiché abbiamo già allocato memoria sufficiente per l'oggetto buffer, possiamo usare glBufferSubData per memorizzare le matrici di proiezione prima di entrare nel ciclo di gioco:
glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);  
Qui è dove posizioniamo la prima parte del buffer uniforme, la matrice di proiezione. Prima di disegnare oggetti, ad ogni iterazione di rendering, aggiorniamo la seconda parte del buffer: la matrice della vista.
glm::mat4 view = camera.GetViewMatrix();	       
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);  
Questo è tutto per il buffer uniforme. Ogni vertex contenente un blocco matrici uniformi ora conterrà i dati memorizzati in uboMatrices. Ora, se dovessimo disegnare 4 cubi usando 4 shader diversi, le loro matrici di proiezione e vista rimarrebbero le stesse.
glBindVertexArray (cubeVAO);
shaderRed.use ();
glm :: mat4 model;
model = glm :: translate (model, glm :: vec3 (-0.75f, 0.75f, 0.0f)); // move to the top left
shaderRed.setMat4 (" model ", model);
glDrawArrays (GL_TRIANGLES, 0, 36);
// ... draw a green cube
// ... draw a blue cube
// ... draw a yellow cube
L'unica variabile uniforme che dobbiamo impostare è model . L'utilizzo di un buffer uniforme in questa configurazione ci evita di effettuare chiamate per impostare il valore delle variabili uniformi per ogni shader. Il risultato è qualcosa del genere:
unifrom_buffer_objects
Grazie a diversi shader e a un cambiamento nella matrice del modello, 4 cubi si sono spostati nelle loro parti dello schermo e hanno un colore diverso. Questo è uno scenario relativamente semplice in cui possiamo usare buffer uniformi, ma qualsiasi altro grande progetto di rendering potrebbe avere più di cento shader attivi; questo è esattamente il caso in cui i respingenti uniformi si mostrano in tutto il loro splendore.
È possibile trovare il codice sorgente per l'uniforme dell'applicazione di esempio qui .
I buffer uniformi presentano numerosi vantaggi rispetto all'impostazione di singole variabili uniformi. Innanzitutto, l'impostazione di molte variabili uniformi contemporaneamente è più rapida rispetto all'impostazione di più variabili uniformi più volte. In secondo luogo, se si desidera modificare la stessa variabile uniforme su più shader, è molto più semplice modificare la variabile uniforme una volta nel buffer uniforme. Un ultimo vantaggio, non così ovvio, ma puoi usare molte più variabili uniformi negli shader usando un buffer uniforme. OpenGL ha un limite alla quantità di dati che possono essere elaborati da Uniforms. Puoi ottenere queste informazioni con GL_MAX_VETEX_UNIFORM_COMPONENTS . Quando si utilizza il buffer uniforme, questa limitazione è significativamente maggiore. Quando raggiungi il limite dell'utilizzo di variabili uniformi (ad esempio, quando esegui un'animazione scheletrica) puoi sempre utilizzare buffer uniformi.