1 puan yazan GN⁺ 2024-11-11 | 1 yorum | WhatsApp'ta paylaş
  • 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 uvec4 dizisine 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_t ve 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, \0 fill karakterinde discard, 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
  • imgui ve stb_truetype da 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 xy konumu 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ğerinden 0x20 çıkarılmış değeri indeks olarak kullanır
    • 0x20 SPACE ile 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 uint dizisi olarak düzenlenip uvec4 birimleri halinde gruplanabilir
  • Raw char değerleri doğrudan uint olarak 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, vec3 ve uint değerlerini tek bir binding içinde temiz biçimde karıştırmak zordur
  • Metin, 4 karakterlik birimler halinde uint32_t iç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 \0 karakteriyle doldurulur
  • per-instance verisi word_data struct'ı ile temsil edilir
    • pos_and_scale[3]: xy konumu ve scale
    • word: çıktılanacak dört karakter
  • Uygulama, mesajı 4 karakterlik parçalara böler; her parçayı uint32_t değerine dönüştürür ve konum offset'iyle birlikte word_data dizisinde 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 edilecek word değerini ve texture koordinatına karşılık gelen değeri çıktılar
  • gl_Position, per-instance pos_and_scale verisini kullanarak üçgen vertex'lerini NDC koordinat sisteminde ekrana yerleştirir
  • Render edilecek word, giriş attribute'u olan uint değerinden fragment shader'a olduğu gibi geçirilir
    • Interpolation uygulanmaması için flat qualifier kullanılır
  • Texture koordinatı gl_VertexIndex ile sentezlenir
    • 12 >> gl_VertexIndex & 1, 0, 0, 1, 1 dizisini oluşturur
    • 9 >> gl_VertexIndex & 1, 1, 0, 0, 1 dizisini 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) ile vec2(1.f,1.f) arasındadır; glif piksel koordinatı ise uvec2(0,0) ile uvec2(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ğerine floor uygulanarak word piksel koordinatına kuantize edilir
    • Koordinat aralığı uvec2(0..31, 0..15) ile sınırlandırılır
    • word_pixel_coord.x / 8 ile dört karakterden hangi karakter alanında olunduğu bulunur
    • word_pixel_coord.x % 8 ile glif içindeki x koordinatı elde edilir
  • Karakter kodu font_data için indekse dönüştürülür
    • İlk glif 0x20 SPACE olduğundan offset olarak printable_character - 0x20 kullanılır
    • Bu offset ile uvec4 glif bitmap'i alınır
  • y koordinatı, glyph_pixel_coord.y / 4 üzerinden uvec4 içindeki belirli bir uint değ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 uint yapı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
  • x koordinatı biti seçmek için 7-glyph_pixel_coord.x kullanı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_pixel değeriyle mix(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ı \0 karakteriyle doldurur
  • Fragment shader, çıktılanacak karakterin \0 olup olmadığını kontrol eder
  • \0 fill karakteriyle karşılaşıldığında arka plan da çizilmez ve discard ç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

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

    • Anlık mod metin render etmeyi denediğim her seferde, küçük boyutlarda metni iyi gösterebildiğimi söyleyemem. İlk ShaderToy'da da vec2(30, -30) içindeki 30'u 300 yapınca artefaktlar görünüyor
      Bunu 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
    • Unity oyun geliştirme yapıyorum ve bu taraf gerçekten tersine gidiyormuş gibi hissettiriyor. Birkaç yıl önce tek bir kişinin yaptığı en iyi font render etme aracını Unity satın alıp varsayılan özellik olarak ekledi, sonrasında geliştirme neredeyse durdu ve rakip font render etme pazarı da öldü
      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
    • Konu dışı ama ilginç bir örnek olarak, HTML/CSS/JS ile yapılmış bir Matrix efekti 1024 bayta sığıyor: https://codegolf.stackexchange.com/a/17414
  • 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

    • “Başa dönmek” sözüne uygun olarak, hâlâ görmediyseniz 1968 tarihli On the Design of Display Processors makalesine bakmaya değer: http://cva.stanford.edu/classes/cs99s/papers/myer-sutherland...
      O makale donanımdan bahsediyor ama yazılımın da samsarası var
    • SDF metin ya da MSDF gibi yöntemler artık en az bir nesil geride kaldı. Günümüzde neredeyse herkes https://sluglibrary.com gibi yaklaşımlar kullanıyor; yani shader içinde font Bezier eğrileri doğrudan rasterize ediliyor
      Bir zamanlar bu fikrin çok temel bir sürümünü yapmıştım: https://www.shadertoy.com/view/sdXBDs
    • Örneğin doku yüklemenin düzgün çalışmadığı durumlarda debug metni için oldukça zekice. Yine de asıl metnin sprite sheet'i 16. yüzyıl manuel dizgiyle kıyaslaması sevimli olsa da, gerçekte bir baskı yardımcısının metal harfleri dizmesi bir saat sürüyordu; GPU'ya sprite sheet yüklemekse 10 ms bile almıyor ve sonrasında sonsuz kez ayarlanabiliyor
      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/...

    • Bunun da ötesinde, GPU'da font eğrilerini doğrudan değerlendiren yöntemler de mümkün ve ölçek ya da perspektiften bağımsız olarak yüksek kalite sağlayabiliyor. Verimli şekilde uygulaması çok zor ama yapılabiliyor
      Ö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

    • “Performans nasıl?” sorusuna benim duymak istediğim cevap, “VSCode artık VRAM'imden yüzlerce MB ile birkaç GB arasında yer yemiyor” olurdu
  • 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 snprintf ile CPU’ya eşlenmiş GPU tamponuna doğrudan yazdırabiliyordum. Bunun riskli bir yöntem olduğunun farkındayım
    Tek tek karakterleri vertex shader ile çizmek yerine yalnızca tek bir tam ekran üçgeni çizip UV koordinatları yerine gl_FragCoord kullandı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...

    • En sevdiğim piksel yazı tipi paketi şu: https://int10h.org/oldschool-pc-fonts/
    • “NES” yazı tipinin aslında 1976 tarihli arcade oyunu Quiz Show’dan geldiğini ancak yakın zamanda öğrendim. Bu yazı tipi diğer siyah-beyaz Kee/Atari oyunlarında da kullanılmış ve yazı tipi verisi quizshow MAME ROM setinin içinde, nedense nibble düzeyinde parçalanmış halde bulunuyor
      İ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

    • Bu anlayış epey eski. Son yaklaşık 15 yılın GPU’larında tekstür erişimi bit işlemlerinden kabaca 100 kat daha yavaş
  • 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

    • Maliyet yüksek değil. Bu teknik performanstan çok, debug metnini ekrana mümkün olduğunca kolay basmaya yakın. Shader’a biraz veri ekliyorsunuz ve anında metin çıkıyor
      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ünümüzde bile GPU belleğini aşırı israf etmemek genelde iyi bir fikirdir. Büyük metin kutusu tekstürleri PCI veri yolu üzerinden aktarılmak zorundadır ve yükleme zamanlamasına ya da GPU kaynağının bellekten atılıp atılmadığına bağlı olarak takılmalara yol açabilir
      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
    • N karakteri kapsayan tek bir dikdörtgen çizip glifi shader içinde seçmek, her karakter için ayrı bir dikdörtgen çizmekten daha hızlı olurdu. En azından eş genişlikli yazı tiplerinde bu böyledir. Yine de ekranı doldurabilecek karakter sayısının bir sınırı olduğundan gerçek fark muhtemelen büyük olmaz
      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
    • Yüklemeden önce tüm dizgiyi render edebilirsiniz ama o zaman özünde CPU render etme kullanmış olursunuz; bu da aynı işi GPU’nun yapmasından daha yavaş olur
      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
    • Uygulamaya bağlı. Özellikle sağdan sola yazılan karakterler, CJK ve emoji ile karşılaşabiliyorsanız bu en kolay yöntemdir. Metinlerin çoğu her karede değişmez, bu yüzden tekstürü önbelleğe almak değerlidir ve bizim için yeterince iyidir
  • 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üş

    • Hayır, bitmap'i dokuya kaydetmeyeceğini söylemişti. Bunu doğrudan shader koduna gömmek farklı bir şey
      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