Apprenez OpenGL. Leçon 7.2 - Dessin de texte

image À un moment donné de votre aventure graphique, vous voudrez sortir du texte via OpenGL. Contrairement à ce à quoi vous pourriez vous attendre, obtenir une simple ligne à l'écran est assez difficile avec une bibliothèque de bas niveau comme OpenGL. Si vous n'avez pas besoin de plus de 128 caractères différents pour dessiner du texte, ce ne sera pas difficile. Des difficultés surviennent lorsque les caractères ne correspondent pas à la hauteur, la largeur et le décalage. Selon l'endroit où vous vivez, vous devrez peut-être plus de 128 caractères. Mais que faire si vous voulez des caractères spéciaux, des caractères mathématiques ou musicaux? Dès que vous comprendrez que dessiner du texte n'est pas la tâche la plus simple, vous vous rendrez compte qu'il ne devrait probablement pas appartenir à une API de bas niveau comme OpenGL.


Etant donné qu'OpenGL ne fournit aucun moyen pour rendre le texte, toutes les difficultés de ce cas sont sur nous. Puisqu'il n'y a pas de "Symbole" primitif graphique, nous devrons l'inventer nous-mêmes. Il existe déjà des exemples prêts à l'emploi: dessinez un symbole via GL_LINES , créez des modèles 3D de symboles ou dessinez des symboles sur des quadrangles plats dans un espace en trois dimensions.


Le plus souvent, les développeurs sont trop paresseux pour boire du café et choisissent la dernière option. Dessiner ces quadrangles texturés n'est pas aussi difficile que de choisir la bonne texture. Dans ce didacticiel, nous allons apprendre quelques façons et écrire notre rendu de texte avancé mais flexible en utilisant FreeType.



Classique: polices raster


Il était une fois à l'époque des dinosaures, le rendu de texte comprenait la sélection d'une police (ou sa création) pour l'application et la copie des caractères souhaités sur une grande texture appelée police bitmap. Cette texture contient tous les caractères nécessaires dans certaines parties. Ces caractères sont appelés glyphes. Chaque glyphe a une zone spécifique de coordonnées de texture qui lui est associée. Chaque fois que vous dessinez un personnage, vous sélectionnez un glyphe spécifique et dessinez uniquement la partie souhaitée sur un quadrillage plat.



Ici vous pouvez voir comment nous rendrions le texte "OpenGL". Nous prenons la police raster et échantillonnons les glyphes nécessaires à partir de la texture, en choisissant soigneusement les coordonnées de la texture, que nous dessinerons sur plusieurs quadrangles. En activant le mélange et en maintenant l'arrière-plan transparent, nous obtenons une chaîne de caractères à l'écran. Cette police bitmap a été générée à l'aide du générateur de polices bitmap Codehead .


Cette approche a ses avantages et ses inconvénients. Cette approche a une implémentation simple, car les polices bitmap sont déjà tramées. Cependant, ce n'est pas toujours pratique. Si vous avez besoin d'une police différente, vous devez générer une nouvelle police bitmap. De plus, l'augmentation de la taille des caractères affichera rapidement des bords pixellisés. De plus, les polices bitmap sont souvent liées à un petit ensemble de caractères, donc les caractères Unicode ne seront probablement pas affichés.


Cette technique était populaire il n'y a pas si longtemps (et conserve toujours sa popularité), car elle est très rapide et fonctionne sur n'importe quelle plate-forme. Mais à ce jour, il existe d'autres approches pour rendre le texte. L'un d'eux rend les polices TrueType à l'aide de FreeType.


Modernité: FreeType


FreeType est une bibliothèque qui télécharge des polices, les rend en bitmaps et prend en charge certaines opérations liées aux polices. Cette bibliothèque populaire est utilisée sur Mac OS X, Java, Qt, PlayStation, Linux et Android. La possibilité de charger des polices TrueType rend cette bibliothèque suffisamment attrayante.


Une police TrueType est une collection de glyphes définis non pas par des pixels, mais par des formules mathématiques. Comme pour les images vectorielles, une image de police tramée peut être générée en fonction de la taille de police préférée. En utilisant les polices TrueType, vous pouvez facilement afficher des glyphes de différentes tailles sans perte de qualité.


FreeType peut être téléchargé sur le site officiel . Vous pouvez soit compiler FreeType vous-même, soit utiliser des versions précompilées, le cas échéant, sur le site. N'oubliez pas de lier votre programme à freetype.lib et assurez-vous que le compilateur sait où chercher les fichiers d'en-tête.


Ensuite, joignez les fichiers d'en-tête corrects:


 #include <ft2build.h> #include FT_FREETYPE_H 

