Aprenda o OpenGL. Lição 7.2 - Texto de desenho

imagem Em algum momento de sua aventura gráfica, você desejará gerar texto através do OpenGL. Ao contrário do que você poderia esperar, obter uma linha simples na tela é bastante difícil com uma biblioteca de baixo nível, como o OpenGL. Se você não precisar de mais de 128 caracteres diferentes para desenhar texto, não será difícil. As dificuldades surgem quando os caracteres não correspondem à altura, largura e deslocamento. Dependendo de onde você mora, você pode precisar de mais de 128 caracteres. Mas e se você quiser caracteres especiais, matemáticos ou musicais? Assim que você entender que desenhar texto não é a tarefa mais fácil, você perceberá que ele provavelmente não deve pertencer a uma API de nível inferior como o OpenGL.


Como o OpenGL não fornece nenhum meio para renderizar texto, todas as dificuldades deste caso estão sobre nós. Como não existe um "símbolo" gráfico primitivo, teremos que inventá-lo. Já existem exemplos prontos: desenhe um símbolo através de GL_LINES , crie modelos 3D de símbolos ou desenhe símbolos em quadrângulos planos no espaço tridimensional.


Na maioria das vezes, os desenvolvedores têm preguiça de tomar café e escolher a última opção. Desenhar esses quadrados texturizados não é tão difícil quanto escolher a textura certa. Neste tutorial, aprenderemos algumas maneiras e escreveremos nosso renderizador de texto avançado, mas flexível, usando o FreeType.



Clássico: Fontes Raster


Uma vez na época dos dinossauros, a renderização de texto incluía a seleção de uma fonte (ou a criação) para o aplicativo e a cópia dos caracteres desejados em uma textura grande chamada fonte de bitmap. Essa textura contém todos os caracteres necessários em certas partes. Esses caracteres são chamados de glifos. Cada glifo tem uma área específica de coordenadas de textura associada a ele. Cada vez que você desenha um personagem, seleciona um glifo específico e desenha apenas a parte desejada em um quadrilátero plano.



Aqui você pode ver como renderizamos o texto "OpenGL". Pegamos a fonte de bitmap e amostramos os glifos necessários da textura, selecionando cuidadosamente as coordenadas da textura, que desenharemos no topo de vários quadrângulos. Ativando a mesclagem e mantendo o plano de fundo transparente, obtemos uma série de caracteres na tela. Essa fonte de bitmap foi gerada usando o gerador de fontes de bitmap da Codehead .


Essa abordagem tem seus prós e contras. Essa abordagem possui uma implementação simples, pois as fontes de bitmap já são rasterizadas. No entanto, isso nem sempre é conveniente. Se você precisar de uma fonte diferente, precisará gerar uma nova fonte de bitmap. Além disso, aumentar o tamanho dos caracteres mostrará rapidamente bordas pixeladas. Além disso, as fontes de bitmap geralmente estão vinculadas a um pequeno conjunto de caracteres; portanto, os caracteres Unicode provavelmente não serão exibidos.


Essa técnica era popular há pouco tempo (e ainda mantém sua popularidade), porque é muito rápida e funciona em qualquer plataforma. Mas até o momento, existem outras abordagens para renderizar texto. Uma delas é renderizar fontes TrueType usando o FreeType.


Modernidade: FreeType


O FreeType é uma biblioteca que baixa fontes, as processa em bitmaps e fornece suporte para algumas operações relacionadas a fontes. Essa biblioteca popular é usada no Mac OS X, Java, Qt, PlayStation, Linux e Android. A capacidade de carregar fontes TrueType torna essa biblioteca bastante atraente.


Uma fonte TrueType é uma coleção de glifos definidos não por pixels, mas por fórmulas matemáticas. Como nas imagens vetoriais, uma imagem de fonte rasterizada pode ser gerada com base no tamanho de fonte preferido. Usando fontes TrueType, você pode facilmente renderizar glifos de vários tamanhos sem perda de qualidade.


O FreeType pode ser baixado do site oficial . Você pode compilar o FreeType você mesmo ou usar versões pré-compiladas, se houver, no site. Lembre-se de vincular seu programa ao freetype.lib e verifique se o compilador sabe onde procurar os arquivos de cabeçalho.


Em seguida, anexe os arquivos de cabeçalho corretos:


 #include <ft2build.h> #include FT_FREETYPE_H 

