Merender Teks Tanpa Tekstur
(poniesandlight.co.uk)- Dalam situasi ketika perlu cepat menampilkan pesan debug di dalam render pass, menyiapkan font atlas bisa terasa membebani, sehingga metode menggambar teks hanya dengan konstanta fragment shader menjadi berguna
- Glyph direpresentasikan sebagai bitmap 8x16 piksel, dan 96 karakter printable ASCII dimasukkan ke dalam array
uvec4sebesar 1536 byte untuk diakses langsung dari shader - Data font dapat dibuat dari font terminal PSF1 dengan melewati header 4 byte dan area glyph non-cetak 512 byte, lalu mengekstrak 96 glyph berikutnya
- Rendering menggunakan
uint32_tper 4 karakter dan struct per-instance berisi posisi serta skala, sehingga seluruh teks digambar dengan satu instanced draw call - Karena teknik ini berfokus pada output debug sederhana, batasan seperti padding 4 karakter, karakter fill
\0,discard, dan koreksi endian harus ditangani bersama oleh aplikasi dan shader
Menggunakan konstanta shader sebagai pengganti font atlas
- Rendering teks pada umumnya merender glyph yang tersedia ke font atlas, mengikatnya sebagai tekstur, lalu menggambar segitiga yang sesuai untuk tiap glyph
imguidanstb_truetypejuga memakai cara ini, tetapi untuk menampilkan pesan debug dengan cepat, proses persiapannya bisa merepotkan- Alternatifnya adalah menyimpan data yang setara dengan font atlas sebagai konstanta integer di dalam fragment shader
- Integer dapat digunakan seperti bitmap: posisi
xyfragmen dipetakan ke posisi bit tertentu, lalu jika bit tersebut menyala keluarkan warna foreground, jika mati keluarkan warna background
Glyph bitmap 8x16 dan tabel ASCII
- Satu byte hanya merepresentasikan satu baris piksel, sehingga untuk membuat glyph yang lebih mudah dibaca digunakan 16 byte per glyph
- Satu glyph menjadi kanvas 8x16 piksel, dan satu
uvec4GLSL dapat memuat tepat 16 byte yang dibutuhkan - Jika menyimpan 96 glyph printable ASCII, total datanya menjadi 1536 byte
- Array
font_data[96]menggunakan nilai ASCII dikurangi0x20sebagai indeks- Targetnya adalah glyph printable ASCII yang dimulai dari
0x20SPACE - Dalam contoh kode, hanya sebagian entri ditampilkan untuk menghemat ruang
- Targetnya adalah glyph printable ASCII yang dimulai dari
- Tabel bitmap lengkap disertakan dalam source code Island
Mengekstrak bitmap dari font PSF1
- Encoding bitmap yang dibutuhkan hampir sama dengan font terminal dalam format PSF1
- Prosedur mengekstrak data dari font terminal PSF1 sederhana
- Buka file font dengan hex editor seperti ImHex
- Lewati header 4 byte
- Lewati bagian glyph non-cetak sebesar 512 byte
- Ekspor 96 glyph berikutnya, yaitu 1536 byte, dengan “Copy as → C Array”
- Array char yang diekstrak dapat diedit menjadi array
uint, lalu dikelompokkan peruvec4 - Jika raw char langsung digabungkan menjadi
uint, urutan endian akan terbalik, sehingga perlu dikoreksi kembali saat sampling - Data asli font piksel yang digunakan diambil dari font piksel gratis Tamsyn karya Scott Fial
Menyusun satu instanced draw call
- Rendering teks ditangani dengan satu instanced draw call
- Draw call menggunakan dua attribute stream
- Stream per-draw hanya memiliki informasi yang dibutuhkan untuk menggambar quad biasa
- Stream per-instance berisi offset posisi di layar dan teks yang akan ditampilkan
- Offset posisi menggunakan float x, y, dan ruang float yang tersisa dapat diisi nilai skala font
- Di Vulkan, semua komponen dalam vertex output binding harus memiliki karakteristik interpolation yang sama, sehingga sulit mencampur
vec3danuintsecara rapi dalam satu binding - Teks dipacking ke
uint32_tdalam satuan 4 karakter- Karena satuan minimum tipe data vertex attribute dasar biasanya 32 bit, 4 karakter dimasukkan sekaligus
- Panjang pesan harus habis dibagi 4
- Bagian yang kurang diisi dengan karakter
\0
- Data per-instance direpresentasikan sebagai struct
word_datapos_and_scale[3]: posisixydan scaleword: empat karakter yang akan ditampilkan
- Aplikasi membagi pesan menjadi chunk 4 karakter, mengonversi tiap chunk menjadi
uint32_t, lalu mengakumulasikannya ke arrayword_databersama offset posisi - Saat rendering, array ini di-bind sebagai per-instance binding untuk pipeline drawing debug text, lalu instance digambar sebanyak jumlah quad
Meneruskan posisi dan karakter di Vertex Shader
- Vertex shader mengeluarkan
gl_Position,wordyang akan dirender, dan nilai yang setara dengan koordinat tekstur gl_Positionmemakai data per-instancepos_and_scaleuntuk menempatkan vertex segitiga di layar dalam sistem koordinat NDC- word yang akan dirender diteruskan apa adanya dari input attribute
uintke fragment shader- Agar tidak diinterpolasi, digunakan qualifier
flat
- Agar tidak diinterpolasi, digunakan qualifier
- Koordinat tekstur disintesis dari
gl_VertexIndex12 >> gl_VertexIndex & 1menghasilkan urutan0, 0, 1, 19 >> gl_VertexIndex & 1menghasilkan urutan1, 0, 0, 1- Kombinasi ini menghasilkan koordinat uv
(0,1), (0,0), (1,0), (1,1)tanpa branch
- Vertex shader juga menerima warna foreground dan background sebagai data per-instance, lalu meneruskannya ke fragment shader
Sampling glyph di Fragment Shader
- Agar fragment shader dapat merender teks, dibutuhkan tiga informasi
- Koordinat uv fragmen yang telah diinterpolasi
- Data karakter yang akan ditampilkan,
in_word - Array bitmap glyph
font_data
- Koordinat uv berada dalam rentang float ternormalisasi dari
vec2(0.f,0.f)hinggavec2(1.f,1.f), sedangkan koordinat piksel glyph berada dariuvec2(0,0)hinggauvec2(7,15) - Keseluruhan word 4 karakter diperlakukan sebagai area selebar 32 piksel dan setinggi 16 piksel
uv.xy * vec2(8 * WORD_LEN, 16)di-flooruntuk mengkuantisasi menjadi koordinat piksel word- Rentang koordinat dibatasi ke
uvec2(0..31, 0..15) word_pixel_coord.x / 8digunakan untuk mencari area karakter mana dari empat karakterword_pixel_coord.x % 8digunakan untuk mendapatkan koordinat x di dalam glyph
- Kode karakter dikonversi menjadi indeks
font_data- Karena glyph pertama adalah
0x20SPACE, digunakan offsetprintable_character - 0x20 - Bitmap glyph
uvec4diambil dengan offset tersebut
- Karena glyph pertama adalah
- Koordinat y memilih
uinttertentu di dalamuvec4melaluiglyph_pixel_coord.y / 4uintini memuat data piksel untuk 4 baris- Karena char yang diekstrak dari ImHex langsung digabungkan menjadi
uint, urutan barisnya terbalik - Ini dikoreksi dengan mengindeks dari belakang memakai
(8*(3-(glyph_pixel_coord.y)%4))
- Koordinat x memilih bit dengan
7-glyph_pixel_coord.x- Bit paling signifikan pada byte disimpan pada indeks tertinggi, sehingga perlu pengindeksan terbalik agar sesuai dari kiri ke kanan
- Nilai akhir
current_pixeldigunakan untuk menerapkanmix(background_colour, foreground_colour, current_pixel)guna menentukan warna
Menangani string pendek dan karakter fill
- Jika panjang string tidak habis dibagi 4, aplikasi mengisi bagian yang kurang dengan karakter
\0 - Fragment shader memeriksa apakah karakter yang akan ditampilkan adalah
\0 - Saat menemui karakter fill
\0, shader tidak menggambar background dan menjalankandiscard - Penanganan ini memungkinkan string pendek tetap ditampilkan sambil mempertahankan batasan packing 4 karakter
Bentuk penggunaan dan lokasi kode
- Di proyek Island, teks debug dapat ditampilkan di layar dengan memanggil
le::DebugPrint - Kode lengkap fragment shader dapat dilihat di github
- Contoh kode meneruskan data string dan menampilkan pesan berbentuk
"That's all, %s"di layar
1 komentar
Opini Hacker News
Kalau ingin mencobanya sendiri, cukup ikuti aritmetikanya. Sangat mudah mengimplementasikannya dari nol di ShaderToy, dan kalau Anda suka hal seperti ini, ini juga menyenangkan untuk dimainkan pada Sabtu pagi
Membuatnya dari nol memang seru, tetapi kalau butuh petunjuk awal, ada contoh yang baru saya buat: https://www.shadertoy.com/view/Mc3cW2
Ada juga banyak hack teks cerdik buatan orang lain, termasuk contoh Matrix di bawah 300 karakter https://www.shadertoy.com/view/llXSzj atau efek tampilan CRT hijau https://www.shadertoy.com/view/XtfSD8
vec2(30, -30)diganti menjadi 300, artefak akan terlihatSaya penasaran apakah ada trik untuk menanganinya dengan benar. Dalam kasus saya, multisampling tekstur di dalam fragment shader paling efektif, tetapi tetap belum sebagus standar modern
Belum lama ini saya mencoba membuat aplikasi yang terlihat seperti font konsol native, dan perlu mengutak-atik lebih dari 2 jam hanya untuk membuatnya sekitar 90% mirip
Ini jenaka, hacky, dan menyenangkan. Sebenarnya hampir semua teknik rendering 3D memang begitu, tetapi hasilnya tidak terlalu indah kecuali tujuannya mereplikasi BBS zaman dulu
Menambahkan lebih banyak bit bisa memperbaikinya, tetapi jauh sebelum tampilannya benar-benar bagus, Anda akan menemukan cara yang lebih mudah untuk mengatur semua bit itu. Pada akhirnya, hampir tidak ada solusi yang lebih efisien daripada membuatnya sebagai piksel hitam-putih di program gambar lalu menyimpannya sebagai tekstur, jadi kembali ke titik awal
Kalau penasaran dengan cara yang lebih umum dipakai engine rendering 3D modern untuk menggambar teks, cari teks SDF dan teknik terkait seperti MSDF. Caranya adalah menggunakan atlas tekstur tradisional pada tahap praproses untuk membuat atlas signed distance field
Makalah itu membahas hardware, tetapi software juga mengalami reinkarnasi
Dulu saya pernah membuat versi yang sangat dasar dari konsep ini: https://www.shadertoy.com/view/sdXBDs
Bukan berarti ini bukan trik keren; ini memang trik yang keren
Ada juga opsi merender teks sebagai mesh. TextMeshPro melangkah lebih jauh dengan menggunakan signed distance field untuk menangani skala sembarang
https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...
Contoh: https://sluglibrary.com
Mesh dan SDF jauh lebih sederhana di sisi GPU, tetapi akurasinya bisa menurun jika diperbesar terlalu besar, dan aliasing bisa muncul jika mesh diperkecil terlalu kecil
Sangat keren. Akan menarik kalau ada perbandingan performa dengan pendekatan tekstur “tradisional”
Untuk tugas sederhana seperti ini di GPU masa kini, jawaban atas pertanyaan “bagaimana performanya?” biasanya tampaknya “bisa jalan”
Sebastian Lague membuat video bagus yang membahas berbagai teknik rendering font
https://youtu.be/SO83KQuuZvg
Saya pernah mencoba teknik serupa, dengan memasukkan seluruh data font ke dalam source code fragment shader. Dengan begitu, kita bisa mencetak langsung ke buffer GPU yang dipetakan ke CPU menggunakan
snprintf. Saya tahu ini cara yang berbahayaAlih-alih menggambar tiap karakter dengan vertex shader, saya hanya menggambar satu segitiga layar penuh dan menggunakan
gl_FragCoordsebagai pengganti koordinat UV. Ini bukan cara paling efisien, tetapi ini fitur debugging dan dalam praktiknya cukup cepatBerbeda dari nama berkasnya, yang dipakai bukan font NES, melainkan font dari ROM IBM PC. “NES font” dan font piksel 8x8 lain bisa ditemukan di web
https://github.com/rikusalminen/triangles/blob/nesfont/shade...
Menariknya, game ini menyimpan data pertanyaan dan jawaban di pita 8-track
Keren. Jarang saya melihat algoritma rendering teks yang belum pernah saya coba sendiri. Di startup saya, kami mengimplementasikan berbagai macam, tetapi saya butuh independensi resolusi dan antialiasing, jadi pendekatan ini mungkin tidak akan membantu
Ini juga mungkin tidak bisa digeneralisasi ke semua berkas font kurva Bézier. Mengubah kurva menjadi piksel bisa sulit, terutama saat glyph berpotongan dengan dirinya sendiri. Secara umum, rendering teks standar terasa sudah terselesaikan, sementara use case nonstandar sangat keras untuk dicoba
Secara konsep, pendekatan ini tampak mirip dengan metode Will Dobbie yang saya suka. Hanya saja jauh lebih sederhana. Keduanya mengambil data font mentah dan menggunakannya langsung di shader. Bedanya, pendekatan ini menyimpan data piksel dalam array, sedangkan Will menyimpan data path SVG sebagai “tekstur vektor”
Kalau penasaran, ada demo keren dari Will: https://wdobbie.com/warandpeace/
Dulu saya pernah berpikir untuk mencoba hal seperti ini, tetapi pemahaman saya adalah GPU sangat efisien untuk rendering tekstur dan relatif lambat untuk manipulasi bit. Meski ini menghemat sedikit memori, saya penasaran apakah dalam praktiknya lebih cepat daripada memakai atlas
Mungkin kita bisa mendapatkan keunggulan keduanya dengan melakukan bit packing pada tekstur biasa lalu membiarkan fragment shader mendekodenya
Saya hampir tidak tahu grafika komputer modern, jadi ini murni pertanyaan: apakah biaya mengunggah tekstur kecil ke GPU memang sebesar itu? Saya bertanya-tanya, tidak bisakah seluruh string dirender ke tekstur 2D lalu tekstur itu ditampilkan pada dua segitiga?
Sebaliknya, Anda harus menulis kode untuk membuat atlas font, atau mencari dan memuat atlas yang sudah ada, dan untuk itu juga butuh kode loading. Atau Anda harus menggambar seluruh pesan ke tekstur, lalu melakukan cache hasilnya sampai pesannya berubah
Selain itu, masih perlu manajemen resource dan binding, sedangkan pendekatan ini tidak membutuhkan resource. Dengan kata lain, ini bukan solusi teks umum, melainkan teknik untuk menampilkan teks debug di layar
Sebagai catatan, sebagian besar browser dan OS bekerja dengan cara menggambar teks ke tekstur. Font digambar secara dinamis ke texture atlas, lalu glyph dari atlas itu digunakan untuk membuat tekstur lain bagi bagian jendela aplikasi. Jika Anda menampilkan batas tekstur di browser, Anda bisa melihat semua tekstur; Rendering->Layer borders memberi garis tepi sian pada tiap tekstur
Jika CPU yang relatif lambat harus merender banyak kotak teks tekstur independen, biayanya cepat menumpuk dan menggerogoti anggaran
Menggambar dengan glyph atlas tetap jauh lebih baik dari sisi pemanfaatan resource. Pipeline rendering teks modern sering memakai SDF atau kurva Bézier terenkode untuk meningkatkan keterbacaan glyph saat zoom in/out, dan ini juga cara yang baik untuk lebih menghemat memori
Dari sudut pandang upload, pada akhirnya ada X byte glyph yang harus masuk ke memori GPU dengan satu atau lain cara. Entah tekstur, data uniform, atau konstanta shader, dari sisi performa tidak ada perbedaan besar. Justru jika dimasukkan sebagai konstanta shader seperti pada artikel asli, compiler shader harus memproses semua deklarasi konstanta itu sehingga biaya di sisi CPU bisa lebih besar
Yang penting di sisi GPU adalah hierarki memori mana yang disentuh saat membaca data glyph. Texture fetch memakai cache L1 khusus di sebagian besar GPU, dan mungkin lebih besar daripada cache L1 umum. Urutan data juga penting. Tekstur biasanya disimpan dalam variasi urutan Morton untuk menghindari cache miss saat melakukan shading blok piksel. Untuk renderer teks berbasis atlas dalam penggunaan nyata, sebaiknya memakai tekstur
Koreksi: saya salah membaca pertanyaannya. Jika yang dibandingkan adalah menggambar glyph individual di GPU versus menggambar seluruh blok teks di CPU, maka ini adalah trade-off antara kecepatan dan ruang. Jawabannya bergantung pada seberapa banyak memori yang akan dipakai untuk teks, apakah teksnya berubah, apakah perlu efek per karakter, dan sebagainya
Tambahan lagi, meski disebut “tanpa tekstur”, pendekatan ini juga sebenarnya tekstur. Teksturnya hanya disimpan dalam format dan lokasi yang berbeda. Rendering font yang benar-benar tanpa tekstur mengevaluasi kurva vektor secara langsung
Cukup membingungkan karena setelah mengatakan tidak akan menyimpan bitmap di shader, ia justru menjelaskan cara menyimpan bitmap di shader
Singkatnya, ini menyematkan font bitmap ke dalam shader
Bisa dianalogikan sebagai perbedaan antara menyimpan data di file terpisah yang harus dibaca saat runtime dan menyertakan data langsung di kode sumber