holodepth

WebGL · Shader sistemi · GPU programları

WebGL: Shader sistemine giriş

Buffer’lardan gelen ham sayısal veri, ekrana “kendiliğinden” dönüşmez; görüntü, bu veriye uygulanan kuralların sonucudur. O kuralların yaşadığı yer shader katmanıdır: GPU üzerinde çalışan, pipeline’ın kalbini oluşturan küçük programlar. Altın cümle: Shader, GPU’nun veri üzerinde çalışan hesaplama katmanıdır.

Bu sayfanın rolü “GLSL nasıl yazılır?” değil; GPU shader ile nasıl çalışır? sorusunu oturtmaktır: hangi veri hangi kapıdan girer (attribute/uniform), hangi veriler hangi köprüyle taşınır (varying) ve pipeline’da nerede devreye girer. Syntax, efektler, ışık hesapları ve uygulamalı shader yazımı ilgili alt sayfalarda detaylanacaktır.

Shader nedir? (kısa)

Shader, GPU’da çalışan özel bir programdır. “Bir kere çalışır” gibi düşünmeyin: aynı shader, her verteks ve her fragment için tekrar tekrar (paralel) çalıştırılır.

Pratikte WebGL’de “shader” dediğimiz şey, tek başına bir dosya değil; bir GPU programı ailesidir: en az bir vertex shader ve en az bir fragment shader birlikte çalışır. Siz bir kez bu programı GPU’ya yüklersiniz; sonra GPU, sahnenin ölçeğine göre bu kodu binlerce/milyonlarca kez yürütür.

  • Vertex shader: Konum ve geometri tarafını işler; her verteks için “bu nokta ekranda nereye düşecek?” cevabını üretir.
  • Fragment shader: Renk ve piksel tarafını işler; rasterization’ın ürettiği her fragment için “bu örnek noktası hangi renge dönüşecek?” kararını verir.

Bu sayfada “nasıl yazılır?”a girmiyoruz; sadece sistem rolünü sabitliyoruz. Detay sayfalarda, vertex shader’ın gl_Position üretimi ve fragment shader’ın renk/texture mantığı somutlaştırılacak.

GPU programlama mantığı: paralel düşünme

Shader yazmak, CPU programlamasından farklı bir zihinsel model ister. CPU kodu bir kez yazar ve akışı adım adım yürütür; GPU ise aynı kuralları çok büyük veri üzerinde eşzamanlı yürütmek üzere tasarlanmıştır.

GPU tarafında temel prensip şudur: tek program, çok veri. Yani siz bir kural yazarsınız; GPU bu kuralı binlerce iş parçacığı gibi düşünebileceğiniz yürütmelerle, farklı verteks/fragment girdileri üzerinde aynı anda uygular. Bu yüzden shader kodu, çoğu zaman “fonksiyon gibi” düşünülür: giriş alır, çıktı üretir; dış dünyayla ilişkisi sınırlıdır.

  • CPU (sıralı): Akış kontrolü, adım adım yürüyen mantık.
  • GPU (paralel): “Aynı kuralı, çok sayıda öğeye uygula” yaklaşımı.

Bu modelin iki önemli sonucu vardır:

  • Dallanma sezgisi: Çok yoğun if/else ayrışmaları, paralel yürüyen grupları farklı yollara bölüp “aynı anda” ilerlemeyi zorlaştırabilir. Bu, burada bir optimizasyon dersi değil; paralel düşünmenin “neden sade kural?” istediğini anlatan bir sezgidir.
  • Yerel bakış: Fragment shader, çoğu zaman “tek bir fragment”i görür; vertex shader “tek bir verteks”i görür. Yanındaki pikseli/verteksi bilmek istediğinizde, bunun mekanizması doğrudan “komşuya bak” değil, farklı veri akışları ve tekniklerdir.

Sezgi: Shader, tek bir ressamın tek tek boyaması değil; milyonlarca ressama aynı anda “şu kurala göre boya” diyen bir orkestra şefidir.

