Pelajari OpenGL. Pelajaran 7.2 - Menggambar Teks

gambar Pada titik tertentu dalam petualangan grafis Anda, Anda akan ingin menampilkan teks melalui OpenGL. Berlawanan dengan apa yang Anda harapkan, mendapatkan garis sederhana di layar cukup sulit dengan perpustakaan tingkat rendah seperti OpenGL. Jika Anda tidak perlu lebih dari 128 karakter berbeda untuk menggambar teks, maka itu tidak akan sulit. Kesulitan muncul ketika karakter tidak cocok dengan tinggi, lebar, dan offset. Tergantung di mana Anda tinggal, Anda mungkin perlu lebih dari 128 karakter. Tetapi bagaimana jika Anda menginginkan karakter khusus, karakter matematika atau musik? Segera setelah Anda memahami bahwa menggambar teks bukan tugas termudah, Anda akan menyadari bahwa itu kemungkinan besar tidak seharusnya menjadi bagian dari API tingkat rendah seperti OpenGL.


Karena OpenGL tidak menyediakan sarana apa pun untuk merender teks, semua kesulitan dari kasus ini ada pada kami. Karena tidak ada "Simbol" primitif grafis, kita harus menciptakannya sendiri. Sudah ada contoh yang sudah jadi: menggambar simbol melalui GL_LINES , membuat model simbol 3D, atau menggambar simbol pada persegi empat datar dalam ruang tiga dimensi.


Paling sering, pengembang terlalu malas untuk minum kopi dan memilih opsi terakhir. Menggambar segi empat bertekstur ini tidak sesulit memilih tekstur yang tepat. Dalam tutorial ini, kita akan belajar beberapa cara dan menulis renderer teks canggih namun fleksibel menggunakan FreeType.



Klasik: Font Raster


Sekali waktu di masa dinosaurus, rendering teks termasuk memilih font (atau membuatnya) untuk aplikasi dan menyalin karakter yang diinginkan ke tekstur besar yang disebut font bitmap. Tekstur ini mengandung semua karakter yang diperlukan di bagian-bagian tertentu. Karakter-karakter ini disebut glyphs. Setiap mesin terbang memiliki area spesifik koordinat tekstur yang terkait dengannya. Setiap kali Anda menggambar karakter, Anda memilih mesin terbang tertentu dan menggambar hanya bagian yang diinginkan pada quad datar.



Di sini Anda dapat melihat bagaimana kami akan merender teks "OpenGL". Kami mengambil font raster dan mencicipi mesin terbang yang diperlukan dari tekstur, dengan hati-hati memilih koordinat tekstur, yang akan kami gambar pada beberapa segi empat. Mengaktifkan pencampuran dan menjaga latar belakang transparan, kita mendapatkan serangkaian karakter di layar. Font bitmap ini dibuat menggunakan generator font bitmap Codehead .


Pendekatan ini memiliki pro dan kontra. Pendekatan ini memiliki implementasi yang sederhana, karena font bitmap sudah dirasterisasi. Namun, ini tidak selalu nyaman. Jika Anda membutuhkan font lain, Anda perlu membuat font bitmap baru. Selain itu, meningkatkan ukuran karakter akan dengan cepat menunjukkan tepi pixelated. Selain itu, font bitmap sering dikaitkan dengan sekumpulan karakter kecil, sehingga karakter Unicode kemungkinan besar tidak akan ditampilkan.


Teknik ini sangat populer belum lama ini (dan masih mempertahankan popularitasnya), karena sangat cepat dan bekerja pada platform apa pun. Namun hingga saat ini, ada pendekatan lain untuk merender teks. Salah satunya adalah rendering font TrueType menggunakan FreeType.


Modernitas: FreeType


FreeType adalah pustaka yang mengunduh font, menjadikannya bitmap, dan menyediakan dukungan untuk beberapa operasi terkait font. Perpustakaan populer ini digunakan pada Mac OS X, Java, Qt, PlayStation, Linux, dan Android. Kemampuan memuat font TrueType membuat pustaka ini cukup menarik.


