Aprende OpenGL. Lección 7.2 - Dibujar texto

imagen En algún momento de tu aventura gráfica, querrás generar texto a través de OpenGL. Al contrario de lo que cabría esperar, obtener una línea simple en la pantalla es bastante difícil con una biblioteca de bajo nivel como OpenGL. Si no necesita más de 128 caracteres diferentes para dibujar texto, entonces no será difícil. Las dificultades surgen cuando los caracteres no coinciden con la altura, el ancho y el desplazamiento. Dependiendo de dónde viva, es posible que necesite más de 128 caracteres. Pero, ¿qué pasa si quieres personajes especiales, matemáticos o musicales? Tan pronto como comprenda que dibujar texto no es la tarea más fácil, se dará cuenta de que lo más probable es que no pertenezca a una API de nivel tan bajo como OpenGL.


Dado que OpenGL no proporciona ningún medio para representar texto, todas las dificultades de este caso están en nosotros. Como no hay un "Símbolo" primitivo gráfico, tendremos que inventarlo nosotros mismos. Ya hay ejemplos listos: dibuje un símbolo a través de GL_LINES , cree modelos 3D de símbolos o dibuje símbolos en cuadrángulos planos en un espacio tridimensional.


Muy a menudo, los desarrolladores son demasiado flojos para tomar café y elegir la última opción. Dibujar estos cuadrángulos texturizados no es tan difícil como elegir la textura correcta. En este tutorial, aprenderemos algunas formas y escribiremos nuestro renderizador de texto avanzado pero flexible usando FreeType.



Clásico: fuentes ráster


Érase una vez en el tiempo de los dinosaurios, la representación del texto incluía seleccionar una fuente (o crearla) para la aplicación y copiar los caracteres deseados en una textura grande llamada fuente de mapa de bits. Esta textura contiene todos los caracteres necesarios en ciertas partes. Estos caracteres se llaman glifos. Cada glifo tiene un área específica de coordenadas de textura asociadas. Cada vez que dibujas un personaje, seleccionas un glifo específico y dibujas solo la parte deseada en un quad plano.



Aquí puede ver cómo representaríamos el texto "OpenGL". Tomamos la fuente de trama y tomamos muestras de los glifos necesarios de la textura, eligiendo cuidadosamente las coordenadas de textura, que dibujaremos sobre varios cuadrángulos. Al activar la mezcla y mantener el fondo transparente, obtenemos una serie de caracteres en la pantalla. Esta fuente de mapa de bits se generó utilizando el generador de fuentes de mapa de bits de código de cabeza .


Este enfoque tiene sus pros y sus contras. Este enfoque tiene una implementación simple, ya que las fuentes de mapa de bits ya están rasterizadas. Sin embargo, esto no siempre es conveniente. Si necesita una fuente diferente, debe generar una nueva fuente de mapa de bits. Además, aumentar el tamaño de los caracteres mostrará rápidamente bordes pixelados. Además, las fuentes de mapa de bits a menudo están vinculadas a un pequeño conjunto de caracteres, por lo que los caracteres Unicode probablemente no se mostrarán.


Esta técnica era popular no hace mucho tiempo (y aún conserva su popularidad), porque es muy rápida y funciona en cualquier plataforma. Pero hasta la fecha, hay otros enfoques para representar texto. Uno de ellos es renderizar fuentes TrueType usando FreeType.


Modernidad: FreeType


FreeType es una biblioteca que descarga fuentes, las convierte en mapas de bits y proporciona soporte para algunas operaciones relacionadas con fuentes. Esta popular biblioteca se usa en Mac OS X, Java, Qt, PlayStation, Linux y Android. La capacidad de cargar fuentes TrueType hace que esta biblioteca sea lo suficientemente atractiva.


Una fuente TrueType es una colección de glifos definidos no por píxeles, sino por fórmulas matemáticas. Al igual que con las imágenes vectoriales, se puede generar una imagen de fuente rasterizada en función del tamaño de fuente preferido. Con las fuentes TrueType, puede renderizar fácilmente glifos de varios tamaños sin pérdida de calidad.


FreeType se puede descargar desde el sitio oficial . Puede compilar FreeType usted mismo o usar versiones precompiladas, si las hay, en el sitio. Recuerde vincular su programa a freetype.lib y asegúrese de que el compilador sepa dónde buscar los archivos de encabezado.


Luego adjunte los archivos de encabezado correctos:


 #include <ft2build.h> #include FT_FREETYPE_H 

