12/17
5. Attributs de sommets
Passage d’attributs entre vertex et fragment shaders
Jusqu’à présent, la seule information associée à un sommet était sa position, qui était utilisée directement dans le vertex shader. Les variables calculées dans le vertex shader peuvent également être passés en tant qu’entrée du fragment shader.
Notez qu’une variable définie dans le vertex shader est calculée par sommet, alors qu’une variable du fragment shader est défini pour chaque fragment (pixel d’un triangle). Pour passer de l’un à l’autre, les paramètres reçus dans le fragment shader correspondent à l’interpolation linéaire des valeurs associées à celle du vertex shader, cette interpolation étant calculée à partir des valeurs des sommets de la primitive (généralement d’un triangle).
Le principe permettant de "passer" une variable entre le vertex et le fragment shader est le suivant
-
La variable doit être qualifiée comme attribut out dans le vertex shader.
-
La variable correspondante dans le fragment shader est qualifié par in.
Exemple
Considérons l’exemple suivant permettant de passer l’attribut de position au fragment shader
-
Vertex shader
#version 330 core
// This value is received from the VBO data
layout (location = 0) in vec4 position;
// This value will be passed to the fragment shader
out vec4 position_frag;
uniform mat4 R;
void main()
{
position_frag = position;
gl_Position = R*position;
}
-
Fragment shader
#version 330 core
// Input value received from vertex shader
in vec4 position_frag;
out vec4 FragColor;
void main()
{
FragColor = abs(2*position_frag);
}
[ source ]

Dans ce cas, la position 3D initiale du sommet est utilisée pour indiquer la couleur du fragment.
Assurez-vous que vous comprenez pourquoi le triangle est affiché avec ces couleurs. Notez en particulier l’action de l’interpolation linéaire sur les valeurs reçue en paramètre d’entrée du fragment shader.
Que ce passe-t-il si vous considérez désormais le code suivant dans le vertex shader ?
position_frag = R*position;
Expliquez en particulier la couleur affichée sur le triangle.
-
Ces exemples illustrent le cas spécifique où les couleurs appliquées sur la primitive sont une fonction de la position. Notez qu’il est possible d’appliquer n’importe quelle fonction: essayez par exemple le code suivant dans le fragment shader.
void main()
{
float a = pow(cos(position_frag.x*40),2.0);
float b = position_frag.y+0.5;
FragColor = vec4(a,b,b,1);
}
Attributs des sommets
En général, les couleurs et les autres attributs associés aux sommets sont envoyés en tant que données à la carte graphique, similairement à celles de positions, en utilisant les VBO.
Considérons le cas d’un triangle où chacun des trois sommets est associé, respectivement, à la couleur rouge, verte et bleue.