Font TrueType adalah kumpulan mesin terbang yang didefinisikan bukan oleh piksel, tetapi oleh rumus matematika. Seperti halnya gambar vektor, gambar font raster dapat dibuat berdasarkan ukuran font yang diinginkan. Menggunakan font TrueType, Anda dapat dengan mudah membuat mesin terbang dari berbagai ukuran tanpa kehilangan kualitas.


FreeType dapat diunduh dari situs resmi . Anda dapat mengkompilasi FreeType sendiri, atau menggunakan versi yang telah dikompilasi, jika ada, di situs. Ingat untuk menautkan program Anda ke freetype.lib dan pastikan bahwa kompiler tahu ke mana harus mencari file header.


Kemudian lampirkan file header yang benar:


 #include <ft2build.h> #include FT_FREETYPE_H 

Karena FreeType dirancang dengan cara yang agak aneh (pada saat menulis aslinya, beri tahu saya jika ada sesuatu yang berubah), Anda dapat meletakkan file header-nya hanya di root folder dengan file header. Menghubungkan FreeType dengan cara lain (misalnya, #include <3rdParty/FreeType/ft2build.h> ) dapat memicu konflik file header.

Apa yang dilakukan FreeType? Memuat font TrueType dan menghasilkan gambar bitmap untuk setiap mesin terbang dan menghitung beberapa metrik mesin terbang. Kita bisa mendapatkan gambar bitmap untuk menghasilkan tekstur dan memposisikan setiap mesin terbang tergantung pada metrik yang diterima.


Untuk mengunduh font, kita perlu menginisialisasi FreeType dan memuat font sebagai wajah (seperti FreeType memanggil font). Dalam contoh ini, kita memuat font TrueType arial.ttf , disalin dari folder 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; 

Masing-masing fungsi FreeType mengembalikan nilai bukan nol jika terjadi kegagalan.


Setelah kita memuat wajah font wajah, kita perlu menentukan ukuran font yang diinginkan, yang akan kita ekstrak:


 FT_Set_Pixel_Sizes(face, 0, 48); 

Fungsi ini mengatur lebar dan tinggi mesin terbang. Dengan mengatur lebar ke 0 (nol) kami memungkinkan FreeType untuk menghitung lebar tergantung pada ketinggian yang ditetapkan.


Wajah FreeType berisi kumpulan mesin terbang. Kami dapat mengaktifkan mesin terbang dengan menelepon FT_Load_Char . Di sini kami mencoba memuat mesin terbang X :


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

Dengan menetapkan FT_LOAD_RENDER sebagai salah satu flag unduhan, kami memberitahu FreeType untuk membuat bitmap skala abu-abu 8-bit, yang kemudian dapat kita peroleh seperti ini:


 face->glyph->bitmap; 

Mesin terbang yang dimuat dengan FreeType tidak memiliki ukuran yang sama dengan font bitmap. Bitmap yang dihasilkan dengan FreeType adalah ukuran minimum untuk ukuran font tertentu dan hanya cukup untuk menampung satu karakter. Misalnya, gambar bitmap dari mesin terbang . jauh lebih kecil dari bitmap glyph X Untuk alasan ini, FreeType juga mengunduh beberapa metrik yang menunjukkan ukuran dan di mana karakter tunggal harus ditempatkan. Di bawah ini adalah gambar yang menunjukkan metrik yang menghitung FreeType untuk setiap mesin terbang.



Setiap mesin terbang terletak di garis dasar (garis horizontal dengan panah). Beberapa tepat pada garis dasar ( X ), beberapa di bawah ( g , p ). Metrik ini secara akurat menentukan offset untuk penentuan posisi mesin terbang secara akurat pada garis dasar, menyesuaikan ukuran mesin terbang dan untuk mencari tahu berapa banyak piksel yang perlu Anda tinggalkan untuk menggambar mesin terbang berikutnya. Berikut ini adalah daftar metrik yang akan kami gunakan:


  • lebar : lebar mesin terbang dalam piksel, akses dengan face->glyph->bitmap.width
  • height : glyph height dalam pixel, akses oleh face->glyph->bitmap.rows
  • bearingX : offset horizontal dari titik kiri atas mesin terbang relatif ke asal, akses oleh face->glyph->bitmap_left
  • bearingY : offset vertikal dari titik kiri atas mesin terbang relatif ke asal, akses oleh face->glyph->bitmap_top
  • muka : offset horisontal dari awal mesin terbang berikutnya dalam 1/64 piksel relatif terhadap asal, akses oleh face->glyph->advance.x

Kita dapat memuat mesin terbang simbol, mendapatkan metriknya dan menghasilkan tekstur setiap kali kita ingin menggambarnya di layar, tetapi untuk membuat tekstur untuk setiap simbol pada setiap frame bukanlah metode yang baik. Lebih baik kita menyimpan data yang dihasilkan di suatu tempat dan memintanya saat kita membutuhkannya. Kami mendefinisikan struktur yang nyaman yang akan kami simpan di std::map :


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

Dalam artikel ini, kami akan menyederhanakan hidup kami dan hanya akan menggunakan 128 karakter pertama. Untuk setiap karakter, kami akan menghasilkan tekstur dan menyimpan data yang diperlukan dalam struktur bertipe Character , yang akan kami tambahkan ke Characters bertipe std::map . Dengan demikian, semua data yang diperlukan untuk menggambar karakter disimpan untuk digunakan di masa depan.


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

Di dalam loop, untuk masing-masing 128 karakter pertama, kita mendapatkan mesin terbang, menghasilkan tekstur, mengatur pengaturannya dan menyimpan metrik. Sangat menarik untuk dicatat bahwa kami menggunakan GL_RED sebagai argumen untuk internalFormat dan format tekstur. Bitmap yang dihasilkan mesin terbang adalah gambar skala abu-abu 8-bit, yang masing-masing pikselnya menempati 1 byte. Untuk alasan ini, kami akan menyimpan buffer bitmap sebagai nilai warna tekstur. Ini dicapai dengan membuat tekstur di mana setiap byte sesuai dengan komponen warna merah. Jika kita menggunakan 1 byte untuk mewakili warna tekstur, jangan lupa tentang batasan OpenGL:


 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 

OpenGL mengharuskan semua tekstur memiliki offset 4-byte, mis. ukurannya harus kelipatan 4 byte (mis. 8 byte, 4000 byte, 2048 byte) atau (dan) mereka harus menggunakan 4 byte per piksel (seperti dalam format RGBA), tetapi karena kami menggunakan 1 byte per piksel, mereka dapat memiliki perbedaan lebar. Dengan mengatur offset penyelarasan unpack (apakah ada terjemahan yang lebih baik?) Untuk 1, kami menghilangkan kesalahan offset yang dapat menyebabkan segfault.


Juga, ketika kami selesai bekerja dengan font itu sendiri, kami harus menghapus sumber daya FreeType:


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

Shader


Untuk menggambar mesin terbang, gunakan vertex shader berikut:


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

Kami menggabungkan posisi simbol dan koordinat tekstur dalam satu vec4 . Vertex shader menghitung produk koordinat dengan matriks proyeksi dan mentransfer koordinat tekstur ke shader fragmen:


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

Shader fragmen menerima 2 variabel global - gambar monokrom mesin terbang dan warna mesin terbang itu sendiri. Pertama, kami sampel nilai warna mesin terbang. Karena data tekstur disimpan dalam komponen merah tekstur, kami hanya mengambil sampel komponen r sebagai nilai transparansi. Dengan mengubah transparansi warna, warna yang dihasilkan akan transparan ke latar belakang mesin terbang dan buram ke piksel asli mesin terbang. Kami juga mengalikan warna RGB dengan variabel textColor untuk mengubah warna teks.


Tetapi agar mekanisme kami berfungsi, Anda harus mengaktifkan pencampuran:


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

Sebagai matriks proyeksi, kita akan memiliki matriks proyeksi ortografis. Untuk menggambar teks, pada kenyataannya, matriks perspektif tidak diperlukan dan penggunaan proyeksi ortografis juga memungkinkan kita untuk mengatur semua koordinat titik dalam koordinat layar jika kita mengatur matriks seperti ini:


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

Kami mengatur bagian bawah matriks ke 0.0f , bagian atas ke ketinggian jendela. Akibatnya, koordinat y mengambil nilai dari bagian bawah layar ( y = 0 ) ke bagian atas layar ( y = 600 ). Ini berarti bahwa titik (0, 0) menunjukkan dan sudut kiri bawah layar.


Sebagai kesimpulan, buat VBO dan VAO untuk menggambar segi empat. Di sini kami menyimpan cukup memori dalam VBO sehingga kami dapat memperbarui data untuk menggambar karakter.


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

Sebuah quadrangle datar membutuhkan 6 simpul dari 4 angka floating point, jadi kami memesan 6 * 4 = 24 float memori. Karena kami akan sering mengubah data titik, kami mengalokasikan memori menggunakan GL_DYNAMIC_DRAW .


Tampilkan satu baris teks di layar


Untuk menampilkan satu baris teks, kami mengekstrak struktur Character yang sesuai dengan simbol dan menghitung dimensi segi empat dari metrik simbol. Dari dimensi yang dihitung dari segi empat, dengan cepat kami membuat satu set 6 simpul dan memperbarui data titik menggunakan glBufferSubData .


Untuk kenyamanan, RenderText fungsi RenderText yang akan menggambar serangkaian karakter:


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

Isi fungsi relatif jelas: perhitungan asal, ukuran dan simpul segi empat. Perhatikan bahwa kami mengalikan setiap metrik dengan scale . Setelah itu, perbarui VBO dan gambar quad.


Baris kode ini membutuhkan perhatian:


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

Beberapa karakter, seperti p dan g , digambarkan dengan jelas di bawah garis dasar, yang berarti bahwa quad harus jauh lebih rendah daripada parameter y dari fungsi RenderText . Offset tepat y_offset dapat diekspresikan dari metrik mesin terbang:



Untuk menghitung offset, kita perlu lengan lurus untuk mengetahui jarak di mana simbol berada di bawah garis dasar. Jarak ini ditunjukkan oleh panah merah. Jelas, y_offset = bearingY - height dan ypos = y + y_offset .


Jika semuanya dilakukan dengan benar, Anda dapat menampilkan teks di layar seperti ini:


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

Hasilnya akan terlihat seperti ini:



Contoh kode ada di sini (tautan ke situs penulis asli).


Untuk memahami quadrangles mana yang digambar, matikan blending:



Dari gambar ini, jelaslah bahwa sebagian besar segi empat berada di atas garis dasar imajiner, meskipun beberapa karakter, seperti ( dan p , digeser ke bawah.


Apa selanjutnya


Artikel ini menunjukkan cara merender font TrueType dengan FreeType. Pendekatan ini fleksibel, dapat diukur dan efisien pada berbagai pengkodean karakter. Namun, pendekatan ini mungkin terlalu berat untuk aplikasi Anda, karena tekstur dibuat untuk setiap karakter. Font bitmap yang produktif lebih disukai karena kami memiliki satu tekstur untuk semua mesin terbang. Pendekatan terbaik adalah menggabungkan dua pendekatan dan mengambil yang terbaik: on-the-fly menghasilkan font raster dari mesin terbang yang diunduh menggunakan FreeType. Ini akan menyelamatkan renderer dari banyak penggantian tekstur dan, tergantung pada kemasan tekstur, akan meningkatkan kinerja.


Tetapi FreeType memiliki satu kelemahan lagi: mesin terbang ukuran tetap, yang berarti bahwa ketika ukuran mesin terbang yang diberikan meningkat, langkah-langkah mungkin muncul di layar dan ketika diputar, mesin terbang mungkin terlihat buram. Valve diselesaikan (tautan ke arsip web) masalah ini beberapa tahun yang lalu menggunakan bidang jarak yang ditandatangani. Mereka melakukannya dengan sangat baik dan menunjukkannya pada aplikasi 3D.


PS : Kami punya telegram conf untuk koordinasi transfer. Jika Anda memiliki keinginan serius untuk membantu penerjemahan, maka Anda dipersilakan!

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


All Articles