sdl-painter – Metin Çizimi (faz-4)

Sevgili yazılımperver dostlarım, tekrar merhaba.

Önceki yazımda, resimleri dosyadan okuyup GPU’ya aktardık ve ekrana çizdik. Bu yazıda ise sıra metinlerde. Oyun ekranındaki puan, bir düğmenin üzerindeki yazı ya da uygulamanın durum bilgisi derken, metin çizimi de kısa sürede ihtiyaç duyduğumuz, bununla birlikte OpenGL ile geliştirmesi nispeten meşakkatli kabiliyetlerden birisi oluyor. uEngine4’te (https://github.com/yazilimperver/uEngine4/blob/main/code/gl/gl_painter/gl_font.cpp), freetype kullanıyorduk.

Aslında resim çizimi için hazırladığımız altyapı burada da işimize yarıyor olacak. Fonttan elde ettiğimiz karakter görüntülerini bir dokuya yerleştirip ekrana çiziyoruz. İşin farklı tarafı, bu görüntülerin yan yana nasıl dizileceği ve satırların nasıl hizalanacağı. Gelin önce font yükleme tarafından başlayalım.

SDL_ttf ve Font Sınıfı

Font dosyasını okumak ve karakterleri piksel görüntüsüne dönüştürmek için SDL_ttf kullanıyoruz. Çizimi ise yine kendi IRenderer altyapımız üzerinden yapıyoruz.

Kütüphanede bu işi Font sınıfıyla yapıyor olacağız. Bir font yüklemek için dosya yolunu ve punto boyutunu vermeniz yeterli:

Buradaki dosya yolunu örnek olarak ekledim; uygulamalarınızda, herhangi bir fontu kullanabilirsiniz. Farklı punto boyutları için de ayrı Font nesneleri oluşturuyoruz.

İçeride TTF_OpenFont ile açılan kaynak, Font yok edilirken kapatılıyor. SDL_ttf’nin başlatılması ve kapatılması da referans sayacıyla yönetiliyor. Dolayısıyla birden fazla font kullandığımızda her biri için ayrı bir başlatma işlemi yapmamıza gerek kalmıyor.

Font hazır olduğunda SetFont ile bunu seçmeniz gerekiyor. Geçerli bir Painter ve başarıyla yüklenmiş bir font için ilk çizim örneği:

Metnin rengini Pen belirliyor. Şekillerdeki dolgu rengi için kullandığımız Brush‘ı ise burada kullanmıyoruz.

Resimlerde olduğu gibi burada da kaynakların yaşam süresine dikkat etmemiz gerekiyor. Fontun oluşturduğu GPU dokuları, onları yükleyen Painter kapanmadan bırakılmalı. İşimiz tamamen bittiğinde, son End() çağrısından sonra painter.SetFont(nullptr) ve ardından font.reset() ile iki referansı da bırakabiliriz; varsa diğer font referanslarını da aynı süre içinde bırakmalıyız. Güncel uygulamada bir Font ilk kullanıldığı renderer’a bağlandığı için, birden fazla Painter kullanıyorsanız her biri için ayrı font oluşturmanız gerekiyor.

Karakter Görüntüleri ve Glyph Atlası

Font içindeki bir harfin veya sembolün çizimde kullanılan biçimine glyph diyoruz (metin görselleştirmede glyp bilindik bir kavram detaylar için https://en.wikipedia.org/wiki/Glyph). Font::GetGlyph, istenen karakter daha önce hazırlanmışsa onu önbellekten dönüyor, ilk kez çağrılıyorsa SDL_ttf ile görüntüsünü oluşturup atlasa ekliyor.

Buradaki atlas, birden fazla karakter görüntüsünü aynı doku üzerinde topladığımız alan. Önceki yazıda bahsettiğimiz, bir dokunun belirli bölgesini UV koordinatlarıyla çizme yaklaşımının aynısını burada da uyguluyoruz. Glyph yapısındaki ilgili alanlar ise şöyle:

width ve height karakterin boyutlarını, UV’ler ise atlas içindeki yerini veriyor. advance, bu karakterden sonra yatayda ne kadar ilerleyeceğimizi belirtiyor. Bu değer görüntü genişliğiyle aynı olmak zorunda değil; harfler arasındaki yerleşimde bu kullanılıyor. bearing_x ve bearing_y de karakteri hedef çizim konumuna yerleştirirken kullanılan ofsetler.

Peki neden her karakter için ayrı bir doku oluşturmuyoruz? Çünkü her doku değişimi, çizimleri bir araya getiren RenderBatcher için yeni bir çizim çağrısı anlamına gelebiliyor. Ortak atlas sayesinde aynı sayfadaki karakterleri, çizim durumu ve batch kapasitesi elverdiğinde tek çağrıda çizebiliyoruz.

Güncel atlas, 512 × 512 piksellik RGBA sayfalardan oluşuyor. Karakterler soldan sağa yerleştiriliyor; satır dolunca bir alt satıra, sayfa dolunca yeni sayfaya geçiliyor. Filtreleme sırasında komşu karakterlerin birbirine karışmaması için aralarında bir piksellik pay bırakıyoruz. Yerleştirdiğimiz karakterlerin konumunu sonradan değiştirmiyoruz; böylece çizim kuyruğundaki UV koordinatları geçerliliğini koruyor.

Karakter görüntüsü beyaz hazırlanıyor, asıl renk çizim sırasında Pen renginden geliyor. Bu sayede aynı fontu farklı renklerde kullanmak için görüntüleri yeniden üretmiyoruz. Renk köşe verisinde taşındığı için, yalnızca metin renginin değişmesi de batch’i bölmüyor. Aşağıdaki figürde bu hususa yönelik anlattıklarımı bulabilirsiniz.

Karakter görüntüsü atlasta bir kez hazırlanır; aynı UV bölgesi farklı kalem renkleriyle yeniden kullanılır.

DrawText ve Taban Çizgisi

DrawText(x, y, text) API’sinde dikkat etmeniz gereken bir ayrıntı var: y, metnin üst kenarını değil, baseline dediğimiz taban çizgisini belirtiyor. Harflerin yerleşimini bu çizgiye göre yapıyoruz; örneğin “g” ve “p” gibi harflerin bir bölümü çizginin altına uzanıyor.

Metni üstten belirli bir konuma yerleştirmek istediğimizde fontun Ascent() değerini ekleyebiliriz:

İçeride her karakter için atlasın ilgili bölgesini taşıyan iki üçgen üretiyoruz. Karakteri çizdikten sonra advance kadar sağa ilerliyoruz. Satır sonu (\n) varsa bir sonraki satıra geçiliyor; satırlar arasındaki mesafeyi de Font::LineHeight() belirliyor. Punto boyutuyla satır yüksekliği aynı ölçü değil, bu nedenle satır aralığını doğrudan PointSize() üzerinden hesaplamıyoruz. Aşağıdaki resimde, ilgili karakterlerin çiziminde kullandığımız değişken ve API’lerin kullanımını görebilirsiniz.

y taban çizgisini belirtir; üst kenardan yerleştirmek için Ascent() eklenir. advance ise sonraki karakterin başlangıcına kadar olan yatay mesafedir.

Türkçe Karakterler ve UTF-8

Kütüphanenin bir diğer önemli özelliği de UTF-8 desteği. Malum, eğer Türkçe karakterleri görselleştireceksek, metinlerin UTF-8 olarak gelebilir. std::string bu anlamda bir kabiliyet içermiyor; yalnızca karakterleri tutuyor. Türkçe karakterler birden fazla byte içerebildiği için metni tek tek char değerleri üzerinden çizmemiz yeterli olmuyor.

Bunun için, src/text_utf8.h içindeki DecodeUTF8 API’sini kullanıyoruz. Bu API, byteları unicode kod noktalarına çeviriyor. Ardından ilgili glyph’i fonttan çekiyoruz. Burada, geçersiz bir diziyle karşılaşırsak, U+FFFD yerine koyma karakterini döndürüp bir byte ilerliyor. Türkçe metnin doğru görünmesi için seçtiğimiz fontun da ilgili karakterleri içermesi gerekiyor tabi.

Dikdörtgen İçinde Hizalama ve Satır Sarmalama

Bir düğmenin veya bilgi kutusunun yazısını çizerken her seferinde konum hesaplamak istemeyiz 🙂 Bunun için DrawText çağrısının dikdörtgen alan API’si de var:

Metinler her zaman dikey olarak ortalanıyor. Yatayda ise kLeftkCenter veya kRight seçebiliyoruz. Buradaki dikdörtgen, metni resimlerdeki gibi ölçeklemiyor; sadece yerleşim için kullanılıyor.

Uzun bir metnin alt satıra geçmesini istiyorsalk TextWrap::kWord vermeniz gerekiyor:

Her bir satır, kelime sınırlarından bölünüyor. Tek bir kelime alana sığmıyorsa UTF-8 karakter sınırlarından bölünerek devam ediliyor. Varsayılan kNone seçeneğinde ise otomatik sarmalama yapılmıyor, yani bölme yok ve metin dışarı taşabilir.

Kutunun yüksekliğini metne göre ayarlamak için CountTextLines(text, width, TextWrap::kWord) ile satır sayısını alıp font->LineHeight() ile çarpabiliriz. Bu hesaplamada da önce kullanılacak fontu SetFont ile seçmemiz önemli.

MeasureText ölçüm sırasında harf çiftleri arasındaki aralık düzenlemesini (kerning) hesaba katıyor, çizim ise glyph’lerin advance değerlerini topluyor. Bu nedenle bazı metinlerde ölçülen genişlik ile çizilen genişlik, hizalama ve sarmalamada ufak farklılıklar olabilir, şu an için açıkçası çok büyük bir problem olmadığını düşünüyorum. Bununla birlikte ileride buna da eğilebiliriz.

Şimdiye kadar anlattıklarımı özetleyen bir resme bakalım:

Yatay hizalama ve satır sarmalama ayrı seçimlerdir. kNone ile uzun metin taşar; kWord ile kelimelerden bölünür.

Dönüşümler ve Opaklık

Önceki yazılarda kullandığımız TranslateRotate ve Scale, API’lerini metinler ile kullanabiliyoruz. Örneğin bir etiketi belirli bir konuma taşıyıp döndürmek için aşağıdaki kodr parçasını kullanabilirsiniz:

Bu örnekte dönüş merkezi, metnin başlangıcındaki taban çizgisi noktası. Save ve Restore ile dönüşümü ve opaklığı geri alabiliyoruz. Font seçimi ise buna dahil değil; arada SetFont ile fontu değiştirdiyseniz önceki fontu ayrıca geri seçmelisiniz.

Demo

Evet yine geldik en eğlenceli kısma 🙂 Bu kabiliyetleri bir arada görmek için repodaki examples/graphics/text.cpp örneğine bakabilirsiniz. Demo; farklı punto boyutlarını, üç yatay hizalamayı, renkli ve yarı saydam metinleri, ayrıca dönüşüm uygulanmış bir yazıyı gösteriyor. Windows ve Linux’ta yaygın font yollarını deneyerek kullanabileceği bir font arıyor.

Aşağıda demonun çizim kodunu güncel kütüphaneyle çalıştırarak aldığım çıktıyı görebilirsiniz.

text.cpp demosunun çizim kodundan alınan gerçek OpenGL çıktısı. Arial fontu kullanıldı.

Sonuç

Bu fazda, resim çizimi için hazırladığımız doku altyapısının üzerine font yüklemeyi ve metin yerleşimini nasıl yapabileceğimizi gördük. Özellikle oyunlarda, önemli bir konu olan ve OpenGL ile çalışanları, ilk etapta uğraştıran bu kabilyeti de SDLPainter ile nasıl halledebileceğinizi sizlere aktarmaya çalıştım.

Bu yazımla, temel kabiliyetlerin birçoğuna bakmış olduk. Bir sonraki yazımda ise Vulkan renderer’ına bakacağız. Her zaman olduğu gibi, kaynak koda ve diğer örneklere sdl-painter reposundan ulaşabilirsiniz.

Bir sonraki yazıda görüşmek üzere, keyifli kodlamalar 🙂

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site istenmeyenleri azaltmak için Akismet kullanır. Yorum verilerinizin nasıl işlendiğini öğrenin.