Les vertex et fragment shaders correspondants sont les suivants
-
Vertex shader
#version 330 core
layout (location = 0) in vec4 position;
layout (location = 1) in vec4 color;
out vec4 color_frag;
void main()
{
color_frag = color;
gl_Position = position;
}
-
Fragment shader
#version 330 core
in vec4 color_frag;
out vec4 FragColor;
void main()
{
FragColor = color_frag;
}
Différentes organisations mémoires sont possibles pour stocker les informations de sommets et couleurs. Nous donnons l’exemple de trois d’entre elles (Afin de simplifier le code, nous considérons l’exemple du cas du triangle fixe sans rotation)
-
Deux VBO séparés: un VBO pour les positions, un autre pour les couleurs
-
Un seul VBO contenant d’abord toutes les positions, puis toutes les couleurs
-
Un seul VBO entrelaçant pour chaque sommet: une position, puis une couleur.
Schéma des différentes organisation en mémoire
Le code correspondant à chaque représentation est décrit-ci après.
VBO multiples
L’une des possibilités consiste à stocker les positions et les couleurs dans des vecteurs différents, qui sont eux-mêmes associés à des VBO différents.
-
Un buffer dédié aux positions
-
Un buffer dédié aux couleurs
// ********************************* //
// Create data
// ********************************* //
// Positions
const std::vector<GLfloat> position = {
-0.5f, -0.5f, 0.0f, // position 0
0.5f, -0.5f, 0.0f, // position 1
0.0f, 0.5f, 0.0f // position 2
};
// Colors
const std::vector<GLfloat> color = {
1.0f, 0.0f, 0.0f, // color 0 - red
0.0f, 1.0f, 0.0f, // color 1 - green
0.0f, 0.0f, 1.0f // color 2 - blue
};
// ********************************* //
// Send data on the GPU
// ********************************* //
// Fill VBO for position
GLuint vbo_position = 0;
glGenBuffers(1, &vbo_position);
glBindBuffer(GL_ARRAY_BUFFER, vbo_position);
glBufferData(GL_ARRAY_BUFFER, position.size()*sizeof(GLfloat), &position[0], GL_STATIC_DRAW );
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Fill VBO for color
GLuint vbo_color = 0;
glGenBuffers(1, &vbo_color);
glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
glBufferData(GL_ARRAY_BUFFER, color.size()*sizeof(GLfloat), &color[0], GL_STATIC_DRAW );
glBindBuffer(GL_ARRAY_BUFFER, 0);
// ********************************* //
// Set shader attributes
// ********************************* //
glGenVertexArrays(1,&vao);
glBindVertexArray(vao);
// position at layout 0
glBindBuffer(GL_ARRAY_BUFFER, vbo_position);
glEnableVertexAttribArray( 0 );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nullptr );
// color at layout 1
glBindBuffer(GL_ARRAY_BUFFER, vbo_color);
glEnableVertexAttribArray( 1 );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 0, nullptr );
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
Téléchargez l’archive et assurez-vous de pouvoir compiler et exécuter le code. Assurez-vous de comprendre l’organisation des VBO ainsi que du VAO. Notez que les positions sont indexées au layout 0, alors que les couleurs sont indexées au layout 1.
Données empilées
Il est possible d’organiser les buffers différemment en empilant l’ensemble des données dans un seul large buffer. Par exemple, en plaçant toutes les positions, puis ensuite, toutes les couleurs.
// ********************************* //
// Create data
// ********************************* //
const std::vector<GLfloat> vertex_data = {
-0.5f, -0.5f, 0.0f, // position 0
0.5f, -0.5f, 0.0f, // position 1
0.0f, 0.5f, 0.0f, // position 2
1.0f, 0.0f, 0.0f, // color 0 - red
0.0f, 1.0f, 0.0f, // color 1 - green
0.0f, 0.0f, 1.0f // color 2 - blue
};
// ********************************* //
// Send data on the GPU
// ********************************* //
// Fill VBO for position and color
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertex_data.size()*sizeof(GLfloat), &vertex_data[0], GL_STATIC_DRAW );
glBindBuffer(GL_ARRAY_BUFFER, 0);
// ********************************* //
// Set shader attributes
// ********************************* //
glGenVertexArrays(1,&vao);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// position at layout 0
glEnableVertexAttribArray( 0 );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nullptr );
// color at layout 1
glEnableVertexAttribArray( 1 );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(3*3*sizeof(GLfloat)) );
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
Notez l’utilisation de l’offset dans le VAO pour indiquer le début des informations de couleurs.
Données entrelacées
Une autre possibilité consiste à entrelacer les données de positions et de couleurs dans le buffer. Cette fois, chaque coordonnée (x,y,z) du sommet i est suivie par les couleurs (r,g,b) du même sommet.
// ********************************* //
// Create data
// ********************************* //
const std::vector<GLfloat> vertex_data = {
-0.5f, -0.5f, 0.0f, // position 0
1.0f, 0.0f, 0.0f, // color 0 - red
0.5f, -0.5f, 0.0f, // position 1
0.0f, 1.0f, 0.0f, // color 1 - green
0.0f, 0.5f, 0.0f, // position 2
0.0f, 0.0f, 1.0f // color 2 - blue
};
// ********************************* //
// Send data on the GPU
// ********************************* //
// Fill VBO for position and color
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertex_data.size()*sizeof(GLfloat), &vertex_data[0], GL_STATIC_DRAW );
glBindBuffer(GL_ARRAY_BUFFER, 0);
// ********************************* //
// Set shader attributes
// ********************************* //
glGenVertexArrays(1,&vao);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// position at layout 0
glEnableVertexAttribArray( 0 );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 2*3*sizeof(GLfloat), nullptr );
// color at layout 1
glEnableVertexAttribArray( 1 );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 2*3*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)) );
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
Cette fois, l’offset et le stride sont utilisés dans le VAO pour indiquer l’organisation mémoire.
Comparaisons
Ces trois différentes organisations mémoires possèdent chacune des avantages/inconvénients en fonction du contexte.
-
Buffers multiples
-
(+) Permet de séparer aisément les traitements sur la position et les couleurs. Correspond généralement à la représentation algorithmique et aux structures de données C++ .
-
(-) Temps de calcul le plus élevé sur GPU: chaque accès à un attribut d’un sommet (position, couleur) nécessite l’accès à deux emplacements mémoires éloignés (buffers différents).
-
-
Buffer empilé
-
(+) Permet toujours le traitement des positions et couleurs en tant que suite de valeurs contigües, mais moins flexibles que l’utilisation de buffers distincts.
-
(-) Chaque lecture sur un attribut de sommet nécessite toujours l’accès à une mémoire non contigüe (début et fin de buffer).
-
-
Données entrelacées
-
(+) Utilisation la plus efficace pour le GPU: chaque sommet possède l’ensemble des attributs dans la même zone mémoire.
-
(-) Moins pratique d’utilisation au niveau du code C++ . Nécessite l’utilisation d’offsets et de stride.
-
En pratique, les applications standards réalisées dans ce cours peuvent utiliser n’importe quelle représentation mémoire sans différence notable de performances. Pour maximiser la flexibilité et la lisibilité du code, nous utiliserons des buffers multiples pour stocker les attributs de sommets.
Dans le cas de productions où le code doit être optimisé pour sa rapidité (typiquement moteurs de jeux vidéos, très grande quantité de données), l’utilisation de données entrelacées serait à privilégier.