Como o FreeType foi projetado de uma maneira um pouco estranha (no momento em que escrevi o original, deixe-me saber se algo mudou), você pode colocar seus arquivos de cabeçalho apenas na raiz da pasta com os arquivos de cabeçalho. Conectar o FreeType de alguma outra maneira (por exemplo, #include <3rdParty/FreeType/ft2build.h> ) pode provocar um conflito no arquivo de cabeçalho.

O que o FreeType faz? Carrega fontes TrueType e gera uma imagem de bitmap para cada glifo e calcula algumas métricas de glifo. Podemos obter imagens de bitmap para gerar texturas e posicionar cada glifo, dependendo das métricas recebidas.


Para baixar uma fonte, precisamos inicializar o FreeType e carregá-la como face (como o FreeType chama a fonte). Neste exemplo, carregamos a fonte TrueType arial.ttf , copiada da pasta 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 uma dessas funções do FreeType retorna um valor diferente de zero em caso de falha.


Depois de carregar a fonte da face, precisamos especificar o tamanho da fonte desejado, que extrairemos:


 FT_Set_Pixel_Sizes(face, 0, 48); 

Esta função define a largura e a altura do glifo. Ao definir a largura como 0 (zero), permitimos que o FreeType calcule a largura, dependendo da altura definida.


O Face FreeType contém uma coleção de glifos. Podemos ativar algum glifo chamando FT_Load_Char . Aqui tentamos carregar o glifo X :


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

Ao definir FT_LOAD_RENDER como um dos sinalizadores de download, pedimos ao FreeType para criar um bitmap de escala de cinza de 8 bits, que podemos obter assim:


 face->glyph->bitmap; 

Os glifos carregados com o FreeType não têm o mesmo tamanho que no caso de fontes de bitmap. Um bitmap gerado com o FreeType é o tamanho mínimo para um determinado tamanho de fonte e é suficiente apenas para conter um caractere. Por exemplo, uma imagem de bitmap de um glifo . muito menor que o bitmap do glifo X Por esse motivo, o FreeType também baixa algumas métricas que mostram qual tamanho e onde um único caractere deve estar localizado. Abaixo está uma imagem mostrando quais métricas o FreeType calcula para cada glifo.



Cada glifo está localizado na linha de base (linha horizontal com uma seta). Alguns estão exatamente na linha de base ( X ), outros estão abaixo ( g , p ). Essas métricas determinam com precisão os deslocamentos para o posicionamento preciso dos glifos na linha de base, ajustando o tamanho dos glifos e para descobrir quantos pixels você precisa deixar para desenhar o próximo glifo. A seguir, é apresentada uma lista das métricas que usaremos:


  • width : largura do glifo em pixels, acesso por face->glyph->bitmap.width
  • height : altura do glifo em pixels, acesso por face->glyph->bitmap.rows
  • bearingX : deslocamento horizontal do ponto superior esquerdo do glifo em relação à origem, acesso por face->glyph->bitmap_left
  • BearingY : deslocamento vertical do ponto superior esquerdo do glifo em relação à origem, acesso por face->glyph->bitmap_top
  • avanço : deslocamento horizontal do início do próximo glifo em 1/64 pixels em relação à origem, acesso por face->glyph->advance.x

Podemos carregar um glifo de um símbolo, obter suas métricas e gerar uma textura toda vez que queremos desenhá-lo na tela, mas criar texturas para cada símbolo em cada quadro não é um bom método. Melhor salvar os dados gerados em algum lugar e solicitá-los quando precisarmos. Definimos uma estrutura conveniente que armazenaremos em std::map :


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

Neste artigo, simplificaremos nossa vida e usaremos apenas os primeiros 128 caracteres. Para cada caractere, geraremos uma textura e salvaremos os dados necessários em uma estrutura do tipo Character , que adicionaremos aos Characters tipo std::map . Assim, todos os dados necessários para desenhar um personagem são salvos para 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 do loop, para cada um dos 128 primeiros caracteres, obtemos um glifo, geramos uma textura, definimos suas configurações e salvamos as métricas. É interessante notar que usamos GL_RED como argumentos para internalFormat e format texturas. Um bitmap gerado por glifo é uma imagem em escala de cinza de 8 bits, cada pixel ocupando 1 byte. Por esse motivo, armazenaremos o buffer de bitmap como o valor da cor da textura. Isso é obtido criando uma textura na qual cada byte corresponde ao componente vermelho da cor. Se usarmos 1 byte para representar cores de textura, não se esqueça das limitações do OpenGL:


 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 

O OpenGL exige que todas as texturas tenham um deslocamento de 4 bytes, ou seja, seu tamanho deve ser múltiplo de 4 bytes (por exemplo, 8 bytes, 4000 bytes, 2048 bytes) ou (e) eles devem usar 4 bytes por pixel (como no formato RGBA), mas como usamos 1 byte por pixel, eles podem ter diferentes largura. Ao definir o deslocamento do alinhamento da descompactação (existe uma tradução melhor?) Para 1, eliminamos os erros de deslocamento que podem causar segfaults.


Além disso, quando terminarmos de trabalhar com a própria fonte, limpe os recursos do FreeType:


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

Shaders


Para desenhar glifos, use o seguinte sombreador de vértice:


 #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 a posição do símbolo e as coordenadas da textura em um vec4 . O shader de vértice calcula o produto das coordenadas com a matriz de projeção e transfere as coordenadas da textura para o shader de fragmento:


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

O shader de fragmento aceita 2 variáveis ​​globais - uma imagem monocromática do glifo e a cor do próprio glifo. Primeiro, amostramos o valor da cor do glifo. Como os dados da textura são armazenados no componente vermelho da textura, apenas amostramos o componente r como o valor da transparência. Alterando a transparência da cor, a cor resultante será transparente para o plano de fundo do glifo e opaca para os pixels reais do glifo. Também multiplicamos as cores RGB pela variável textColor para alterar a cor do texto.


Mas, para que nosso mecanismo funcione, você precisa ativar a mistura:


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

Como matriz de projeção, teremos uma matriz de projeção ortográfica. Para desenhar texto, de fato, uma matriz de perspectiva não é necessária e o uso da projeção ortográfica também nos permite especificar todas as coordenadas de vértices nas coordenadas da tela se definirmos a matriz assim:


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

Definimos a parte inferior da matriz como 0.0f , a parte superior para a altura da janela. Como resultado, a coordenada y leva valores da parte inferior da tela ( y = 0 ) para a parte superior da tela ( y = 600 ). Isso significa que o ponto (0, 0) indica e o canto inferior esquerdo da tela.


Em conclusão, crie VBO e VAO para desenhar os quadrângulos. Aqui reservamos memória suficiente no VBO para que possamos atualizar os dados para desenhar 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); 

