Avec la disponibilité d'outils comme DirectX et OpenGL, écrire une application de bureau qui restitue des éléments 3D n'est pas très difficile de nos jours. Cependant, comme de nombreuses technologies, il existe parfois des obstacles qui empêchent les développeurs d'essayer 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, avec une meilleure documentation et un processus plus facile pour devenir un DirectX ou Développeur OpenGL .
DirectX, introduit et maintenu par Microsoft, est une technologie spécifique au Plate-forme Windows . D'autre part, OpenGL est une API multiplateforme pour l'arène graphique 3D dont la spécification est maintenue par le groupe Khronos.
Dans cette introduction à OpenGL, je vais vous expliquer 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 créé 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 rendre à l'écran. Le modèle 3D dans la scène rendue sera rotatif et zoomable, pour donner une meilleure impression de profondeur et de dimension.
c++ en utilisant des fichiers d'en-tête
Avant de commencer, nous devrons préparer notre environnement de développement avec des outils utiles pour ce projet. La toute première chose dont nous avons besoin est le framework Qt et les utilitaires pertinents, qui peuvent être téléchargés à partir de 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 commencer par essayer. Cet article nécessite une certaine familiarité avec le framework Qt. Cependant, si vous n'êtes pas familier avec le framework, ne vous sentez pas découragé de suivre, car le prototype repose sur des fonctionnalités assez triviales du framework.
Vous pouvez aussi utiliser Microsoft Visual Studio 2013 sur Windows. Dans ce cas, veuillez vous assurer que vous utilisez le bon Complément Qt pour Visual Studio .
À ce stade, vous souhaiterez peut-être cloner le référentiel de GitHub et suivez-le pendant que vous lisez cet article.
Nous commencerons 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 allons ajouter 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 entrer du texte, choisir une police, ajuster 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, l'objectif ici est de convertir un texte saisi par l'utilisateur en un modèle 3D et de le rendre sur 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:
les cinq lois de la perception gestaltiste
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.
Contient l'implémentation de l'objet dérivé de QOpenGLWidget. Ce widget est utilisé pour rendre la scène 3D.
Contient l'implémentation du widget principal de l'application. Ces fichiers n'ont pas été modifiés depuis leur création par l'assistant Qt Creator.
Contient la fonction principale (…), qui crée le widget principal de l'application et l'affiche à l'écran.
Contient des fonctionnalités de création de scène 2D.
Contient des structures qui stockent les objets du modèle 3D et permettent aux opérations de travailler dessus (sauvegarde, chargement, etc.).
Contient l'implémentation de la classe qui permet la création d'un objet de modèle de scène 3D.
Par souci de concision, nous ignorerons 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, des aspects non seulement importants mais également pertinents pour l'encodage et le rendu de modèles 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 des espaces «vides». Il existe des moyens plus simples de rendre du texte de base en utilisant OpenGL, mais nous adoptons cette approche afin de couvrir certains détails concrets du rendu 3D avec OpenGL.
À générer cette image , nous instancions un objet QImage avec l'indicateur QImage :: Format_Mono. Étant donné que tout ce que nous devons savoir, c'est quels pixels font partie du texte et lesquels ne le sont pas, une image monochrome devrait fonctionner parfaitement. Lorsque l'utilisateur entre 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. Chaque pixel ici 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 le rendu. Pour le rendu 3D basé sur OpenGL, Qt fournit un widget appelé QOpenGLWidget. Pour utiliser ce widget, trois fonctions peuvent être remplacées:
Nous initialiserons 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 permet au programme d'afficher uniquement 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 de l'ombrage plat. La troisième ligne permet au programme de rendre les triangles quelle que soit la direction vers laquelle pointent leurs normales.
Une fois initialisé, nous rendons le modèle sur 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);
Dans la méthode paintGl de substitution, nous utilisons un tableau de sommets et un tableau de données normales pour dessiner les triangles de 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
' '
' 'layout (location = 0) in vec3 coordVertexes;
' 'layout (location = 1) in vec3 coordNormals;
' 'flat out float lightIntensity;
' '
' 'uniform mat4 matrixVertex;
' 'uniform mat4 matrixNormal;
' '
' 'void main()
' '{
' ' gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
' ' lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);
' '}')); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( '#version 400
' '
' 'flat in float lightIntensity;
' '
' 'layout (location = 0) out vec4 FragColor;
' 'uniform vec3 fragmentColor;
' '
' 'void main()
' '{
' ' FragColor = vec4(fragmentColor * lightIntensity, 1.0);
' '}')); 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 en utilisant un langage appelé GLSL . Le langage est conçu pour faciliter la manipulation des données 3D avant leur rendu. Ici, nous aurons besoin de deux shaders: le vertex shader et le fragment shader. Dans le vertex shader, nous transformerons les coordonnées avec la matrice de transformation pour appliquer la rotation et le zoom, et pour calculer la couleur. Dans le shader de fragment, nous attribuerons une couleur au fragment. Ces programmes 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 soient accessibles ou affecté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 shader de vertex, nous calculons la nouvelle position de sommet en appliquant la matrice de transformation sur les sommets d'origine:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Pour calculer cette matrice de transformation, nous calculons quelques matrices distinctes: échelle de l'écran, conversion de la scène, mise à l'échelle, rotation et centre. Nous trouvons alors le produit de ces matrices pour calculer la matrice de transformation finale. Commencez par convertir le centre du modèle à 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 périphérique de pointage. L'utilisateur peut cliquer sur la scène et faire glisser pour la faire pivoter. Lorsque l'utilisateur clique, nous stockons la position du curseur, et après un mouvement, nous avons 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 nous appuyons 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 translaté de 0,5 pour le maintenir derrière le plan à partir duquel la scène est rendue. Enfin, pour conserver le rapport hauteur / largeur 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 tous ces éléments, nous calculons la matrice de transformation finale comme suit:
un LBO (LBO)
void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast(qMin(width(), height())); float scaleScreenVert = static_cast(dimMin / static_cast(height())); float scaleScreenHorz = static_cast(dimMin / static_cast(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(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; }
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 mémoire vidéo pour passer des données dans les deux sens entre la mémoire vidéo et le programme. Même si nous venons de rendre une seule ligne de texte en 3D, des scènes plus complexes peuvent être rendues de manière très similaire.
Pour être honnête, ce tutoriel a à peine effleuré la surface de la modélisation et du rendu 3D. Il s'agit d'un vaste sujet, et ce didacticiel 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.