IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre OpenGL moderne

Quatrième partie : OpenGL avancé


précédentsommairesuivant

VIII. GLSL Avancé

Ce tutoriel ne présente pas vraiment de fonctionnalités avancées super cool qui apportent une énorme amélioration visuelle à votre scène. Ce tutoriel plonge dans certains aspects plus ou moins intéressant du GLSL et certaines astuces devraient vous aider dans vos projets futurs. Grossièrement, ce sont des fonctionnalités à connaître et qui peuvent rendre la vie plus simple lors de la création d’applications OpenGL et GLSL.

Nous allons voir quelques variables GLSL intéressantes, de nouvelles façons d’organiser les entrées et sorties des shaders et le superbe outil que sont les tampons uniformes.

VIII-A. Variables GLSL

Les shaders ne contiennent que le minimum. Si nous avons besoin de données autres que les entrées du shader, nous devons passer ces données en argument. Nous avons appris à le faire avec les attributs de sommets, les variables uniformes et les échantillonneurs. En réalité, il existe d’autres variables proposées par GLSL et préfixées par gl_ qui offrent de nouvelles façons pour récupérer ou écrire des données. Nous en avons déjà vus deux d’entre elles : gl_Position, le vecteur en sortie du vertex shader, et gl_FragCoord, la couleur en sortie du fragment shader.

Nous allons voir quelques variables intéressantes d’entrée et de sortie proposées par GLSL et qui peuvent nous être utiles. Notez que nous n’allons pas couvrir toutes les variables du GLSL. Vous pouvez accéder à une liste de celle-ci dans le wiki d’OpenGL.

VIII-A-1. Variables du vertex shader

Nous connaissons déjà gl_Position, représentant la position du sommet dans l’espace de découpage, calculée dans le vertex shader. Il est obligatoire de définir la variable gl_Position dans le vertex shader pour obtenir quelque chose à l’écran. Rien de nouveau.

VIII-A-1-a. gl_PointSize

Lors du rendu, nous pouvons choisir la primitive GL_POINTS. Dans ce cas, chaque sommet représente un point. Il est possible de définir la taille des points grâce à la fonction glPointSize(), mais il est aussi possible d’influer la valeur dans le vertex shader.

Par défaut, vous ne pouvez pas affecter la taille des points dans le vertex shader. Vous devez l’activer avec l’option GL_PROGRAM_POINT_SIZE :

 
Sélectionnez
glEnable(GL_PROGRAM_POINT_SIZE);

Une méthode simple pour modifier la taille des points est d’utiliser la composante z de la position dans l’espace de découpage des sommets. Elle correspond à la distance entre le sommet et le spectateur. La taille des points doit augmenter plus ils sont loin du spectateur.

 
Sélectionnez
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    gl_PointSize = gl_Position.z;    
}

Le résultat est que les points sont de plus en plus grand lorsqu’on s’éloigne d’eux :

Points en OpenGL dessinés avec gl_PointSize() et modifié dans le vertex shader

Vous pouvez imaginer la taille des points pour chaque sommet dans votre système de particules.

VIII-A-1-b. gl_VertexID

Les variables gl_Position et gl_PointSize sont des sorties, car leur valeur est lue après l’exécution du vertex shader : ces variables nous permettent de modifier le rendu en leur donnant une valeur. Le vertex shader fournit aussi des variables d’entrée intéressantes et que nous pouvons lire, notamment gl_VertexID.

La variable gl_VertexID contient l’identifiant du sommet en cours de dessin. Lorsque vous faites un rendu indexé (avec la fonction glDrawElements()), cette variable contient l’indice du sommet en cours de dessin. Lors d’un rendu sans indices (avec la fonction glDrawArrays()), cette variable contient le numéro du sommet en cours d’affichage (en démarrant à partir du début de l’appel de dessin).

Bien que cela ne soit pas utile pour le moment, c’est toujours bon de savoir que nous avons accès à une telle information.

VIII-A-2. Variables du fragment shader

Le fragment shader fournit aussi des variables intéressantes. Le GLSL nous proposent deux variables d’entrées intéressantes : gl_FragCoord et gl_FrontFacing.