Étant donné que FreeType est conçu d'une manière légèrement étrange (au moment de la rédaction de l'original, faites-moi savoir si quelque chose a changé), vous ne pouvez placer ses fichiers d'en-tête qu'à la racine du dossier contenant les fichiers d'en-tête. La connexion de FreeType d'une autre manière (par exemple, #include <3rdParty/FreeType/ft2build.h> ) peut provoquer un conflit de fichier d'en-tête.

Que fait FreeType? Charge les polices TrueType et génère une image bitmap pour chaque glyphe et calcule certaines métriques de glyphe. Nous pouvons obtenir des images bitmap pour générer des textures et positionner chaque glyphe en fonction des métriques reçues.


Pour télécharger une police, nous devons initialiser FreeType et charger la police en tant que visage (comme FreeType appelle la police). Dans cet exemple, nous chargeons la police TrueType arial.ttf , copiée à partir du dossier C: / Windows / Fonts.


 FT_Library ft; if (FT_Init_FreeType(&ft)) std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; FT_Face face; if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face)) std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl; 

Chacune de ces fonctions FreeType renvoie une valeur différente de zéro en cas d'échec.


Une fois la police de la face chargée, nous devons spécifier la taille de police souhaitée, que nous allons extraire:


 FT_Set_Pixel_Sizes(face, 0, 48); 

Cette fonction définit la largeur et la hauteur du glyphe. En définissant la largeur à 0 (zéro), nous permettons à FreeType de calculer la largeur en fonction de la hauteur définie.


Face FreeType contient une collection de glyphes. Nous pouvons FT_Load_Char un glyphe en appelant FT_Load_Char . Ici, nous essayons de charger le glyphe X :


 if (FT_Load_Char(face, 'X', FT_LOAD_RENDER)) std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl; 

En définissant FT_LOAD_RENDER comme l'un des indicateurs de téléchargement, nous demandons à FreeType de créer une image bitmap en niveaux de gris 8 bits, que nous pouvons ensuite obtenir comme ceci:


 face->glyph->bitmap; 

Les glyphes chargés avec FreeType n'ont pas la même taille que dans le cas des polices bitmap. Un bitmap généré avec FreeType est la taille minimale pour une taille de police donnée et ne suffit que pour contenir un caractère. Par exemple, une image bitmap d'un glyphe . beaucoup plus petit que le bitmap du glyphe X Pour cette raison, FreeType télécharge également certaines mesures qui montrent quelle taille et où un seul caractère doit être localisé. Ci-dessous, une image montrant les mesures que FreeType calcule pour chaque glyphe.



Chaque glyphe est situé sur la ligne de base (ligne horizontale avec une flèche). Certains sont exactement sur la ligne de base ( X ), certains sont en dessous ( g , p ). Ces mesures déterminent avec précision les décalages pour positionner avec précision les glyphes sur la ligne de base, ajuster la taille des glyphes et savoir combien de pixels vous devez laisser pour dessiner le glyphe suivant. Voici une liste des mesures que nous utiliserons:


  • width : largeur du glyphe en pixels, accès par face->glyph->bitmap.width
  • hauteur : hauteur du glyphe en pixels, accès par face->glyph->bitmap.rows
  • portantX : décalage horizontal du point supérieur gauche du glyphe par rapport à l'origine, accès par face->glyph->bitmap_left
  • portantY : décalage vertical du point supérieur gauche du glyphe par rapport à l'origine, accès par face->glyph->bitmap_top
  • avance : décalage horizontal du début du glyphe suivant en 1/64 pixels par rapport à l'origine, accès par face->glyph->advance.x

