I. Introduction

Image non disponible

Les Uniform Buffer Objects (ou UBO pour faire court) ont été introduits avec OpenGL 3.1. La bible des tampons de variables uniformes peut être trouvée ici : GL_ARB_uniform_buffer_object.

Les tampons de variables uniformes sont des zones allouées dans la mémoire vidéo de la carte graphique (ce sont des tampons GPU) qui permettent de passer des données de l'application hôte aux programmes GLSL.

Le principal avantage d'utiliser les tampons de variables uniformes est qu'ils peuvent être partagés entre plusieurs shaders GLSL. Donc, un seul UBO est suffisant pour tous les shaders utilisant les mêmes données.

Du point de vue du shader GLSL, un tampon de variables uniformes est un tampon mémoire en lecture seule.

Prenons un exemple simple : nous avons besoin de passer à un shader la position de la caméra, la position de la lumière ainsi que la couleur diffuse de la lumière. Avec les variables uniformes habituelles, nous aurions ces entrées :

 
Sélectionnez
uniform vec4 camera_position; 
uniform vec4 light_position; 
uniform vec4 light_diffuse;

Si nous avons besoin de ces variables dans tous les shaders, nous devons mettre à jour toutes les variables uniformes pour chacun des shaders (avec des appels à glUniform4f() dans OpenGL, ou gh_gpu_program.unoform4f() dans GLSL Hacker). S'il n'y a qu'un seul shader, il n'y a pas vraiment de problème, mais s'il y en a 10 ou même 100 différents, c'est ennuyeux. Pour les variables qui nécessitent des mises à jour fréquentes, comme la position de la caméra (ou les matrices de transformation), il est nécessaire d'avoir un meilleur moyen pour les mettre toutes à jour en une seule fois.

Un tampon de variables uniformes est une solution simple et efficace. Dans l'application hôte (C/C++), nous utilisons la structure de données suivante :

 
Sélectionnez
struct shader_data_t 
{ 
  float camera_position[4]; 
  float light_position[4]; 
  float light_diffuse[4]; 
} shader_data;

Ensuite nous créons un UBO de cette structure de données. Une fois l'UBO créé, initialisé avec les données et lié (comme n'importe quel autre type d'objet OpenGL), nous pouvons lire les données à partir du tampon GPU dans un shader en utilisant la déclaration suivante :

 
Sélectionnez
#version 150 
... 
layout (std140) uniform shader_data 
{ 
  vec4 camera_position; 
  vec4 light_position; 
  vec4 light_diffuse; 
}; 
... 
void main() 
{ 
  ... 
}

Dans cet exemple de code, shader_data est un bloc de variables uniformes (ou plus généralement : un bloc interface comme nous allons le voir dans un futur article).

Nous avons juste besoin de mettre à jour une fois par trame (si nécessaire) cet UBO et tous les shaders accédant au tampon auront les données des variables uniformes à jour.

Chaque contexte de rendu OpenGL ne peut avoir qu'un nombre limité d'UBO liés en même temps. Avec une GeForce GTX 660 par exemple, nous pouvons lier jusqu'à 84 UBO à la fois. Chaque shader GLSL (vertex, pixel, compute, tessellation ou geometry) peuvent avoir jusqu'à 14 blocs de variables uniformes pour une GTX 660. La somme de tous les blocs de variables uniformes de tous les shaders pour une GTX 660 est de 84…

Un UBO possède aussi une taille limitée. Avec une GTX 660, sa taille ne peut pas dépasser 64 ko ou 65536 octets.

Ces valeurs (84, 14, 65536) sont des limites dépendantes du GPU. Nous pouvons demander à OpenGL ces limites avec la fonction glGetIntergerv() :

 
Sélectionnez
Limites de la GeForce GTX 660 (R337.50) : 
- GL_MAX_UNIFORM_BUFFER_BINDINGS -> 84 
- GL_MAX_UNIFORM_BLOCK_SIZE -> 65536 
- GL_MAX_VERTEX_UNIFORM_BLOCKS -> 14 
- GL_MAX_FRAGMENT_UNIFORM_BLOCKS -> 14 
- GL_MAX_GEOMETRY_UNIFORM_BLOCKS -> 14

Pour une puce Intel HD Graphics 4600, nous obtenons :

 
Sélectionnez
Limites de la Intel HD Graphics 4600 (v3652) : 
- GL_MAX_UNIFORM_BUFFER_BINDINGS -> 84 
- GL_MAX_UNIFORM_BLOCK_SIZE -> 16384 
- GL_MAX_VERTEX_UNIFORM_BLOCKS -> 14 
- GL_MAX_FRAGMENT_UNIFORM_BLOCKS -> 14 
- GL_MAX_GEOMETRY_UNIFORM_BLOCKS -> 14