VIII-A-2-a. gl_FragCoord

Nous avons déjà vu plusieurs fois la variable gl_FragCoord dans le tutoriel sur les tests de profondeur, car la composante z de la variable gl_FragCoord est égale à la valeur de la profondeur pour ce fragment. Toutefois, nous pouvons aussi utiliser les composants x et y du vecteur pour obtenir des effets intéressants.

Les composants x et y de la variable gl_FragCoord sont les coordonnées du fragment, dans l’espace écran, partant du bas gauche de la fenêtre. Nous avons défini une fenêtre de 800x600 avec glViewport(). Nous aurons donc des valeurs de 0 à 800 pour la composante x et de 0 à 600 pour la composante y.

En utilisant le fragment shader, nous pouvons générer une couleur différente suivant la position du fragment dans la fenêtre. Une utilisation classique est l’emploi de la variable gl_FragCoord en affichant une couleur différente suivant les calculs du fragment. Par exemple, nous pouvons découper l’écran en deux, en affichant une couleur pour la partie gauche et une autre pour la partie de droite. Voici un fragment shader qui le fait suivant les coordonnées du fragment dans la fenêtre :

 
Sélectionnez
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);        
}

Comme la largeur de la fenêtre est égale à 800, chaque fois que la composante x du pixel est inférieure à 400, il doit être sur le côté gauche de la fenêtre et donne donc une couleur différente.

Cube en OpenGL dessiner avec deux couleurs grâce à gl_FragCoord

Nous pouvons maintenant utiliser deux fragment shader distincts qui vont afficher un résultat différent sur chaque partie de la fenêtre. C’est utile pour tester des techniques d’éclairage différentes.

VIII-A-2-b. gl_FrontFacing

gl_FrontFacing est une autre variable d’entrée du fragment shader. Dans le tutoriel sur l’élimination des faces, nous avons mentionné qu’OpenGL est capable de déterminer si la face est une face avant ou arrière grâce à l’ordre des sommets. Si nous n’utilisons pas l’élimination des faces (en activant GL_FACE_CULL), alors la variable gl_FrontFacing indique si le fragment actuel fait partie de la face avant ou arrière, nous pouvons alors décider de calculer une couleur différente pour l’une et l’autre.

La variable gl_FrontFacing est un booléen qui est à true si le fragment fait partie de la face avant. Sinon, la variable est à false. Par exemple, nous pouvons créer un cube ayant une texture différente à l’intérieur :

 
Sélectionnez
#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);
}

Si nous regardons à l’intérieur du conteneur, nous pouvons voir qu’une texture différente est utilisée :

Le containeur OpenGL utilise deux textures différentes grâce à gl_FrontFacing

Notez que, si vous activez l’élimination des faces, vous ne pourrez pas voir l’intérieur du conteneur et l’utilisation de gl_FrontFacing sera donc inutile.

VIII-A-2-c. gl_FragDepth

La variable d’entrée gl_FragCoord permet de lire les coordonnées en espace écran et d’obtenir la valeur de profondeur du fragment actuel. Toutefois, c’est une variable en lecture seule. Nous ne pouvons pas influer sur les coordonnées en espace écran du fragment, mais il est possible de définir la profondeur du fragment. GLSL nous offre une variable de sortie appelée gl_FragDepth qui peut être utilisée pour modifier la profondeur du fragment dans le shader.

Pour définir la valeur de profondeur, il suffit d’écrire une valeur entre 0,0 et 1,0 dans la variable gl_FragDepth :

 
Sélectionnez
gl_FragDepth = 0.0; // le fragment a maintenant une profondeur de 0.0

Si le shader ne fournit pas de valeur à la variable gl_FragDepth, la valeur de gl_FragCoord.z sera utilisée.

Lorsque la profondeur est définie par nous-même, OpenGL désactive l’optimisation du test de profondeur anticipé (comme vu dans le tutoriel sur les tests de profondeur). OpenGL ne peut pas connaître la profondeur avant d’exécuter le fragment shader, comme ce dernier pourrait entièrement en changer la valeur.