Nous pouvons charger un glyphe d'un symbole, obtenir ses métriques et générer une texture chaque fois que nous voulons le dessiner à l'écran, mais créer des textures pour chaque symbole sur chaque cadre n'est pas une bonne méthode. Mieux, nous enregistrerons les données générées quelque part et les demanderons lorsque nous en aurons besoin. Nous définissons une structure pratique que nous allons stocker dans std::map :


 struct Character { GLuint TextureID; // ID   glm::ivec2 Size; //   glm::ivec2 Bearing; //      GLuint Advance; //       }; std::map<GLchar, Character> Characters; 

Dans cet article, nous simplifierons notre vie et n'utiliserons que les 128 premiers caractères. Pour chaque caractère, nous générerons une texture et enregistrerons les données nécessaires dans une structure de type Character , que nous ajouterons aux Characters type std::map . Ainsi, toutes les données nécessaires pour dessiner un personnage sont enregistrées pour une utilisation future.


 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Disable byte-alignment restriction for (GLubyte c = 0; c < 128; c++) { // Load character glyph if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl; continue; } // Generate texture GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D( GL_TEXTURE_2D, 0, GL_RED, face->glyph->bitmap.width, face->glyph->bitmap.rows, 0, GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer ); // Set texture options glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Now store character for later use Character character = { texture, glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), face->glyph->advance.x }; Characters.insert(std::pair<GLchar, Character>(c, character)); // Characters[c] = character; } 

À l'intérieur de la boucle, pour chacun des 128 premiers caractères, nous obtenons un glyphe, générons une texture, définissons ses paramètres et enregistrons les métriques. Il est intéressant de noter que nous utilisons GL_RED comme arguments pour internalFormat et format textures. Un bitmap généré par glyphe est une image en niveaux de gris de 8 bits, dont chaque pixel occupe 1 octet. Pour cette raison, nous allons stocker le tampon bitmap comme valeur de couleur de texture. Ceci est réalisé en créant une texture dans laquelle chaque octet correspond à la composante rouge de la couleur. Si nous utilisons 1 octet pour représenter les couleurs de texture, n'oubliez pas les limitations d'OpenGL:


 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 

OpenGL nécessite que toutes les textures aient un décalage de 4 octets, c'est-à-dire leur taille doit être un multiple de 4 octets (par exemple 8 octets, 4000 octets, 2048 octets) ou (et) ils doivent utiliser 4 octets par pixel (comme au format RGBA), mais comme nous utilisons 1 octet par pixel, ils peuvent avoir différents largeur. En définissant le décalage d'alignement de décompression (y a-t-il une meilleure traduction?) À 1, nous éliminons les erreurs de décalage qui pourraient provoquer des erreurs de segmentation.


De plus, lorsque nous avons fini de travailler avec la police elle-même, nous devons effacer les ressources FreeType:


 FT_Done_Face(face); //     face FT_Done_FreeType(ft); //   FreeType 

Shaders


Pour dessiner des glyphes, utilisez le vertex shader suivant:


 #version 330 core layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex_coord> out vec2 TexCoords; uniform mat4 projection; void main() { gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); TexCoords = vertex.zw; } 

Nous combinons la position du symbole et les coordonnées de texture dans un vec4 . Le vertex shader calcule le produit des coordonnées avec la matrice de projection et transfère les coordonnées de texture au fragment shader:


 #version 330 core in vec2 TexCoords; out vec4 color; uniform sampler2D text; uniform vec3 textColor; void main() { vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); color = vec4(textColor, 1.0) * sampled; } 

Le fragment shader accepte 2 variables globales - une image monochrome du glyphe et la couleur du glyphe lui-même. Tout d'abord, nous échantillonnons la valeur de couleur du glyphe. Étant donné que les données de texture sont stockées dans le composant rouge de la texture, nous n'échantillons que le composant r comme valeur de transparence. En modifiant la transparence de la couleur, la couleur résultante sera transparente à l'arrière-plan du glyphe et opaque aux vrais pixels du glyphe. Nous multiplions également les couleurs RVB avec la variable textColor pour changer la couleur du texte.


Mais pour que notre mécanisme fonctionne, vous devez activer le mixage:


 glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 

En tant que matrice de projection, nous aurons une matrice de projection orthographique. Pour dessiner du texte, en fait, une matrice en perspective n'est pas nécessaire et l'utilisation de la projection orthographique nous permet également de définir toutes les coordonnées des sommets en coordonnées d'écran si nous définissons la matrice comme ceci:


 glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f); 

Nous définissons le bas de la matrice à 0.0f , le haut à la hauteur de la fenêtre. Par conséquent, la coordonnée y prend des valeurs du bas de l'écran ( y = 0 ) vers le haut de l'écran ( y = 600 ). Cela signifie que le point (0, 0) indique et le coin inférieur gauche de l'écran.


En conclusion, créez VBO et VAO pour dessiner les quadrangles. Ici, nous réservons suffisamment de mémoire dans VBO pour que nous puissions ensuite mettre à jour les données pour dessiner des caractères.


 GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); 

Un quadrilatère plat nécessite 6 sommets de 4 nombres à virgule flottante, nous réservons donc 6 * 4 = 24 flottants de mémoire. Puisque nous allons changer les données de sommet assez souvent, nous GL_DYNAMIC_DRAW mémoire en utilisant GL_DYNAMIC_DRAW .


Afficher une ligne de texte à l'écran