Dado que FreeType está diseñado de una manera un tanto extraña (al momento de escribir el original, avíseme si algo ha cambiado), puede colocar sus archivos de encabezado solo en la raíz de la carpeta con los archivos de encabezado. Conectar FreeType de alguna otra manera (por ejemplo, #include <3rdParty/FreeType/ft2build.h> ) puede provocar un conflicto en el archivo de encabezado.

¿Qué hace FreeType? Carga fuentes TrueType y genera una imagen de mapa de bits para cada glifo y calcula algunas métricas de glifo. Podemos obtener imágenes de mapa de bits para generar texturas y posicionar cada glifo según las métricas recibidas.


Para descargar una fuente, necesitamos inicializar FreeType y cargar la fuente como cara (como FreeType llama a la fuente). En este ejemplo, arial.ttf fuente TrueType arial.ttf , copiada de la carpeta 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; 

Cada una de estas funciones FreeType devuelve un valor distinto de cero en caso de falla.


Una vez que hemos cargado la fuente , debemos especificar el tamaño de fuente deseado, que extraeremos:


 FT_Set_Pixel_Sizes(face, 0, 48); 

Esta función establece el ancho y la altura del glifo. Al establecer el ancho en 0 (cero) permitimos que FreeType calcule el ancho en función de la altura establecida.


Face FreeType contiene una colección de glifos. Podemos activar algunos glifos llamando a FT_Load_Char . Aquí intentamos cargar el glifo X :


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

Al configurar FT_LOAD_RENDER como uno de los indicadores de descarga, le decimos a FreeType que cree un mapa de bits en escala de grises de 8 bits, que luego podemos obtener de esta manera:


 face->glyph->bitmap; 

Los glifos cargados con FreeType no tienen el mismo tamaño que en el caso de las fuentes de mapa de bits. Un mapa de bits generado con FreeType es el tamaño mínimo para un tamaño de fuente determinado y solo es suficiente para contener un carácter. Por ejemplo, una imagen de mapa de bits de un glifo . mucho más pequeño que el mapa de bits del glifo X Por esta razón, FreeType también descarga algunas métricas que muestran qué tamaño y dónde debe ubicarse un solo carácter. A continuación se muestra una imagen que muestra qué métricas calcula FreeType para cada glifo.



Cada glifo se encuentra en la línea de base (línea horizontal con una flecha). Algunos están exactamente en la línea de base ( X ), algunos están debajo ( g , p ). Estas métricas determinan con precisión los desplazamientos para posicionar con precisión los glifos en la línea base, ajustando el tamaño de los glifos y para averiguar cuántos píxeles debe dejar para dibujar el siguiente glifo. La siguiente es una lista de las métricas que utilizaremos:


  • ancho : ancho de glifo en píxeles, acceso por face->glyph->bitmap.width
  • height : altura del glifo en píxeles, acceso por face->glyph->bitmap.rows
  • bearingX : desplazamiento horizontal del punto superior izquierdo del glifo relativo al origen, acceso por face->glyph->bitmap_left
  • bearingY : desplazamiento vertical del punto superior izquierdo del glifo relativo al origen, acceso por face->glyph->bitmap_top
  • avance : desplazamiento horizontal del comienzo del siguiente glifo en 1/64 píxeles con relación al origen, acceso por face->glyph->advance.x

Podemos cargar un glifo de un símbolo, obtener sus métricas y generar una textura cada vez que queramos dibujarlo en la pantalla, pero crear texturas para cada símbolo en cada cuadro no es un buen método. Mejor guardaremos los datos generados en algún lugar y los solicitaremos cuando los necesitemos. Definimos una estructura conveniente que almacenaremos en std::map :


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

En este artículo, simplificaremos nuestra vida y utilizaremos solo los primeros 128 caracteres. Para cada carácter, generaremos una textura y guardaremos los datos necesarios en una estructura de tipo Character , que agregaremos a los Characters tipo std::map . Por lo tanto, todos los datos necesarios para dibujar un personaje se guardan para su uso futuro.


 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; } 

Dentro del bucle, para cada uno de los primeros 128 caracteres, obtenemos un glifo, generamos una textura, establecemos su configuración y guardamos las métricas. Es interesante notar que usamos GL_RED como argumentos para format internalFormat y format texturas. Un mapa de bits generado por glifos es una imagen en escala de grises de 8 bits, cada píxel ocupa 1 byte. Por esta razón, almacenaremos el búfer de mapa de bits como el valor del color de la textura. Esto se logra creando una textura en la que cada byte corresponde al componente rojo del color. Si usamos 1 byte para representar colores de textura, no olvide las limitaciones de OpenGL:


 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 

OpenGL requiere que todas las texturas tengan un desplazamiento de 4 bytes, es decir su tamaño debe ser un múltiplo de 4 bytes (por ejemplo, 8 bytes, 4000 bytes, 2048 bytes) o (y) deben usar 4 bytes por píxel (como en formato RGBA), pero dado que usamos 1 byte por píxel, pueden tener diferentes ancho. Al establecer el desplazamiento de alineación de desempaquetado (¿hay una mejor traducción?) A 1, eliminamos los errores de desplazamiento que podrían causar segfaults.


Además, cuando terminemos de trabajar con la fuente en sí, deberíamos borrar los recursos de FreeType:


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

Sombreadores


Para dibujar glifos, use el siguiente sombreador de vértices:


 #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; } 

