Aller au contenu

Programmation Graphique : CM séance 06

10. Compléments

10.1. Langage GLSL

10.1.1. Structs et tableaux uniform

Le passage des données vers une variable uniform de type struct se fait membre à membre. Soit par exemple dans un shader :

struct Foo {
    float bar;
    vec3 fum;
};
uniform Foo foo;

alors dans le programme client en C++ il faudra écrire :

glUniformf   (glGetUniformLocation (program, "foo.bar"), 10.0);
glUniform3fv (glGetUniformLocation (program, "foo.fum"), some_vec3);

Remarque : si la variable ou le champ ne sont pas trouvés, la valeur renvoyée par glGetUniformLocation est -1, et l'appel de glUniform* est sans effet.

Le principe est le même pour un tableau, il faut passer la valeur pour chaque indice. S'il y par exemple dans un shader :

uniform float values[5];

alors dans le client on écrira :

glUniformf (glGetUniformLocation (program, "values[0]"), v[i]);
glUniformf (glGetUniformLocation (program, "values[1]"), v[i]);
...

ou avec une boucle, en C++17 :

1
2
3
4
5
6
7
#include <cstdio>
float v[5] = {...};
for (int i = 0; i < 5; i++) {
    char s[100];
    snprintf (s, sizeof(s), "values[%d]", i);
    glUniformf (glGetUniformLocation (program, s), v[i]);
}

ou en C++20 avec std::format :

#include <format>
float v[5] = {...};
for (int i = 0; i < 5; i++) {
    std::string s = std::format("values[{}]", i);
    glUniformf (glGetUniformLocation (program, s.c_str()), v[i]);
}

Le principe est le même pour un struct contenant un tableau :

glUniform... (glGetUniformLocation (program, "foo.bar[1]"), value);

ou pour un tableau de struct :

glUniform... (glGetUniformLocation (program, "foo[1].bar"), value);

pour chaque indice.

10.1.2. Blocs d'interface

Les variables d'interface in, out ou uniform peuvent devenir nombreuses. Il peut aussi y avoir des problèmes de convention de nommage entre les out d'un shader et les in du suivant dans le pipeline.

GLSL permet de regrouper des variables d'une même catégorie dans un bloc d'interface (interface block). Leur syntaxe ressemble à celle des struct :

storage_qualifier block_name {
    type variable;
    ...
} [instance_name];

Le storage qualifier peut être in, out ou uniform. Le nom de bloc block_name est utilisé pour transmettre les données out d'un shader aux données in du shader suivant. Par exemple si on regroupe les out d'un vertex shader dans un bloc d'interface nommé VertexData :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#version 330
in vec4 vPos;
in vec4 vCol;
in vec3 vNor;
out VertexData {
    vec4 color;
    vec3 normal;
};
...

void main()
{
    gl_Position = matProj * matCam * matWorld * vPos;
    color = vCol;
    normal = matNor * vNor;
}

Alors on devra déclarer les in du fragment shader avec le même nom de bloc d'interface :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#version 330
in VertexData {
    vec4 color;
    vec3 normal;
};
out vec4 fragColor;

void main()
{
    ....
    vec4 result = vec4(sumLight, 1.0) * color;
    fragColor = clamp(result, 0.0, 1.0);
}

Remarque : dans un shader il peut y avoir plusieurs blocs d'interface avec le même nom, à condition qu'ils aient des storage qualifier différents ; cela sera très utile pour "intercaler" un geometry shader entre le vertex shader et le fragment shader, sans avoir à renommer les variables.

Le nom instance_name est optionnel :

  • s'il est absent (bloc anonyme), les noms de variables du bloc peuvent être utilisées directement dans la suite du shader (ce qui est fait dans l'exemple ci-dessus) ;
  • s'il est présent, les champs du bloc doivent être préfixées par instance_name. lors de leur utilisation ;
  • le nom instance_name peut varier d'un shader à l'autre ;
  • s'il a un nom d'instance dans un shader, alors le bloc correspondant dans les autres shaders doit aussi avoir un nom d'instance.

Par exemple dans le vertex shader :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#version 330
in vec4 vPos;
in vec4 vCol;
in vec3 vNor;
out VertexData {
    vec4 color;
    vec3 normal;
} vd_out;
...

