Ketika kami mulai mengembangkan
profiler kinerja kami, kami tahu bahwa kami akan melakukan hampir semua rendering UI sendiri. Segera kami harus memutuskan pendekatan mana yang akan dipilih untuk rendering font. Kami memiliki persyaratan berikut:
- Kita harus dapat me-render font apa pun dalam ukuran apa pun secara waktu nyata untuk beradaptasi dengan font sistem dan ukurannya yang dipilih oleh pengguna Windows.
- Rendering font harus sangat cepat, tidak ada pengereman saat rendering font diizinkan.
- UI kami memiliki banyak animasi yang halus, sehingga teks harus dapat bergerak dengan lancar di sekitar layar.
- Ini harus dapat dibaca dengan ukuran font kecil.
Tidak menjadi spesialis hebat pada waktu itu, saya mencari informasi di Internet dan menemukan banyak teknik yang digunakan untuk membuat font. Saya juga berbicara dengan Direktur Teknis Permainan Gerilya Michail van der Leu. Perusahaan ini bereksperimen dengan banyak cara untuk merender font, dan mesin rendering mereka adalah salah satu yang terbaik di dunia. Mihil secara singkat menguraikan idenya untuk teknik rendering font baru. Meskipun kami sudah memiliki cukup teknik yang sudah tersedia, ide ini menggelitik saya dan saya mulai menerapkannya, tidak memperhatikan dunia indah rendering font yang terbuka untuk saya.
Dalam seri artikel ini, saya akan menjelaskan secara rinci teknik yang kami gunakan, membagi deskripsi menjadi tiga bagian:
- Pada bagian pertama, kita akan belajar cara membuat mesin terbang secara real time menggunakan 16xAA, disampel dari grid yang seragam.
- Pada bagian kedua, kita akan beralih ke grid yang diputar untuk melakukan antialiasing tepi horizontal dan vertikal dengan indah. Kita juga akan melihat bagaimana shader jadi hampir sepenuhnya dikurangi menjadi satu tekstur dan tabel pencarian.
- Pada bagian ketiga, kita akan belajar cara meraster mesin terbang secara real time menggunakan Compute dan CPU.
Anda juga dapat melihat hasil yang selesai di profiler, tetapi di sini adalah contoh layar dengan font Segoe UI yang dirender menggunakan font renderer kami:
Inilah peningkatan huruf S, ukuran raster hanya 6x9 texels. Data vektor asli dirender sebagai lintasan, dan pola sampel yang diputar dirender dari persegi panjang hijau dan merah. Karena ini diberikan dengan resolusi yang jauh lebih besar dari 6 × 9, warna abu-abu tidak terwakili dalam rona akhir piksel, ini menampilkan rona subpixel. Ini adalah visualisasi debugging yang sangat berguna untuk memastikan bahwa semua perhitungan di tingkat subpixel berfungsi dengan benar.
Ide: menyimpan pelapis alih-alih warna
Masalah utama yang perlu ditangani oleh penyaji font adalah menampilkan data font vektor yang dapat diskalakan dalam kotak piksel tetap. Metode transisi dari ruang vektor ke piksel jadi dalam teknik yang berbeda sangat berbeda. Dalam sebagian besar teknik ini, data kurva dirasterisasi sebelum dirender ke penyimpanan sementara (misalnya, tekstur) untuk mendapatkan ukuran tertentu dalam piksel. Penyimpanan sementara digunakan sebagai cache mesin terbang: ketika mesin terbang yang sama diberikan beberapa kali, mesin terbang diambil dari cache dan digunakan kembali untuk menghindari rasterisasi lagi.
Perbedaan dalam teknik terlihat jelas dalam bagaimana data disimpan dalam format data menengah. Sebagai contoh, sistem font Windows rasterisasi mesin terbang ke ukuran tertentu dalam piksel. Data disimpan sebagai
rona per piksel. Bayangan menggambarkan perkiraan terbaik dari cakupan oleh mesin terbang pixel ini. Saat merender, piksel disalin dari cache mesin terbang ke grid piksel target. Ketika mengkonversi data ke format piksel, mereka tidak skala dengan baik, oleh karena itu, ketika memperkecil, fuzzy glyph muncul, dan ketika memperbesar, glyph muncul di mana blok terlihat jelas. Oleh karena itu, untuk setiap ukuran akhir, mesin terbang dirender ke dalam cache mesin terbang.
Bidang Jarak Jauh yang Ditandatangani menggunakan pendekatan yang berbeda. Alih-alih rona untuk piksel,
jarak ke tepi terdekat mesin terbang dipertahankan. Keuntungan dari metode ini adalah bahwa untuk tepi melengkung, skala data jauh lebih baik daripada nuansa. Saat mesin terbang memperbesar, kurva tetap halus. Kelemahan dari pendekatan ini adalah tepi yang lurus dan tajam dihaluskan. Jauh lebih baik daripada SDF dicapai oleh solusi canggih seperti
FreeType , yang menyimpan data warna.
Dalam kasus di mana rona dipertahankan untuk piksel, Anda harus terlebih dahulu menghitung cakupannya. Sebagai contoh, stb_truetype memiliki
contoh yang baik tentang bagaimana Anda dapat menghitung cakupan dan rona. Cara populer lainnya untuk memperkirakan cakupan adalah dengan sampel mesin terbang pada frekuensi yang lebih tinggi daripada resolusi akhir. Ini menghitung jumlah sampel yang sesuai dengan mesin terbang di area piksel target. Jumlah hit dibagi dengan jumlah sampel maksimum yang mungkin menentukan rona. Karena cakupan telah dikonversi ke rona untuk resolusi dan
penyejajaran grid piksel tertentu, mustahil untuk menempatkan mesin terbang di antara piksel target: rona tidak akan dapat dengan benar mencerminkan cakupan yang sebenarnya dengan sampel dari jendela piksel target. Untuk ini, serta beberapa alasan lain yang akan kami pertimbangkan nanti, sistem seperti itu tidak mendukung gerakan subpixel.
Tetapi bagaimana jika kita perlu dengan bebas memindahkan mesin terbang antar piksel? Jika rona dihitung terlebih dahulu, kami tidak dapat menemukan apa rona yang seharusnya ketika bergerak di antara piksel di area piksel target. Namun, kami dapat menunda konversi dari cakupan ke rona pada saat render. Untuk melakukan ini, kami tidak akan menyimpan tempat teduh, tetapi
lapisan . Kami mencicipi mesin terbang dengan frekuensi 16 resolusi target, dan untuk setiap sampel kami menyimpan sedikit pun. Saat mengambil sampel pada kisi 4 × 4, cukup untuk menyimpan hanya 16 bit per piksel. Ini akan menjadi
topeng penutup kami. Selama rendering, kita perlu menghitung berapa banyak bit yang masuk ke jendela target pixel, yang memiliki resolusi yang sama dengan repositori texel, tetapi secara fisik tidak melekat padanya. Animasi di bawah ini menunjukkan sebagian mesin terbang (biru) yang dirasterisasi dalam empat texels. Setiap texel dibagi menjadi kotak 4 × 4 sel. Kotak abu-abu menunjukkan jendela piksel yang bergerak dinamis melintasi mesin terbang. Pada waktu berjalan, jumlah sampel yang jatuh ke dalam jendela piksel dihitung untuk menentukan rona.
Secara singkat tentang teknik rendering font dasar
Sebelum beralih ke membahas implementasi sistem rendering font kami, saya ingin secara singkat berbicara tentang teknik utama yang digunakan dalam proses ini: font hinting dan rendering subpixel (teknik ini disebut ClearType pada Windows). Anda dapat melewati bagian ini jika Anda hanya tertarik pada teknik antialiasing.
Dalam proses penerapan renderer, saya belajar lebih banyak tentang sejarah panjang pengembangan rendering font. Penelitian berfokus sepenuhnya pada satu-satunya aspek rendering font - keterbacaan pada ukuran kecil. Membuat penyaji yang sangat baik untuk font besar cukup sederhana, tetapi sangat sulit untuk menulis sistem yang mempertahankan keterbacaan pada ukuran kecil. Studi rendering font memiliki sejarah yang panjang, sangat mencolok. Baca, misalnya, tentang
tragedi raster . Adalah logis bahwa ini adalah masalah utama bagi spesialis komputer, karena pada tahap awal komputer, resolusi layarnya cukup rendah. Ini pasti salah satu tugas pertama yang harus dihadapi pengembang OS: bagaimana membuat teks dapat dibaca pada perangkat dengan resolusi layar rendah? Yang mengejutkan saya, sistem rendering font berkualitas tinggi sangat berorientasi pixel. Misalnya, mesin terbang dibangun sedemikian rupa sehingga mulai di perbatasan piksel, lebarnya adalah kelipatan dari jumlah piksel, dan konten disesuaikan agar sesuai dengan piksel. Teknik ini disebut meshing. Saya sudah terbiasa bekerja dengan permainan komputer dan grafik 3D, di mana dunia dibangun dari unit dan diproyeksikan ke piksel, jadi saya sedikit terkejut. Saya menemukan bahwa di bidang rendering font ini adalah pilihan yang sangat penting.
Untuk menunjukkan pentingnya menghubungkan, mari kita lihat skenario yang mungkin untuk rasterisasi mesin terbang. Bayangkan bahwa mesin terbang dirasterisasi pada grid pixel, tetapi bentuk mesin terbang tidak cocok dengan struktur grid:
Antialiasing akan membuat piksel di kanan dan kiri mesin terbang menjadi sama abu-abu. Jika mesin terbang sedikit bergeser sehingga lebih cocok dengan batas piksel, maka hanya satu piksel yang akan diwarnai, dan itu akan menjadi benar-benar hitam:
Sekarang mesin terbang cocok dengan piksel dengan baik, warnanya menjadi kurang buram. Perbedaan ketajamannya sangat besar. Font barat memiliki banyak mesin terbang dengan garis-garis horizontal dan vertikal, dan jika mereka tidak cocok dengan grid pixel dengan baik, nuansa abu-abu membuat font menjadi buram. Bahkan teknik anti-aliasing terbaik tidak mampu mengatasi masalah ini.
Petunjuk font diusulkan sebagai solusi. Pembuat font harus menambahkan informasi ke font mereka tentang bagaimana mesin terbang harus snap ke piksel jika mereka tidak cocok dengan sempurna. Sistem rendering font mendistorsi kurva ini untuk memasangnya ke grid pixel. Ini sangat meningkatkan kejelasan font, tetapi harus dibayar:
- Font menjadi sedikit terdistorsi . Font tidak terlihat persis seperti yang dimaksudkan.
- Semua mesin terbang harus dilampirkan ke grid piksel: awal mesin terbang dan lebar mesin terbang. Oleh karena itu, mustahil untuk menghidupkannya di antara piksel.
Menariknya, dalam menyelesaikan masalah ini, Apple dan Microsoft melakukan berbagai cara. Microsoft mematuhi kejelasan absolut, dan Apple berupaya menampilkan font dengan lebih akurat. Di Internet Anda dapat menemukan orang-orang mengeluh tentang font yang buram pada mesin Apple, tetapi banyak orang menyukai apa yang mereka lihat di Apple. Itu sebagian soal selera.
Ini adalah tulisan Joel di Perangkat Lunak, dan di
sini adalah tulisan
Peter Bilak tentang topik ini, tetapi jika Anda mencari di internet, Anda dapat menemukan lebih banyak informasi.
Karena resolusi DPI di layar modern meningkat dengan cepat, muncul pertanyaan apakah pengisyaratkan font akan diperlukan di masa depan, seperti sekarang ini. Dalam kondisi saya saat ini, saya menemukan font yang mengisyaratkan teknik yang sangat berharga untuk rendering font dengan jelas. Namun, teknik yang dijelaskan dalam artikel saya dapat menjadi alternatif yang menarik di masa depan, karena mesin terbang dapat dengan bebas ditempatkan di atas kanvas tanpa distorsi. Dan karena ini pada dasarnya adalah teknik anti-aliasing, itu dapat digunakan untuk tujuan apa pun, dan tidak hanya untuk rendering font.
Akhirnya, saya akan berbicara singkat tentang
rendering subpixel . Di masa lalu, orang menyadari bahwa Anda dapat melipattigakan resolusi horizontal layar dengan menggunakan sinar monitor komputer merah, hijau, dan biru. Setiap piksel dibangun dari sinar-sinar ini, yang secara fisik terpisah. Mata kita mencampurkan nilainya, menciptakan warna piksel tunggal. Ketika mesin terbang hanya mencakup sebagian dari pixel, maka hanya balok yang ditumpangkan pada mesin terbang dihidupkan, yang tiga kali lipat resolusi horizontal. Jika Anda memperbesar gambar layar menggunakan teknik seperti ClearType, Anda dapat melihat warna di sekitar tepi mesin terbang:
Menariknya, pendekatan yang akan saya bahas dalam artikel ini dapat diperluas ke rendering sub-pixel. Saya sudah mengimplementasikan prototipe-nya. Satu-satunya kelemahan adalah bahwa karena penambahan pemfilteran dalam teknik seperti ClearType, kita perlu mengambil lebih banyak sampel tekstur. Mungkin saya akan mempertimbangkan ini di masa depan.
Rendering mesin terbang menggunakan grid seragam
Misalkan kita mengambil sampel mesin terbang dengan resolusi 16 kali target dan menyimpannya dalam tekstur. Saya akan menjelaskan bagaimana ini dilakukan di bagian ketiga artikel. Pola pengambilan sampel adalah kisi yang seragam, yaitu, 16 titik pengambilan sampel didistribusikan secara merata di atas texel. Setiap mesin terbang diberikan dengan resolusi yang sama dengan resolusi target, kami menyimpan 16 bit per texel, dan masing-masing bit sesuai dengan sampel. Seperti yang akan kita lihat dalam proses penghitungan masker cakupan, urutan penyimpanan sampel adalah penting. Secara umum, titik pengambilan sampel dan posisi mereka untuk satu texel terlihat seperti ini:
Mendapatkan texels
Kami akan menggeser jendela piksel dengan bit cakupan yang tersimpan di texels. Kita perlu menjawab pertanyaan berikut: berapa banyak sampel yang akan masuk ke jendela piksel kita? Diilustrasikan oleh gambar berikut:
Di sini kita melihat empat texels, di mana mesin terbang sebagian overlay. Satu piksel (ditunjukkan dengan warna biru) mencakup bagian dari texels. Kita perlu menentukan berapa banyak sampel yang dilewati jendela piksel kita. Pertama kita perlu yang berikut ini:
- Hitung posisi relatif dari jendela piksel dibandingkan dengan 4 texels.
- Dapatkan texels yang berpotongan dengan jendela piksel kami.
Implementasi kami didasarkan pada OpenGL, jadi asal ruang tekstur dimulai dari kiri bawah. Mari kita mulai dengan menghitung posisi relatif dari jendela piksel. Koordinat UV yang diteruskan ke pixel shader adalah koordinat UV dari pusat piksel. Dengan asumsi bahwa UV dinormalisasi, pertama-tama kita dapat mengkonversi UV ke ruang texel dengan mengalikannya dengan ukuran tekstur. Mengurangkan 0,5 dari pusat piksel, kita mendapatkan sudut kiri bawah dari jendela piksel. Dengan membulatkan nilai ini ke bawah, kami menghitung posisi kiri bawah dari texel kiri bawah. Gambar menunjukkan contoh dari tiga titik ini dalam ruang texel:
Perbedaan antara sudut kiri bawah piksel dan sudut kiri bawah texel grid adalah posisi relatif dari jendela piksel dalam koordinat yang dinormalisasi. Pada gambar ini, posisi jendela piksel adalah [0,69, 0,37]. Dalam kode:
vec2 bottomLeftPixelPos = uv * size -0.5;
vec2 bottomLeftTexelPos = floor(bottomLeftPixelPos);
vec2 weigth = bottomLeftPixelPos - bottomLeftTexelPos;
Dengan menggunakan petunjuk textGather, kita bisa mendapatkan empat texels sekaligus. Ini hanya tersedia di OpenGL 4.0 dan lebih tinggi, sehingga Anda dapat menjalankan empat texelFetch sebagai gantinya. Jika kita baru saja melewati tekstur. Kumpulkan koordinat UV, maka dengan kecocokan sempurna dari jendela piksel dengan texel, masalah akan muncul:
Di sini kita melihat tiga texel horizontal dengan jendela pixel (diperlihatkan dengan warna biru) persis cocok dengan texel pusat. Berat yang dihitung mendekati 1.0, tetapi teksturTujuan memilih texels tengah dan kanan sebagai gantinya. Alasannya adalah bahwa perhitungan yang dilakukan oleh tekstur Mengumpulkan mungkin sedikit berbeda dari perhitungan berat floating point. Perbedaan dalam pembulatan perhitungan GPU dan perhitungan bobot titik mengambang menghasilkan gangguan di sekitar pusat piksel.
Untuk mengatasi masalah ini, Anda harus memastikan bahwa perhitungan berat dijamin agar sesuai dengan sampling teksturGather. Untuk melakukan ini, kami tidak akan pernah mencicipi pusat piksel, dan sebagai gantinya, kami akan selalu mengambil sampel di tengah kisi texel 2 × 2. Dari posisi bawah texel kiri yang dihitung dan sudah dibulatkan ke bawah, kami menambahkan texel penuh untuk sampai ke pusat kisi texel.
Gambar ini menunjukkan bahwa menggunakan pusat grid texel, empat titik pengambilan sampel yang diambil oleh teksturGather akan selalu berada di tengah texel. Dalam kode:
vec2 centerTexelPos = (bottomLeftTexelPos + vec2(1.0, 1.0)) / size;
uvec4 result = textureGather(fontSampler, centerTexelPos, 0);
Masker horizontal jendela piksel
Kami mendapat empat texels dan bersama-sama mereka membentuk grid 8 × 8 bit cakupan. Untuk menghitung bit dalam jendela piksel, pertama-tama kita perlu mengatur ulang bit di luar jendela piksel. Untuk melakukan ini, kita akan membuat window mask pixel dan melakukan bitwise AND antara mask pixel dan mask cakupan texel. Masker horisontal dan vertikal dilakukan secara terpisah.
Topeng pixel horisontal harus bergerak bersama dengan berat horisontal, seperti yang ditunjukkan dalam animasi ini:
Gambar menunjukkan topeng 8-bit dengan nilai 0x0F0 bergeser ke kanan (nol dimasukkan di sebelah kiri). Dalam animasi, topeng dianimasikan secara linear dengan berat, tetapi dalam kenyataannya, sedikit pergeseran adalah operasi selangkah demi selangkah. Topeng mengubah nilai ketika jendela piksel melintasi batas sampel. Dalam animasi berikutnya, ini ditampilkan dalam kolom merah dan hijau, animasi langkah demi langkah. Nilai hanya berubah ketika pusat sampel berpotongan:
Agar topeng hanya bergerak di tengah sel, tetapi tidak pada tepinya, pembulatan sederhana sudah cukup:
unsigned int pixelMask = 0x0F0 >> int(round(weight.x * 4.0));
Sekarang kita memiliki topeng pixel dari string 8-bit penuh yang mencakup dua texels. Jika kita memilih jenis penyimpanan yang tepat dalam cakupan 16-bit kami, ada cara untuk menggabungkan texel kiri dan kanan dan melakukan penyembunyian piksel horizontal untuk garis 8-bit penuh pada satu waktu. Namun, ini menjadi masalah dengan masking vertikal ketika kita pindah ke grid yang diputar. Karena itu, sebagai gantinya, kami menggabungkan satu sama lain dua texels kiri dan secara terpisah dua texels kanan untuk membuat dua topeng cakupan 32-bit. Kami menutupi hasil kiri dan kanan secara terpisah.
Topeng untuk texels kiri menggunakan 4 bit atas dari topeng pixel, dan masker untuk texels kanan menggunakan 4 bit yang lebih rendah. Dalam kisi yang seragam, setiap baris memiliki mask horizontal yang sama, jadi kita bisa menyalin mask untuk setiap baris, setelah itu mask horizontal akan siap:
unsigned int leftRowMask = pixelMask >> 4;
unsigned int rightRowMask = pixelMask & 0xF;
unsigned int leftMask = (leftRowMask << 12) | (leftRowMask << 8) | (leftRowMask << 4) | leftRowMask;
unsigned int rightMask = (rightRowMask << 12) | (rightRowMask << 8) | (rightRowMask << 4) | rightRowMask;
Untuk menutupi, kami menggabungkan dua texels kiri dan dua texels kanan, dan kemudian menutupi garis horizontal:
unsigned int left = ((topLeft & leftMask) << 16) | (bottomLeft & leftMask);
unsigned int right = ((topRight & rightMask) << 16) | (bottomRight & rightMask);
Sekarang hasilnya mungkin terlihat seperti ini:
Kita sudah dapat menghitung bit dari hasil ini menggunakan instruksi bitCount. Kita harus membaginya bukan dengan 16, tetapi oleh 32, karena setelah masking vertikal kita masih bisa memiliki 32 bit potensial, dan bukan 16. Berikut adalah render lengkap mesin terbang pada tahap ini:
Di sini kita melihat huruf S yang diperbesar yang diberikan berdasarkan data vektor asli (garis putih) dan visualisasi titik pengambilan sampel. Jika titik berwarna hijau, maka di dalam mesin terbang, jika merah, maka tidak. Grayscale menampilkan rona yang dihitung pada tahap ini. Dalam proses rendering font, ada banyak kemungkinan kesalahan, mulai dari rasterisasi, metode menyimpan data dalam atlas tekstur, dan untuk menghitung rona akhir. Visualisasi semacam itu sangat berguna untuk memvalidasi perhitungan. Mereka sangat penting untuk men-debug artefak di tingkat sub-pixel.
Penutup vertikal
Sekarang kita siap untuk menutupi bit vertikal. Untuk menutupi secara vertikal, kami menggunakan metode yang sedikit berbeda. Untuk menghadapi pergeseran vertikal, penting untuk mengingat bagaimana kami menyimpan bit: dalam urutan yang bijaksana. Intinya adalah empat bit paling signifikan, dan garis atas adalah empat bit paling signifikan. Kita cukup membersihkan satu per satu, menggesernya berdasarkan posisi vertikal dari jendela piksel.
Kami akan membuat topeng tunggal yang menutupi seluruh ketinggian dua texels. Sebagai hasilnya, kami ingin menyimpan empat baris
penuh texels dan menutupi semua yang lain, yaitu, mask akan menjadi 4 × 4 bit, yang sama dengan 0xFFFF. Berdasarkan posisi jendela piksel, kami menggeser garis bawah dan menghapus garis atas.
int shiftDown = int(round(weightY * 4.0)) * 4;
left = (left >> shiftDown) & 0xFFFF;
right = (right >> shiftDown) & 0xFFFF;
Akibatnya, kami juga menutupi bit vertikal di luar jendela piksel:
Sekarang cukup bagi kita untuk menghitung bit yang tersisa di texels, yang dapat dilakukan dengan operasi bitCount, kemudian bagi hasilnya dengan 16 dan dapatkan warna yang diinginkan!
float shade = (bitCount(left) + bitCount(right)) / 16.0;
Sekarang render penuh dari surat itu terlihat seperti ini:
Dilanjutkan ...
Pada bagian kedua, kita akan mengambil langkah berikutnya dan melihat bagaimana Anda dapat menerapkan teknik ini ke grid yang dirotasi. Kami akan menghitung skema ini:
Dan kita akan melihat bahwa hampir semua ini dapat dikurangi menjadi beberapa tabel.
Terima kasih kepada Sebastian Aaltonen (
@SebAaltonen ) atas bantuannya dalam menyelesaikan masalah teksturKumpulkan dan, tentu saja, kepada Michael van der Leu (
@MvdleeuwGG ) untuk ide-idenya dan percakapan yang menarik di malam hari.