Comunicazione con gli shader

Lo stato di OpenGL non è l'unico modo in cui si possono inviare dati agli shader. È possibile definire variabili personalizzate all'interno degli shader, a cui la CPU può accedere e in cui può scrivere. Queste variabili sono costanti dal punto di vista dello shader, perciò non possono essere usate in alcun modo per inviare dati nella direzione opposta.

In realtà, nelle versioni più recenti di GLSL gran parte delle variabili predefinite di OpenGL sono state abolite e le variabili personalizzate sono l'unico modo per comunicare con gli shader.

Le variabili personalizzate sono dichiarate come variabili globali in tutti gli shader che ne fanno uso. In fase di linking del program, le variabili con lo stesso nome in shader diversi (anche di tipo diverso) diventano la stessa variabile. Una volta inizializzata dall'esterno, una variabile mantiene lo stesso valore fino a un nuovo link del program.
Variabili con lo stesso nome in program diversi non interferiscono tra loro.

Le variabili personalizzate possono essere dichiarate di due tipi diversi: uniform e attribute.

Variabili uniform

Le variabili di tipo uniform sono definite con la parola chiave uniform che precede il tipo. Possono essere usate dallo shader esattamente come una costante globale.

#version 120

uniform float alpha;       // dichiara una variabile uniform di nome alpha

void main()
{
  vec4 color = vec4(gl_Color.rgb,gl_Color.a * alpha);
  ...
}

Tramite OpenGL è possibile scrivere il valore della variabile in qualunque momento, eccetto durante la definizione di una primitiva. Questo significa che le variabili uniform sono sempre costanti per l'intera primitiva.

Per scrivere il valore della variabile uniform si deve prima ottenere il suo indirizzo. Per questo esiste la funzione:

GLint glGetUniformLocation(GLuint program,const GLchar * name);

program è l'id del program in cui è la variabile da modificare, name è una stringa che contiene il nome della variabile così come è stata dichiarata all'interno del program.
Se la variabile è un array, è possibile (e molto spesso obbligatorio) ottenere anche l'indirizzo dei singoli elementi. Per farlo, si deve concatenare la normale notazione con le parentesi quadre al nome: "nome[indice]".

La funzione ritorna l'indirizzo della variabile, o -1 se la variabile non esiste o comincia per il prefisso riservato "gl_".
Ciò accade piuttosto spesso (per shader sperimentali), perché l'ottimizzatore elimina automaticamente durante la compilazione tutte le variabili inutilizzate. In questa situazione la variabile è definita correttamente, ma nessuno spazio di memoria è stato allocato per essa.

Si può scrivere il contenuto della variabile con una delle funzioni di OpenGL che cominciano con glUniform. È definita una funzione di questo genere per ogni tipo predefinito di variabile all'interno di uno shader.

void glUniform1f(GLint location,GLfloat v0);                                       // float
void glUniform2f(GLint location,GLfloat v0,GLfloat v1);                            // vec2
void glUniform3f(GLint location,GLfloat v0,GLfloat v1,GLfloat v2);                 // vec3
void glUniform4f(GLint location,GLfloat v0,GLfloat v1,GLfloat v2,GLfloat v3);      // vec4

void glUniform1i(GLint location,GLint v0);                                         // int
void glUniform2i(GLint location,GLint v0,GLint v1);                                // ivec2
void glUniform3i(GLint location,GLint v0,GLint v1,GLint v2);                       // ivec3
void glUniform4i(GLint location,GLint v0,GLint v1,GLint v2,GLint v3);              // ivec4

Per le variabili booleane è possibile usare sia le funzioni per gli interi che quelle per i float, con la convenzione che 0 (o 0.0) vale false e qualunque altro valore vale true.

Sono definite anche funzioni per modificare array di tipi fondamentali. Nelle funzioni che seguono, v0 deve essere un puntatore a un array di variabili di dimensione pari a count per la dimensione della variabile predefinita (1,2,3 o 4).