En écrivant dans la variable gl_FragDepth, vous devez prendre en considération cet impact sur les performances. Toutefois, en OpenGL 4.2, nous pouvons obtenir un compromis en redéclarant la variable gl_FragDepth au début du fragment shader :

 
Sélectionnez
layout (depth_<condition>) out float gl_FragDepth;

La condition peut être une des valeurs suivantes :

Condition

Description

any

La valeur par défaut. Le test de profondeur anticipé est désactivé et vous allez perdre en performance.

greater

Vous ne pouvez écrire qu’une valeur. supérieure à gl_FragCoord.z.

less

Vous ne pouvez écrire qu’une valeur inférieure à gl_FragCoord.z.

unchanged

Si vous écrivez dans la variable gl_FragDepth, vous allez utiliser gl_FragCoord.z.

En indiquant greater ou less comme condition, OpenGL peut donc partir du principe que vous allez écrire une valeur plus grande ou inférieure que la valeur actuelle. De cette façon, OpenGL est encore capable de faire un test de profondeur anticipé dans les cas où la valeur de profondeur est inférieure ou supérieure à la valeur du fragment.

Voici un exemple où nous incrémentons la valeur de profondeur dans le fragment, mais où nous conservons le test de profondeur anticipé :

 
Sélectionnez
#version 420 core // notez la version GLSL !
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;

void main()
{             
    FragColor = vec4(1.0);
    gl_FragDepth = gl_FragCoord.z + 0.1;
}

Notez que cette fonctionnalité n’est disponible qu’à partir d’OpenGL 4.2.

VIII-B. Blocs d’interface

Jusqu’à présent, chaque fois que nous voulions envoyer des données du vertex shader au fragment shader, nous déclarions plusieurs variables des deux côtés. les déclarer une par une est la méthode la plus simple pour transférer des données, mais, pour des applications de plus en plus imposantes, nous voudrions certainement envoyer plus de variables, dont des tableaux ou des structures.

Pour nous aider à organiser ces variables, GLSL nous propose des blocs d’interface qui nous permettent de regrouper ces variables. La déclaration d’un tel bloc ressemble beaucoup à la déclaration d’une structure, sauf que nous devons utiliser les mots clés in et out pour définir un bloc d’entrée ou de sortie.

 
Sélectionnez
#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;
}

Cette fois, nous avons déclaré un bloc d’interface appelé vs_out qui regroupe toutes les sorties que nous souhaitons envoyer au shader suivant. Cet exemple est trivial, mais vous pouvez imaginer combien cela peut aider à organiser les entrées sorties de vos shaders. C’est aussi pratique lorsque nous voulons regrouper des entrées/sorties dans des tableaux, comme nous allons le voir dans le prochain tutoriel sur les geometry shaders.

Ensuite, nous allons déclarer un bloc d’entrée dans le shader suivant, soit, le fragment shader. Le nom du bloc (VS_OUT) doit être le même, mais l’instance (vs_out dans le vertex shader) peut être n’importe quoi d’autre. Évitez donc d’utiliser des noms apportant confusion comme vs_out alors que ce sont des variables d’entrée.

 
Sélectionnez
#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
}

Tant que les deux noms d’interface correspondent, l’entrée et la sortie seront connectées. C’est une autre fonctionnalité utile pour vous aider à connecter vos shaders ; elle s’avère d’autant plus utile lors de l’implémentation d’étapes supplémentaires comme le geometry shader.

VIII-C. Tampon de variables uniformes

Nous avons utilisé OpenGL depuis quelque temps et appris pas mal d’astuces, mais aussi quelques trucs pénibles. Par exemple, lorsque nous utilisons plus d’un shader, nous devons définir des variables uniformes, et ce, même si ce sont exactement les mêmes pour chaque shader. Pourquoi donc s’embêter à les redéfinir ?

OpenGL nous offre un outil nommé les tampons de variables uniformes (uniform buffer objects). Cela nous permet de déclarer un ensemble de variables uniformes globales, qui sera disponible au travers de plusieurs shaders. Lors de l’utilisation d’un tampon de variables uniformes, nous avons donc un ensemble de variables uniformes défini une seule fois. Nous pouvons toujours définir des variables uniformes manuellement, qui sont accessibles uniquement par un shader. La création et la configuration d’un tampon de variables uniformes demande toutefois un peu de travail.