Et pour une AMD Radeon HD 7970 :

 
Sélectionnez
Limites de la AMD Radeon HD 7970 (Catalyst 14.4) : 
- GL_MAX_UNIFORM_BUFFER_BINDINGS -> 75 
- GL_MAX_UNIFORM_BLOCK_SIZE -> 65536 
- GL_MAX_VERTEX_UNIFORM_BLOCKS -> 15 
- GL_MAX_FRAGMENT_UNIFORM_BLOCKS -> 15 
- GL_MAX_GEOMETRY_UNIFORM_BLOCKS -> 15

II. Détails d'OpenGL

Voyons comment créer et mettre à jour un tampon de variables uniformes.

Création et initialisation :

 
Sélectionnez
GLuint ubo = 0; 
glGenBuffers(1, &ubo); 
glBindBuffer(GL_UNIFORM_BUFFER, ubo); 
glBufferData(GL_UNIFORM_BUFFER, sizeof(shader_data), &shader_data, GL_DYNAMIC_DRAW); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);

Mise à jour d'un UBO : nous récupérons un pointeur sur la mémoire GPU (cette opération est appelée « mapping ») et nous effectuons simplement une copie de mémoire vers mémoire :

 
Sélectionnez
glBindBuffer(GL_UNIFORM_BUFFER, gbo); 
GLvoid* p = glMapBuffer(GL_UNIFORM_BUFFER, GL_WRITE_ONLY); 
memcpy(p, &shader_data, sizeof(shader_data)) 
glUnmapBuffer(GL_UNIFORM_BUFFER);

Maintenant, voyons comment faire la liaison entre un UBO et le programme GLSL. La première étape est de trouver l'indice du bloc de variables uniformes dans le shader :

 
Sélectionnez
unsigned int block_index = glGetUniformBlockIndex(program, "shader_data");

La seconde étape est de connecter le bloc de variables uniformes à l'UBO. Nous connectons le bloc interface avec l'UBO en utilisant l'indice du point de liaison du tampon de variables uniformes. Par défaut, si vous appelez seulement glBindBuffer(GL_UNIFORM_BUFFER,gbo), l'UBO est lié au premier point de liaison (index=0). Si nous en avons besoin, et c'est obligatoire lorsque nous avons plus d'un UBO, nous pouvons spécifier un point de liaison particulier pour l'UBO.

La spécification d'un point de liaison (ici index=2) est effectuée avec :

 
Sélectionnez
GLuint binding_point_index = 2; 
glBindBufferBase(GL_UNIFORM_BUFFER, binding_point_index, ubo);

Nous aurions pu lier l'UBO sur le point de liaison 23 ou même 80. Le nombre maximal des points de liaison peut être récupéré avec GL_MAX_UNIFORM_BUFFER_BINDINGS. Pour la GeForce GTX 660, il y a 84 points de liaison possibles dans le même contexte OpenGL. OpenGL maintient une sorte de tableau de pointeurs sur les différents tampons de variables uniformes. Chaque entrée de ce tableau est un point de liaison. Pour la GTX 660, ce tableau contient 84 éléments. L'image suivante montre le tableau des points de liaison des tampons de variables uniformes d'une GTX 660 avec trois UBO liés aux points 0, 2 et 82 :

Image non disponible

Une fois que l'UBO est rattaché à un point de liaison, nous pouvons créer la connexion entre l'UBO et le shader :

 
Sélectionnez
GLuint binding_point_index = 2; 
glUniformBlockBinding(program, block_index, binding_point_index);

Je pense avoir couvert les notions de base sur les objets de tampon de variables uniformes. Nous savons ce qu'est un UBO, comment le gérer et lire son contenu à partir du shader GLSL.

III. Démonstrations et références

J'ai codé une petite démonstration avec GLSL Hacker qui dessine un rectangle texturé avec un programme GLSL. Rien de spécial. Les choses deviennent intéressantes si nous regardons la façon dont les matrices de la camera sont envoyées au shader : avec un tampon de variables uniformes. Cette démonstration est disponible dans le dossier host_api/gl-310-arb-uniform-buffer/ du pack de démonstration de GLSL Hacker.

Dans GLSL Hacker 0.7.0+, j'ai ajouté une nouvelle bibliothèque Lua/Python appelée gh_gpu_shader. Cette bibliothèque bas niveau permet de gérer tous les types de tampons GPU, notamment les tampons de variables uniformes.

IV. Références

V. Remerciements

Cet article est une traduction autorisée de l'article paru sur Geeks3D.com.

Merci à Winjerome pour sa relecture attentive et Phanloga pour sa relecture orthographique.