void glUniform1fv(GLint location,GLsizei count,const GLfloat * v0);                // float
void glUniform2fv(GLint location,GLsizei count,const GLfloat * v0);                // vec2
void glUniform3fv(GLint location,GLsizei count,const GLfloat * v0);                // vec3
void glUniform4fv(GLint location,GLsizei count,const GLfloat * v0);                // vec4

void glUniform1iv(GLint location,GLsizei count,GLint * v0);                        // int
void glUniform2iv(GLint location,GLsizei count,GLint * v0);                        // ivec2
void glUniform3iv(GLint location,GLsizei count,GLint * v0);                        // ivec3
void glUniform4iv(GLint location,GLsizei count,GLint * v0);                        // ivec4

count deve essere minore o uguale al numero di elementi del'array. Se è minore, solo i primi count elementi sono modificati.

Infine, ci sono anche le funzioni per modificare le matrici uniform. Il parametro transpose è vero se la matrice deve essere trasposta prima di essere copiata. count contiene il numero di matrici consecutive da copiare, se la destinazione è un array.

void glUniformMatrix2fv(GLint location,GLsizei count,GLboolean transpose,const GLfloat * value);    // matrici 2x2
void glUniformMatrix3fv(GLint location,GLsizei count,GLboolean transpose,const GLfloat * value);    // matrici 3x3
void glUniformMatrix4fv(GLint location,GLsizei count,GLboolean transpose,const GLfloat * value);    // matrici 4x4

Le chiamate a funzioni glUniform possono essere integrate all'interno di display list. Ovviamente, ciò non vale per glGetUniformLocation.

Variabili attribute

Le variabili attribute sono dichiarate come variabili globali premettendo la parola chiave attribute al nome della variabile, allo stesso modo delle variabili uniform.

attribute float color2;

A contrario delle variabili uniform, le variabili attribute possono essere modificate in qualunque momento, anche all'interno della definizione di una primitiva, perché il loro scopo è quello di offrire attributi personalizzati ai vertici.
Per lo stesso motivo, però, le variabili attribute sono visibili soltanto all'interno dei vertex shader.

Il procedimento per modificare una variabile attribute è simile al precedente. Per prima cosa bisogna procurarsi il suo indirizzo:

GLint glGetAttribLocation(GLuint program,const GLchar * name);

Mentre è possibile cambiare il valore di una variabile attribute all'interno la definizione di una primitiva, il suo indirizzo deve essere ottenuto prima di incominciare la definizione.

In alternativa all'uso della funzione precedente, prima della compilazione del program, è possibile forzare l'allocazione di una variabile attribute in una posizione predefinita dal programmatore. Si usa la funzione:

void glBindAttribLocation(GLuint program,GLuint index,const GLchar * name);

Dove program è l'id del program, index è la posizione desiderata per la variabile attribute e name è il nome della variabile attribute com'è definita all'interno dello shader. index deve essere compreso tra 0 e GL_MAX_VERTEX_ATTRIBS-1.

Se la variabile è una matrice con n colonne, questa funzione occupa anche gli n-1 valori successivi a index per allocare le colonne oltre la prima (index+1 per la seconda colonna, index+2 per la terza e così via).

Tramite glBindAttribLocation è possibile fare in modo che program diversi allochino variabili con lo stesso significato nella stessa posizione. Ciò permette di modificare l'attributo in tutti i program simultaneamente, con un'unica chiamata di funzione.

Le funzioni per modificare le variabili attribute hanno un funzionamento simile a quelle per le variabili uniform, ma sono definite solo per i float:

void glVertexAttrib1f(GLuint index,GLfloat v0);                                         // float
void glVertexAttrib2f(GLuint index,GLfloat v0,GLfloat v1);                              // vec2
void glVertexAttrib3f(GLuint index,GLfloat v0,GLfloat v1,GLfloat v2);                   // vec3
void glVertexAttrib4f(GLuint index,GLfloat v0,GLfloat v1,GLfloat v2,GLfloat v3);        // vec4

