gKit2 light
configurer un format de sommet, vertex array object

cf tuto4GL.cpp

pour dessiner des triangles, il faut décrire les informations associées aux sommets, indiquer ou les trouver, leur organisation mémoire, et indiquer à quelles entrées du vertex shader elles sont associées.

le cas simple permettant de dessiner quelques triangles est présenté dans afficher plusieurs triangles, modifier les paramètres uniform d'un shader program.

vertex array object

c'est un objet openGL, appelé vertex array object, qui stocke la description du format de sommets.

la création des objets openGL utilise des fonctions de la forme glGenXXX( int n, GLuint *names ). cette famille de fonctions permet de créer plusieurs objets en même temps et renvoye un tableau d'identifiants des nouveaux objets. pour en créer un seul, on peut utiliser :

GLuint vao;
glGenVertexArrays(1, &vao);

il ne reste plus qu'à le sélectionner pour configurer le pipeline :

glBindVertexArray(vao);

les informations des sommets, les attributs, sont stockées dans un ou plusieurs vertex buffers, qui ne sont que des tableaux alloués dans la mémoire de la carte graphique. cf vertex buffers et index buffer pour les créer et leur affecter des données.

remarque : relisez interface C openGL section shaders pour les explications sur les uniform, in out et leurs rapports avec les tableaux de sommets...

Si le vertex shader etait écrit en c++, on pourrait écrire directement une description de sommet :

// declarer la structure qui represente un sommet et ses attributs
struct vertex
{
vec3 position;
vec3 color;
};
// un tableau de sommets / maillage
vertex mesh[]= { ... };
vecteur generique, utilitaire.
Definition: vec.h:146
representation de l'indexation complete d'un sommet
Definition: wavefront.cpp:176

on aurait aussi pu utiliser un tableau par attribut, au lieu de les grouper dans une structure. mais quelque soit le cas, il est necessaire de pouvoir calculer l'adresse d'un attribut du ieme sommet d'un tableau. par exemple pour transformer les positions des sommets des triangles...

le compilateur c++ le fait automatiquement, puisqu'il decide de l'organisation memoire du tableau et de la structure :

vec3 *p= &mesh[id].position;

par contre, si on ne connait qu'un pointeur non typé void *t, au lieu du tableau mesh, il faut connaitre le nombre d'octets occuppés par la structure vertex, sizeof(vertex) et la place du membre position dans la structure offsetof(vertex, position) pour calculer l'adresse d'un attribut du ieme sommet :

void *t= (void *) mesh;
void *p= (char *) t + id * sizeof(vertex) + offsetof(vertex, position);

ce sont exactement ces informations : t, sizeof() et offsetof() qu'il faut décrire à openGL pour que le gpu calcule l'adresse de chaque attribut de chaque sommet pour exécuter les vertex shaders. par contre, l'application ne connait pas l'adresse gpu du tableau, t, mais un identifiant de buffer...

le vertex array object stocke, pour chaque attribut déclaré par le vertex shader :

l'indexation des sommets (l'index buffer) peut aussi être associé à un vertex array objet.

la configuration se fait en plusieurs étapes :

remarque : les attributs sont numérotés automatiquement par le compilateur / linker, et il faut utiliser glGetAttribLocation() pour connaître leur identifiant. Il est aussi possible d'imposer l'identifiant d'un attribut directement dans le source du shader cf et sans utiliser glGetAttribLocation( ) ?.

exemple : le cas classique, le vertex buffer, identifiant buffer, contient les positions des sommets. et le vertex shader déclare : in vec3 position;

// selectionner le vertex array object a configurer, si necessaire
// glBindVertexArray(vao);
// recuperer l'identifiant de l'attribut declare dans le vertex shader
GLint attribute= glGetAttribLocation(program, "position");
if(attribute < 0)
// probleme, l'attribut n'existe pas... erreur de compilation / link du program ?
// selectionner le vertex buffer contenant les donnees
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// configurer l'attribut
glVertexAttribPointer(attribute,
3, GL_FLOAT, // size et type, position est un vec3 dans le vertex shader
GL_FALSE, // pas de normalisation des valeurs
0, // stride 0, les valeurs sont les unes a la suite des autres, cf sizeof(vertex)
0 // offset 0, les valeurs sont au debut du buffer, cf offsetof(vertex, attrib)
);
glEnableVertexAttribArray(attribute);

Selon le type des attributs il faut utiliser la bonne déclaration :