void main()
{
    gl_Position = matProj * matCam * matWorld * vPos;
    vd_out.color = vCol;
    vd_out.normal = matNor * vNor;
}

De même dans le fragment shader :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#version 330
in VertexData {
    vec4 color;
    vec3 normal;
} vd_in;
out vec4 fragColor;

void main()
{
    ....
    vec4 result = vec4(sumLight, 1.0) * vd_in.color;
    fragColor = clamp(result, 0.0, 1.0);
}

Remarques :

  • Il n'est pas possible de faire un bloc d'interface in pour un vertex shader, ni out pour un fragment shader.

  • Les blocs d'interface GLSL sont transparents pour l'application, autrement dit il n'y a rien à modifier au niveau du code OpenGL.

Les blocs d'interface vont être très utiles pour les UBO et les geometry shaders vus ensuite.

10.2. Shaders multiples

Dans un programme OpenGL, il est fréquent d'avoir différents shader programs selon les objets que l'on veut dessiner.

10.2.1. Classe ShaderProg

On introduit dans l'exemple fw61-multi.cpp une classe ShaderProg pour faciliter l'usage de shader programs multiples ; tout le code relatif aux shaders qui était dans la classe MyApp est déménagé dans cette classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ShaderProg {
public:
    enum ShaderType  { T_VERTEX, T_FRAGMENT, T_GEOMETRY, T_NUM };
    enum ShaderCateg { C_COLOR, C_TEXTURE, C_DIFFUSE, C_SPECULAR, C_NUM };

    const char* m_default_shader_texts[C_NUM][T_NUM] = {
        ...
    };

    ShaderProg (ShaderCateg categ,
               const std::string shader_paths[T_NUM]);
    ~ShaderProg();

    const GLuint get_program();
    void use_program();
    void bind_attrib_locations();           // voir plus loin
    GLint get_uniform (const char* name);
    bool compile_program();
    bool compile_shader (GLuint shader, const char* name);
    bool link_program();
    bool load_shader_code (const ShaderType type, const std::string path);
    void print_shaders();
};

Chaque catégorie de shader program possède des shaders par défaut, mémorisés dans le tableau m_default_shader_texts. La ligne de commande permet de les remplacer par des fichiers, avec la syntaxe suivante :

$ ./fw61-multi --help
USAGE:
  ./fw61-multi [-vs|-fs|-gs categ path] [-ps categ]
  categ: color|texture|diffuse|specular

Au niveau de MyApp, les shader programs sont alloués dans load_programs et libérés dans tear_programs :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void load_programs()
{
    m_prog_color = new ShaderProg {
        ShaderProg::C_COLOR, m_shader_paths[ShaderProg::C_COLOR] };
    m_prog_color->compile_program();
    ...
}

void tear_programs()
{
    delete m_prog_color; m_prog_color = nullptr;
    ...
}

Ligne 4, le tableau m_shader_paths contient les chemins issus de la ligne de commande.

Pour dessiner un objet avec un shader program donné, il suffit d'appeler sa méthode use_program :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void displayGL()
{
    
    vmath::mat4 mat_proj, mat_cam, mat_world, mat_MVP;
    set_projection (mat_proj, mat_cam);

    ShaderProg* prog = m_prog_color;
    prog->use_program();

    mat_world = vmath::translate (-0.8f, +0.7f, 0.f)
              * vmath::scale (0.7f)
              * vmath::rotate (m_anim_angle, 0.f, 1.f, 0.15f);
    mat_MVP = mat_proj * mat_cam * mat_world;

    glUniformMatrix4fv (prog->get_uniform ("matMVP"), 1, GL_FALSE, mat_MVP);

    m_triangles->draw();
    ...
}

La matrice mat_world est recalculée pour chaque dessin, pour permettre de placer plus facilement les objets.

10.2.2. Locations des VA

La location des vertex attributes, autrement dit des variables en entrée des vertex shaders, peut varier d'un shader program à l'autre.

Si l'on veut pouvoir afficher un même objet plusieurs fois avec plusieurs shader program, il ne sert plus à rien de récupérer les locations avec

GLint pos_loc = glGetAttribLocation (program, "vPos");
et de les passer au constructeur de l'objet.

