Gölgelendiriciler
Merhaba Üçgen eğitselinde belirtildiği gibi, gölgelendiriciler GPU üzerinde saklanan küçük programlardır. Bu programlar grafik iş hattının her bir bölümü için çalıştırılır. Temel olarak gölgelendiriciler, girdileri çıktılara dönüştüren programlardan fazlası değildir. Ayrıca, gölgelendiriciler birbirleriyle iletişim kurmalarına izin verilmeyen yalıtılmış programlardır; sahip oldukları tek iletişim girdi ve çıktıları üzerinden gerçekleşir.
Önceki eğitselde, gölgelendiricilere ve nasıl doğru kullanılacağına kısaca değindik. Şimdi gölgelendiricileri -özellikle OpenGL Shading Language- daha genel bir şekilde açıklayacağız.
GLSL
Gölgelendiriciler C benzeri bir dil olan GLSL’de yazılmıştır. GLSL, grafiklerle kullanmak için uygun hâle getirilmiştir ve özellikle vektör ve matris manipülasyonunu hedefleyen kullanışlı özellikler içerir.
Gölgelendiriciler her zaman bir versiyon bildirimi ile başlar. Bunu bir dizi girdi-çıktı değişkenleri, uniformlar ve kendi main işlevi takip eder. Her gölgelendiricinin girdi noktası, herhangi bir girdi değişkenini işlediğimiz ve çıktı değişkenlerine sonuçları yazdırdığımız kendi main
işlevindedir. Uniformlar’ın ne olduğunu bilmiyorsanız endişelenmeyin, onları birazdan öğreneceğiz.
Bir gölgelendirici genel anlamda aşağıdaki yapıya sahiptir:
Özellikle köşe noktası gölgelendirici hakkında konuşurken, her girdi değişkeni köşe nokta özniteliği olarak da bilinir. Donanım tarafından sınırlanmış, tanımlamaya izin verilen bir maksimum köşe noktası sayısı vardır. OpenGL, her zaman en az 16 adet 4-bileşenli köşe noktası özelliğinin bulunduğunu garanti eder; fakat bazı donanımlar daha fazlasına izin verebilir. Bunu GL_MAX_VERTEX_ATTRIBS
değerini sorgulayarak öğrenebilirsiniz:
Genelde çoğu amaç için yeterli olabilecek 16 değerini döndürür.
Tipler
GLSL, hangi tipte bir değişkenle çalışmak istediğimizi belirlemek için diğer programlama dillerine benzer veri tiplerine sahiptir. GLSL, C gibi dillerden bildiğimiz temel tiplerin çoğuna sahiptir: int
, float
, double
, uint
, bool
. Ayrıca, GLSL eğitseller boyunca vektörler
ve matrisler
olarak sık sık kullanacağımız iki kapsayıcı tipe de sahiptir. Matrisleri daha sonraki bir eğitselde konuşacağız.
Vektörler
GLSL’de vektör, yukarıda bahsedilen temel tiplerin herhangi biri için 1,2,3 veya 4 bileşenli bir kapsayıcıdır. Aşağıdaki şekilleri alabilirler (n
bileşen sayısını temsil eder):
vecn
:n
float’tan oluşan başlangıç vektörü.bvecn
:n
boolean içeren vektör.ivecn
:n
integer içeren vektör.uvecn
:n
unsigned tamsayı içeren vektör.dvecn
:n
double sayı içeren vektör.
Float tipi çoğu amaç için yeterli olacağından çoğunlukla vecn
vektörünü kullanacağız.
Bir vektörün bileşenlerine vec.x
ile erişilebilir, burada x
, vektörün ilk bileşenidir. Birinci, ikinci, üçüncü ve dördüncü bileşene erişmek için sırasıyla .x
, .y
, .z
ve .w
üyelerini kullanabilirsiniz. Ayrıca GLSL, aynı bileşenlere erişerek, renkler için rgba
veya doku koordinatları için stpq
kullanmanıza izin verir.
Vektör veri tipi, swizzling denilen biraz ilginç ve esnek bileşen seçimine izin verir. Swizzling aşağıdaki sözdizimine olanak tanır:
Orijinal vektör bu bileşenlere sahip olduğu sürece (aynı türde) yeni bir vektör oluşturmak için en fazla 4 harften oluşan herhangi bir kombinasyonu kullanabilirsiniz; örneğin bir vec2
‘nin .z
bileşenine erişimi mümkün değildir. Ayrıca, vektörleri farklı vektör yapıcı çağrılarına argüman olarak iletebiliriz, böylece gerekli argüman sayısını azaltabiliriz:
Vektör, her tür giriş ve çıkış için kullanabileceğimiz esnek veri tipidir. Eğitim boyunca, vektörleri nasıl yaratıcı bir şekilde yönetebileceğimize dair bolca örnek göreceksiniz.
Girdiler ve Çıktılar
Gölgelendiriciler kendi başlarına küçük hoş programlardır, ama bir bütünün parçasıdırlar ve bu nedendenle her bir gölgelendirici üzerinde girdi ve çıktılara sahip olmak isteriz ki böylece bir şeyleri etrafta hareket ettirebilelim. GLSL, bu amaç için özelllikle in
ve out
anahtar kelimelerini tanımlamıştır. Her bir gölgelendirici, bu anahtar kelimeleri kullanarak girdi ve çıktılar tanımlayabilir ve bir çıktı değişkeni, bir sonraki gölgelendirici bölümünün bir girdi değişkeni ile eşleştiğinde aktarılabilir. Köşe noktası ve parça gölgelendirici biraz farklıdır.
Köşe noktası gölgelendirici bir tür girdi almalıdır, aksi taktirde oldukça etkisiz olur. Köşe noktası gölgelendirici girdi bakımından ayrılır, çünkü girdisini köşe nokta verileri üzerinden alır. Köşe nokta verilerinin nasıl düzenlendiğini tanımlamak için, girdi değişkenlerini “location” meta verisi ile belirtilir. Böylece CPU üzerinde köşe noktası özniteliklerini ayarlayabiliriz. Bunu bir önceki derste layout (location = 0)
olarak görmüştük. Köşe noktası gölgelendirici, bu nedenle girdileri için fazladan düzen tanımı gerektirir, böylece onu köşe nokta verileri ile bağlayabiliriz.
layout (location = 0)
belirtecini çıkartmak ve OpenGL kodunuzda glGetAttribLocation ile öznitelik konumlarını sorgulamak da mümkündür, ancak ben bunları Köşe noktası gölgelendirici içinde atamayı tercih ederim. Bunu anlaması daha kolaydır ve sizi (ve OpenGL’ i) bir takım işlerden kurtarır.
Diğer istisna ise parça gölgelendiricinin bir vec4
renk çıkışı değişkeni gerektirmesidir, çünkü parça gölgelendiricilerin bir son çıktı rengi üretmesi gerekir. Parça gölgelendiricide çıktı rengi belirtmeyi başaramazsanız OpenGL nesnenizi siyah (veya beyaz) yapar.
Dolayısıyla bir gölgelendiriciden veri göndermek istiyorsak, gönderen gölgelendiriciden bir çıktı ve alıcı gölgelendiriciye, benzer bir girdi bildirmek zorunda kalırız. Tipler ve isimler her iki tarafta da aynı olduğunda, OpenGL bu değişkenleri birbirleriyle bağlar ve gölgelendiriciler arası veri göndermek mümkün olur (bu bir program nesnesi bağlanırken yapılır). Bunun pratikte nasıl çalıştığını size göstermek için geçen dersteki gölgelendiricileri değiştireceğiz, köşe nokta gölgelendiricinin, parça gölgelendirici rengine karar vermesine izin vereceğiz.
Köşe Nokta Gölgelendirici
Parça Gölgelendirici
Bir vertexColor değişkenini, köşe nokta gölgelendiricide atadığımız çıktıyı vec4
olarak belirttiğimizi ve parça gölgelendiricide benzer bir vertexColor girdisi tanımladığımızı görebilirsiniz. Bunların ismi ve tipi aynı olduğu için, parça gölgelendiricide vertexColor, köşe nokta gölgelendirici içindeki vertexColor’a bağlanır. Köşe nokta gölgelendiricide koyu kırmızı bir renk atadığımız için sonuç parçaları de koyu kırmızı olmalıdır. Aşağıdaki görseller çıktıyı gösteriyor.
İşte başlıyoruz! Biz sadece köşe nokta gölgelendiriciden parça gölgelendiriciye bir değer göndermeyi başardık. Haydi biraz baharatlandıralım ve uygulamamızdan parça gölgelendiriciye bir renk gönderebilecek miyiz görelim!
Uniformlar
Uniformlar, CPU üzerindeki uygulamamızdan GPU üzerindeki gölgelendiricilere veri aktarmanın bir başka yoludur. Ancak uniformlar köşe nokta öznitelikleri ile karşılaştırılınca biraz farklıdır. Öncelikle, uniformlar globaldir. Global, bir uniform değişkenin, her bir gölgelendirici program nesnesine özgü olması ve gölgelendirici programının herhangi bir bölümündeki herhangi bir gölgelendiriciden erişilebilir olması anlamına gelir. İkinci olarak, uniform değeri neye ayarladığınıza göre, uniformlar sıfırlanana veya güncellenene kadar değerlerini koruyacaktır.
GLSL’de bir uniform tanımlamak için basitçe, bir gölgelendiriciye, bir tip ve isimle birlikte uniform
anahtar kelimesini ekleriz. Bu noktadan itibaren, gölgelendiricide yeni tanımlanan uniformu kullanabiliriz. Bakalım bu sefer üçgenin rengini üniform ile ayarlayabilir miyiz?:
Parça gölgelendiricide bir uniform vec4
tanımladık ve parçanın çıktı rengini bu uniformun değerine atadık. Uniformlar global değişkenler olduğundan, onları istediğimiz bir gölgelendiricinin içinde tanımlayabiliriz. Bu uniformu köşe nokta gölgelendiricide kullanmıyoruz, böylece onu burada tanımlamamıza ihtiyaç yok.
GLSL kodu içinde, hiçbir yerde kullanmadığınız bir uniform tanımlarsanız, derleyiciniz bu değişkeni birkaç sinir bozucu hataya neden oluşturan derlenmiş hâlinden sessizce kaldıracaktır; aklınızda bulunsun.
Uniform şu an boş; henüz uniform’a bir veri eklemedik. Öncelikle, gölgelendiricimizde uniform’un (location) özelliğini bulmamız gerek. Uniform’un konumunu bildiğimizde, değerini gücelleyebiliriz. Parça gölgelendiriciye tek renk geçmek yerine, rengi zamanla kademeli bir şekilde değiştirerek canlandıralım:
Önce, çalışma zamanını glfwGetTime() ile alırız. Sonra, sin fonksiyonunu kullanarak rengi 0.0
- 1.0
aralığında çeşitlendirir ve sonucu greenValue’da saklarız.
Sonra ourColor uniform’unun konumunu glGetUniformLocation kullanarak sorgularız. Sorgu işlevine, gölgelendirici programını ve üniformun (konumdan almak istediğimiz) adını sağladık.
glGetUniformLocation -1
döndürürse, konumu bulamamıştır. Son olarak glUniform4f fonksiyonunu kullanarak üniformun değerini ayarlayabiliriz. Üniform konumunu bulmanın, önce gölgelendirici programını kullanmanızı gerektirmediğini unutmayın, ancak bir üniformu güncellemek şu anda aktif olan gölgelendirici programında üniformu ayarladığı için önce programı (glUseProgram’ı çağırarak) kullanmanızı gerektirir.
OpenGL, çekirdeğinde bir C kütüphanesi olduğundan aşırı yükleme için (overloading) native (doğal) desteğe sahip değildir. Bir fonksiyonun farklı tiplerle çağrılabildiği her yerde OpenGL gereken her tip için yeni fonksiyonlar tanımlar; glUniform bunun mükemmel bir örneğidir. Fonksiyon, belirlemek istediğiniz üniformun tipi için özel bir son ek gerektirir. Birkaç olası son ek:
f
: fonsiyon bir float
bekliyor
i
: fonsiyon bir int
bekliyor
ui
: fonsiyon bir unsigned int
bekliyor
3f
: fonsiyon 3 float
bekliyor
fv
: fonsiyon bir float
vektörü/dizisi bekliyor
OpenGL’de bir seçeneği ayarlamak istediğiniz zaman, basitçe, tipinize uygun olan aşırı yüklenmiş fonksiyonu seçin. Bizim durumumuzda, her bir üniforma 4 float atamak istiyoruz, bu yüzden verimizi glUniform4f
fonksiyonuyla aktaracağız(ayrıca fv
hâlini kullanabileceğimize de dikkat edin).
Şuan uniform değişkenlerinin değerlerini nasıl atayacağımızı biliyoruz, bunları render için kullanabiliriz. Rengi kademeli olarak değiştirmek istiyorsak, bu uniformu her oyun döngüsü yinelemesini (kareye göre değişir) güncellemek isteriz aksi hâlde eğer bir kere atarsak üçgen tek bir düz rengi korur. Bu yüzden greenValue yi hesaplarız ve her render yinelemesinde uniformu güncelleriz:
Kod, önceki kodun nispeten basit bir uyarlamasıdır. Bu kez, üçgeni çizmeden önce her bir yineleme için tek biçimli bir değeri güncelledik. Uniformu doğru bir şekilde güncellerseniz, üçgeninizin renginin kademeli olarak yeşilden siyaha ve yeşile döndüğünü görmelisiniz.
Takıldıysanız buradan kaynak kodunu inceleyin.
Gördüğünüz gibi uniformlar, render yinelemelerinde değişebilecek nitelikleri ayarlamak veya uygulamanız ile shaderlarınız arasında veri alışverişi yapmak için yararlı bir araçtır, ancak her vertex için bir renk belirlemek istiyorsak ne olur? Bu durumda, köşelerimiz kadar uniform tanımlamamız etmemiz gerekirdi. Daha iyi bir çözüm, yapacağımız şey olan vertex özelliklerine daha fazla veri eklemek olacaktır.
Daha fazla özellik!
Önceki derste, bir VBO’yu nasıl doldurabileceğimizi, vertex özellik pointerlarını nasıl yapılandırabileceğimizi ve hepsini bir VAO’da nasıl saklayabileceğimizi gördük. Bu kez, vertex verisine renk verileri de eklemek istiyoruz. Renk vertex dizisine 3 float
şeklinde renk verilerini ekleyeceğiz. Üçgenin her bir köşesine sırasıyla kırmızı, yeşil ve mavi renk atarız:
Artık vertex shadera gönderilecek daha fazla veriye sahip olduğumuzdan, renk değerimizi bir vertex özelliği girişi olarak alacak şekilde vertex shaderı ayarlamak gerekir. aColor
özelliğinin konumunu düzen belirleyici ile 1
olarak ayarladığımızı unutmayın:
Artık, fragment’in rengi için bir uniform kullanmayacağımızdan, şimdi ourColor çıktı değişkenini kullandığımız için, fragment shaderı da değiştirmek zorunda kalacağız:
Başka bir vertex niteliği eklediğimiz ve VBO hafızasını güncellediğimiz için vertex niteliği işaretçilerini yeniden yapılandırmamız gerekiyor. VBO’nun belleğindeki güncellenmiş veriler şimdi biraz şuna benziyor:
Mevcut düzeni bilerek, vertex formatını glVertexAttribPointer
ile güncelleyebiliriz:
GlVertexAttribPointer’ın ilk birkaç argümanı nispeten basittir. Bu kez vertex niteliğini 1
özellik konumunda yapılandırıyoruz. Renk değerleri 3 float
büyüklüğüne sahiptir ve değerleri normalleştirmiyoruz.
Şimdi iki vertex niteliğine sahip olduğumuz için stride değerini yeniden hesaplamamız gerekiyor. Veri dizisindeki bir sonraki öznitelik değerini (örneğin, pozisyon vektörünün bir sonraki x
bileşeni) elde etmek için, üç pozisyon değeri ve üç renk değeri için 6
float
ı sağa hareket ettirmeliyiz. Bu bize bayt cinsinden bir float
boyutunun 6 katı adım değeri verir (= 24
bayt).
Ayrıca, bu sefer bir ofset belirtmeliyiz. Her vertex için, konum vertex özniteliği öncedir, bu nedenle 0
ofsetini tanımlarız. Renk özniteliği, konum verilerinden sonra başlar, bu nedenle ofset, bayt cinsinden 3 * sizeof (float)
dır (=12
bayt).
Uygulamayı çalıştırmak aşağıdaki görüntüyle sonuçlanmalıdır:
Kaynak kodunu buradan inceleyin
Resim tam olarak beklediğiniz gibi olmayabilir, çünkü şu anda gördüğümüz büyük renk paleti değil, sadece 3 renk sağladık. Tüm bunlar, fragment shaderda fragment enterpolasyonu denilen bir şeyin sonucu. Bir üçgen oluştururken, rasterleştirme aşaması genellikle başlangıçta belirtilen vertexlerden çok daha fazla fragmente neden olur. Rasterizer, daha sonra bu fragmentlerin her birinin pozisyonlarını, üçgen şeklinde bulundukları yere göre belirler. Bu konumlara dayanarak, tüm parça gölgelendiricinin giriş değişkenlerini enterpolasyon yapar. Örneğin, üst noktanın yeşil ve alt noktanın mavi renkte olduğu bir çizgimiz var. Fragment shader, çizginin %70
pozisyonundaki bir pozisyon etrafında kalan bir parçada çalıştırılırsa, sonuçtaki renk girişi özelliği, yeşil ve mavinin doğrusal bir birleşimi olur; Daha kesin olmak gerekirse: %30
mavi ve %70
yeşil.
Bu tam olarak üçgende olan şey. 3 vertexe ve dolayısıyla 3 renge sahibiz ve üçgenin piksellerine bakılırsa, muhtemelen fragment shaderın renkleri bu pikseller arasına yerleştirdiği yaklaşık 50000 fragmentten oluşuyor. Renklere iyi bakarsanız, her şeyin mantıklı olduğunu göreceksiniz: önce kırmızıdan maviye, mor sonra maviye. Parça enterpolasyonu, tüm fragment shaderın giriş niteliklerine uygulanır.
Kendi shader sınıfımız
Shaderların yazılması, derlenmesi ve yönetilmesi oldukça zahmetli olabilir. Shader konusuna son bir dokunuş olarak, shaderları diskten okuyan, derleyen ve bağlayan, hataları kontrol eden ve kullanımı kolay bir shader sınıfı oluşturarak hayatımızı biraz daha kolaylaştıracağız. Bu aynı zamanda size şimdiye kadar öğrendiğimiz bilgilerin bir kısmını yararlı soyut nesnelere nasıl yerleştirebileceğimize dair bir fikir verir.
Gölgelendirici sınıfını, esas olarak öğrenme amaçları ve taşınabilirlik için tamamen bir header dosyasında oluşturacağız. Gerekli olanları ekleyerek ve sınıf yapısını tanımlayarak başlayalım:
Header dosyasının en üstünde birkaç önişlemci direktifi kullandık. Bu küçük kod satırlarını kullanmak, derleyicinize yalnızca bu header dosyasını eklememiş ve henüz eklenmemişse, birden fazla dosya shader headerını dahil etse bile derler. Bu bağlantı çakışmalarını önler.
Shader sınıfı, shader programının ID’sini tutar. Yapıcısı, diskte basit metin dosyaları olarak saklayabileceğimiz sırasıyla vertex ve fragment shaderın kaynak kodunun dosya yollarını gerektirir. Biraz fazlalık eklemek, yaşamımızı biraz kolaylaştırmak için çeşitli işe yarar fonksiyonlar da ekliyoruz: use
, shader programını etkinleştirir ve tüm set...
fonksiyonları bir uniform konum sorgusu yapar ve değerini ayarlar.
Dosyadan okuma
Dosyanın içeriğini birkaç string
değişkeninin içine aktarmak için C++ filestreamlarını kullanıyoruz:
Daha sonra, shaderları derlememiz ve birbirine bağlamamız gerekir. Derleme / bağlamanın başarısız olup olmadığını da incelediğimize dikkat edin ve öyleyse, hata ayıklama işleminde son derece yararlı olan derleme zamanı hatalarını yazdırın (sonunda bu hata günlüklerine ihtiyacınız olacak):
use
fonksiyonu basittir:
Herhangi bir uniform setter fonksiyonu için benzer şekilde:
Ve tamamlanmış bir shader sınıfımız var. Shader sınıfını kullanmak oldukça kolaydır; shader nesnesini bir kez oluştururuz ve bu noktadan itibaren kullanmaya başlarız:
Burada vertex ve fragment shaderın kaynak kodunu shader.vs
ve shader.fs
isimli iki dosyada sakladık. Shader dosyalarını dilediğiniz gibi adlandırmakta özgürsünüz; Kişisel olarak .vs
ve .fs
uzantıları oldukça sezgisel buluyorum.
Yeni oluşturduğumuz shader sınıfını kullanan kaynak kodunu burada bulabilirsiniz.
Alıştırmalar
Köşe nokta gölgelendiriciyi, üçgen baş aşağı olacak şekilde ayarlayın: çözüm.
Bir uniform ile yatay bir ofset belirtin ve bu ofset değerini kullanarak köşe nokta gölgelendiricide üçgeni ekranın sağ tarafına taşıyın: çözüm.
Köşe noktası konumunu
out
anahtar sözcüğünü kullanarak parça gölgelendiriciye çıkarıp parçanın rengini bu köşe noktası konumuna eşit olarak ayarlayın (köşe noktası konum değerlerinin üçgen boyunca nasıl interpolasyon yaptığını görün). Bir kere bunu yapmayı başardınız; aşağıdaki soruyu cevaplamaya çalışın: neden üçgenimizin sol-alt tarafı siyah?: çözüm.
Orijinal Kaynak: Shaders
Çeviri: Barış Çelik
Last updated