Introduction à OpenGL : un didacticiel de rendu de texte 3D
Publié: 2022-03-11Avec la disponibilité d'outils comme DirectX et OpenGL, écrire une application de bureau qui rend des éléments 3D n'est pas très difficile de nos jours. Cependant, comme de nombreuses technologies, il existe parfois des obstacles qui rendent difficile pour les développeurs qui tentent d'entrer dans ce créneau. Au fil du temps, la course entre DirectX et OpenGL a rendu ces technologies plus accessibles aux développeurs, ainsi qu'une meilleure documentation et un processus plus facile pour devenir un développeur DirectX ou OpenGL qualifié.
DirectX, introduit et maintenu par Microsoft, est une technologie spécifique à la plate-forme Windows. D'autre part, OpenGL est une API multiplateforme pour le domaine graphique 3D dont les spécifications sont maintenues par le groupe Khronos.
Dans cette introduction à OpenGL, j'expliquerai comment écrire une application très simple pour rendre des modèles de texte 3D. Nous utiliserons Qt/Qt Creator pour implémenter l'interface utilisateur, ce qui facilitera la compilation et l'exécution de cette application sur plusieurs plates-formes. Le code source du prototype construit pour cet article est disponible sur GitHub.
Le but de cette application simple est de générer des modèles 3D, de les enregistrer dans un fichier au format simple, de les ouvrir et de les afficher à l'écran. Le modèle 3D dans la scène rendue sera rotatif et zoomable, pour donner une meilleure idée de la profondeur et de la dimension.
Conditions préalables
Avant de commencer, nous devrons préparer notre environnement de développement avec quelques outils utiles pour ce projet. La toute première chose dont nous avons besoin est le framework Qt et les utilitaires correspondants, qui peuvent être téléchargés sur www.qt.io. Il peut également être disponible via le gestionnaire de packages standard de votre système d'exploitation ; si tel est le cas, vous voudrez peut-être essayer d'abord. Cet article nécessite une certaine familiarité avec le framework Qt. Cependant, si vous n'êtes pas familier avec le framework, ne vous découragez pas, car le prototype repose sur certaines fonctionnalités assez triviales du framework.
Vous pouvez également utiliser Microsoft Visual Studio 2013 sous Windows. Dans ce cas, assurez-vous que vous utilisez le complément Qt approprié pour Visual Studio.
À ce stade, vous souhaiterez peut-être cloner le référentiel à partir de GitHub et le suivre pendant que vous lisez cet article.
Présentation d'OpenGL
Nous allons commencer par créer un projet d'application Qt simple avec un seul widget de document. Puisqu'il s'agit d'un widget simple, sa compilation et son exécution ne produiront rien d'utile. Avec Qt designer, nous ajouterons un menu « Fichier » avec quatre éléments : « Nouveau… », « Ouvrir… », « Fermer » et « Quitter ». Vous pouvez trouver le code qui lie ces éléments de menu à leurs actions correspondantes dans le référentiel.
Cliquer sur "Nouveau..." devrait faire apparaître une boîte de dialogue qui ressemblera à ceci :
Ici, l'utilisateur peut saisir du texte, choisir une police, modifier la hauteur du modèle résultant et générer un modèle 3D. Cliquer sur "Créer" devrait enregistrer le modèle et devrait également l'ouvrir si l'utilisateur choisit l'option appropriée dans le coin inférieur gauche. Comme vous pouvez le constater, le but ici est de convertir du texte saisi par l'utilisateur en un modèle 3D et de le rendre à l'écran.
Le projet aura une structure simple et les composants seront décomposés en une poignée de fichiers C++ et d'en-tête :
createcharmodeldlg.h/cpp
Les fichiers contiennent un objet dérivé de QDialog. Cela implémente le widget de dialogue qui permet à l'utilisateur de taper du texte, de sélectionner la police et de choisir d'enregistrer le résultat dans un fichier et/ou de l'afficher en 3D.
gl_widget.h/cpp
Contient l'implémentation de l'objet dérivé de QOpenGLWidget. Ce widget est utilisé pour rendre la scène 3D.
mainwindow.h/cpp
Contient l'implémentation du widget principal de l'application. Ces fichiers sont restés inchangés depuis leur création par l'assistant Qt Creator.
main.cpp
Contient la fonction main(…), qui crée le widget principal de l'application et l'affiche à l'écran.
model2d_processing.h/cpp
Contient la fonctionnalité de création de scène 2D.
model3d.h/cpp
Contient des structures qui stockent des objets de modèle 3D et permettent aux opérations de travailler dessus (sauvegarder, charger, etc.).
model_creator.h/cpp
Contient l'implémentation de la classe qui permet la création d'un objet de modèle de scène 3D.
Implémentation OpenGL
Par souci de brièveté, nous passerons les détails évidents de l'implémentation de l'interface utilisateur avec Qt Designer et le code définissant les comportements des éléments interactifs. Il y a certainement des aspects plus intéressants de cette application prototype, ceux qui sont non seulement importants mais également pertinents pour l'encodage et le rendu du modèle 3D que nous voulons couvrir. Par exemple, la première étape de conversion de texte en modèle 3D dans ce prototype consiste à convertir le texte en une image monochrome 2D. Une fois cette image générée, il est possible de savoir quel pixel de l'image forme le texte, et lesquels ne sont que de l'espace « vide ». Il existe des moyens plus simples de rendre du texte de base à l'aide d'OpenGL, mais nous adoptons cette approche afin de couvrir certains détails du rendu 3D avec OpenGL.
Pour générer cette image, nous instancions un objet QImage avec le drapeau QImage::Format_Mono. Comme tout ce que nous avons besoin de savoir, c'est quels pixels font partie du texte et lesquels n'en font pas partie, une image monochrome devrait très bien fonctionner. Lorsque l'utilisateur saisit du texte, nous mettons à jour de manière synchrone cet objet QImage. En fonction de la taille de la police et de la largeur de l'image, nous faisons de notre mieux pour adapter le texte à la hauteur définie par l'utilisateur.
Ensuite, nous énumérons tous les pixels qui font partie du texte - dans ce cas, les pixels noirs. Ici, chaque pixel est traité comme des unités carrées distinctes. Sur cette base, nous pouvons générer une liste de triangles, calculer les coordonnées de leurs sommets et les stocker dans notre fichier de modèle 3D.
Maintenant que nous avons notre propre format de fichier de modèle 3D simple, nous pouvons commencer à nous concentrer sur son rendu. Pour le rendu 3D basé sur OpenGL, Qt fournit un widget appelé QOpenGLWidget. Pour utiliser ce widget, trois fonctions peuvent être surchargées :
- initializeGl() - c'est là que va le code d'initialisation
- paintGl() - cette méthode est appelée à chaque fois que le widget est redessiné
- resizeGl(int w, int h) - cette méthode est appelée avec la largeur et la hauteur du widget à chaque fois qu'il est redimensionné
Nous allons initialiser le widget en définissant la configuration de shader appropriée dans la méthode initializeGl.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
La première ligne fait que le programme n'affiche que les pixels rendus qui sont plus proches de nous, plutôt que ceux qui sont derrière d'autres pixels et hors de vue. La deuxième ligne spécifie la technique d'ombrage plat. La troisième ligne fait que le programme rend les triangles quelle que soit la direction vers laquelle pointent leurs normales.
Une fois initialisé, nous rendons le modèle à l'écran chaque fois que paintGl est appelé. Avant de remplacer la méthode paintGl, nous devons préparer le tampon. Pour ce faire, nous créons d'abord un handle de tampon. Nous lions ensuite le handle à l'un des points de liaison, copions les données source dans le tampon, et enfin nous disons au programme de dissocier le tampon :
// Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);
À l'intérieur de la méthode paintGl prioritaire, nous utilisons un tableau de sommets et un tableau de données normales pour dessiner les triangles pour chaque image :

QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);
Pour améliorer les performances, nous avons utilisé Vertex Buffer Object (VBO) dans notre application prototype. Cela nous permet de stocker des données dans la mémoire vidéo et de les utiliser directement pour le rendu. Une autre méthode consiste à fournir les données (coordonnées des sommets, normales et couleurs) à partir du code de rendu :
glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();
Cela peut sembler une solution plus simple ; cependant, cela a de sérieuses implications sur les performances, car cela nécessite que les données transitent par le bus de mémoire vidéo - un processus relativement plus lent. Après avoir implémenté la méthode paintGl, il faut faire attention aux shaders :
m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));
Avec OpenGL, les shaders sont implémentés à l'aide d'un langage connu sous le nom de GLSL. Le langage est conçu pour faciliter la manipulation des données 3D avant leur rendu. Ici, nous aurons besoin de deux shaders : vertex shader et fragment shader. Dans vertex shader, nous allons transformer les coordonnées avec la matrice de transformation pour appliquer la rotation et le zoom, et pour calculer la couleur. Dans fragment shader, nous attribuerons une couleur au fragment. Ces programmes de shader doivent ensuite être compilés et liés au contexte. OpenGL fournit des moyens simples de relier les deux environnements afin que les paramètres à l'intérieur du programme puissent être accédés ou assignés de l'extérieur :
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
Dans le code du vertex shader, nous calculons la nouvelle position du vertex en appliquant la matrice de transformation sur les vertex d'origine :
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Pour calculer cette matrice de transformation, nous calculons quelques matrices distinctes : l'échelle de l'écran, la scène de translation, l'échelle, la rotation et le centre. On trouve ensuite le produit de ces matrices afin de calculer la matrice de transformation finale. Commencez par déplacer le centre du modèle vers l'origine (0, 0, 0), qui est également le centre de l'écran. La rotation est déterminée par l'interaction de l'utilisateur avec la scène à l'aide d'un dispositif de pointage. L'utilisateur peut cliquer sur la scène et la faire glisser pour la faire pivoter. Lorsque l'utilisateur clique, on mémorise la position du curseur, et après un mouvement on a la deuxième position du curseur. En utilisant ces deux coordonnées, ainsi que le centre de la scène, nous formons un triangle. Après quelques calculs simples, nous pouvons déterminer l'angle de rotation et nous pouvons mettre à jour notre matrice de rotation pour refléter ce changement. Pour la mise à l'échelle, nous comptons simplement sur la molette de la souris pour modifier le facteur de mise à l'échelle des axes X et Y du widget OpenGL. Le modèle est rétro-translaté de 0,5 pour le maintenir derrière le plan à partir duquel la scène est rendue. Enfin, pour maintenir le rapport d'aspect naturel, nous devons ajuster la diminution de l'expansion du modèle le long du côté le plus long (contrairement à la scène OpenGL, le widget où il est rendu peut avoir des dimensions physiques différentes le long des deux axes). En combinant tout cela, nous calculons la matrice de transformation finale comme suit :
void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }
Conclusion
Dans cette introduction au rendu 3D OpenGL, nous avons exploré l'une des technologies qui permettent à ud d'utiliser notre carte vidéo pour rendre un modèle 3D. C'est beaucoup plus efficace que d'utiliser des cycles CPU dans le même but. Nous avons utilisé une technique d'ombrage très simple et rendu la scène interactive grâce à la gestion des entrées de l'utilisateur à partir de la souris. Nous avons évité d'utiliser le bus de la mémoire vidéo pour faire passer les données entre la mémoire vidéo et le programme. Même si nous n'avons rendu qu'une seule ligne de texte en 3D, des scènes plus compliquées peuvent être rendues de manière très similaire.
Pour être honnête, ce didacticiel a à peine effleuré la surface de la modélisation et du rendu 3D. C'est un vaste sujet, et ce tutoriel OpenGL ne peut pas prétendre que c'est tout ce que vous devez savoir pour pouvoir créer des jeux 3D ou des logiciels de modélisation. Cependant, le but de cet article est de vous donner un aperçu de ce domaine et de montrer avec quelle facilité vous pouvez démarrer avec OpenGL pour créer des applications 3D.