Combinamos la posición del símbolo y las coordenadas de textura en un vec4 . El sombreador de vértices calcula el producto de coordenadas con la matriz de proyección y transfiere las coordenadas de textura al sombreador de fragmentos:


 #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; } 

El sombreador de fragmentos acepta 2 variables globales: una imagen monocroma del glifo y el color del glifo mismo. Primero, tomamos muestras del valor de color del glifo. Dado que los datos de textura se almacenan en el componente rojo de la textura, solo muestreamos el componente r como valor de transparencia. Al cambiar la transparencia del color, el color resultante será transparente al fondo del glifo y opaco a los píxeles verdaderos del glifo. También multiplicamos los colores RGB con la variable textColor para cambiar el color del texto.


Pero para que nuestro mecanismo funcione, debe habilitar la mezcla:


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

Como matriz de proyección, tendremos una matriz de proyección ortográfica. Para dibujar texto, de hecho, no se requiere una matriz de perspectiva y el uso de proyección ortográfica también nos permite especificar todas las coordenadas de vértice en las coordenadas de la pantalla si configuramos la matriz de esta manera:


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

Establecemos la parte inferior de la matriz en 0.0f , la parte superior a la altura de la ventana. Como resultado, la coordenada y toma valores desde la parte inferior de la pantalla ( y = 0 ) hasta la parte superior de la pantalla ( y = 600 ). Esto significa que el punto (0, 0) indica y la esquina inferior izquierda de la pantalla.


En conclusión, cree VBO y VAO para dibujar los cuadrángulos. Aquí reservamos suficiente memoria en VBO para que luego podamos actualizar los datos para dibujar caracteres.


 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 cuadrángulo plano requiere 6 vértices de 4 números de coma flotante, por lo que reservamos 6 * 4 = 24 flotantes de memoria. Como vamos a cambiar los datos de vértices con bastante frecuencia, GL_DYNAMIC_DRAW memoria usando GL_DYNAMIC_DRAW .


Mostrar una línea de texto en la pantalla


Para mostrar una línea de texto, extraemos la estructura de Character correspondiente al símbolo y calculamos las dimensiones del cuadrilátero a partir de las métricas del símbolo. A partir de las dimensiones calculadas del cuadrángulo, sobre la marcha creamos un conjunto de 6 vértices y actualizamos los datos de vértice usando glBufferSubData .


Por conveniencia, RenderText función RenderText que dibuje una cadena de caracteres:


 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); } 

El contenido de la función es relativamente claro: el cálculo del origen, los tamaños y los vértices del cuadrángulo. Observe que multiplicamos cada métrica por scale . Después de eso, actualice VBO y dibuje un quad.


Esta línea de código requiere algo de atención:


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

Algunos caracteres, como p y g , se dibujan notablemente debajo de la línea de base, lo que significa que el quad debe ser notablemente más bajo que el parámetro y de la función RenderText . El desplazamiento exacto y_offset se puede expresar a partir de métricas de glifo:



Para calcular el desplazamiento, necesitamos brazos rectos para averiguar la distancia a la que se encuentra el símbolo debajo de la línea de base. Esta distancia se muestra con la flecha roja. Obviamente, y_offset = bearingY - height e ypos = y + y_offset .


Si todo se hace correctamente, puede mostrar el texto en la pantalla de esta manera:


 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)); 

El resultado debería verse así:



Aquí hay un código de ejemplo (enlace al sitio del autor original).


Para comprender qué cuadrángulos se dibujan, desactive la combinación:



A partir de esta figura, es obvio que la mayoría de los cuadrángulos están en la parte superior de una línea de base imaginaria, aunque algunos caracteres, como ( p , se desplazan hacia abajo).


Que sigue


Este artículo mostró cómo representar fuentes TrueType con FreeType. Este enfoque es flexible, escalable y eficiente en varias codificaciones de caracteres. Sin embargo, este enfoque puede ser demasiado pesado para su aplicación, ya que se crea una textura para cada personaje. Se prefieren las fuentes de mapa de bits productivas porque tenemos una textura para todos los glifos. El mejor enfoque es combinar los dos enfoques y tomar el mejor: sobre la marcha generar una fuente de trama de glifos descargados usando FreeType. Esto ahorrará al renderizador de numerosos cambios de textura y, dependiendo del empaque de textura, aumentará el rendimiento.


Pero FreeType tiene un inconveniente más: los glifos de tamaño fijo, lo que significa que a medida que aumenta el tamaño del glifo renderizado, pueden aparecer pasos en la pantalla y el glifo puede verse borroso cuando se gira. La válvula resolvió (enlace al archivo web) este problema hace varios años usando campos de distancia firmados. Lo hicieron muy bien y lo mostraron en aplicaciones 3D.


PD : Tenemos un telegrama conf para la coordinación de transferencias. Si tiene un serio deseo de ayudar con la traducción, ¡de nada!

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


All Articles