gKit2 light
|
un ensemble de fonctions... pour utiliser un pipeline 3D.
Dans un monde idéal, il y aurait un seul source, décrivant l'application et les shaders, des tableaux de triangles pour décrire les objets, et il n'y aurait pas de différence entre affecter une valeur à une variable / paramètre d'un shader et affecter une valeur à une variable de l'application...
La réalité est très légèrement différente. L'application s'exécute sur le processeur de l'ordinateur et les shaders s'exécutent sur les processeurs de la carte graphique, et les deux disposent de leur propre mémoire, qui ne partagent pas d'espace d'adressage... autrement dit, une variable de shader n'existe pas dans l'application, et réciproquement, une variable de l'application n'existe pas dans un shader. Le cpu et le gpu sont physiquement des machines différentes connectées par un réseau... et c'est au programmeur de compiler un programme pour le cpu, de compiler les shaders pour le gpu et de les faire communiquer / s'exécuter ensemble. Dernier point important, le pipeline propose pas mal d'options différentes, et il n'est pas envisageable d'affecter explicitement une valeur à l'ensemble des paramètres à chaque fois que l'on veut dessiner un objet. Ces 2 problèmes font qu'une api 3d est un très gros ensemble de fonctions / librairie, et que la totalité des paramètres sont implicites.
openGL est utilisable à travers un ensemble de fonctions C, ce qui signifie, pas de surchage, mais une famille de fonctions, des types opaques, pas d'objet, mais des identifiants.
une interface C++ pourrait ressembler à ça :
tout est explicite, on connait le contexte, le buffer, etc.
la version C pur pourrait ressembler à ça :
tout est explicite, mais un peu plus long à écrire.
l'interface d'openGL utilise une convention différente : au lieu de rendre explicite l'objet / la structure manipulée, elle est d'abord "sélectionnée", puis manipulée implicitement :
ce qui permet d'obtenir une syntaxe assez compacte, mais peut être source de nombreuses erreurs, si l'on n'y prête pas attention.
autre détail important, il n'y a pas de surchage en C, on ne peut pas écrire ça pour modifier la valeur d'un paramètre de shader :
ni ça :
mais plutot ça :
chaque surcharge est nommée explicitement, puisque seul le nom de la fonction est utilisé. selon le type de valeur à manipuler, les noms de fonctions sont décorés par un suffixe indiquant le nombre de paramètres (1, 2, 3, ou 4) et leur type (f pour float, i pour int, u pour unsigned int, v pour vector, un pointeur sur une ou plusieurs valeurs, etc.)
pour dessiner, il faut en général créer plusieurs types d'objets openGL, la démarche est toujours la même :
GLuint
, cf la famille de fonctions glGenXXX( ),exemple, pour créer plusieurs buffers openGL, cf glGenBuffers( )
:
pourquoi ? tous ces objets / ressources openGL permettent de manipuler des données qui sont en fait stockées dans la mémoire de la carte graphique. Il n'y a pas d'espace d'adressage commun entre le cpu et le gpu : l'application ne peut pas obtenir un pointeur sur une structure allouée sur le gpu. Par contre, toutes les api 3d proposent la même abstraction. Par exemple, allouer de la mémoire gpu pour stocker les coordonnées des sommets des triangles à dessiner est caché derrière les buffers.
pour créer un seul buffer, il suffit de passer le pointeur sur l'identifiant :
le buffer sera ensuite sélectionné avec glBindBuffer( )
. cf configurer un format de sommet, vertex array object pour plus de détails sur l'utilisation des buffers.
c'est l'équivalent d'une allocation dynamique, cf new[]
ou malloc()
, mais au lieu de récupérer un pointeur, on obtient un identifiant de la zone mémoire allouée sur le gpu.
le vao sera ensuite sélectionné avec glBindVertexArray( )
. cf configurer un format de sommet, vertex array object pour plus de détails
cet objet permet de décrire la structure d'un sommet, c'est l'équivalent openGL d'une déclaration :
pour chaque membre, il y aura un attribut de sommet openGL déclaré en utilisant glVertexAttribPointer()
. cf configurer un format de sommet, vertex array object
la texture sera ensuite sélectionnée avec glBindTexture( )
, cf textures, samplers et pipeline.
c'est la même idée que pour les buffers, une texture est un tableau de couleurs, les fonctions glTexImageXXX()
permettent de déclarer les dimensions de l'image et de décrire la structure qui représente une couleur.
les shaders et les programs sont un peu à part, leur fonction de création ne crée qu'un seul objet à la fois :
le shader program sera sélectionné avec glUseProgram( )
. cf compiler et linker un shader program pour les détails.
openGL inclut un compilateur et un linker de shaders, ainsi qu'un ensemble de fonctions permettant de récupérer les symboles / l'interface des shaders. Ce sont ces informations qui permettent d'affecter une valeur à chaque paramètre avant de dessiner des objets. c'est équivalent à :
mais le pipeline parallèlise l'execution de la boucle, et le vertex shader ne transforme qu'un seul sommet, il est exécuté par N threads :
et les api 3d décrivent les paramètres du vertex shader différemment :
le vertex shader et ses paramètres s'écrivent au final comme ça :
les mot clés uniform
, in
, out
indiquent si chaque paramètre est une entrée, un résultat, ou si c'est un élément d'un tableau, indexé implicitement par l'identifiant du thread qui exécute la fonction...
résultat, pour exécuter la fonction, au lieu d'écrire :
il faut indiquer, paramètre par paramètre, quel objet openGL contient sa valeur : un vertex array object, un uniform, ou un buffer. cf afficher plusieurs triangles, modifier les paramètres uniform d'un shader program
et c'est encore un peu petit plus décomposé : pour affecter une valeur à un paramètre uniform
, il faut d'abord récupérer son identifiant / symbole, avec glGetUniformLocation()
puis utiliser la bonne surcharge de glProgramUniform() / glUniform()
pour affecter une valeur. cf afficher plusieurs triangles, modifier les paramètres uniform d'un shader program.
Affecter les paramètres uniform
un par un n'est pas la seule solution, il est aussi possible et plus performant de les grouper dans un buffer, cf openGL 3.3 : uniform buffers et ressources cpu / gpu et transfert de données pour la mise à jour efficace des buffers.
l'api complète et sa documentation est consultable sur opengl.org, le wiki propose un résumé des fonctionnalités et ainsi que les détails d'utilisation.
Oui, il est très tentant d'écrire une abstraction c++ pour manipuler tout ça... malheureusement, les paramètres implicites empechent de faire une conception propre.
Autre point pénible, openGL est une api historique, conçue par SGI (IrisGL) et rendu publique en 1992, d'ou le nom openGL. Les conventions de programmation, ainsi que les cartes graphiques étaient très différentes à l'époque. Les shaders n'existaient pas, par exemple, la première version ne permettait pas de manipuler de textures, etc. Un premier gros travail de nettoyage est arrivé avec la version 3.3 core profile. Cette version est la base d'openGL ES utilisé sur tablette et téléphone, ainsi que de WebGL. Chaque version d'openGL correspond à l'ajout de nouvelles fonctionnalités pour profiter de chaque génération de carte graphique. Certaines fonctions sont ajoutées pour raison de performances, mais les anciennes sont toujours présentes dans l'api... De même, plusieurs ajouts permettent de simplifier le portage des applications écrites avec Direct3d. Et l'api est très chargée, par exemple : pour modifier le contenu d'un buffer, il y a au moins 4 solutions différentes, il y a aussi 2 manières de décrire les attributs de sommets, la "nouvelle" a été introduite en partie pour raison de performance et en partie parce que la notion de vertex array n'existe pas en Direct3d...
Par contre, openGL 4.5 inclus l'extension direct_state_access
qui permet de se rapprocher d'un modèle objet, mais comme cette api co-existe avec la version historique, le nommage des fonctions est pénible. Cette dernière version de l'api élimine la sélection d'un objet pour modifier ses propriétés. par exemple, au lieu de :
on peut écrire directement :
Reste aussi un problème plus fondamental, la gestion des ressources openGL, l'application ne fait que référencer les objets openGL, le contexte reste le propriétaire, et dernière subtilité, un contexte doit être initialisé pour créer et manipuler des objets openGL.
Mais dans tous les cas, il faut commencer par écrire quelques applications openGL pour comprendre comment le manipuler avant de se lancer dans un exercice d'abstraction / conception.
remarque : après quelques tps, Mesh::draw( ) devrait apparaitre clairement comme la solution simple et assez naive qu'elle est réellement.
exercice : pourquoi ? proposez une construction plus interressante, ou pour commencer : quelle est la principale limite, ou hypothèse de Mesh ?
exercice : uniformisez le nommage des fonctions de openGL 4.5 direct state en éliminant toutes les fonctions historiques et groupez les dans un namespace c++