Présentation
Le rendu de graphiques 3D n'est pas une tâche facile, mais extrêmement intéressant et passionnant. Cet article s'adresse à ceux qui commencent tout juste à se familiariser avec OpenGL ou à ceux qui s'intéressent au fonctionnement des pipelines graphiques et à ce qu'ils sont. Cet article ne fournira pas d'instructions précises sur la façon de créer un contexte et une fenêtre OpenGL, ni sur la façon d'écrire votre première application de fenêtre OpenGL. Cela est dû aux caractéristiques de chaque langage de programmation et au choix d'une bibliothèque ou d'un framework pour travailler avec OpenGL (j'utiliserai C ++ et
GLFW ), d'autant plus qu'il est facile de trouver un tutoriel sur le réseau pour le langage qui vous intéresse. Tous les exemples donnés dans l'article fonctionneront dans d'autres langues avec une sémantique légèrement modifiée des commandes, pourquoi il en est ainsi, je le dirai un peu plus tard.
Qu'est-ce qu'OpenGL?
OpenGL est une spécification qui définit une interface logicielle indépendante de la plate-forme pour l'écriture d'applications utilisant des graphiques informatiques bidimensionnels et tridimensionnels. OpenGL n'est pas une implémentation, mais décrit uniquement les ensembles d'instructions qui doivent être implémentés, c'est-à-dire est une API.
Chaque version d'OpenGL a sa propre spécification, nous travaillerons de la version 3.3 à la version 4.6, car toutes les innovations de la version 3.3 affectent des aspects qui ont peu d'importance pour nous. Avant de commencer à écrire votre première application OpenGL, je vous recommande de découvrir les versions prises en charge par votre pilote (vous pouvez le faire sur le site du fournisseur de votre carte vidéo) et de mettre à jour le pilote vers la dernière version.
Périphérique OpenGL
OpenGL peut être comparé à une grande machine à états, qui a de nombreux états et fonctions pour les changer. L'état OpenGL fait essentiellement référence au contexte OpenGL. Tout en travaillant avec OpenGL, nous passerons par plusieurs fonctions de changement d'état qui changeront le contexte et effectueront des actions en fonction de l'état actuel d'OpenGL.
Par exemple, si nous donnons à OpenGL la commande d'utiliser des lignes au lieu de triangles avant le rendu, alors OpenGL utilisera les lignes pour tous les rendus suivants jusqu'à ce que nous modifiions cette option ou que nous changions le contexte.
Objets dans OpenGL
Les bibliothèques OpenGL sont écrites en C et ont de nombreuses API pour elles pour différents langages, mais ce sont néanmoins des bibliothèques C. De nombreuses constructions à partir de C ne sont pas traduites dans des langages de haut niveau, donc OpenGL a été développé en utilisant un grand nombre d'abstractions, l'une de ces abstractions étant des objets.
Un objet dans OpenGL est un ensemble d'options qui détermine son état. Tout objet dans OpenGL peut être décrit par son identifiant et l'ensemble d'options dont il est responsable. Bien sûr, chaque type d'objet a ses propres options et une tentative de configuration d'options inexistantes pour l'objet entraînera une erreur. C'est là que réside l'inconvénient d'utiliser OpenGL: un ensemble d'options est décrit par une structure C similaire dont l'identifiant est souvent un nombre, ce qui ne permet pas au programmeur de trouver une erreur au stade de la compilation, car un code erroné et correct est impossible à distinguer sémantiquement.
glGenObject(&objectId); glBindObject(GL_TAGRGET, objectId); glSetObjectOption(GL_TARGET, GL_CORRECT_OPTION, correct_option);
Vous rencontrerez un tel code très souvent, donc lorsque vous vous habituerez à ce que c'est que de configurer une machine d'état, cela deviendra beaucoup plus facile pour vous. Ce code ne montre qu'un exemple du fonctionnement d'OpenGL. Par la suite, de vrais exemples seront présentés.
Mais il y a des avantages. La principale caractéristique de ces objets est que nous pouvons déclarer de nombreux objets dans notre application, définir leurs options, et chaque fois que nous démarrons des opérations en utilisant l'état OpenGL, nous pouvons simplement lier l'objet avec nos paramètres préférés. Par exemple, cela peut être des objets avec des données de modèle 3D ou quelque chose que nous voulons dessiner sur ce modèle. La possession de plusieurs objets permet de basculer facilement entre eux pendant le processus de rendu. Avec cette approche, nous pouvons configurer de nombreux objets nécessaires au rendu et utiliser leurs états sans perdre un temps précieux entre les images.
Pour commencer à travailler avec OpenGL, vous devez vous familiariser avec plusieurs objets de base sans lesquels nous ne pouvons rien afficher. En utilisant ces objets comme exemple, nous comprendrons comment lier des données et des instructions exécutables dans OpenGL.
Objets de base: Shaders et programmes de shader. =
Shader est un petit programme qui s'exécute sur un accélérateur graphique (GPU) à un certain point du pipeline graphique. Si nous considérons les shaders de manière abstraite, nous pouvons dire que ce sont les étapes du pipeline graphique, qui:
- Sachez où obtenir les données pour le traitement.
- Savoir comment traiter les données d'entrée.
- Ils savent où écrire des données pour un traitement ultérieur.
Mais à quoi ressemble le pipeline graphique? Très simple, comme ceci:

Jusqu'à présent, dans ce schéma, nous ne nous intéressons qu'à la verticale principale, qui commence par la spécification Vertex et se termine par Frame Buffer. Comme mentionné précédemment, chaque shader a ses propres paramètres d'entrée et de sortie, qui diffèrent par le type et le nombre de paramètres.
Nous décrivons brièvement chaque étape du pipeline afin de comprendre ce qu'il fait:
- Vertex Shader - nécessaire pour traiter les données de coordonnées 3D et tous les autres paramètres d'entrée. Le plus souvent, le vertex shader calcule la position du vertex par rapport à l'écran, calcule les normales (si nécessaire) et génère des données d'entrée vers d'autres shaders.
- Shader de tessellation et shader de contrôle de tessellation - ces deux shaders sont chargés de détailler les primitives provenant du vertex shader et de préparer les données pour le traitement dans le shader géométrique. Il est difficile de décrire ce que ces deux shaders sont capables de faire en deux phrases, mais pour que les lecteurs aient une petite idée, je vais donner quelques images avec des niveaux de chevauchement faibles et élevés:
Je vous conseille de lire cet article si vous souhaitez en savoir plus sur la tessellation. Dans cette série d'articles, nous couvrirons la tessellation, mais ce ne sera pas bientôt. - Shader géométrique - est responsable de la formation de primitives géométriques à partir de la sortie du shader de pavage. En utilisant le shader géométrique, vous pouvez créer de nouvelles primitives à partir des primitives OpenGL de base (GL_LINES, GL_POINT, GL_TRIANGLES, etc.), par exemple, en utilisant le shader géométrique, vous pouvez créer un effet de particule en décrivant la particule uniquement par couleur, centre de cluster, rayon et densité.
- Le shader de rastérisation est l'un des shaders non programmables. Parlant dans un langage compréhensible, il traduit toutes les primitives graphiques de sortie en fragments (pixels), c'est-à-dire détermine leur position sur l'écran.
- Le fragment shader est la dernière étape du pipeline graphique. Le fragment shader calcule la couleur du fragment (pixel) qui sera défini dans le tampon d'image actuel. Le plus souvent, dans le shader de fragment, l'ombrage et l'éclairage du fragment, la cartographie des textures et les cartes normales sont calculés - toutes ces techniques vous permettent d'obtenir des résultats incroyablement beaux.
Les shaders OpenGL sont écrits dans un langage GLSL spécial de type C à partir duquel ils sont compilés et liés dans un programme de shaders. Déjà à ce stade, il semble que l'écriture d'un programme de shader soit une tâche extrêmement longue, car vous devez déterminer les 5 étapes du pipeline graphique et les lier ensemble. Heureusement, ce n'est pas le cas: les ombrages de tessellation et de géométrie sont définis dans le pipeline graphique par défaut, ce qui nous permet de définir seulement deux ombrages - le sommet et les fragments (parfois appelés pixel shader). Il est préférable de considérer ces deux shaders avec un exemple classique:
Vertex shader #version 450 layout (location = 0) in vec3 vertexCords; layout (location = 1) in vec3 color; out vec3 Color; void main(){ gl_Position = vec4(vertexCords,1.0f) ; Color = color; }
Shader de fragment #version 450 in vec3 Color; out vec4 out_fragColor; void main(){ out_fragColor = Color; }
Exemple d'assemblage de shader unsigned int vShader = glCreateShader(GL_SHADER_VERTEX);
Ces deux shaders simples ne calculent rien, ils transmettent simplement les données dans le pipeline. Prenons attention à la façon dont les vertex et fragments shaders sont connectés: dans le vertex shader, la variable Color est déclarée dans laquelle la couleur sera écrite après l'exécution de la fonction principale, tandis que dans le fragment shader, la même variable exacte avec le qualificatif in est déclarée, c'est-à-dire comme décrit précédemment, le fragment shader reçoit des données du sommet au moyen d'une simple poussée des données plus loin dans le pipeline (mais en fait ce n'est pas si simple).
Remarque: Si vous ne déclarez pas et n'initialisez pas une variable de type vec4 dans le fragment shader, rien ne s'affichera à l'écran.
Les lecteurs attentifs ont déjà remarqué la déclaration de variables d'entrée de type vec3 avec d'étranges qualificatifs de disposition au début du vertex shader, il est logique de supposer qu'il s'agit d'une entrée, mais d'où l'obtenons-nous?
Objets de base: tampons et tableaux de sommets
Je pense que cela ne vaut pas la peine d'expliquer ce que sont les objets tampon, nous allons mieux réfléchir à la façon de créer et de remplir un tampon dans OpenGL
float vertices[] = {
Il n'y a rien de difficile à cela, nous attachons le tampon généré à la cible souhaitée (nous découvrirons plus tard laquelle) et chargeons les données en indiquant leur taille et leur type d'utilisation.
GL_STATIC_DRAW - les données du tampon ne seront pas modifiées.
GL_DYNAMIC_DRAW - les données dans le tampon changeront, mais pas souvent.
GL_STREAM_DRAW - les données du tampon changeront à chaque appel de tirage.
C'est génial, maintenant nos données sont situées dans la mémoire du GPU, le programme de shader est compilé et lié, mais il y a une mise en garde: comment le programme sait-il où obtenir les données d'entrée pour le vertex shader? Nous avons téléchargé les données, mais n'avons pas indiqué d'où proviendrait le programme de shader. Ce problème est résolu par un type distinct d'objets OpenGL - les tableaux de vertex.

L'image est tirée de ce tutoriel .
Comme pour les tampons, les tableaux de sommets sont mieux visualisés à l'aide de leur exemple de configuration.
unsigned int VBO, VAO; glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glGenVertexArrays(1, &VAO); glBindVertexArray(VAO);
La création de tableaux de sommets n'est pas différente de la création d'autres objets OpenGL, le plus intéressant commence après la ligne:
glBindVertexArray(VAO);
Un vertex array (VAO) se souvient de toutes les liaisons et configurations qui sont effectuées avec lui, y compris la liaison des objets tampon pour le déchargement des données. Dans cet exemple, il n'y a qu'un seul objet de ce type, mais en pratique, il peut y en avoir plusieurs. Après cela, l'attribut vertex avec un numéro spécifique est configuré:
glBindBuffer(GL_ARRAY_BUFFER, VBO); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
Où avons-nous obtenu ce numéro? Vous vous souvenez des qualificatifs de disposition pour les variables d'entrée de vertex shader? Ce sont eux qui déterminent à quel attribut de sommet la variable d'entrée sera liée. Passez maintenant brièvement en revue les arguments de la fonction pour qu'il n'y ait pas de questions inutiles:
- Le numéro d'attribut que nous voulons configurer.
- Le nombre d'articles que nous voulons prendre. (Puisque la variable d'entrée du vertex shader avec layout = 0 est de type vec3, alors nous prenons 3 éléments de type float)
- Type d'articles.
- Faut-il normaliser les éléments, s'il s'agit d'un vecteur.
- Le décalage pour le sommet suivant (puisque nous avons les coordonnées et les couleurs situées séquentiellement et chacune a le type vec3, alors nous décalons de 6 * sizeof (float) = 24 octets).
- Le dernier argument montre quel décalage prendre pour le premier sommet. (pour les coordonnées, cet argument est de 0 octet, pour les couleurs 12 octets)
Maintenant, nous sommes prêts à rendre notre première image
N'oubliez pas de lier le VAO et le programme shader avant d'appeler le rendu.
{
Si vous avez tout fait correctement, vous devriez obtenir ce résultat:

Le résultat est impressionnant, mais d'où vient le remplissage dégradé dans le triangle, car nous n'avons indiqué que 3 couleurs: rouge, bleu et vert pour chaque sommet individuel? C'est la magie du shader de pixellisation: le fait est que la valeur de couleur que nous avons définie dans le sommet n'entre pas dans le shader de fragment. Nous ne transmettons que 3 sommets, mais beaucoup plus de fragments sont générés (il y a exactement autant de fragments qu'il y a de pixels remplis). Par conséquent, pour chaque fragment, la moyenne des trois valeurs de couleur est prise, en fonction de sa proximité avec chacun des sommets. Ceci est très bien vu aux coins du triangle, où les fragments prennent la valeur de couleur que nous avons indiquée dans les données de sommet.
À l'avenir, je dirai que les coordonnées de texture sont transmises de la même manière, ce qui facilite la superposition de textures sur nos primitives.
Je pense que cela vaut la peine de terminer cet article, le plus difficile est derrière nous, mais le plus intéressant ne fait que commencer. Si vous avez des questions ou si vous avez vu une erreur dans l'article, écrivez à ce sujet dans les commentaires, je vous en serai très reconnaissant.
Dans le prochain article, nous examinerons les transformations, nous en apprendrons sur les variables unifrom et nous apprendrons à imposer des textures aux primitives.