Shader pipeline içindeki yeri

Sistemi bir bütün olarak görmek için verinin yolculuğunu hatırlayalım: Buffer (giriş) → Shader (işleme) → Framebuffer (çıkış).

Shader’lar bu akışın “işleme” istasyonudur: buffer’dan gelen veriyi alır, kuralları uygular ve çıkan sonucu bir sonraki aşamaya taşır.

Buradaki kritik ayrım: buffer “ham malzeme”yi taşır, framebuffer “sonuç”u tutar; shader ise bu ikisi arasında kural uygulayan katmandır. Yani “veri” ancak shader’dan geçince bir görüntü davranışına dönüşür: konumlanır, renklendirilir, gerekirse doku/ışık tepkisi hesaplanır.

Shader’ın pipeline’daki yeri aynı zamanda sınırı da belirler: shader, CPU gibi “sahneyi yönetmez”; ona verilen girdilerle çalışır ve çıktıyı üretir. Bu yüzden bir problemi teşhis ederken şu sorular çok işe yarar:

  • Girdi doğru mu? Attribute/uniform değerleri beklediğiniz format ve uzayda mı?
  • Kural doğru mu? Shader içindeki matematik, beklediğiniz davranışı üretiyor mu?
  • Çıktı nereye gidiyor? Sonuç default framebuffer’a mı, yoksa başka bir hedefe mi yazılıyor?

Zihinsel akış

Buffer → Vertex Shader → Fragment Shader → Framebuffer

Vertex vs Fragment (overview)

Bu sayfada detay yerine rol dağılımını sabitliyoruz:

  • Vertex shader: “Nerede?” sorusunu cevaplar — konumu taşır, geometriyi pipeline’a yerleştirir.
  • Fragment shader: “Ne renk?” sorusunu cevaplar — her fragment için renk ve materyal tepkisini hesaplar.

Aradaki köprü, rasterization’dır: vertex shader’ın konumlandırdığı üçgenler ekranda “örnek noktaları”na (fragment) çevrilir; sonra fragment shader bu örnek noktalarını renge dönüştürür. Yani vertex tarafı yüzeyi kurar, fragment tarafı o yüzeyi boyar.

Bir sezgi daha: genelde verteks sayısı, fragment sayısından çok daha azdır. Bu yüzden vertex shader “daha az öğe üzerinde”, fragment shader ise ekrana yayılan “çok daha fazla örnek üzerinde” çalışır. Bu, shader’ları anlamak için performans konuşmadan bile önemli bir ölçektir.

“Nasıl yazılır?” ve GLSL ayrıntıları, ilgili alt sayfalara bırakılmıştır.

Aşağıdaki playground tek üçgen üzerinde vertex ve fragment shader’ın rollerini canlı okur: köşede vertex yürütmesi, üçgen içinde fragment yoğunluğu ve veri akışı — kod düzenleyici değil, yürütme hissi içindir.

VERTEX 3
FRAGMENT

Ham WebGL · tek drawArrays · hover ile vertex / fragment okuma

Vertex shader
attribute vec3 a_position;
attribute vec3 a_color;

varying vec3 v_color;

void main() {
    gl_Position = vec4(a_position, 1.0);
    v_color = a_color;
}
Fragment shader
precision mediump float;

varying vec3 v_color;

void main() {
    gl_FragColor = vec4(v_color, 1.0);
}
Execution mode

Shader Execution Playground: Üstteki üçgende köşelere yaklaştığınızda vertex shader’ın tek başına bir verteks için ürettiği çıktıyı; üçgen içinde gezerken ise interpolasyonlu varying ile fragment tarafını düşünmenizi sağlar. Üçgen içindeyken imleç altındaki RGB probu interpolasyonlu rengi gösterir; fragment modunda sahne üzerinde hafif akış efekti, sürekli örnekleme hissini destekler. Sağ üstteki kompakt kart, tahmini fragment sayısını (çözünürlükle büyür) vertex 3 ile yan yana koyar.