Comme un tampon de variables uniformes est un tampon, nous le créons comme tout autre tampon avec la fonction glGenBuffers(), lié à la cible GL_UNIFORM_BUFFER et nous stockons toutes les données des variables uniformes dans le tampon, il faut suivre certaines règles pour faire cela. Pour le moment, nous allons simplement prendre un vertex shader et stocker notre matrice de projection et de vue dans un tampon de variables uniformes :

 
Sélectionnez
#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);
}

Dans la majorité de nos exemples, nous définissons la matrice de projection et de vue à chaque itération, et ce, pour chaque shader que nous utilisons. C’est un exemple parfait pour utiliser un tampon de variables uniformes, car nous n’avons alors plus qu’à stocker ces matrices une seule fois.

Ici, nous avons déclaré un bloc de variables uniformes appelé Matrices, qui stocke deux matrices 4x4. Les variables dans un bloc de variables uniformes sont accessibles sans devoir utiliser le nom du bloc comme préfixe. Ensuite, nous stockons ces matrices dans un tampon quelque part dans le code OpenGL et chaque shader déclaré avec ce bloc de variables uniformes pourra accéder aux matrices.

Vous vous demandez sûrement ce qu’est ce layout (std140). Cela indique que le bloc de variables uniformes défini utilise une disposition mémoire spécifique.

VIII-C-1. Disposition du bloc de variables uniformes

Le contenu du bloc de variables uniformes est stocké dans un tampon, qui n’est rien d’autre qu’un morceau de mémoire. Comme ce morceau ne contient aucune information sur ce qu’il contient, nous devons indiquer à OpenGL quelle partie de cette mémoire correspond à quelle variable uniforme du shader.

Imaginez le bloc de variables uniformes suivant dans un shader :

 
Sélectionnez
layout (std140) uniform ExampleBlock
{
    float value;
    vec3  vector;
    mat4  matrix;
    float values[3];
    bool  boolean;
    int   integer;
};

Ce que nous voulons connaître est la taille (en octets) et le décalage (à partir du début du bloc) de chacune de ces variables afin que nous puissions les placer correctement dans le tampon, et ce, en respectant l’ordre. La taille de chacun de ces éléments est définie dans OpenGL et correspond au type de données C++ ; les vecteurs et les matrices étant des (grands) tableaux de nombre flottants. Par contre, OpenGL n’indique pas l’espacement entre les variables. C’est au matériel de positionner les variables comme il le peut. Certaines machines peuvent placer un vec3 à côté d’un float, par exemple, mais toutes ne peuvent pas gérer une telle disposition et devront placer le vec3 dans un emplacement de quatre nombres flottants, puis placer le float. Une bonne fonctionnalité, mais pénible pour nous.

Par défaut, le GLSL utilise une disposition de la mémoire appelée disposition partagée – partagée, car les décalages sont définis par le matériel et sont donc partagés entre plusieurs shaders. Avec une disposition partagée, le GLSL peut repositionner les variables uniformes à des fins d’optimisation tant que l’ordre n’est pas impacté. Comme nous ne savons pas à quel endroit les variables uniformes seront, nous ne savons pas comment remplir notre tampon. Nous pouvons récupérer cette information avec des fonctions comme glGetUniformIndices(), mais cela dépasse le cadre de ce tutoriel.

Même si la disposition partagée permet de gagner un peu d’espace, nous ne souhaitons pas devoir récupérer le décalage de chaque variable uniforme. Une pratique générique est de ne pas utiliser une disposition partagée, mais la disposition std140. La disposition std140 indique explicitement la disposition mémoire de chaque variable en définissant leur décalage respectif grâce à un ensemble de règles. Comme cela est explicitement défini, nous pouvons déterminer manuellement le décalage pour chaque variable.