Une solution consiste à fixer la location. Cela peut être fait "en dur" dans les shaders par

layout (location = 0) in vec4 vPos;

mais cela oblige à le faire dans tous les shaders.

Une solution plus souple consiste à fixer les locations au niveau du client :

// Vertex attribute location imposées
enum VA_Locations{ VPOS_LOC = 0, VCOL_LOC = 1, VNOR_LOC = 2, VTEX_LOC = 3, LAST_LOC };

const char* get_vertex_attribute_name (VA_Locations loc)
{
    switch (loc) {
        case VPOS_LOC  : return "vPos";
        case VCOL_LOC  : return "vCol";
        case VNOR_LOC  : return "vNor";
        case VTEX_LOC  : return "vTex";
        default : return "";
    }
}

puis de propager les locations en appelant

glBindAttribLocation (program, location, variable_name);

Les locations doivent être propagées après glCreateProgram, mais avant glLinkProgram. Une fois que le programme est liée, les locations ne changent plus jusqu'à la destruction ou une nouvelle liaison du programme.

La solution mise en place dans notre exemple est de propager toutes les locations dans le constructeur de ShaderProg, avec la méthode :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void bind_attrib_locations()
{
    for (auto loc = VPOS_LOC; loc < LAST_LOC;
        loc = static_cast<VA_Locations>((int)loc+1))
    {
        const char* name = get_vertex_attribute_name (loc);
        if (!name) continue;
        glBindAttribLocation (m_program, loc, name);
    }
}

Cela simplifie donc l'écriture des classes de dessin car il n'y a plus à stocker de location, on utilise les constantes :

glVertexAttribPointer (VPOS_LOC, 3, GL_FLOAT, GL_FALSE, 
    6*sizeof(GLfloat), reinterpret_cast<void*>(0*sizeof(GLfloat)));
glEnableVertexAttribArray (VPOS_LOC);  

10.2.3. Locations des uniform

La location des uniform peut également varier d'un shader program à l'autre.

Au lieu de fixer la location, on récupère celle-ci chaque fois avec la méthode get_uniform de ShaderProg :

GLint get_uniform (const char* name)
{
    return glGetUniformLocation (m_program, name);
}

qui renvoie -1 si la variable uniform n'a pas été trouvée.

On peut envoyer une donnée dans une uniform dès que le programme est lié ; elle reste mémorisée jusqu'à la destruction ou une nouvelle liaison du programme.

En pratique dans notre exemple, on envoie les données uniform au moment du rendu dans displayGL :

1
2
3
4
glUniformMatrix4fv (prog->get_uniform ("matMVP"), 1, GL_FALSE, mat_MVP);
glUniformMatrix4fv (prog->get_uniform ("matWorld"), 1, GL_FALSE, mat_world);
glUniformMatrix3fv (prog->get_uniform ("matNor"), 1, GL_FALSE, mat_Nor);
glUniform4fv (prog->get_uniform ("mousePos"), 1, m_mousePos);

10.3. Uniform Buffer Objects

Il est possible de regrouper plusieurs variables uniform dans un buffer object, cela s'appelle un Uniform Buffer Object, ou UBO.

L'aspect intéressant est qu'il est possible de partager un UBO entre plusieurs shaders ; cela évite de devoir passer toutes les variables uniform à chaque shader.

C'est particulièrement utile lorsqu'il y a beaucoup de variables uniform à mettre à jour lors du rendu : il suffit de mettre le UBO à jour, ce qui est plus efficace.

Les données qui sont propres à chaque shader, telle que mat_World ou mat_Nor seront quant à elles passées de la manière habituelle.

10.3.1. Bloc uniform

Comme exemple dans fw62-ubo.cpp, on réécrit les shaders en regroupant des données communes dans un bloc d'interface uniform nommé Uniforms :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
layout (std140) uniform Uniforms {    // layout vu ensuite
    mat4 matProj;
    mat4 matCam;
    vec4 mousePos;
    float time;                     // ajouté, pour animations
};
uniform mat4 matWorld;
...

void main()
{
    gl_Position = matProj * matCam * matWorld * vPos;
    ...
}

Le nom de bloc Uniforms sera utilisé dans l'application. Les variables du bloc sont directement utilisables sans préfixe (le bloc est anonyme).