Veri akışı: kim neyi nasıl alır?

Shader sistemini “çalışıyor” yapan şey, veri trafiğidir. Bu bölüm, shader’lar arasındaki geçiş sözleşmesini sabitler.

  • Attributes: Sadece vertex shader’a girer. Vertex başına değişen veridir (konum, normal, UV vb.). Kaynağı buffer’lardır.
  • Varyings: Vertex shader’dan fragment shader’a köprü kurar. Rasterization, bu değerleri üçgenin içine interpole ederek her fragment’e taşır.
  • Uniforms: Hem vertex hem fragment shader tarafından okunabilir. Draw boyunca sabit kalan veridir (zaman, kamera parametreleri, ışık pozisyonu vb.).

Bu üçlü, veri “sıklığı”nı tarif eder: attribute = verteks başına, varying = yüzey boyunca, uniform = draw boyunca. Bu sayede hangi bilginin “hangi hızda” aktığını anlamış olursunuz.

Varying tarafındaki kritik detay, interpolasyonun otomatik olmasıdır: vertex shader’da bir değeri dışarı verdiğinizde, GPU o değeri üçgenin içindeki her fragment’e “yumuşatarak” dağıtır. Bu yüzden fragment shader, çoğu zaman “bu pikselin UV’si/normal’i/renk katsayısı nedir?” sorusunu hazır bir giriş olarak alır.

Uniform’lar ise kontrol paneli gibidir: kamera matrisleri, zaman, ışık konumu gibi değerleri tek tek verteks/fragment verisine kopyalamak yerine, bir kez set eder ve tüm yürütmelerin okumasını sağlarsınız. Bu da “sabit ama her kare güncellenebilen” bir akış kurar.

Sezgi: attribute “ham giriş”, varying “yumuşatılmış taşıma”, uniform ise “sabit kontrol paneli”dir.

Mini teşhis: bir şey “kaymış” görünüyorsa önce hangi kanalın bozulduğunu sorabilirsiniz. Örneğin UV kaydıysa attribute/layout tarafı; üçgen içinde renk geçişi kırılıyorsa varying aktarımı; tüm sahnede aynı anda bir parametre yanlışsa uniform güncellemesi şüphelidir.

Shader neden önemli?

WebGL’de görsel kalite ve performansın büyük kısmı shader katmanında belirlenir. Ekrana düşen her piksel, en az bir kez fragment shader’dan geçmek zorundadır; bu yüzden “kuralın maliyeti”, doğrudan kare maliyetine dönüşür.

Buradaki ölçek sezgisini akılda tutun: vertex shader “yüzeyi kurar”, fragment shader ise ekran alanına yayılan örnekler üzerinde çalışır. Çözünürlük arttıkça veya ekranda kaplanan alan büyüdükçe, fragment tarafında çalışan iş miktarı katlanır. Bu yüzden shader katmanı, yalnızca “görüntünün stili” değil, aynı zamanda “karenin bütçesi”dir.

Shader önemli çünkü WebGL’de pek çok şey matematik olarak ifade edilir: materyal tepkisi, doku örnekleme, ışık katkısı, maskeleme… Hepsi, shader’daki kurala indirgenir. Bu kural sade ve tutarlıysa hem görüntü kontrolü artar, hem de performans davranışı daha öngörülebilir olur.

Bu sayfa efekt öğretmez; ama şu gerçeği sabitler: ekranda gördüğünüz şeyin yasası shader’dır.

Three.js bağlantısı (tek paragraf)

Three.js’te kullandığınız material’lar, çoğu zaman arka planda önceden yazılmış shader programlarının hazır halleridir. Siz “materyal parametresi” değiştirdiğinizde (renk, metalness/roughness gibi), Three.js çoğu durumda bu değişikliği shader’a giden uniform değerlerine çevirir; yani aslında GPU’daki programın “kontrol paneli”ni güncellersiniz. Buradaki bağlantı cümlesi bu kadar: üst katmandaki materyal, altta shader sözleşmesine oturur.