Chaque variable a un alignement de base qui est égal à l’espace que la variable prend (en prenant en compte le remplissage [padding]) dans un bloc de variables uniformes. L’alignement de base est calculé en utilisant les règles de la disposition mémoire std140. Ensuite, pour chaque variable, nous calculons son décalage aligné, qui correspond au décalage en octets d’une variable par rapport au commencement du bloc. Le décalage aligné en octets est une variable qui doit être égale à un multiple de l’alignement de base.

Les règles de disposition peuvent être trouvées dans la spécification des tampons de variables uniformes d’OpenGL ici, mais voici une liste des règles les plus communes. Chaque type de variables en GLSL, tels quel les int, float et bool sont définis sur quatre octets et chaque entité de quatre octets est représentée par N.

Type

Règle de disposition

Scalaire (c’est-à-dire int ou bool)

Chaque scalaire a un alignement de base de N.

Vecteur

Soit 2N, soit 4N. Cela signifie qu’un vec3 a un alignement de base de 4N.

Tableau de scalaires ou de vecteurs

Chaque élément a un alignement de base d’un vec4.

Matrice

Stockée comme un grand tableau de vecteurs colonnes, où chacun de ces vecteurs a l’alignement d’un vec4.

Structure

Équivalente à la taille calculée par ces éléments suivant les règles précédentes.

Comme avec la plus grande partie de la spécification OpenGL, c’est plus simple de comprendre avec un exemple. Nous prenons le bloc de variables uniformes appelé ExampleBlock, que nous introduirons plus tard et nous calculons le décalage aligné de chacun de ses membres avec la disposition std140 :

 
Sélectionnez
layout (std140) uniform ExampleBlock
{
                     // alignement de base  // décalage aligné
    float value;     // 4                   // 0 
    vec3 vector;     // 16                  // 16  (doit être un multiple de 16, donc 4 -> 16)
    mat4 matrix;     // 16                  // 32  (colonne 0)
                     // 16                  // 48  (colonne 1)
                     // 16                  // 64  (colonne 2)
                     // 16                  // 80  (colonne 3)
    float values[3]; // 16                  // 96  (valeur[0])
                     // 16                  // 112 (valeur[1])
                     // 16                  // 128 (valeur[2])
    bool boolean;    // 4                   // 144
    int integer;     // 4                   // 148
};

Comme exercice, essayez de calculer les valeurs de décalage vous-même et comparez-les avec ce tableau. Avec les décalages calculés, basés sur les règles de la disposition std140, nous pouvons remplir le tampon avec les données des variables à chaque décalage avec la fonction glBufferSubData(). Même si ce n’est pas plus efficace, la disposition std140 nous garantit que la disposition mémoire reste la même pour chaque programme ayant déclaré ce bloc de variables uniformes.

En ajoutant le layout (std140) avant la définition du bloc de variables uniformes, nous indiquons à OpenGL d’utiliser la disposition std140. Il y a deux autres dispositions qui nécessitent de récupérer le décalage avant de remplir le tampon. Nous avons déjà vu la disposition partagée, la seconde étant la disposition empaquetée. Lors de l’utilisation de la disposition empaquetée, il n’y a pas de garantie que la disposition reste la même entre les shader (non partagée), car elle permet au compilateur de retirer les variables uniformes du bloc, ce qui peut différer entre les shaders.

VIII-C-2. Utiliser un tampon de variables uniformes

Nous avons vu comment définir les blocs de variables uniformes dans les shaders et comment spécifier la disposition mémoire, mais nous n’avons pas encore vu comment les utiliser.

Premièrement, nous devons créer un tampon de variables uniformes avec la fonction glGenBuffers(). Une fois que nous avons un tampon, nous le lions à la cible GL_UNIFORM_BUFFER et lui allouons assez de mémoire grâce à glBufferData().

 
Sélectionnez
unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // allouer 150 octets de mémoire
glBindBuffer(GL_UNIFORM_BUFFER, 0);

Maintenant, chaque fois que nous mettons à jour ou insérons des données dans le tampon, nous utiliserons uboExampleBlock et la fonction glBufferSubData() pour mettre à jour sa mémoire. Nous ne mettons à jour le tampon de variables uniformes qu’une seule fois, et chaque shader l’utilisant aura accès aux données à jour. Maintenant, comment OpenGL sait-il quel tampon de variables uniformes correspond à quel bloc ?