Lorsque l'objet est décrit par des sommets indexés, en plus de décrire l'organisation mémoire des attributs de sommets, il faut également décrire leur indexation. L'index buffer stocke des triplets d'indices pour décrire chaque triangle. La description du vertex buffer et des sommets se fait toujours de la même manière, par contre, il faut sélectionner le buffer contenant les indices de sommets sur GL_ELEMENT_ARRAY_BUFFER :

// selectionner le vertex array object a configurer, si necessaire
// glBindVertexArray(vao);
// décrire les attributs de sommets
{ ... }
// selectionner l'index buffer et configurer le vao
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);

remarque : nettoyage / revenir à l'état par défaut

il y a en général un index buffer et un vertex buffer sélectionné ainsi que le vao. quelle est la bonne manière de les désélectionner :

rappel : certains paramètres sont implicites... le vao en cours de configuration, par exemple

et avec plusieurs attributs ?

il faut décrire chaque attribut déclaré.

exemple : position_buffer contient les positions, normal_buffer contient les normales des sommets, et les attributs sont déclarés comme : in vec3 position; in vec3 normal; dans le vertex shader :

// selectionner le vertex array object a configurer, si necessaire
// glBindVertexArray(vao);
// recuperer l'attribut
GLint position_attribute= glGetAttribLocation(program, "position");
glBindBuffer(GL_ARRAY_BUFFER, position_buffer);
glVertexAttribPointer(position_attribute, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(position_attribute);
// recuperer l'attribut
GLint normal_attribute= glGetAttribLocation(program, "normal");
glBindBuffer(GL_ARRAY_BUFFER, normal_buffer);
glVertexAttribPointer(normal_attribute, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(normal_attribute);

exemple complet : tuto4GL_normals.cpp

pour récupérer la liste des attributs déclarés par le vertex shader et vérifier qu'ils sont bien associés à un buffer, cf récupérer les uniforms et les attributs utilisés par un shader program

et avec un attribut entier (int, uint) ?

il faut utiliser glVertexAttribIPointer() avec un I au lieu de glVertexAttribPointer().

et si un attribut n'existe plus dans le vertex shader ?? ( glGetAttribLocation( ) < 0 )

le compilateur et le linker des shaders sont capables de déterminer qu'un attribut n'est pas nécessaire pour exécuter un shader. par exemple :

// vertex shader
in vec3 position;
in vec3 normal;
uniform mat4 mvpMatrix;
uniform mat4 mvMatrix;
// normal est un attribut du vertex shader, il n'est pas disponible dans le fragment shader, donc :
out vec3 view_normal;
// il faut declarer un varying, un resultat du vertex shader
void main( )
{
gl_Position= mvpMatrix * vec4(position, 1);
view_normal= mat3(mvMatrix) * normal; // uniquement une rotation, mat3 suffit
}
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition: vec.h:168
// recupere la normale calculee par le vertex shader, meme type, meme nom, mais in au lieu de out
in vec3 view_normal;
// rappel: interpolation en fonction de la position du fragment dans le triangle
out vec4 fragment_color;
void main( )
{
fragment_color= vec4(color, 1);
}
#endif

la couleur des fragments est constante, elle ne depend pas de la normale calculée par le vertex shader, et glGetAttibLocation( ... "normal" ) renvoie -1 dans ce cas, alors que l'attribut normal est correctement déclaré.

si le fragment shader est modifié pour utiliser la normale :

in vec3 view_normal;
out vec4 fragment_color;
void main( )
{
fragment_color= vec4(color * normalize(view_normal).z, 1);
}
#endif
Vector normalize(const Vector &v)
renvoie un vecteur unitaire / longueur == 1.
Definition: vec.cpp:123

glGetAttribLocation( ... "normal" ) renverra bien un identifiant valide dans ce cas. il faut prévoir les 2 cas dans le programme, il y a 2 manières de le gérer,

remarque : les attributs peuvent être numérotés directement par le vertex shader, et dans ce cas l'utilisation de glGetAttribLocation( ) n'est pas nécessaire, cf la section et sans utiliser glGetAttribLocation( ) ?.

et si les attributs sont dans le même vertex buffer ?

les paramètres stride et offset de glVertexAttribPointer( ) permettent d'organiser les données assez librement :

remarque : stride = 0 est interprété comme l'organisation par défaut, les valeurs se suivent, c'est équivalent à stride= sizeof()

exemple : position et normale dans le même buffer, toutes les positions, puis toutes les normales : PPPPPPPPNNNNNNNN

offset(position)= 0; // la premiere position se trouve au debut du buffer
stride(position)= 0; // ou sizeof(vec3), la prochaine position se trouve en suivant...
offset(normal)= sizeof(vec3) * positions.size(); // la premiere normale se trouve apres toutes les positions
stride(normal)= 0; // ou sizeof(vec3)

exemple : alterner les donnees, position + normale sommet 0, position + normale sommet 1, etc : PNPNPNPNPNPNPN

offset(position)= 0; // la premiere position se trouve au debut du buffer
stride(position)= sizeof(vec3) + sizeof(vec3); // la prochaine position se trouve apres la normale du sommet
offset(normal)= sizeof(vec3); // la premiere normale se trouve apres la premiere position
stride(normal)= sizeof(vec3) + sizeof(vec3);


cf exemple : utiliser un seul buffer pour stocker les attributs et configurer le vao pour un exemple complet de création.

vertex buffers et index buffer

les buffers sont des tableaux alloués dans la mémoire de la carte graphique, il faut leur donner une dimension (en octets) et transférer des données, cf glBufferData( ).

on peut également modifier le contenu d'un buffer à n'importe quel moment, cf glBufferSubData( ). en fonction de la fréquence de modifications, il faut choisir l'option correspondante lors de la création, cf le paramètre usage de glBufferData( ). l'option par défaut est GL_STATIC_DRAW qui indique que le buffer est utilisé par glDraw( ) et que son contenu ne devrait pas changer.

mais bien sur, il faut commencer par créer l'objet openGL, cf glGenBuffers( ) :

GLuint buffer;
glGenBuffers(1, &buffer);

pour pouvoir manipuler un buffer, il faut le sélectionner, comme les autres objets openGL, cf glBindBuffer( ) :

et on peut enfin definir sa taille et eventuellement son contenu avec glBufferData().

exemple : créer un vertex buffer pour stocker les positions d'un mesh, cf Mesh::vertex_buffer() et Mesh::vertex_buffer_size(), pour récupérer les infos nécessaires :

Mesh mesh= ... ;
// cree un nouveau buffer
GLuint buffer;
glGenBuffers(1, &buffer);
// selectionner le buffer comme un vertex buffer
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// dimensionner et initialiser le buffer selectionne sur array_buffer
glBufferData(GL_ARRAY_BUFFER, /* length */ mesh.vertex_buffer_size(), /* data */ mesh.vertex_buffer(), /* usage */ GL_STATIC_DRAW);
representation d'un objet / maillage.
Definition: mesh.h:112
const float * vertex_buffer() const
renvoie l'adresse de la position du premier sommet. permet de construire les vertex buffers openGL....
Definition: mesh.h:296
std::size_t vertex_buffer_size() const
renvoie la longueur (en octets) du vertex buffer.
Definition: mesh.h:298

exemple : utiliser un seul buffer pour stocker les attributs et configurer le vao

le buffer est remplit avec toutes les positions, suivies de toutes les texcoords, et enfin toutes les normales : PPPPPPPPTTTTTTTTNNNNNNNN

Mesh mesh= read_mesh( ... );
GLuint vao;
GLuint buffer;
// creer et selectionner le vao
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// creer, initialiser le buffer : positions + normals + texcoords du mesh
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// taille totale du buffer
size_t size= mesh.vertex_buffer_size() + mesh.texcoord_buffer_size() + mesh.normal_buffer_size();
glBufferData(GL_ARRAY_BUFFER, size, nullptr, GL_STATIC_DRAW);
// transfere les positions des sommets
size_t offset= 0;
size= mesh.vertex_buffer_size();
glBufferSubData(GL_ARRAY_BUFFER, offset, size, mesh.vertex_buffer());
// et configure l'attribut 0, vec3 position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, /* stride */ 0, (const GLvoid *) offset);
glEnableVertexAttribArray(0);
// les positions se trouvent au debut du buffer, offset == 0 et se suivent, stride == 0
// transfere les texcoords des sommets
offset= offset + size;
size= mesh.texcoord_buffer_size();
glBufferSubData(GL_ARRAY_BUFFER, offset, size, mesh.texcoord_buffer());
// et configure l'attribut 1, vec2 texcoord
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, /* stride */ 0, (const GLvoid *) offset);
glEnableVertexAttribArray(1);
// les texcoords se trouvent apres les positions, cf offset et se suivent, stride == 0
// transfere les normales des sommets
offset= offset + size;
size= mesh.normal_buffer_size();
glBufferSubData(GL_ARRAY_BUFFER, offset, size, mesh.normal_buffer());
// et configure l'attribut 2, vec3 normal
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, /* stride */ 0, (const GLvoid *) offset);
glEnableVertexAttribArray(2);
// les normales se trouvent apres les positions et les texcoords, cf offset et se suivent, stride == 0
std::size_t texcoord_buffer_size() const
renvoie la taille (en octets) du texcoord buffer.
Definition: mesh.h:308
const float * texcoord_buffer() const
renvoie l'adresse des coordonnees de textures du premier sommet. par convention, c'est un vec2,...
Definition: mesh.h:306
std::size_t normal_buffer_size() const
renvoie la longueur (en octets) du normal buffer.
Definition: mesh.h:303
const float * normal_buffer() const
renvoie l'adresse de la normale du premier sommet. par convention, la normale est un vec3,...
Definition: mesh.h:301
Mesh read_mesh(const char *filename)
charge un fichier wavefront .obj et renvoie un mesh compose de triangles non indexes....
Definition: wavefront.cpp:14