Um quadrilátero plano requer 6 vértices de 4 números de ponto flutuante, portanto reservamos 6 * 4 = 24 flutuações de memória. Como vamos alterar os dados dos vértices com bastante frequência, alocamos memória usando GL_DYNAMIC_DRAW .


Exibir uma linha de texto na tela


Para exibir uma linha de texto, extraímos a estrutura de Character correspondente ao símbolo e calculamos as dimensões do quadrilátero a partir das métricas do símbolo. A partir das dimensões calculadas do quadrilátero, glBufferSubData criamos um conjunto de 6 vértices e atualizamos os dados do vértice usando glBufferSubData .


Por conveniência, RenderText função RenderText que desenhe uma sequência 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); } 

O conteúdo da função é relativamente claro: o cálculo da origem, tamanhos e vértices do quadrilátero. Observe que multiplicamos cada métrica por scale . Depois disso, atualize o VBO e desenhe um quad.


Esta linha de código requer alguma atenção:


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

Alguns caracteres, como p e g , são desenhados visivelmente abaixo da linha de base, o que significa que o quad deve ser visivelmente mais baixo que o parâmetro y da função RenderText . O deslocamento exato y_offset pode ser expresso a partir de métricas de glifo:



Para calcular o deslocamento, precisamos de braços retos para descobrir a distância em que o símbolo está localizado abaixo da linha de base. Esta distância é mostrada pela seta vermelha. Obviamente, y_offset = bearingY - height e ypos = y + y_offset .


Se tudo for feito corretamente, você poderá exibir o texto na tela assim:


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

O resultado deve ficar assim:



Um exemplo de código está aqui (link para o site do autor original).


Para entender quais quadrângulos são desenhados, desative a mesclagem:



A partir desta figura, é óbvio que a maioria dos quadrângulos está no topo de uma linha de base imaginária, embora alguns caracteres, como ( e p , tenham sido deslocados para baixo.


O que vem depois?


Este artigo mostrou como renderizar fontes TrueType com o FreeType. Essa abordagem é flexível, escalável e eficiente em várias codificações de caracteres. No entanto, essa abordagem pode ser muito pesada para o seu aplicativo, pois uma textura é criada para cada personagem. As fontes de bitmap produtivas são preferidas porque temos uma textura para todos os glifos. A melhor abordagem é combinar as duas abordagens e obter o melhor: imediatamente, gere uma fonte raster a partir de glifos baixados usando o FreeType. Isso salvará o renderizador de inúmeras trocas de texturas e, dependendo do pacote de texturas, aumentará o desempenho.


Mas o FreeType tem mais uma desvantagem: glifos de tamanho fixo, o que significa que, à medida que o tamanho do glifo renderizado aumenta, podem aparecer etapas na tela e o glifo pode parecer desfocado quando girado. A Valve resolveu (link para o arquivo da web) esse problema há vários anos usando campos de distância assinados. Eles se saíram muito bem e mostraram isso em aplicativos 3D.


PS : Temos um telegrama conf para coordenação de transferências. Se você tem um desejo sério de ajudar com a tradução, é bem-vindo!

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


All Articles