Le versioni per modificare gli array non sono definite, invece. Ci sono solo alcune funzioni identiche alle precedenti ma che accettano un puntatore ai parametri invece dei parametri diretti. Una di esse permette di modificare anche i vettori di 4 interi.

void glVertexAttrib1fv(GLuint index,const GLfloat * v);              // float
void glVertexAttrib2fv(GLuint index,const GLfloat * v);              // vec2
void glVertexAttrib3fv(GLuint index,const GLfloat * v);              // vec3
void glVertexAttrib4fv(GLuint index,const GLfloat * v);              // vec4

void glVertexAttrib4iv(GLuint index,const GLint * v);                // ivec4

Come per le variabili uniform, anche glVertexAttrib può essere integrato all'interno delle display list.

Altre funzioni di questo tipo possono essere trovate nella reference di OpenGL alla sezione "glVertexAttrib".

Esempio

Si modifica la funzione ApplyLight per ignorare i parametri di illuminazione predefiniti e definire variabili soltanto per quelli che servono. Il programma OpenGL deve occuparsi di inizializzarle tutte.

uniform vec3 light1position;
uniform vec3 light1diffuse;
uniform vec3 light1specular;
uniform vec3 light1ambient;

attribute float matshininess;
attribute vec3 matspecular;

vec4 ApplyLight(in vec4 oColor, in vec3 relativepos, in vec3 normal)
  {
  vec3 intensity = vec3(0.0,0.0,0.0);

  vec3 lightrelative = normalize(light1position - relativepos); // incident light vector
      
  /* ambient */
  intensity += light1ambient * oColor.rgb;

  float dotp = dot(lightrelative,normal);
  if (dotp > 0.0) /* if face is not oriented towards light, skip specular and diffuse */
    {
    /* diffuse */
      {
      vec3 diffuse = light1diffuse * dotp * oColor.rgb;
      intensity += diffuse;
      }
    /* specular */
      {
      vec3 reflected = reflect(lightrelative,normal);
      vec3 viewr = normalize(relativepos);
      float weight = pow(max(0.0,dot(reflected,viewr)),matshininess);
      intensity += light1specular * max(weight,0.0) * matspecular;
      }
    }

  return vec4(clamp(intensity,0.0,1.0),oColor.a);
  }

Il contenuto del listato sovrastante è rilasciato nel pubblico dominio.

Tra le altre cose si è risparmiato un parametro per la funzione, nonché alcune moltiplicazioni per gli (inutili) alpha del colore della luce. Inoltre, la divisione per 4 della shininess non è più necessaria.

Configurazione per l'esempio

#define SHADER_FOLDER "shaders/custom_lighting/"

#define USE_VERTEX_SHADER true
#define USE_FRAGMENT_SHADER false
#define USE_GEOMETRY_SHADER false
#define USE_NORMAL_SHADER false
#define USE_VERTEX_LIGHT_SHADER true
#define USE_FRAGMENT_LIGHT_SHADER false
#define USE_TESSEVAL_LIGHT_SHADER false
#define USE_TESSELLATION_SHADERS false

#define GEOMETRY_INPUT_TYPE GL_TRIANGLES         // irrilevante
#define GEOMETRY_OUTPUT_TYPE GL_TRIANGLE_STRIP   // irrilevante
#define GEOMETRY_MAX_VERTICES 3                  // irrilevante

#define ENABLE_BACK_FACE_CULLING true
#define LOAD_NORMALS true
#define TIMER_MS 0

Il risultato è identico al precedente:

Illuminazione con vertex shader 2

Copyright © 2011 Riccardo Monica
Eccetto ove espressamente indicato altrimenti, il contenuto di questa pagina è disponibile secondo la licenza Creative Commons Attribuzione - Condividi allo stesso modo 3.0.