remarque : ce code fonctionne correctement mais le driver openGL peut faire des trucs louches au buffer, cf ressources cpu / gpu et transfert de données pour les details.

et sans utiliser glGetAttribLocation( ) ?

il suffit de donner l'identifiant directement dans le source du vertex shader avec layout(location= id) in vec3 attribute;:

layout(location= 0) in vec3 position;

l'exemple de création de buffer / configuration de vao précédent suppose que position est l'attribut 0, texcoord est l'attribut 1 et normal est l'attribut 2, ce qui se déclare comme ça :

layout(location= 0) in vec3 position;
layout(location= 1) in vec2 texcoord;
layout(location= 2) in vec3 normal;
vecteur generique, utilitaire.
Definition: vec.h:131

et la création des buffers / configuration du vao peut etre simplifiée, puisque l'on connait les identifiants des attributs. cf l'exemple précédent exemple : utiliser un seul buffer pour stocker les attributs et configurer le vao.

et comment modifier le contenu du buffer ?

Il y a plusieurs solutions,plus ou moins simples a programmer et plus ou moins efficaces. Mais la solution la plus simple consiste à utiliser le bon flag usage lors de la création du buffer :

Plus de détails dans ressources cpu / gpu et transfert de données.

version openGL 4.3

openGL 4.3 a introduit une nouvelle interface pour configurer les VAO, pour simplifier le portage depuis Direct3d, mais aussi pour séparer la déclaration du type des attributs de leur stockage dans un buffer.