On aurait pu aussi déclarer le bloc d'interface avec un nom d'instance, par exemple com ; les différents champs doivent alors être préfixes par com. :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
layout (std140) uniform Uniforms {
    mat4 matProj;
    mat4 matCam;
    vec4 mousePos;
    float time;
} com;
uniform mat4 matWorld;
...

void main()
{
    gl_Position = com.matProj * com.matCam * matWorld * vPos;
    ...
}

10.3.2. Organisation mémoire

Le point crucial est l'organisation des données, qui doit être connue de l'application, en particulier l'offset de chaque champ, sa taille, et la taille de l'alignement mémoire.

Il existe 4 méthodes d'alignement mémoire, que l'on peut passer en argument de layout : packed, shared, std140 et std430. La méthode préconisée est std140 (elle est apparue avec GLSL 1.40) :

  • elle est très simple (pas besoin de demander des offsets) ;
  • son alignement mémoire est connu à l'avance, il est de 16 octets (la taille d'un vec4) ;
  • à cause de cela, il peut y avoir un gaspi de mémoire à cause du padding pour aligner les données.

Dans l'application en C++ on a besoin de définir un struct avec le même alignement mémoire, ce qui est facilité par le spécificateur alignas :

// Type avec alignement GLSL std140 aligné sur 16 octets
struct UBO_Uniforms {
    alignas(16) vmath::mat4 matProj;
    alignas(16) vmath::mat4 matCam;
    alignas(16) vmath::vec4 mousePos;
    alignas(16) GLfloat time;
};

Cela facilitera grandement la mise à jour des données dans le UBO.

10.3.3. Création d'un UBO

Pour créer un UBO, il faut d'abord créer un identifiant avec glGenBuffer, puis le lier à la cible GL_UNIFORM_BUFFER :

1
2
3
4
5
6
7
8
class MyApp {
    GLuint m_UBO_id;

