OpenGL este o bibliotecă ce pune la dispoziție funcționalități de nivel scăzut și nu conține capabilități de nivel ridicat pentru redarea unui text pe ecran. Pentru a realiza acest lucru, dezvoltatorul trebuie să implementeze un sistem de redare a textului ce utilizează functionalitățile low-level ale OpenGL.
Cea mai des utilizată metoda este aceea ca fiecare caracter ce se dorește a fi redat să aibă asociată o textură. Astfel redarea propriu-zisă a unui caracter pe ecran presupune desenarea unui poligon de tip quad ce va avea mapat textura corespunzătoare caracterului ce se dorește a fi afișat.
O primă variantă ar fi ca toate caracterele să se regăsească în cadrul unei singure texturi mari denumită “bitmap font” și la redare să se selecteze din bitmap font pentru mapare coordonatele specifice porțiunii unde se află caracterul dorit.
Figura 1. Bitmap font ce conține toate caracterele și simbolurile
Figura 2. Redare text “Hello World” folosind quad-uri și caracterele extrase din bitmap font
Cea de-a doua variantă (și cea care este folosită și pentru implementare în continuarea laboratorului) este ca pentru fiecare caracter/simbol să se creeze câte o textură individuală ce va avea dimensiunea (lătime/înălțime) caracterului/simbolului. Această metodă permite o mai bună flexibilitate în manipularea fiecărui caracter/simbol în parte pentru poziționarea/scalarea acestuia.
Pentru a obtine imaginea (bitmapul) pentru fiecare caracter/simbol necesar dintr-un font dat, a fost folosită biblioteca FreeType.
FreeType este o bibliotecă open-source care permite încărcarea de fonturi și redarea acestora în bitmapuri inclusiv la nivelul fiecărui caracter/simbol individual.
FreeType poate fi descarcat de la (FreeType).
Detalii despre modul de lucru cu FreeType, crearea și gestiunea bitmapurilor create pot fi găsite la ( Tutoriale FreeType).
În cazul nostru, pentru primele 128 de caractere ASCII ale font-ului folosit, folosind FreeType vom crea câte o textură astfel:
// Then initialize and load the FreeType library FT_Library ft; // All functions return a value different than 0 whenever an error occurred if (FT_Init_FreeType(&ft)) std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; // Load font as face FT_Face face; if (FT_New_Face(ft, font.c_str(), 0, &face)) std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl; // Set size to load glyphs as FT_Set_Pixel_Sizes(face, 0, fontSize);
// for the first 128 ASCII characters, // pre-load/compile their characters and store them 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 ... // Store character information (texture id, size, etc) for later use ... }
Astfel pentru fiecare dintre primele 128 de caractere ASCII din font se crează câte o textură. Se folosește pentru textura creată doar primul canal de culoare (GL_RED) unde se va memora valoarea de culoare din bitmap-ul generat pentru fiecare caracter ca o imagine greyscale pe 8 biți.
Pentru redarea fiecărui caracter se folosesc următoarele shadere:
layout(location = 0) in vec4 vertex; // <x,y,u,v> out vec2 TexCoords; uniform mat4 projection; void main() { gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); TexCoords = vertex.zw; }
Pe coordonatele .xy se transmit coordonatele x,y din cadrul ferestrei de afișare care reprezintă poziția unde va fi afișat caracterul.
Coordonata z va fi zero deoarece afișarea se va face direct pe fața cubului corespunzător volumului vizual canonic în planul Z = 0.
Astfel acestea vor fi transformate in vertex shader doar cu matricea de proiecție.
Se va folosi o proiecție ortografică ce va avea near = far = 0 și pentru a considera colțul stânga sus ca fiind 0,0 va avea bottom inversat cu top astfel:
glm::value_ptr(glm::ortho(0.0f, static_cast<GLfloat>(width), static_cast<GLfloat>(height), 0.0f))
unde width și height sunt rezoluția ferestrei de afișare
Pe coordonatele .zw vor veni coordonatele de textură care vor fi trimise mai departe în TexCoords
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; }
Fragment shader-ul primește ca uniforme id-ul texturii unde a fost încărcat bitmap-ul monocolor al caracterului și culoarea cu care se dorește a fi afișat textul. Astfel, deoarece am folosit doar canalul roșu pentru textură (GL_RED) se face eșantionarea din textură doar de pe acesta (texture(text, TexCoords).r) și se stochează în culoarea finală pe canalul de alpha (componenta a 4-a). În acest fel, considerând quad-ul pe care se afisează ca fiind transparent (in bitmap acolo unde nu există pixeli pentru caracter, va fi intors 0 iar un alpha de 0 înseamnă perfect transparent) textul va fi afișat iar restul conținutului quad-ului pe care a fost mapată textură va fi ignorat permițând astfel combinarea naturală cu ce deja a fost afișat anterior în frame buffer. Ca ultim pas, se înmulțește culoarea obținută cu culoarea textColor pentru a desena textul cu o culoare dorită.
Pentru a putea fi folosit acest mecanism de transparență trebuie activat și folosit mecanismul de blending din OpenGL astfel:
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Pentru redarea unei linii de text se va folosi functia
void RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)
unde:
Întreaga funcție:
void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color) { // Activate corresponding render state //this->TextShader.Use(); if (this->TextShader) { glUseProgram(this->TextShader->program); CheckOpenGLError(); } int loc_text_color = glGetUniformLocation(this->TextShader->program, "textColor"); glUniform3f(loc_text_color, color.r, color.g, color.b); //this->TextShader.SetVector3f("textColor", color); glActiveTexture(GL_TEXTURE0); glBindVertexArray(this->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 + (this->Characters['H'].Bearing.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, 1.0 }, { xpos + w, ypos, 1.0, 0.0 }, { xpos, ypos, 0.0, 0.0 }, { xpos, ypos + h, 0.0, 1.0 }, { xpos + w, ypos + h, 1.0, 1.0 }, { xpos + w, ypos, 1.0, 0.0 } }; // Render glyph texture over quad glBindTexture(GL_TEXTURE_2D, ch.TextureID); // Update content of VBO memory glBindBuffer(GL_ARRAY_BUFFER, this->VBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); // Be sure to use glBufferSubData and not glBufferData glBindBuffer(GL_ARRAY_BUFFER, 0); // Render quad glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArrays(GL_TRIANGLES, 0, 6); glDisable(GL_BLEND); // Now advance cursors for next glyph x += (ch.Advance >> 6) * scale; // Bitshift by 6 to get value in pixels (1/64th times 2^6 = 64) } glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); }
Astfel se procesează fiecare caracter din care este alcătuit quad-ul după cum urmează:
Laborator_BonusTextRenderer
TextRenderer
din /TextRendererVertexShaderText.glsl
și FragmentShaderText.glsl
din /TextRenderer/Shaders