Navigation▲
Tutoriel précédent : ouvrir une fenêtre | Sommaire | Tutoriel suivant : le premier triangle |
II. Contexte▲
C'est notre première rencontre avec la bibliothèque GLEW, « OpenGL Extension Wrangler Library ». GLEW vous aide à contourner le mal de crâne qui vous attend si vous voulez gérer les extensions dans OpenGL. Une fois initialisé, il récupère les extensions disponibles sur votre plateforme, les charge et fournit un accès facile via un simple fichier d'en-tête.
Dans ce tutoriel, nous allons voir l'utilisation des « Vertex Buffer Objects » (VBO) pour la première fois. Comme son nom l'indique, ils sont utilisés pour stocker des sommets.
Les objets du monde 3D que vous essayez de virtualiser, que ce soit des monstres, des châteaux ou un simple cube tournant, sont toujours construits en connectant des groupes de sommets ensemble.
Les VBO sont la manière la plus efficace de charger des sommets sur le GPU. Ce sont des tampons qui sont stockés sur la mémoire vidéo et ils fournissent le plus faible temps d'accès au GPU, ils sont donc vivement recommandés.
Ce tutoriel et le suivant sont les seuls de cette série qui s'appuient sur le pipeline fixe plutôt que sur le pipeline programmable.
En réalité, aucune transformation ne va avoir lieu dans ces deux tutoriels. Nous nous appuyons simplement sur la manière dont le flux de données transite dans le pipeline.
Une étude plus approfondie du pipeline suivra dans les prochains tutoriels, mais pour l'instant, il suffit de comprendre qu'avant d'atteindre le rasterizer (qui dessine effectivement les points, lignes et triangles en utilisant les coordonnées écran), les sommets visibles ont leurs coordonnées X, Y et Z entre les bornes [-1.0, 1.0]. Le rasterizer fait correspondre les coordonnées à l'espace écran (exemple : si la largeur de l'écran est 1024 alors la coordonnée 0.0 correspond à 0 et 1.0 correspond à 1023).
Finalement, le rasterizer dessine les primitives selon la topologie spécifiée dans la fonction de dessin (voir l'explication de code ci-dessous). Comme nous n'attachons pas de shader au pipeline, nos sommets ne subissent aucune transformation.
Ça signifie que nous avons juste à leur donner une valeur entre les bornes précédentes afin de les rendre visibles. En fait, définir zéro pour X et Y place le sommet au milieu des deux axes et par là même au milieu de l'écran.
II-A. Installation de GLEW▲
GLEW est disponible sur le site http://glew.sourceforge.net/.
La plupart des distributions Linux fournissent un paquet pour lui.
Sur Ubuntu vous pouvez l'installer en utilisant la ligne de commande suivante :
apt-get install libglew1.6 libglew1.6-dev
III. Explication du code▲
#include
<GL/glew.h>
Ici nous incluons l'unique fichier d'en-tête de GLEW.
Si vous incluez d'autres fichiers d'en-têtes OpenGL, vous devez faire attention à inclure ce fichier d'en-tête avant les autres sinon GLEW va s'en plaindre. Afin de lier le programme avec GLEW, vous devez ajouter « -lGLEW » au makefile.
#include
"math_3d.h"
Dans ce tutoriel nous commençons à utiliser des structures d'aides telles que les vecteurs. Nous étofferons ce fichier d'en-tête au fur et à mesure de nos avancées.
GLenum res =
glewInit();
if
(res !=
GLEW_OK)
{
fprintf(stderr, "Error: '%s'
\n
"
, glewGetErrorString(res));
return
1
;
}
Ici nous initialisons GLEW et vérifions les erreurs éventuelles. Cela doit être fait après l'initialisation de GLUT.
Vector3f Vertices[1
];
Vertices[0
] =
Vector3f(0.0
f, 0.0
f, 0.0
f);
Nous créons un tableau contenant une structure Vector3f (type défini dans math_3d.h) et initialisons XYZ à zéro. Ainsi le point apparaîtra au milieu de l'écran.
GLuint VBO;
Nous allouons un GLuint en variable globale au programme pour stocker l'identifiant du VBO.
Vous verrez par la suite que la majorité (si pas tous) des objets OpenGL sont représentés par une variable de type GLuint.
glGenBuffers(1
, &
VBO);
OpenGL définit plusieurs fonctions glGen* pour créer des objets de différents types.
Elles prennent souvent deux paramètres :
- le premier spécifie le nombre d'objets à créer ;
- le second est l'adresse d'un tableau de GLuint pour stocker les identifiants alloués par le pilote (faites attention à avoir un tableau assez grand pour récupérer le résultat).
Les futurs appels à cette fonction ne généreront pas les mêmes identifiants à moins qu'ils aient d'abord été détruits avec glDeleteBuffers.
Notez qu'à ce stade vous ne spécifiez pas ce que vous avez l'intention de faire avec les tampons et donc ils peuvent être considérés comme « génériques ». Leur spécification est la tâche de la fonction suivante.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
OpenGL a une manière plutôt unique d'utiliser les identifiants.
Dans de nombreuses bibliothèques, l'identifiant est simplement passé à une fonction correspondante et l'action est effectuée sur cet identifiant. En OpenGL, nous attachons l'identifiant à un nom de cible et effectuons ensuite les commandes sur cette cible. Ces commandes affectent l'identifiant attaché jusqu'à ce qu'un autre soit attaché à sa place ou que la fonction ci-dessus soit appelée avec 0 comme identifiant.
La cible GL_ARRAY_BUFFER signifie que le tampon va contenir un tableau de sommets.
Une autre cible utile est GL_ELEMENT_ARRAY_BUFFER qui signifie que le tampon contient les indices des sommets d'un autre tampon.
D'autres cibles sont aussi disponibles et nous verrons leur utilité dans de futurs tutoriels.
glBufferData(GL_ARRAY_BUFFER, sizeof
(Vertices), Vertices, GL_STATIC_DRAW);
Après avoir attaché notre objet, nous le remplissons de données.
L'appel ci-dessus prend le nom de la cible (le même que celui utilisé pour l'attachement), la taille des données en octets, l'adresse du tableau de sommets et un indicateur indiquant le modèle d'utilisation de ces données.
Comme nous n'allons pas changer le contenu du tampon, nous spécifions GL_STATIC_DRAW. L'opposé serait GL_DYNAMIC_DRAW.
Bien que ce ne soit qu'un conseil pour OpenGL, il est bon de réfléchir à l'indicateur correct à utiliser. Le pilote peut s'appuyer dessus pour des stratégies d'optimisation (comme la meilleure place en mémoire pour stocker le tampon).
glEnableVertexAttribArray(0
);
Dans le tutoriel sur les shaders, vous verrez que les attributs de sommet utilisés par le shader (position, normale…) ont un indice associé, qui vous permet de créer un lien entre les données du programme C/C++ et le nom de l'attribut au sein du shader.
De plus vous pouvez aussi activer chaque indice d'attribut de sommet.
Dans ce tutoriel, nous n'utilisons pas encore de shader, mais la position du sommet que nous avons chargé dans le tampon est traitée comme l'indice 0 d'attribut de sommet dans le pipeline fixe (qui devient actif s'il n'y a pas de shader lié).
Vous devez activer chaque attribut de sommet, sinon les données associées ne seront pas accessibles dans le pipeline.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
Ici, nous attachons encore notre tampon comme nous nous préparons à appeler la fonction de dessin.
Dans ce petit programme, nous n'avons qu'un seul tampon de sommets donc faire cet appel à chaque image est redondant, mais dans les programmes plus complexes, où il y a de multiples tampons pour stocker vos divers modèles, vous devez mettre à jour l'état du pipeline avec le tampon que vous avez l'intention d'utiliser.
glVertexAttribPointer(0
, 3
, GL_FLOAT, GL_FALSE, 0
, 0
);
Cet appel dit au pipeline comment interpréter les données dans le tampon.
Le premier paramètre spécifie l'indice de l'attribut. Dans notre cas, nous savons que c'est zéro par défaut, mais quand nous commencerons à utiliser les shaders, nous aurons besoin soit de le spécifier explicitement, soit de le demander.
Le second paramètre est le nombre de composantes dans l'attribut (trois pour X, Y et Z).
Le troisième paramètre est le type de données de toutes les composantes.
Le paramètre suivant indique si nous voulons que notre attribut soit normalisé avant son utilisation dans le pipeline. Dans notre cas, nous voulons que nos données passent sans être modifiées.
Le cinquième paramètre (appelé « stride ») est le nombre d'octets entre deux instances de l'attribut dans le tampon. Quand il n'y a qu'un seul attribut (exemple : le tampon contient uniquement des positions) et que les données sont contiguës, nous passons zéro. Si nous avons un tableau de structures qui contiennent une position et une normale (chacune étant un vecteur de trois flottants), nous passerons la taille de la structure en octets (6 * 4 = 24).
Le dernier paramètre est utile dans le cas de l'exemple précédent. Nous devons spécifier le décalage à l'intérieur de la structure où le pipeline va trouver notre attribut. Dans le cas de la structure avec la position et la normale, le décalage de la position est zéro alors que le décalage de la normale est 12.
glDrawArrays(GL_POINTS, 0
, 1
);
Enfin, nous appelons la fonction de dessin de la géométrie.
Toutes les commandes que nous avons vues jusqu'ici sont importantes, mais elles ne font que préparer le terrain pour l'appel de cette fonction. C'est là que le GPU commence réellement son travail. Il va combiner les paramètres de la fonction de dessin avec l'état qui a été construit jusqu'à maintenant et rendre le résultat sur l'écran.
OpenGL fournit plusieurs types de fonctions de dessin et chacune est propre à un cas particulier. En général il est possible de les diviser en deux catégories :
- les dessins ordonnés ;
- les dessins indicés.
Les dessins ordonnés sont les plus simples. Le GPU parcourt le tampon de sommets, sommet par sommet et les interprète selon la topologie spécifiée dans la fonction de dessin. Par exemple, si vous spécifiez GL_TRIANGLES alors les sommets 0 à 2 deviennent le premier triangle, 3 à 5 le second…
Si vous voulez que le même sommet apparaisse dans plus d'un triangle, vous devez le spécifier autant de fois dans le tampon de sommets, ce qui est un gâchis de place.
Les dessins indicés sont plus complexes et impliquent un tampon supplémentaire : le tampon d'indices. Le tampon d'indices contient les indices des sommets dans le tampon de sommets. Le GPU scanne le tampon d'indices et de la même manière que dans la description précédente les indices 0 à 2 forment le premier triangle…
Si vous voulez que le même sommet apparaisse dans deux triangles, vous avez juste à spécifier son indice deux fois dans le tampon d'indices. Le tampon de sommets a besoin de n'en contenir qu'une seule copie.
Les dessins indicés sont plus communs dans les jeux, car la majorité des modèles sont créés à partir de triangles qui représentent une surface (peau d'une personne, mur de château…) avec beaucoup de partages de sommets entre eux.
Dans ce tutoriel, nous utilisons la fonction de dessin la plus simple : glDrawArrays. C'est un dessin ordonné où il n'y a pas de tampon d'indices.
Nous spécifions la topologie à « points » ce qui signifie que chaque sommet est un point.
Le paramètre suivant est l'indice du premier sommet à dessiner. Dans notre cas, nous voulons commencer au début du tampon donc nous spécifions zéro, mais ça nous permet de stocker plusieurs modèles dans le même tampon et choisir celui à afficher selon le décalage dans le tampon.
Le dernier paramètre est le nombre de sommets à dessiner.
glDisableVertexAttribArray(0
);
C'est une bonne pratique de désactiver chaque attribut de sommet quand il n'est pas immédiatement utilisé.
Le laisser activé quand un shader ne l'utilise pas est un moyen sûr de rencontrer des problèmes.
IV. Sources▲
Vous pouvez télécharger les sources de ce projet en suivant ce lien :
V. Remerciements▲
Merci à Etay Meiri de nous permettre de traduire son tutoriel.
Merci à LittleWhite pour ses corrections, à Torgar ainsi qu'à ClaudeLELOUP pour leur relecture.
Navigation▲
Tutoriel précédent : ouvrir une fenêtre | Sommaire | Tutoriel suivant : le premier triangle |