c'est glVertexAttribFormat() qui décrit le type d'un attribut avec les mêmes paramètres que glVertexAttribPointer()

// selectionner le vertex array object a configurer, si necessaire
// glBindVertexArray(vao);
glVertexAttribFormat(/* attribut */ 0, /* size*/ 3, /* type */ GL_FLOAT, /* normalized */ GL_FALSE, /* relative offset */ 0);

remarque : pour le attributs entiers, il faut utiliser glVertexAttribIFormat() au lieu de glVertexAttribFormat().

glVertexAttribBinding() et glBindVertexBuffer()permettent d'associer un attribut à un buffer :

GLuint buffer;
{
// creation et initialisation du buffer contenant les donnees.
}
// selectionner le vertex array object a configurer, si necessaire
// glBindVertexArray(vao);
{
// configuration du type des attributs
}
glVertexAttribBinding( /* attribut */ 0, /* binding */ 0);
glBindVertexBuffer(/* binding */ 0, /* buffer */ buffer, /* offset */ 0, /* stride */ 0);

les paramètres de glBindVertexBuffer() sont les mêmes que ceux de glVertexAttribPointer(), ils permettent de décrire l'organisation des données dans le buffer. La seule différence est l'utilisation de relative offset par attribut en plus de offset par buffer. relative offset permet de savoir ou se trouvent les données d'un attribut dans la région du buffer, et offset indique de manière globale ou trouver les attributs du premier sommet dans le buffer.

par exemple, si l'on souhaite utiliser une organisation intrelacée : PNPNPNPNPNPNPN, qui décrit la position et la normale de chaque sommet :

relative offset(P)= 0
relative offset(N)= sizeof(P)
stride(P)= sizeof(P) + sizeof(N)
stride(N)= sizeof(P) + sizeof(N)
binding(P)= 0
binding(N)= 0

pour l'autre organisation PPPPPPPPNNNNNNNN :

relative offset(P)= 0;
relative offset(N)= SIZEOF(P) * n;
stride(P)= 0 // sizeof(P)
stride(N)= 0 // sizeof(N)
binding(P)= 0
binding(N)= 0

L'intéret de cette nouvelle interface pour configurer un vao est de permettre de changer très facilement de buffer, sans avoir à décrire plusieurs fois le type des attributs, et c'est probablement plus efficace pour le driver aussi : il est plus efficace de changer de buffers entre 2 objets à dessiner que de changer de vao.