1 poin oleh GN⁺ 2024-11-11 | 1 komentar | Bagikan ke WhatsApp
  • 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 uvec4 sebesar 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_t per 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
  • imgui dan stb_truetype juga 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 xy fragmen 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 uvec4 GLSL 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 dikurangi 0x20 sebagai indeks
    • Targetnya adalah glyph printable ASCII yang dimulai dari 0x20 SPACE
    • Dalam contoh kode, hanya sebagian entri ditampilkan untuk menghemat ruang
  • 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 per uvec4
  • 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 vec3 dan uint secara rapi dalam satu binding
  • Teks dipacking ke uint32_t dalam 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_data
    • pos_and_scale[3]: posisi xy dan scale
    • word: empat karakter yang akan ditampilkan
  • Aplikasi membagi pesan menjadi chunk 4 karakter, mengonversi tiap chunk menjadi uint32_t, lalu mengakumulasikannya ke array word_data bersama 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, word yang akan dirender, dan nilai yang setara dengan koordinat tekstur
  • gl_Position memakai data per-instance pos_and_scale untuk menempatkan vertex segitiga di layar dalam sistem koordinat NDC
  • word yang akan dirender diteruskan apa adanya dari input attribute uint ke fragment shader
    • Agar tidak diinterpolasi, digunakan qualifier flat
  • Koordinat tekstur disintesis dari gl_VertexIndex
    • 12 >> gl_VertexIndex & 1 menghasilkan urutan 0, 0, 1, 1
    • 9 >> gl_VertexIndex & 1 menghasilkan urutan 1, 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) hingga vec2(1.f,1.f), sedangkan koordinat piksel glyph berada dari uvec2(0,0) hingga uvec2(7,15)
  • Keseluruhan word 4 karakter diperlakukan sebagai area selebar 32 piksel dan setinggi 16 piksel
    • uv.xy * vec2(8 * WORD_LEN, 16) di-floor untuk mengkuantisasi menjadi koordinat piksel word
    • Rentang koordinat dibatasi ke uvec2(0..31, 0..15)
    • word_pixel_coord.x / 8 digunakan untuk mencari area karakter mana dari empat karakter
    • word_pixel_coord.x % 8 digunakan untuk mendapatkan koordinat x di dalam glyph
  • Kode karakter dikonversi menjadi indeks font_data
    • Karena glyph pertama adalah 0x20 SPACE, digunakan offset printable_character - 0x20
    • Bitmap glyph uvec4 diambil dengan offset tersebut
  • Koordinat y memilih uint tertentu di dalam uvec4 melalui glyph_pixel_coord.y / 4
    • uint ini 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_pixel digunakan untuk menerapkan mix(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 menjalankan discard
  • 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

 
GN⁺ 2024-11-11
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

    • Setiap kali mencoba rendering teks immediate mode, saya tidak pernah berhasil membuat teks terlihat bagus pada ukuran kecil. Di ShaderToy pertama pun, kalau angka 30 pada vec2(30, -30) diganti menjadi 300, artefak akan terlihat
      Saya 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
    • Saya mengembangkan game Unity, dan rasanya bidang ini benar-benar bergerak mundur. Beberapa tahun lalu Unity membeli alat rendering font terbaik yang dibuat satu orang lalu memasukkannya sebagai fitur bawaan, dan setelah itu pengembangannya hampir berhenti, sementara pasar rendering font pesaing juga mati
      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
    • Sedikit di luar topik tetapi menarik: efek Matrix yang dibuat dengan HTML/CSS/JS muat dalam 1024 byte: https://codegolf.stackexchange.com/a/17414
  • 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

    • Sesuai dengan ungkapan “kembali ke titik awal”, kalau belum pernah melihatnya, makalah tahun 1968 On the Design of Display Processors layak dibaca: http://cva.stanford.edu/classes/cs99s/papers/myer-sutherland...
      Makalah itu membahas hardware, tetapi software juga mengalami reinkarnasi
    • Pendekatan seperti teks SDF atau MSDF kini sudah tertinggal setidaknya satu generasi. Sekarang hampir semua orang memakai pendekatan seperti https://sluglibrary.com, yaitu merasterisasi kurva Bézier font langsung di shader
      Dulu saya pernah membuat versi yang sangat dasar dari konsep ini: https://www.shadertoy.com/view/sdXBDs
    • Misalnya, dalam situasi ketika upload tekstur tidak berfungsi dengan benar, ini cukup cerdik untuk teks debug. Namun meski perumpamaan penulis tentang sprite sheet dengan penyusunan huruf manual abad ke-16 itu lucu, secara realistis seorang asisten cetak butuh satu jam untuk menyusun huruf logam, sedangkan mengunggah sprite sheet ke GPU memakan waktu kurang dari 10 ms dan setelah itu bisa disetel tanpa batas
      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/...

    • Lebih jauh lagi, kurva font juga bisa dievaluasi langsung di GPU, dan dapat menghasilkan kualitas tinggi terlepas dari skala atau perspektif. Implementasi yang efisien sangat sulit, tetapi tetap memungkinkan
      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”

    • Jawaban yang saya harapkan untuk “bagaimana performanya?” adalah “VSCode tidak lagi memakan ratusan MB hingga beberapa GB VRAM saya”
  • 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 berbahaya
    Alih-alih menggambar tiap karakter dengan vertex shader, saya hanya menggambar satu segitiga layar penuh dan menggunakan gl_FragCoord sebagai pengganti koordinat UV. Ini bukan cara paling efisien, tetapi ini fitur debugging dan dalam praktiknya cukup cepat
    Berbeda 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...

    • Paket font piksel favorit saya adalah ini: https://int10h.org/oldschool-pc-fonts/
    • Baru-baru ini saya mengetahui bahwa font “NES” berasal dari game arcade Quiz Show tahun 1976. Font ini juga digunakan di game Kee/Atari hitam-putih lainnya, dan data font-nya ada di set ROM MAME quizshow, tetapi entah kenapa dipecah per nibble
      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

    • Pemahaman itu sudah cukup lama. Pada GPU sekitar 15 tahun terakhir, texture lookup kira-kira 100 kali lebih lambat daripada operasi bit
  • 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?

    • Biayanya tidak besar. Teknik ini lebih untuk menampilkan teks debugging di layar semudah mungkin, bukan untuk performa. Tambahkan sedikit data ke shader, dan teks langsung muncul
      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
    • Bahkan sekarang pun biasanya lebih baik tidak terlalu memboroskan memori GPU. Tekstur kotak teks besar harus ditransfer melalui bus PCI, dan bisa menimbulkan jeda bergantung pada saat upload atau apakah resource GPU dieviction
      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
    • Menggambar satu persegi panjang yang menutupi N karakter lalu memilih glyph di shader kemungkinan lebih cepat daripada menggambar persegi panjang terpisah untuk tiap karakter. Setidaknya untuk font monospace begitu. Namun jumlah karakter yang bisa memenuhi layar terbatas, jadi perbedaan nyatanya kemungkinan tidak besar
      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
    • Anda bisa merender seluruh string sebelum upload, tetapi itu pada dasarnya berarti memakai rendering CPU, sehingga kemungkinan akan lebih lambat daripada GPU melakukan pekerjaan yang sama
      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
    • Tergantung aplikasinya. Terutama jika Anda mungkin berhadapan dengan aksara kanan-ke-kiri, CJK, dan emoji, itu adalah cara paling mudah. Sebagian besar teks tidak berubah tiap frame, jadi caching tekstur layak dilakukan, dan bagi kami itu sudah cukup baik
  • 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

    • Bukan, maksudnya tidak menyimpan bitmap di tekstur. Itu berbeda dengan menyematkannya langsung di kode shader
      Bisa dianalogikan sebagai perbedaan antara menyimpan data di file terpisah yang harus dibaca saat runtime dan menyertakan data langsung di kode sumber