Pour afficher une ligne de texte, nous extrayons la structure de Character correspondant au symbole et calculons les dimensions du quadrilatère à partir des métriques du symbole. À partir des dimensions calculées du quadrilatère, nous créons à la volée un ensemble de 6 sommets et glBufferSubData jour les données des sommets à l'aide de glBufferSubData .


Pour plus de commodité, RenderText fonction RenderText qui dessinera une chaîne de caractères:


 void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color) { // Activate corresponding render state s.Use(); glUniform3f(glGetUniformLocation(s.Program, "textColor"), color.x, color.y, color.z); glActiveTexture(GL_TEXTURE0); glBindVertexArray(VAO); // Iterate through all characters std::string::const_iterator c; for (c = text.begin(); c != text.end(); c++) { Character ch = Characters[*c]; GLfloat xpos = x + ch.Bearing.x * scale; GLfloat ypos = y - (ch.Size.y - ch.Bearing.y) * scale; GLfloat w = ch.Size.x * scale; GLfloat h = ch.Size.y * scale; // Update VBO for each character GLfloat vertices[6][4] = { { xpos, ypos + h, 0.0, 0.0 }, { xpos, ypos, 0.0, 1.0 }, { xpos + w, ypos, 1.0, 1.0 }, { xpos, ypos + h, 0.0, 0.0 }, { xpos + w, ypos, 1.0, 1.0 }, { xpos + w, ypos + h, 1.0, 0.0 } }; // Render glyph texture over quad glBindTexture(GL_TEXTURE_2D, ch.textureID); // Update content of VBO memory glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); glBindBuffer(GL_ARRAY_BUFFER, 0); // Render quad glDrawArrays(GL_TRIANGLES, 0, 6); // Now advance cursors for next glyph (note that advance is number of 1/64 pixels) x += (ch.Advance >> 6) * scale; // Bitshift by 6 to get value in pixels (2^6 = 64) } glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); } 

Le contenu de la fonction est relativement clair: le calcul de l'origine, des tailles et des sommets du quadrilatère. Notez que nous avons multiplié chaque métrique par scale . Après cela, mettez à jour VBO et dessinez un quad.


Cette ligne de code nécessite une certaine attention:


 GLfloat ypos = y - (ch.Size.y - ch.Bearing.y); 

Certains caractères, tels que p et g , sont nettement dessinés sous la ligne de base, ce qui signifie que le quad doit être sensiblement inférieur au paramètre y de la fonction RenderText . Le décalage exact y_offset peut être exprimé à partir des métriques de glyphe:



Pour calculer le décalage, nous avons besoin de bras droits pour déterminer la distance à laquelle le symbole est situé sous la ligne de base. Cette distance est indiquée par la flèche rouge. Évidemment, y_offset = bearingY - height et ypos = y + y_offset .


Si tout est fait correctement, vous pouvez afficher le texte à l'écran comme ceci:


 RenderText(shader, "This is sample text", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f)); RenderText(shader, "(C) LearnOpenGL.com", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f)); 

Le résultat devrait ressembler à ceci:



Un exemple de code est ici (lien vers le site de l'auteur d'origine).


Pour comprendre quels quadrangles sont dessinés, désactivez le mélange:



À partir de cette figure, il est évident que la plupart des quadrangles sont au-dessus d'une ligne de base imaginaire, bien que certains caractères, tels que ( et p , soient décalés vers le bas.


Et ensuite?


Cet article a montré comment rendre les polices TrueType avec FreeType. Cette approche est flexible, évolutive et efficace sur différents encodages de caractères. Cependant, cette approche peut être trop lourde pour votre application, car une texture est créée pour chaque personnage. Les polices bitmap productives sont préférées car nous avons une texture pour tous les glyphes. La meilleure approche consiste à combiner les deux approches et à tirer le meilleur parti: générer à la volée une police raster à partir de glyphes téléchargés à l'aide de FreeType. Cela permettra d'économiser le rendu de nombreux changements de texture et, selon le conditionnement de la texture, augmentera les performances.


Mais FreeType a un autre inconvénient: les glyphes de taille fixe, ce qui signifie qu'à mesure que la taille du glyphe rendu augmente, des étapes peuvent apparaître à l'écran et une fois tourné, le glyphe peut sembler flou. Valve a résolu (lien vers l'archive Web) ce problème il y a plusieurs années en utilisant des champs de distance signés. Ils s'en sont très bien sortis et l'ont montré sur des applications 3D.


PS : Nous avons un télégramme conf pour la coordination des transferts. Si vous avez un sérieux désir d'aider à la traduction, alors vous êtes les bienvenus!

Source: https://habr.com/ru/post/fr473990/


All Articles