    void initGL {
        ...
        // Création UBO
        glGenBuffers (1, &m_UBO_id);
        glBindBuffer (GL_UNIFORM_BUFFER, m_UBO_id);

On peut ensuite réserver la taille mémoire nécessaire (sans passer de données, d'où le NULL), puis délier le UBO (on se servira par la suite de son identifiant) :

 9
10
        glBufferData (GL_UNIFORM_BUFFER, sizeof(UBO_Uniforms), NULL, GL_STATIC_DRAW); 
        glBindBuffer (GL_UNIFORM_BUFFER, 0);

À la fin, pour détruire le UBO il suffit d'écrire

1
2
3
4
5
    void tearGL()
    {
        ...
        glDeleteBuffers (1, &m_UBO_id);
    }

10.3.4. Point de liaison

On peut avoir plusieurs UBO et choisir quels shaders les utilise. On relie les UBO et les shaders en leur associant un binding point (un entier >= 0).

On relie donc à un binding point notre UBO après sa création dans initGL :

1
2
3
4
5
6
7
8
9
// Au début du programme
const GLint UBO_BINDING_POINT = 0;

class MyApp {

    void initGL ()
    {
        ...
        glBindBufferBase (GL_UNIFORM_BUFFER, UBO_BINDING_POINT, m_UBO_id); 

puis on relie chaque shader program au même binding point, en précisant le nom du bloc d'interface uniform, ici "Uniforms" :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
        std::vector<GLuint> program_ids {
             m_prog_color->get_program(),
             m_prog_texture->get_program(),
             m_prog_diffuse->get_program(),
             m_prog_specular->get_program()
        };
        for (GLuint prog : program_ids) {
            glUniformBlockBinding (prog, 
                glGetUniformBlockIndex (prog, "Uniforms"), 
                UBO_BINDING_POINT);
        }
    }

10.3.5. Envoi des données

Tout est en place, on peut maintenant envoyer les données dans le UBO, soit à la fin de initGL, soit au début de chaque rendu dans displayGL, en appelant la fonction glBufferSubData :

void glBufferSubData (GLenum target,    // GL_UNIFORM_BUFFER
                      GLintptr offset,
                      GLsizeiptr size,
                      const void * data);

Le calcul de offsets et size est grandement facilité par la définition que l'on avait faite avec le bon alignement mémoire, pour rappel :

// Type avec alignement GLSL std140 aligné sur 16 octets
struct UBO_Uniforms {
    alignas(16) vmath::mat4 matProj;
    alignas(16) vmath::mat4 matCam;
    alignas(16) vmath::vec4 mousePos;
    alignas(16) GLfloat time;
};

En effet on peut utiliser directement la macro offsetof et l'opérateur sizeof du C++ :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void displayGL()
{
    ...
    vmath::mat4 mat_proj, mat_cam;
    set_projection (mat_proj, mat_cam);

    // On met les données dans le UBO
    glBindBuffer (GL_UNIFORM_BUFFER, m_UBO_id);
    glBufferSubData (GL_UNIFORM_BUFFER, 
        offsetof(UBO_Uniforms, matProj),  sizeof(vmath::mat4), &mat_proj);
    glBufferSubData (GL_UNIFORM_BUFFER, 
        offsetof(UBO_Uniforms, matCam),   sizeof(vmath::mat4), &mat_cam);
    glBufferSubData (GL_UNIFORM_BUFFER, 
        offsetof(UBO_Uniforms, mousePos), sizeof(vmath::vec4), &m_mousePos);
    GLfloat time_f = glfwGetTime();  // GLdouble nécessite v4.0+, mal géré
    glBufferSubData (GL_UNIFORM_BUFFER, 
        offsetof(UBO_Uniforms, time), sizeof(GLfloat), &time_f);
    glBindBuffer (GL_UNIFORM_BUFFER, 0);
    ...
}

Tous les shaders liés au binding point de l'UBO auront alors accès au données, à condition qu'ils aient déclaré le bloc d'interface uniform prévu à cet effet.

Remarques :

10.4. Geometry shader

On a vu dans le pipeline de rendu qu'il y avait plusieurs shaders optionnels entre le vertex shader (VS) et le fragment shader (FS).

Nous nous intéressons ici au geometry shader (GS), apparu à la version 3.2.

Un geometry shader prend en entrée la liste des sommets qui forment une primitive graphique : un point, une ligne ou un triangle.

Ces informations proviennent du vertex shader, et sont regroupées pour l'occasion.

Le geometry shader peut soit recopier les données en sortie (pass-through), ou modifier, supprimer ou rajouter des primitives, voire changer leur nature. Il peut aussi modifier les positions, couleurs, etc.

Toutes ces primitives seront ensuite rasterisées et passées au fragment shader.

10.4.1. Choix des primitives

Il faut commencer par indiquer au GS quelles primitives seront traitées en entrée, au moyen d'un layout :

layout (input_qualifier) in;

Le input qualifier peut être l'un éléments des suivants :

  • points : dessins avec GL_POINTS ;
  • lines : dessins avec GL_LINES ou GL_LINE_STRIP ;
  • lines_adjacency : dessins avec GL_LINES_ADJACENCY ou GL_LINE_STRIP_ADJACENCY ;
  • triangles : dessins avec GL_TRIANGLES, GL_TRIANGLE_STRIP ou GL_TRIANGLE_FAN ;
  • triangles_adjacency : dessins avec GL_TRIANGLES_ADJACENCY ou GL_TRIANGLE_STRIP_ADJACENCY.

Les entrées pour points, lines, triangles reçoivent 1 à 3 sommets ; celles pour lines_adjacency et triangles_adjacency en reçoivent 4 ou 6.

Il faut aussi spécifier le type des primitives qui seront émises en sortie :

layout (output_qualifier, max_vertices = n) out;

Le output qualifier est à choisir parmi : points, line_strip, triangle_strip. Le nombre maximal de sommets qui seront émis est spécifié par max_vertices ; les sommets en excès seront ignorés.

Voici un exemple de l'entête d'un geometry shader qui prend en entrée un triangle, et émet en sortie un triangle :

layout (triangles) in;
layout (triangle_strip, max_vertices=3) out;

10.4.2. Entrées-sorties

Les variables en entrée reçoivent toujours un tableau sans taille, avec une valeur par sommet.

Elles peuvent être déclarées de la manière habituelle, sans bloc d'interface :

in vec4 color[];
in vec3 normal[];

Les noms doivent correspondre aux noms des variables en sortie du vertex shader.

On peut ensuite examiner les valeurs avec l'indice de chaque sommet de la primitive (par exemple color[0], color[1], etc).

Les noms en sortie sont des variables simples, qui doivent correspondre aux noms utilisés dans le fragment shader. Par exemple

out vec4 color;
out vec3 normal;

Problème : on n'a pas le droit de redéclarer des variables. Il faut soit renommer les variables en sortie (et en entrée dans le fragment shader), soit utiliser des interfaces de bloc, ce qui évite de modifier le fragment shader, et permet donc "d'intercaler" un GS entre un VS et un FS pour faire des essais (à condition que le VS et le FS utilisent les mêmes interfaces de bloc) :

in VertexData {
    vec4 color;
    vec3 normal;
} vd_in[];

out VertexData {
    vec4 color;
    vec3 normal;
} vd_out;

GLSL fournit de plus des variables prédéfinies en entrée :

in gl_PerVertex
{
    vec4 gl_Position;
    float gl_PointSize;
    ...
} gl_in[];

dont les valeurs proviennent du VS, et en sortie :

out gl_PerVertex
{
    vec4 gl_Position;
    float gl_PointSize;
    ...
};

Il existe encore d'autres variables prédéfinies, voir ici.

10.4.3. Utilisation

Le décor étant planté, voici comment le GS peut générer des primitives :

  • pour émettre un sommet, le GS doit renseigner toutes les variables out, puis appeler EmitVertex() ;

  • lorsque le nombre de sommets émis est suffisant pour dessiner une primitive, on appelle EndPrimitive() ;

  • on peut continuer a émettre des sommets, ceux-ci seront destinés à la primitive suivante, jusqu'à ce qu'on appelle EndPrimitive(), et ainsi de suite ;

  • le nombre total de sommets émis ne doit pas dépasser max_vertices.

Exemple de geometry shader pass-through qui reçoit un triangle, et le transmet sans le modifier :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#version 330
layout (triangles) in;
layout (triangle_strip, max_vertices=3) out;

in VertexData {
    vec4 color;
} vd_in[];

out VertexData {
    vec4 color;
} vd_out;

void main()
{
    // Recopie des sommets
    for (int i = 0; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position; 
        vd_out.color = vd_in[i].color;
        EmitVertex();
    }
    EndPrimitive();
}

Utilisation avec le program shader color de fw62-ubo.cpp :

./fw62-ubo -gs color tmp1.glsl 

On constate que le wire cube a disparu, car il n'est pas constitué de triangles !

Deuxième exemple, où l'on va rajouter un petit triangle rouge au sommet numéro 0 de chaque triangle en entrée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#version 330
layout (triangles) in;
layout (triangle_strip, max_vertices=6) out;

in VertexData {
    vec4 color;
} vd_in[];

out VertexData {
    vec4 color;
} vd_out;

void main()
{
    // Recopie des sommets
    for (int i = 0; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position; 
        vd_out.color = vd_in[i].color;
        EmitVertex();
    }
    EndPrimitive();

    // Ajout d'un triangle rouge au sommet 0
    vd_out.color = vec4(1.0, 0, 0, 1.0);
    gl_Position = gl_in[0].gl_Position + vec4(-0.2, 0, 0, 0); 
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4(0.2, 0, 0, 0); 
    EmitVertex();
    gl_Position = gl_in[0].gl_Position + vec4(0, -0.2, 0, 0); 
    EmitVertex();
    EndPrimitive();
}

À noter, les coordonnées sont données dans l'espace normalisé du viewport \([\,-1 \ldots 1,\,-1 \ldots 1,\,-1 \ldots 1\,]\).

Troisième exemple avec une animation en fonction du temps :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#version 330
layout (triangles) in;
layout (triangle_strip, max_vertices=6) out;

in VertexData {
    vec4 color;
} vd_in[];

out VertexData {
    vec4 color;
} vd_out;

layout (std140) uniform Uniforms {
    mat4 matProj;
    mat4 matCam;
    vec4 mousePos;
    float time;
};

vec4 rotate_vertex (in vec4 pos_in, int k)
{
    float pi = 3.141592654, radius = 0.3, speed = 5.0;
    float angle = time*speed + k*2*pi/3.0, c = cos(angle), s = sin(angle);
    mat3 rotM = mat3(
        vec3 (  c,   s, 0.0),
        vec3 ( -s,   c, 0.0),
        vec3 (0.0, 0.0, 1.0));
    vec3 rotV = rotM * vec3(radius, 0, 0);
    return pos_in + vec4(rotV, 0.0);
}

void main()
{
    // On tourne le premier sommet
    gl_Position = rotate_vertex (gl_in[0].gl_Position, 0);
    vd_out.color = vd_in[0].color;
    EmitVertex();
    // On recopie les suivants
    for (int i = 1; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position; 
        vd_out.color = vd_in[i].color;
        EmitVertex();
    }
    EndPrimitive();

    // Ajout d'un triangle rouge au sommet 0, avec rotation
    vd_out.color = vec4(1.0, 0, 0, 1.0);
    for (int i = 0; i < 3; i++) {
        gl_Position = rotate_vertex (gl_in[0].gl_Position, i);
        EmitVertex();
    }
    EndPrimitive();
}

Exemple trois bis pour le shader program diffuse de l'exemple, en rajoutant les normales :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#version 330
layout (triangles) in;
layout (triangle_strip, max_vertices=6) out;

in VertexData {
    vec4 color;
    vec3 normal;
} vd_in[];

out VertexData {
    vec4 color;
    vec3 normal;
} vd_out;

layout (std140) uniform Uniforms {
    mat4 matProj;
    mat4 matCam;
    vec4 mousePos;
    float time;
};

vec4 rotate_vertex (in vec4 pos_in, int k)
{
    float pi = 3.141592654, radius = 0.3, speed = 5.0;
    float angle = time*speed + k*2*pi/3.0, c = cos(angle), s = sin(angle);
    mat3 rotM = mat3(
        vec3 (  c,   s, 0.0),
        vec3 ( -s,   c, 0.0),
        vec3 (0.0, 0.0, 1.0));
    vec3 rotV = rotM * vec3(radius, 0, 0);
    return pos_in + vec4(rotV, 0.0);
}

void main()
{
    // On tourne le premier sommet
    gl_Position = rotate_vertex (gl_in[0].gl_Position, 0);
    vd_out.color = vd_in[0].color;
    vd_out.normal = vd_in[0].normal;
    EmitVertex();
    // On recopie les suivants
    for (int i = 1; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position; 
        vd_out.color = vd_in[i].color;
        vd_out.normal = vd_in[i].normal;
        EmitVertex();
    }
    EndPrimitive();

    // Ajout d'un triangle rouge au sommet 0, avec rotation
    vd_out.color = vec4(1.0, 0, 0, 1.0);
    vd_out.normal = vec3(0.0, 0.0, 1.0);
    for (int i = 0; i < 3; i++) {
        gl_Position = rotate_vertex (gl_in[0].gl_Position, i);
        EmitVertex();
    }
    EndPrimitive();
}

Quatrième exemple où on affiche les triangles en fil de fer et on affiche les normales, pour le shader program diffuse de l'exemple fw62-ubo.cpp :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#version 330
layout (triangles) in;
layout (line_strip, max_vertices=12) out;

in VertexData {
    vec4 color;
    vec3 normal;
} vd_in[];

out VertexData {
    vec4 color;
    vec3 normal;
} vd_out;

void main()
{
    // Normale vers l'avant pour rendre visible
    vd_out.normal = vec3(0.0, 0.0, 1.0);

    // On recopie le triangle en fil de fer
    for (int i = 0; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position; 
        vd_out.color = vd_in[i].color;
        EmitVertex();
        int j = (i+1) % 3;
        gl_Position = gl_in[j].gl_Position; 
        vd_out.color = vd_in[j].color;
        EmitVertex();
        EndPrimitive();
    }

    // On rajoute les normales
    vd_out.color = vec4(1.0, 0.0, 0.0, 1.0);
    for (int i = 0; i < 3; i++) {
        gl_Position = gl_in[i].gl_Position; 
        EmitVertex();
        gl_Position = gl_in[i].gl_Position + vec4(vd_in[i].normal, 1.0); 
        EmitVertex();
        EndPrimitive();
    }
}