Dans le contexte OpenGL, il y a un nombre de points de liaison auxquels nous pouvons lier le tampon. Une fois le tampon de variables uniformes créé, nous le lions à un de ces points de liaison et nous lions aussi le bloc de variables uniformes à ce même point, les liant ainsi. Le diagramme suivant illustre cela :

Diagramme des points de liaison des variables uniformes dans OpenGL

Comme vous pouvez le voir, nous pouvons lier plusieurs tampons de variables uniformes à plusieurs points de liaison. Comme un shader A et un shader B ont un bloc de variables uniformes lié à un même point de liaison 0, leur bloc partage les même données de variables uniformes, provenant de uboMatrices. La seule contrainte est que les deux shaders doivent définir le même bloc de variables uniformes Matrices.

Pour définir un bloc de variables uniformes à un point de liaison spécifique, nous appelons glUniformBlockBinding(), qui prend un program shader comme premier argument, un index de bloc de variables uniformes et un point de liaison auquel le lier. L’index du bloc de variables uniformes est un emplacement correspondant au bloc de variables uniformes du shader. Il peut être obtenu en appelant la fonction glGetUniformBlockIndex(), qui accepte un program shader et le nom du bloc de variables uniformes. Nous pouvons définir le bloc de variables uniformes Lights du diagramme au point de liaison 2 comme suit :

 
Sélectionnez
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

Remarquez que nous devons répéter cela pour chaque shader .

À partir d’OpenGL 4.2, il est possible de stocker les points de liaison d’un bloc de variables uniformes directement dans le shader en ajoutant un indicateur de disposition, permettant d’éviter d’appeler glGetUniformBlockIndex() et glUniformBlockBinding(). Le code suivant défini le point de liaison pour le bloc Lights :

 
Sélectionnez
layout(std140, binding = 2) uniform Lights { ... };

Ensuite, nous devons lier le tampon de variables uniformes au même point de liaison, ce qui peut être réalisé avec glBindBufferBase() ou glBindBufferRange().

 
Sélectionnez
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// ou
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);

La fonction glBindBufferBase() nécessite une cible, un index de point de liaison et un tampon de variables uniformes. Cette fonction lie le tampon uboExampleBlock au point de liaison 2. À partir de maintenant, les deux côtés du point de liaison sont liés. Vous pouvez aussi utiliser la fonction glBindBufferRange() qui attend, en plus, un paramètre pour le décalage et un autre pour la taille. De cette façon, vous pouvez lier une sous-partie du tampon de variables uniformes au point de liaison. En utilisant glBindBufferRange(), vous pouvez disposer de plusieurs blocs de variables uniformes lié à un seul tampon de celles-ci.

Maintenant que tout est configuré, nous pouvons commencer à ajouter des données au tampon de variables uniformes. Nous pourrions ajouter toutes les données comme un seul tableau d’octets ou mettre à jour des parties comme vous le souhaitez avec la fonction glBufferSubData(). Pour mettre à jour la variable booléenne uniquement, nous pourrions mettre à jour le tampon de variables uniformes comme suit :

 
Sélectionnez
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // les booléens du GLSL sont sur quatre octets, donc nous le stockons comme un entier
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);

La même procédure s’applique pour toutes les autres variables uniformes du bloc, mais avec différents arguments pour cibler les autres sous-parties.

VIII-C-3. Un exemple simple

Mettons en application l’utilisation d’un tampon de variables uniformes dans un vrai exemple. Si nous regardons en arrière dans tous les exemples de code, nous avons utilisé continuellement trois matrices : la projection, la vue et les matrices de modèles. Seule la matrice des modèles change régulièrement. Si nous avons plusieurs shaders qui utilisent ce même ensemble de matrices, nous pouvons certainement utiliser un tampon de variables uniformes.

Nous allons stocker les matrices de projection et de vue dans un bloc de variables uniformes Matrices. Nous n’allons pas stocker la matrice de modèle, car elle change régulièrement entre chaque shader et il n’y a donc aucun bénéfice de la stocker dans le tampon.

 
Sélectionnez
#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);
}

