Doku Kullanmadan Metin Render Etmek
(poniesandlight.co.uk)- Render pass içinde hızlıca debug mesajları basmanız gereken durumlarda font atlası hazırlamak zahmetli olduğundan, yalnızca fragment shader sabitleri ile metin çizme yöntemi kullanışlıdır
- Glifler 8x16 piksel bitmap olarak temsil edilir; printable ASCII kapsamındaki 96 karakter, shader içinde doğrudan sorgulanmak üzere 1536 baytlık bir
uvec4dizisine yerleştirilir - Font verisi, PSF1 terminal fontu içinden 4 baytlık header ve 512 baytlık yazdırılmayan glif bölümünü atladıktan sonra sonraki 96 glifi çıkararak oluşturulabilir
- Render işlemi, 4 karakterlik birimlerde
uint32_tve konum/ölçek içeren per-instance yapı kullanarak tüm metni tek bir instanced draw call ile çizer - Basit debug çıktısına odaklanan bir teknik olduğu için 4 karakterlik padding,
\0fill karakterindediscard, endianness düzeltmesi gibi kısıtların uygulama ve shader tarafında birlikte ele alınması gerekir
Font atlası yerine shader sabitleri kullanmak
- Klasik metin render etme yaklaşımı, olası glifleri bir font atlasına render etmek, bunu texture olarak bind etmek ve ardından her glife uygun üçgenleri çizmektir
imguivestb_truetypeda bu yöntemi kullanır; ancak hızlı debug mesajı çıktısı için hazırlık süreci zahmetli olabilir- Alternatif, font atlasına karşılık gelen veriyi fragment shader içindeki integer sabitler olarak saklamaktır
- Integer değerler bitmap gibi kullanılabildiğinden, fragment'ın
xykonumu belirli bir bit konumuna eşlenir; ilgili bit açıksa ön plan rengi, kapalıysa arka plan rengi çıktılanır
8x16 bitmap glifler ve ASCII tablosu
- Bir bayt yalnızca bir piksel satırını temsil ettiğinden, daha okunabilir glifler oluşturmak için glif başına 16 bayt kullanılır
- Bir glif 8x16 piksellik bir canvas olur ve GLSL'deki tek bir
uvec4, gereken 16 baytı tam olarak tutabilir - Printable ASCII kapsamındaki 96 glif saklandığında toplam veri 1536 bayt olur
font_data[96]dizisi, ASCII değerinden0x20çıkarılmış değeri indeks olarak kullanır0x20SPACEile başlayan printable ASCII glifleri hedeflenir- Örnek kodda yerden tasarruf için yalnızca bazı girdiler gösterilir
- Bitmap tablosunun tamamı Island kaynak kodunda yer alır
PSF1 fontundan bitmap çıkarma
- Gerekli bitmap kodlaması, PSF1 formatındaki terminal fontlarıyla neredeyse aynıdır
- PSF1 terminal fontundan veri çıkarma adımları basittir
- Font dosyasını ImHex gibi bir hex editor ile açın
- 4 baytlık header'ı atlayın
- 512 baytlık yazdırılmayan glif bölümünü atlayın
- Sonraki 96 glifi, yani 1536 baytı, “Copy as → C Array” ile dışa aktarın
- Çıkarılan char dizisi
uintdizisi olarak düzenlenipuvec4birimleri halinde gruplanabilir - Raw char değerleri doğrudan
uintolarak peş peşe eklendiğinde endianness tersine döndüğü için, örnekleme sırasında bunun tekrar düzeltilmesi gerekir - Kullanılan piksel fontun özgün verisi, Scott Fial'ın ücretsiz piksel fontu Tamsyn üzerinden alınmıştır
Tek bir instanced draw call yapısı
- Metin render etme işlemi tek bir instanced draw call ile gerçekleştirilir
- Draw call iki attribute stream kullanır
- per-draw stream yalnızca sıradan bir quad çizmek için gereken bilgileri içerir
- per-instance stream ekrandaki konum offset'ini ve çıktılanacak metni içerir
- Konum offset'i için x, y float değerleri kullanılır; kalan float alanına font scale değeri konabilir
- Vulkan'da vertex output binding'in tüm component'leri aynı interpolation özelliğine sahip olmak zorunda olduğundan,
vec3veuintdeğerlerini tek bir binding içinde temiz biçimde karıştırmak zordur - Metin, 4 karakterlik birimler halinde
uint32_tiçine pack edilir- Temel vertex attribute veri tipinin en küçük birimi genellikle 32 bit olduğundan 4 karakter bir kerede tutulur
- Mesaj uzunluğu 4'e tam bölünebilmelidir
- Eksik kısımlar
\0karakteriyle doldurulur
- per-instance verisi
word_datastruct'ı ile temsil edilirpos_and_scale[3]:xykonumu ve scaleword: çıktılanacak dört karakter
- Uygulama, mesajı 4 karakterlik parçalara böler; her parçayı
uint32_tdeğerine dönüştürür ve konum offset'iyle birlikteword_datadizisinde biriktirir - Render sırasında bu dizi debug text drawing pipeline'ın per-instance binding'i olarak bind edilir ve quad sayısı kadar instance çizilir
Vertex Shader'da konum ve karakter aktarma
- Vertex shader,
gl_Position, render edilecekworddeğerini ve texture koordinatına karşılık gelen değeri çıktılar gl_Position, per-instancepos_and_scaleverisini kullanarak üçgen vertex'lerini NDC koordinat sisteminde ekrana yerleştirir- Render edilecek word, giriş attribute'u olan
uintdeğerinden fragment shader'a olduğu gibi geçirilir- Interpolation uygulanmaması için
flatqualifier kullanılır
- Interpolation uygulanmaması için
- Texture koordinatı
gl_VertexIndexile sentezlenir12 >> gl_VertexIndex & 1,0, 0, 1, 1dizisini oluşturur9 >> gl_VertexIndex & 1,1, 0, 0, 1dizisini oluşturur- Bu kombinasyonla
(0,1), (0,0), (1,0), (1,1)uv koordinatları branch kullanmadan üretilir
- Vertex shader, ön plan ve arka plan renklerini de per-instance verisinden alıp fragment shader'a iletir
Fragment Shader'da glif örnekleme
- Fragment shader'ın metni render edebilmesi için üç bilgiye ihtiyacı vardır
- Interpolation uygulanmış fragment uv koordinatı
- Çıktılanacak karakter verisi
in_word - Glif bitmap dizisi
font_data
- uv koordinatı normalize edilmiş float aralığında
vec2(0.f,0.f)ilevec2(1.f,1.f)arasındadır; glif piksel koordinatı iseuvec2(0,0)ileuvec2(7,15)arasındadır - 4 karakterlik word'ün tamamı, yatayda 32 piksel ve dikeyde 16 piksel olan bir alan olarak ele alınır
uv.xy * vec2(8 * WORD_LEN, 16)değerineflooruygulanarak word piksel koordinatına kuantize edilir- Koordinat aralığı
uvec2(0..31, 0..15)ile sınırlandırılır word_pixel_coord.x / 8ile dört karakterden hangi karakter alanında olunduğu bulunurword_pixel_coord.x % 8ile glif içindeki x koordinatı elde edilir
- Karakter kodu
font_dataiçin indekse dönüştürülür- İlk glif
0x20SPACEolduğundan offset olarakprintable_character - 0x20kullanılır - Bu offset ile
uvec4glif bitmap'i alınır
- İlk glif
- y koordinatı,
glyph_pixel_coord.y / 4üzerindenuvec4içindeki belirli biruintdeğerini seçer- Bu
uint, 4 satırlık piksel verisi içerir - ImHex'ten çıkarılan char değerleri doğrudan peş peşe eklenip
uintyapıldığı için satır sırası tersine dönmüştür (8*(3-(glyph_pixel_coord.y)%4))yöntemiyle sondan başlayarak indekslenip düzeltilir
- Bu
- x koordinatı biti seçmek için
7-glyph_pixel_coord.xkullanılır- Baytın en yüksek anlamlı biti en yüksek indekste saklandığından, soldan sağa eşlemek için ters yönde indeksleme gerekir
- Son
current_pixeldeğeriylemix(background_colour, foreground_colour, current_pixel)uygulanarak renk belirlenir
Kısa string'ler ve fill karakteri işleme
- String uzunluğu 4'e tam bölünmüyorsa uygulama eksik kısımları
\0karakteriyle doldurur - Fragment shader, çıktılanacak karakterin
\0olup olmadığını kontrol eder \0fill karakteriyle karşılaşıldığında arka plan da çizilmez vediscardçalıştırılır- Bu işlem, 4 karakterlik packing kısıtını korurken kısa string'leri de çıktılama olanağı sağlar
Kullanım şekli ve kod konumu
- Island projesinde
le::DebugPrintçağrılarak ekrana debug metni çıktılanabilir - Fragment shader'ın tam kodu github üzerinde görülebilir
- Kod örneği, string verisini geçirerek ekrana
"That's all, %s"biçiminde bir mesaj çıktılar
1 yorum
Hacker News yorumları
Kendiniz denemek istiyorsanız, aritmetiği adım adım takip etmeniz yeterli. Bunu ShaderToy'da sıfırdan uygulamak inanılmaz derecede kolay ve böyle şeyleri seviyorsanız cumartesi sabahı oyuncağı olarak da eğlenceli
Sıfırdan yapmak eğlenceli ama başlamak için ipucu gerekiyorsa az önce yaptığım bir örnek var: https://www.shadertoy.com/view/Mc3cW2
Başkalarının yaptığı pek çok zekice metin hilesi de var; 300 karakterden kısa bir Matrix örneği https://www.shadertoy.com/view/llXSzj ya da yeşil CRT ekran efekti https://www.shadertoy.com/view/XtfSD8 gibi
vec2(30, -30)içindeki 30'u 300 yapınca artefaktlar görünüyorBunu düzgün ele almanın bir püf noktası olup olmadığını merak ediyorum. Benim durumumda fragman shader içinde dokuyu çoklu örneklemek en iyi sonucu verdi ama yine de güncel teknikler kadar iyi değildi
Kısa süre önce yerel konsol fontu gibi görünen bir uygulama yapmayı denedim ve yalnızca %90 kadar benzetmek için bile 2 saatten fazla uğraşmam gerekti
Dahice ve hack ruhunu taşıdığı için keyifli. Aslında neredeyse tüm 3D render etme teknikleri böyle ama ortaya çıkan şey eski elektronik ilan panolarını yeniden üretmeye çalışmıyorsa özellikle güzel görünmüyor
Daha fazla bit ekleyerek iyileştirilebilir ama çok daha iyi görünmeden önce, bütün bitleri ayarlamanın daha kolay bir yolunu bulmanız muhtemel. Sonuçta siyah-beyaz pikselleri bir çizim programında hazırlayıp doku olarak kaydetmekten daha verimli bir çözüm pek olmayacağı için yine başladığınız yere dönüyorsunuz
Modern 3D render motorlarının metni çizmek için kullandığı daha genel yöntemi merak ediyorsanız SDF metin ve MSDF gibi ilgili tekniklere bakabilirsiniz. Bunlar, ön işleme aşamasında geleneksel bir doku atlası kullanarak signed distance field atlası oluşturur
O makale donanımdan bahsediyor ama yazılımın da samsarası var
Bir zamanlar bu fikrin çok temel bir sürümünü yapmıştım: https://www.shadertoy.com/view/sdXBDs
Bunun havalı bir numara olmadığını söylemiyorum; gerçekten havalı bir numara
Metni mesh olarak render etme seçeneği de var. TextMeshPro bunu bir adım ileri taşıyıp signed distance field kullanarak keyfi ölçeklemeyi destekliyor
https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...
Örnek: https://sluglibrary.com
Mesh ve SDF tarafında GPU kısmı çok daha basit ama fazla büyütüldüğünde hassasiyet kaybı olabilir, mesh çok küçültüldüğünde ise aliasing oluşabilir
Çok havalı. “Geleneksel” doku yöntemiyle bir performans karşılaştırması olsa ilginç olurdu
Bugünün GPU'larında bu kadar basit işleri yaparken “performans nasıl?” sorusunun cevabı çoğunlukla “çalışıyor” gibi görünüyor
Sebastian Lague, çeşitli font render etme tekniklerini ele alan güzel bir video yapmış
https://youtu.be/SO83KQuuZvg
Benzer bir tekniği denemiştim; tüm yazı tipi verisini fragment shader kaynak kodunun içine koymuştum. Böylece
snprintfile CPU’ya eşlenmiş GPU tamponuna doğrudan yazdırabiliyordum. Bunun riskli bir yöntem olduğunun farkındayımTek tek karakterleri vertex shader ile çizmek yerine yalnızca tek bir tam ekran üçgeni çizip UV koordinatları yerine
gl_FragCoordkullandım. En verimli yöntem değildi ama bir debug özelliğiydi ve pratikte yeterince hızlıydıDosya adına rağmen NES değil, IBM PC ROM yazı tipini kullanıyor. “NES font” ve diğer 8x8 piksel yazı tipleri web’de bulunabilir
https://github.com/rikusalminen/triangles/blob/nesfont/shade...
İlginç bir şekilde bu oyun, soru ve cevap verilerini 8 izli banda kaydediyordu
Harika. Bizzat denemediğim bir metin render etme algoritmasına sık rastlanmıyor. Startup’ımda çeşitli yöntemler uyguladık ama benim çözünürlükten bağımsızlık ve anti-aliasing ihtiyacım vardı, bu yüzden bu yaklaşım bize yardımcı olmazdı
Bunun tüm Bezier eğrili yazı tipi dosyalarına genellenmesi mümkün olmayabilir. Eğrileri piksele dönüştürmek, özellikle glif kendi kendisiyle kesiştiğinde zor olabilir. Genel olarak standart metin render etme işi çözülmüş gibi hissettiriyor, standart dışı kullanım senaryolarını denemek ise oldukça acımasız
Bu yaklaşım kavramsal olarak hoşuma giden Will Dobbie yöntemine benziyor gibi görünüyor. Sadece çok daha basit. İkisi de ham yazı tipi verisini alıp doğrudan shader içinde kullanıyor. Fark şu ki bu yöntem piksel verisini bir dizide saklarken, Will SVG yol verisini “vektör tekstür” olarak saklıyor
Merak edenler için Will’in harika bir demosu var: https://wdobbie.com/warandpeace/
Bir zamanlar ben de böyle bir şey yapmayı düşünmüştüm ama GPU’ların özellikle tekstür render etmede verimli, bit işlemleri konusunda ise görece yavaş olduğunu sanıyordum. Burada biraz bellek tasarrufu sağlansa bile, gerçekten atlas kullanmaktan daha hızlı olup olmadığını merak ediyorum
Bit paketlemeyi normal bir tekstüre yapıp fragment shader’ın bunu çözmesini sağlamak, iki tarafın da avantajını verebilir gibi geliyor
Modern bilgisayar grafiğinden neredeyse hiç anlamadığım için samimi bir soru soruyorum: küçük bir tekstürü GPU’ya yüklemenin maliyeti gerçekten bu kadar yüksek mi? Tüm dizgiyi 2D bir tekstüre render edip sonra o tekstürü iki üçgende göstermek neden olmasın diye merak ediyorum
Buna karşılık bir yazı tipi atlası oluşturacak kod yazmanız ya da mevcut bir atlas bulup yüklemeniz gerekir; bunun için de yükleme kodu lazımdır. Ya da tüm mesajı bir tekstüre çizmeniz ve mesaj değişene kadar sonucu önbelleğe almanız gerekir
Buna kaynak yönetimi ve binding de eklenir, ama bu yaklaşım hiçbir kaynağa ihtiyaç duymaz. Yani bu genel amaçlı bir metin çözümü değil, ekrana debug metni basma tekniği
Bu arada çoğu tarayıcı ve işletim sistemi metni tekstüre çizerek çalışır. Yazı tiplerini dinamik olarak tekstür atlasına çizer, sonra o atlasın gliflerini kullanarak uygulama penceresinin bazı bölümleri için ek tekstürler üretirler. Tarayıcıda tekstür sınırlarını gösterirseniz tüm tekstürleri görebilirsiniz; Rendering->Layer borders her tekstürü camgöbeği bir çerçeveyle işaretler
Görece yavaş bir CPU çok sayıda bağımsız tekstür metin kutusu render etmek zorunda kalırsa, bu hızla birikerek bütçeyi kemirebilir
Glif atlası kullanarak çizmek hâlâ kaynak kullanımı açısından çok daha iyidir. Modern metin render etme ardışık düzenleri, yakınlaştırma ve uzaklaştırmada glif okunabilirliğini artırmak için sık sık SDF veya kodlanmış Bezier eğrileri kullanır; bu da belleği daha verimli kullanmanın iyi bir yoludur
Yükleme açısından bakarsak, sonuçta X bayt glif veriniz vardır ve bunun bir şekilde GPU belleğine gitmesi gerekir. Tekstür, uniform veri ya da shader sabiti olması performans açısından büyük fark yaratmaz. Hatta orijinal yazıdaki gibi shader sabiti olarak koyarsanız, tüm sabit tanımlarını shader derleyicisinin işlemesi gerektiği için CPU tarafında daha maliyetli bile olabilir
GPU tarafında asıl önemli olan, glif verisini okurken hangi bellek katmanına dokunduğunuzdur. Tekstür fetch işlemleri çoğu GPU’da özel bir L1 önbelleği kullanır; bu da muhtemelen genel L1 önbellekten daha büyüktür. Verinin sıralaması da önemlidir. Tekstürler genelde, piksel blokları shade edilirken önbellek ıskalarını önlemek için Morton sıralamasının bir varyasyonuyla depolanır. Gerçek kullanımda atlas tabanlı bir metin render edici için tekstür kullanmak daha iyi olurdu
Düzeltme: Soruyu yanlış okumuşum. Eğer karşılaştırma, GPU’da ayrı glifler çizmek ile CPU’da tüm metin bloğunu çizmek arasındaysa, bu hız ve alan arasında bir ödünleşimdir. Metin için ne kadar bellek harcamak istediğinize, metnin değişip değişmediğine, karakter başına efekt gerekip gerekmediğine vb. göre cevap değişir
Ayrıca “tekstürsüz” deniyor ama bu yöntem de aslında bir tekstür. Sadece tekstür farklı bir biçimde ve farklı bir yerde saklanıyor. Gerçek anlamda tekstürsüz yazı tipi render etme, vektör eğrilerini anlık olarak değerlendirmektir
Shader'a bitmap kaydetmeyeceğini söyleyip tam olarak bitmap'i shader'a kaydetme yöntemini anlattığı için oldukça kafa karıştırıcı
Özetle, bitmap yazı tipini shader'a gömmüş
Bunu, çalışma zamanında okunması gereken ayrı bir dosyada veri saklamakla kaynak kodun içine veriyi doğrudan dahil etmek arasındaki farka benzetebilirsiniz