WebGL · Shader sistemi · Fragment
WebGL: Fragment shader (görüntü ve renk katmanı)
Fragment shader, pipeline’ın en yoğun çalışan istasyonudur: geometri “kurulduktan” sonra, yüzeyin içini renk, doku ve ışık tepkisiyle dolduran asıl boyama katmanı burasıdır. Ekrana düşen her örnek noktasının (fragment) nasıl görüneceğine bu katman karar verir.
Bu sayfa GLSL syntax dersi değildir. Burada hedef, fragment shader’ın pipeline’daki rolünü, “fragment ≠ piksel” ayrımını ve veri akışını (varying/uniform/texture sampling) netleştirmektir.
Fragment nedir? (pikselden önceki durak)
“Fragment” terimi çoğu zaman “piksel” ile karıştırılır ama arada kritik bir fark vardır: fragment, rasterization sonucunda üretilen bir piksel adayıdır. Her fragment ekrandaki bir piksele karşılık gelebilir; fakat bazı testlerden geçemezse nihai piksele dönüşmeden elenir.
- Örnek noktası: Fragment, “bu piksel için bir hesap yapılacak” kaydı gibidir.
- Elenebilir: Depth test, stencil veya shader içindeki discard gibi kurallar fragment’i framebuffer’a yazılmadan önce düşürebilir.
Rasterization sonrası veri akışı
Vertex shader’dan çıkan üçgenler, ekran çözünürlüğüne göre çok sayıda fragment’e bölünür. Fragment shader işte bu fragment’lerin her biri için çalıştırılır ve şu soruya cevap verir: “Bu örnek noktası hangi renge dönüşecek?”
Buradaki önemli ölçek: sahnedeki birkaç bin verteks, ekranda milyonlarca fragment’e dönüşebilir. Fragment shader’ın “pahalı” olması bu yüzden sürpriz değildir — bu katman, doğrudan ekran alanıyla ölçeklenir.
Interpolation: varying veri akışı
Fragment shader’ın “büyüsü”nün önemli bir kısmı interpolasyonla gelir. Vertex shader, köşelerde bazı değerler üretir; rasterization bu değerleri üçgenin içindeki her fragment’e otomatik olarak dağıtır. Böylece yüzey boyunca pürüzsüz geçişler mümkün olur.
Buradaki kritik fikir şudur: fragment shader “boşlukta” çalışmaz. Ona gelen girişlerin önemli bir kısmı, köşelerde tanımlanmış değerlerin yüzey içine ağırlıklı dağıtımıdır. Bu sayede “köşede var olan bilgi”, yüzeyin her noktasında anlamlı bir değere dönüşür.
- Veri köprüsü: UV, vertex rengi veya normal gibi değerler vertex shader’dan “varying” olarak çıkar.
- Dağıtım: GPU bu değerleri üçgen içine interpole eder ve her fragment shader yürütmesine giriş olarak verir.
- Sonuç: Doku yerleşimi ve yumuşak renk geçişleri, fragment başına hazır bir giriş akışıyla çalışır.
İnce ama önemli detay: perspektif projeksiyonda bu dağıtım çoğu zaman perspective-correct olacak şekilde yapılır. Aksi halde texture’lar yüzeyde “kaymış” gibi görünürdü. Yani GPU, yalnızca “ortalama alıp geçmez”; ekran uzayına düşen geometriyi doğru görsel algıyla eşleştirecek şekilde interpolasyonu düzeltir.
Bir başka sezgi: bazı verileri “yumuşatmak” istemezsiniz (ör. yüzey ID’si gibi keskin değerler). Bu tür değerler için interpolasyon davranışı ayrıca kontrol edilebilir; burada sadece şunu sabitliyoruz: varying’ler varsayılan olarak sürekli yüzey hissini üretmek için taşınır.
Bu bölümde yalnızca mekanizmayı sabitliyoruz; “hangi hesapla” kısmı vertex/fragment detay sayfalarında açılacak.
Shader kodu (secondary)
precision mediump float;
varying vec2 v_uv;
varying vec3 v_color;
uniform sampler2D u_tex;
void main() {
vec4 tex = texture2D(u_tex, v_uv);
if (tex.a < 0.5) discard;
gl_FragColor = vec4(v_color, 1.0) * tex;
}
Fragment shader’ın görevleri
Fragment shader, görsel deneyimin “nihai karar vericisi” gibidir. Aynı geometri, farklı fragment kurallarıyla tamamen farklı görünebilir.
Bu “nihai karar” ifadesini şöyle okuyun: fragment shader, yüzeyin her örnek noktasında hangi görsel davranışın geçerli olacağını belirler. Yani geometri aynı kalsa bile “materyal hissi” (matlık/parlaklık, pürüzlülük, renk geçişi) fragment tarafındaki kurallarla şekillenir.
- Renk üretimi: Yüzeyin temel rengi (albedo) ve tonlaması hesaplanır; çoğu zaman bu, doğrudan texture’dan gelen renk ile shader içi katsayıların birleşimidir.
- Texture sampling: UV koordinatlarına göre texture’dan renk örneği çekilir. Buradaki kritik nokta, örneklemenin “her fragment” için yapılmasıdır: doku, yüzeye bu katmanda giydirilir.
- Işık tepkisi: Yüzeyin ışığa verdiği tepki (parlaklık/gölge hissi) matematiksel olarak belirlenir. Normal, view yönü ve ışık yönü gibi girdiler çoğu zaman burada birleşir.
- Şeffaflık ve maskeleme: Bazı fragment’ler için alfa üretilebilir ya da belirli kurallarla fragment tamamen elenebilir (discard). Bu, “yüzeyin nerede var olup nerede yok sayılacağı”nı belirler.
- Çoklu çıktı (bağlama): Bazı akışlarda fragment shader yalnızca “renk” üretmez; aynı anda yardımcı çıktılar (ör. normal/ID/depth benzeri bilgiler) üretebilir. Bu, render hedefi sözleşmeleriyle birleşerek daha ileri akışlara kapı açar.
Bu bölümün özü: fragment shader, “son renge giden kural kitabı”dır. Hangi veriyi nereden aldığınız (varying/uniform/texture) ve nasıl birleştirdiğiniz, ekranda gördüğünüz hissi belirler.
Çıktı mantığı: renk nereye gider?
Fragment shader’ın görevi, bir renk (veya çoklu hedeflerde birden fazla çıktı) üretmektir. Bu çıktı bir sonraki aşamada framebuffer’a yazılmak üzere değerlendirilir.
Buradaki “yazılmak üzere değerlendirilir” ifadesi önemli: fragment shader renk üretir; ama bu renk her zaman doğrudan ekrana gitmez. Sonuç, o an bağlı olan hedefe (default framebuffer veya offscreen render target) yazılır ve depth/stencil/blending gibi testlerle birleşerek nihai framebuffer değerini oluşturur.
- Hedef seçimi: Ekrana çiziyorsanız çıktı default framebuffer’a gider; bir render hedefi bağlıysa çıktı bir texture’a (veya çoklu hedeflerde birden fazlasına) yazılır.
- Alfa (A): RGBA içindeki A kanalı “şeffaflık sözleşmesi”nin parçasıdır. Ancak tek başına yeterli değildir; gerçek görünüm, blending state ile birlikte oluşur.
- Çoklu çıktı fikri: Bazı akışlarda fragment shader yalnızca “renk” değil, aynı anda yardımcı veriler de üretir (ör. normal/ID gibi). Bu sayfa MRT öğretmez; sadece “çıktı tek yerde bitmek zorunda değil” sezgisini sabitler.
- WebGL1 notu: Tarihsel olarak çıktı
gl_FragColorüzerinden düşünülür. - WebGL2 notu: Modern yaklaşımda “çıktı değişkenleri” (out) kullanılır. Mantık aynı kalır: amaç, framebuffer’a yazılacak rengi üretmektir.
Sezgi: fragment shader, “renk üreten fonksiyon”dur; framebuffer ise “bu rengin nereye ve hangi kurallarla yazılacağını” belirleyen sahnedir. Bu ayrımı netleştirdiğinizde, “renk doğru ama ekranda yanlış görünüyor” türü sorunlar daha hızlı sınıflanır: shader mı, yoksa yazım/test sözleşmesi mi?
Texture sampling (doku örnekleme)
GPU’nun gücü, aynı örnekleme kuralını milyonlarca fragment üzerinde paralel uygulayabilmesinde ortaya çıkar. Fragment shader, texture’dan veri okur (sample eder), sonra bu veriyi kendi kurallarıyla renge dönüştürür.
Texture sampling’i bir “lookup” gibi düşünebilirsiniz: elinizde bir görüntü hafızası (texture) vardır; siz de o hafızaya bir adres verirsiniz; GPU size o adresteki rengi döndürür. Bu dönüş değeri genelde bir renk vektörüdür (ör. RGBA) ve fragment shader’daki matematiğin doğrudan girdisi hâline gelir.
Bu sayfa “hangi filtreleme modu?” gibi ayrıntılara girmeden şu sezgiyi sabitler: texture, fragment shader için bir veri kaynağıdır; UV ise bu kaynağa bakan adres sistemidir.
UV koordinatları çoğu zaman normalize bir adresleme gibi çalışır: \(0\)–\(1\) aralığı texture’ın bir köşesinden diğer köşesine karşılık gelir. Rasterization ve interpolasyon sayesinde, fragment shader her örnek noktası için “o noktaya düşen UV” değerini hazır alır; siz de bu UV ile texture’dan renk örneği çekersiniz.
Pipeline sözleşmesi açısından texture, shader’a çoğu zaman bir sampler (uniform) üzerinden bağlanır: CPU tarafı “hangi texture hangi birimde?” bilgisini kurar; fragment shader ise o sampler’ı kullanarak örnekleme yapar. Yani sampling sadece bir matematik değil; aynı zamanda “veri kaynağını doğru bağlama” sözleşmesidir.
Depth, discard ve maskeleme
Bazı durumlarda bir fragment’in “yazılmasını” istemeyebilirsiniz. Bu karar, shader içinde veya framebuffer testleriyle verilebilir.
Bu bölümün ana fikri şu: fragment shader bir renk üretse bile, sistem “bu rengi yaz” demek zorunda değildir. Pipeline’ın sonuna gelmeden önce bazı kapılar vardır; fragment bu kapılardan geçerse framebuffer’a yazılır, geçemezse elenir.
- Discard: Shader içinde belirli bir koşulda fragment’i tamamen yok sayarsınız (ör. şeffaf delikler, maskeleme).
- Depth test: Derinliğe göre önde olan kazanır; arkadaki fragment’ler framebuffer’a yazılmadan elenebilir.
Discard sezgisi: “bu yüzeyin bu noktasında aslında bir piksel yok” demektir. Yani geometri orada var gibi görünse bile, fragment düzeyinde o noktayı görünmez kılarsınız. Bu yaklaşım, alfa üretmekten farklıdır: alfa, yazılan renğin karışımını etkiler; discard ise “hiç yazma” kararıdır.
Depth test sezgisi: aynı ekran noktasına birden fazla yüzey aday olabilir. Depth test, hangisinin “önde” olduğunu seçer. Bu, yalnızca doğru görünüm için değil, aynı zamanda gereksiz yazımı engellemek için de temel bir sözleşmedir: arkada kalan fragment’ler çoğu zaman yazım aşamasına gelmeden düşebilir.
Bu iki mekanizma birlikte, “fragment ≠ piksel” ayrımını somutlaştırır: her fragment, ancak kurallardan geçerse ekranda bir piksel etkisine dönüşür.
Not: burada amaç “hangi teknikle ne efekt yapılır?” değil; fragment’in her zaman “piksel” olmadığını ve elenebildiğini somutlaştırmaktır.
Performans perspektifi: en pahalı aşama
Ekran çözünürlüğü arttıkça (ör. 4K), fragment sayısı geometrik olarak artar. Bu yüzden fragment shader, çoğu zaman en pahalı istasyondur: aynı kural, çok daha fazla örnek üzerinde yürür.
Burada “geometrik artış”ı somutlayalım: çözünürlüğü iki katına çıkardığınızda (genişlik ve yükseklik), ekrandaki örnek/piksel sayısı yaklaşık dört katına çıkar. Fragment shader da bu yüzey alanı üzerinde çalıştığı için, aynı kuralı çok daha fazla kez yürütmek zorunda kalır.
Bir de görünmeyen çarpan vardır: overdraw. Aynı ekran noktasına birden fazla yüzey aday olduğunda, fragment shader birden fazla kez çalışabilir; sonra depth test/blending gibi kurallar “hangisi yazılacak?” kararını verir. Bu yüzden maliyet, sadece çözünürlük değil; sahnede üst üste binen yüzey miktarıyla da büyür.
Buradan çıkarılacak ders “optimizasyon listesi” değil; ölçek farkıdır: fragment tarafındaki her ekstra hesap, milyonlarca kez çalışabilir. Bu yüzden fragment shader’ı “net ve tutarlı kural” olarak kurmak, hem görüntü hem maliyet açısından belirleyicidir.
Bu sayfanın bağlamında çıkarım şu: fragment shader’a eklediğiniz her matematik veya sampling, çoğu zaman “ekran alanı × overdraw” kadar çoğalır. Bu yüzden fragment tarafını tasarlarken önce kuralı netleştirmek (ne hesaplıyorum, hangi veriyle, hangi çıktı için) performans konuşmadan bile doğru mühendislik refleksidir.
Altın ayrım
Vertex shader “nerede?” sorusunu cevaplar; fragment shader ise “nasıl görünecek?” sorusunu.
Three.js bağlantısı (tek paragraf)
Three.js’te gördüğünüz renk, ışık, gölge ve texture davranışlarının büyük kısmı, altta fragment shader içinde koşturulan matematiksel kurallara indirgenir. Üst katmanda verdiğiniz estetik kararlar, pratikte shader’a giden parametreler ve fragment başına çalışan hesaplar olarak somutlaşır.