Rien de spécial ici, sauf que nous utilisons un bloc de variables uniformes avec la disposition std140. Dans cette application, nous allons afficher quatre cubes où chaque cube utilise un shader program différent. Chacun de ces quatre shader programs utilise le même vertex shader, mais un fragment shader qui affiche une couleur différente pour chaque shader.

Premièrement, nous associons le bloc de variables uniformes du vertex shader au point de liaison 0. Notez que nous devons le faire pour chaque shader.

 
Sélectionnez
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);

Ensuite, nous créons le tampon de variables uniformes et le lions aussi au point de liaison 0 :

 
Sélectionnez
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));

Premièrement, nous devons allouer assez de mémoire pour notre tampon, soit deux fois la taille d’un glm::mat4. La taille des types des matrices de GLSL correspond exactement aux mat4 du GLSL. Ensuite, nous lions la partie spécifique du tampon, soit, dans notre cas, l’intégralité du tampon, au point de liaison 0.

Maintenant, il ne nous reste plus qu’à remplir le tampon. Si nous gardons la valeur du champ de vision constant dans la matrice de projection (donc, plus de zoom), nous n’avons qu’à la définir dans notre application. Ce qui veut dire que nous n’avons qu’à l’insérer dans le tampon. Comme nous avons déjà alloué assez de mémoire pour le tampon, nous pouvons utiliser glBufferSubData() pour stocker la matrice de projection avant d’entrer dans la boucle de jeu :

 
Sélectionnez
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);

Ici, nous stockons la matrice de projection dans la première partie du tampon de variables uniformes. Avant nous dessinons les objets à chaque itération de rendu, puis nous mettons à jour la seconde partie du tampon avec la matrice de vue :

 
Sélectionnez
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);

Et c’est tout pour le tampon de variables uniformes. Chaque vertex shader qui contient le bloc de variables uniformes Matrices contiendra maintenant les données stockées par le tampon uboMatrices. Donc, si nous devons maintenant afficher quatre cubes avec quatre shaders différent, leur matrices de projection et de vue seront les mêmes :

 
Sélectionnez
glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f));    // se déplacer en haut à gauche
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);        
// ... dessiner le cube vert 
// ... dessiner le cube bleu 
// ... dessiner le cube jaune

La seule variable uniforme qu’il nous reste à définir est la variable model. En utilisant un tampon de variables uniformes, cela nous évite quelques appels à celles-ci par shader. Le résultat est le suivant :

Image de quatre cubes avec leurs variables uniformes définient avec les tampons de variables uniformes d'OpenGL

Chacun de ces cubes est placé dans chaque côté de la fenêtre en altérant la matrice du modèle et, grâce au fragment shader, chacun affiche une couleur différente. C’est un scénario plutôt simple où nous pouvons utiliser les tampons de variables uniformes, mais une grande application graphique peut avoir des centaines de shader actifs et, dans ces cas, les tampons de variables uniformes brillent.

Vous pouvez trouver le code source de l’exemple ici.

Les tampons de variables uniformes ont plusieurs avantages face aux simples variables uniformes. Premièrement, la définition de nombreuses variables en une fois est plus rapide que de définir chaque variable une à une. Ensuite, si vous souhaitez changer la même variable uniforme dans plusieurs shaders, il est bien plus facile de le faire en une fois grâce au tampon de variables uniformes. Le dernier avantage, qui n’est pas immédiat, est que vous pouvez utiliser plus de variables uniformes dans les shaders grâce aux tampons. OpenGL a une limite sur la quantité de données uniformes. Celle-ci peut être obtenue avec l’option GL_MAX_VERTEX_UNIFORM_COMPONENTS. En utilisant les tampons de variables uniformes, la limite est plus haute. Donc, chaque fois que vous atteignez le nombre maximal de variables uniformes (lors d’une animation squelettique, par exemple), vous pouvez toujours utiliser un tampon de variables uniformes.

VIII-D. Remerciements

Ce tutoriel est une traduction réalisée par Alexandre Laurent dont l’original a été écrit par Joey de Vries et qui est disponible sur le site Learn OpenGL.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 Joey de Vries. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.