holodepth

Three.js · İleri konular · GLSL

GLSL nedir? GPU’nun ana gölgeleme dili

GLSL (OpenGL Shading Language), C ailesine yakın sözdizimine sahip, GPU üzerinde doğrudan çalışan yüksek seviyeli bir gölgeleme (shading) dilidir. Tarayıcıda JavaScript çoğu zaman tek iş parçacığında CPU üzerinde sahneyi düzenlerken; üçgen ve piksel işleri için yazdığınız GLSL kodu, donanım tarafında çok sayıda paralel yürütme biriminde aynı anda işlenir — bu yüzden gerçek zamanlı grafikte «milisaniye içinde milyonlarca işlem» mümkün olur.

Bu sayfa, GLSL’yi bir «ayrı dil öğrenimi» olarak değil, WebGL / Three.js bağlamında nerede konuştuğunuzu netleştirmek için yazar: vertex ve fragment aşamaları, değişken sözleşmelTemel sözdizimi ve veri tipleri GLSL tip güvenli (strongly typed) bir dildir: her değişkenin türü açıkça yazılır; farklı tipler arasında çoğu zaman açık dönüşüm (float(…), int(…)) gerekir. JavaScript’teki gevşek sayı birleştirmeleri burada geçmez — küçük bir tür uyumsuzluğu derleme hatasına gider. float: Ondalıklı sayılar; literallerde 1 yerine çoğu zaman 1.0 yazmanız beklenir. vec2, vec3, vec4: İki, üç veya dört bileşenli vektörler — konum (xyz), renk (rgb/rgba) veya dokuma koordinatları için günlük dil. mat2, mat3, mat4: Dönüşüm ve projeksiyon için matrisler. sampler2D: 2B dokudan (texture) örnekleme yapmak için kullanılan özel tip; koordinat ile birlikte texture() çağrılarında görülür (sürüme / bağlama göre isimler değişebilir). eri (uniform, attribute, varying / in / out), temel tipler ve ilk okunabilir bir fragment örneği. Render hattının tam haritası için Render Pipeline: bir kare nasıl doğar? ile köprü kurabilirsiniz; köprü içeriği «shader kelimesi» için ise Shader nedir? sayfasına dönebilirsiniz.

Shader türleri: köşeler mi, pikseller mi?

Donanım hattında sıra şöyledir: önce primitiflerin köşeleri işlenir, ardından bu köşelerden oluşan üçgenler rasterizasyon ile ekran ızgarasındaki piksel adaylarına (fragments) yayılır; en sonda bu adaylar için renk ve yardımcı çıktılar hesaplanır. GLSL’de bu iki büyük program türü — vertex ve fragment gölgelendirici — tam da bu iki farklı «ölçekte» çalışır: biri seyrek (köşe sayısı kadar), diğeri yoğun (örtüşen piksel başına, bazen aynı piksel için birden çok kez).

«Pixel shader» adı fragment gölgelendiriciyi çağrıştırır; fakat ekranda gördüğünüz her nokta ile bire bir örtüşmez: derinlik testi, şeffaflık birleştirme veya MSAA gibi adımlar fragment çıktısından sonra da devreye girer. Bu yüzden fragment tarafını «nihai piksel rengi fabrikası» değil, henüz rekabet halinde olan renk adayı üreticisi gibi düşünmek daha doğrudur.

Vertex shader: köşe başına geometri

Her çağrıda, giriş öznitelikleri (position, uv, normal vb.) okunur; tipik akışta bunlar model → dünya → görüntü → projeksiyon zinciriyle çarpılarak clip space’e yakın bir temsilde gl_Position olarak sabitlenir. Burada yürütülen matematik, «bu üçgen ekranın hangi bölgesine projeksiyonla düşecek?» sorusuna odaklanır; piksel başına tekrarlanan ışık hesaplarının çoğu ise bir sonraki aşamaya bırakılır. İstisnai olarak displacement veya iskelet (skinning) tabanlı deformasyon gibi vertex ağırlıklı efektlerde «görünüş»e yakın kararlar burada verilebilir — yine de çıktı bir konum / ara veri paketidir, tampondaki nihai renk değildir.

Fragment shader: aday piksel başına görünüş

Rasterizasyon, üçgenin kapladığı ızgarayı dolaşır ve her örtüşme için bir fragment üretir; fragment gölgelendirici bu noktada devreye girer. Dokudan örnekleme, BRDF benzeri ışık terimleri, parlama maskeleri, prosedürel desen — çoğu «materyalin yüzü» burada hesaplanır. Vertex’ten gelen değerler (örneğin düzleştirilmiş normal veya UV) bu aşamaya taşınır; bu köprünün dilbilgisi ayrıntısı için aşağıdaki Depolama ve arabirim bölümüne geçebilirsiniz; burada yalnızca rol ayrımı yeterlidir.

Three.js’te MeshStandardMaterial gibi hazır sınıflar, bu iki programı motorun içinde bir araya getirir; siz yalnızca parametreleri beslersiniz. ShaderMaterial ile gövdeyi kısmen özelleştirir, RawShaderMaterial ile ise çoğu zaman önek, birleşim ve standart uniform setini kendiniz kurarak hatta doğrudan müdahil olursunuz — hangi yolun seçileceği bakım maliyetine ve taşınabilirlik beklentisine bağlıdır.

Temel sözdizimi ve veri tipleri

Tarayıcıda karşınıza çıkan dil aslında masaüstü OpenGL’in bire bir kopyası değil; çoğu zaman GLSL ES (Embedded Shading Language) profiline uyarlanmış bir alt kümedir. Buna rağmen çekirdek disiplin aynıdır: GLSL tip güvenli (strongly typed) bir dildir — her değişkenin türü kaynakta bellidir, birleştirme kuralları katıdır ve JavaScript’te alıştığınız «bir yerde sayı, bir yerde dize» esnekliği yoktur. İki farklı skaleri yan yana getirmek çoğu zaman yapıcı veya açık dönüşüm (float(…), int(…)) ile mümkün olur; aksi halde derleyici hatayı dosyanın satırına iliştirir — bu, ilk başta sinir bozucu görünse de çalışma anında sürpriz yaşamamanız için bir güvenlik ağıdır.

Integer ve boolean tipleri de sık görülür: int ile döngü sayacı veya doku seviyesi seçimi, bool ile dallanma maskeleri yazılır; int ile float arasında ise çarpma toplama yapmadan önce dönüşüm gerekir. Tam sayı bölme (3 / 2) ile kayan nokta bölme (3.0 / 2.0) farklı sonuç verebileceğini aklınızda bulundurun — prosedürel desenlerde sık düşülen bir tuzaktır.

float ve literaller

Grafik kodunda baskın skaler float’tır; literallerde çoğu zaman 1.0 biçimi beklenir (1 tek başına bir bağlamda int sayılabilir). Üstel gösterim
(1e-3) ve son ek (.0) kuralları derleyiciye göre hafifçe değişebilir; hata aldığınızda önce noktalı virgülü, sonra literal biçimini kontrol etmek ucuz bir teşhistir.

vec2, vec3, vec4

Aynı anlamsal paketi tek değişkende taşımak için vektör tipleri kullanılır: konum için xyz (çoğu zaman dördüncü bileşen w ile birlikte vec4), renk için rgb veya şeffaflıkla rgba, dokular için UV düzleminde vec2. Yapıcı çağrılar (vec3(1.0, 0.0, 0.5)) ve bileşenlere esnek erişim (swizzling) okunabilirliği artırır; swizzle kurallarının kısa özeti bu sayfada Altın kurallar bölümündedir — burada yalnızca «aynı matematiksel nesneyi tek pakette taşıma» fikrini sabitleyin.

mat2, mat3, mat4

Dönüşüm ve projeksiyon matrisleri tipik olarak mat4 ile temsil edilir; GPU dünyasında sütun öncelikli (column-major) düzen beklenir ve
mat4 * vec4 gibi çarpımlar vertex aşamasında günlük dil haline gelir. mat3 normal vektörlerini dünya uzayına taşırken; mat2 ise 2B dönüşümler veya doku uzayındaki küçük lineer haritalarda görülür. Matris–vektör boyut uyumsuzluğu yine derleme aşamasında yakalanır — JavaScript’te çoğu zaman sessizce genişleyen dizilerle karıştırmayın.

sampler2D ve doku okuma

sampler2D, bellekteki bir 2B dokuya bağlı opak bir tutacı temsil eder: üzerinde aritmetik «birleştirme» yapılmaz; onu yalnızca texture benzeri yerleşik işlevlere verirsiniz. Koordinatlar çoğu zaman normalleştirilmiş UV ile gelir; tam piksel ızgarasına kilitlenmek istediğinizde (örneğin veri dokusu) sürüme göre texelFetch gibi alternatifler devreye girer. İşlev adları ve aşırı yükleme kuralları GLSL ES sürümüne göre değişebilir; Three.js hazır yollarında bu ayrım çoğu zaman motorun ürettiği öneklerle yumuşatılır, ham kaynak yazarken ise bağladığınız sürüme bakmanız gerekir.

Bu bölüm «hangi veri uniform ile gelir?» sorusunu bilerek açmaz — o sözleşme Depolama ve arabirim başlığında ele alınır; burada yalnızca GPU içinde taşınan paketlerin şekli netleşir.

Depolama ve arabirim: uniform, attribute, varying / in · out

Veriyi «kim gönderiyor, kim tüketiyor, çizim çağrısı boyunca sabit mi değişiyor?» diye ayırmak, shader yazmayı sürdürülebilir kılar. Bu üçlü, Shader türleri bölümündeki vertex / fragment rol ayrımını veri sözleşmesine çevirir: vertex tarafı köşe başına okur, fragment tarafı aday piksel başına okur; ikisinin ortasında ise «köşeden gelen değer üçgen içinde nasıl karışır?» sorusunun cevabı yatar.

Sınır çoğu zaman tek bir draw call ile çizilen öbek etrafında çizilir: aynı program ve aynı uniform kümesiyle gönderilen tüm köşeler bir pakettir; paket değişince — başka materyal, başka tampon, başka pipeline durumu — farklı sözleşme devreye girer. Bu yüzden «uniform’u güncelledim ama sahne değişmedi» teşhisinde ilk bakılan yer, güncellemenin gerçekten o çizim çağrısından önce ve doğru isimle yapılıp yapılmadığıdır.

Uniform · çağrı başına sabit parametreler

Uniform değerler CPU tarafından (örneğin Three.js’te JavaScript ile material.uniforms üzerinden) GPU’ya yazılır ve aynı çizim çağrısı içinde hem tüm köşeler hem de o çağrıdan doğan tüm fragment’ler için aynı kalır: zaman, fare, çözünürlük, ışık rengi, model–görüntü matrisi, doku karışım katsayıları… Bir uniform’u «köşe başına farklılaştırmak» isterseniz aslında ihtiyaç duyduğunuz şey çoğu zaman uniform değil, aşağıdaki attribute veya örnekleme (instancing) verisidir — aksi halde her köşe için ayrı çağrı göndermek zorunda kalırsınız.

Attribute · köşe başına tampon verisi

Attribute’lar yalnızca vertex shader’da okunur; her invokasyon bir köşeyi temsil eder ve BufferGeometry içindeki öznitelik tamponlarından beslenir: konum, UV, tangent, renk, özel kanallar… Tip seçimi için üstteki Temel sözdizimi ve veri tipleri bölümüne dönün — burada önemli olan, bu verinin çizim çağrısı boyunca köşe kimliğiyle birlikte anlam kazanmasıdır. Örnekleme yolunda aynı geometri tekrarlanırken köşe verisine ek «örnek kimliği» kanalları bağlanır; bu da yine vertex girişidir, uniform değildir.

Varying ve in · out · köprü ve enterpolasyon

Vertex çıktısı ile fragment girdisi arasındaki eşleşme, klasik GLSL ES 1.00’da varying anahtar sözcüğüyle adlandırılır; GLSL ES 3.00’da ise vertex’te out, fragment’te aynı isimle in kullanılır. Üçgen rasterize edilirken köşelerden gelen bu ara değerler çoğu kanal için enterpolasyon ile iç noktalara yayılır — bu yüzden seyrek vertex normali, yumuşak görünen bir yüzey normali gibi davranır.

Her ara değer aynı şekilde «yumuşamaz»: flat niteliği ile üçgen başına tek değer taşımak isteyebilirsiniz (örneğin yüz kimliği veya sabit bir kanal). Ayrıca derinlik veya ekran uzayında doğrusal olmayan büyüme gerektiren durumlarda donanımın perspective-correct enterpolasyonu devreye girer; pratikte hâlâ «köşeden gelen üç değerin karışımı» düşüncesini korursunuz, yalnızca matematiksel ağırlık farklıdır. Bu bölümün somut kod yolu için İlk fragment örneği bölümüne bakabilirsiniz.

Sürüm ve bağlam kontrol listesi (WebGL 1 / 2 · Three.js)

Eski örneklerde varying ve gl_FragColor görürsünüz; WebGL 2 / GLSL ES 3.00’da ise fragment çıktısı çoğu zaman sizin tanımladığınız out vec4 değişkenine yazılır ve vertex–fragment köprüsü out/in çiftleriyle kurulur. Three.js hazır materyalleri bu ayrımı önek ve birleştirme ile yönetir; siz ShaderMaterial veya RawShaderMaterial ile kaynak yazarken hedeflediğiniz sürümü (WebGL bağlamı, WebGPU yolu, ham metin mi motor önekli mi) açıkça seçin — aksi halde «aynı değişken adı neden iki derleyicide farklı davranıyor?» sorusu çoğu zaman burada biter.

İlk fragment adımı: zaman ve UV ile renk

Aşağıdaki örnekler eğitim amaçlıdır: klasik GLSL ES 1.00 düşüncesiyle varying ve gl_FragColor kullanır; gerçek bir Three.js projesinde vertex tarafında vUv’nin nasıl besleneceği sahneye ve geometriye bağlıdır (tam ekran dörtgen, düzlem veya model). Burada amaç, paralel piksel düşüncesini ve main içinde renk üretimini göstermektir.

// Örnek: UV’yi fragment'a taşıyan minimal vertex (GLSL ES 1.00 tarzı)
attribute vec3 position;
attribute vec2 uv;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

varying vec2 vUv;

void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
precision mediump float;

uniform float uTime;
varying vec2 vUv;

void main() {
  float red = abs(sin(uTime));
  gl_FragColor = vec4(red, vUv.x, vUv.y, 1.0);
}

uTime uniform’ını JavaScript tarafında her kare güncellerseniz kırmızı kanal nefes alır; vUv ise ekranda konumsal renk geçişi üretir — «parlaklık maskesi» (glow) gibi efektler de aynı mantıkla, birkaç satır matematik ve örnekleme ile büyür.

Yazım alışkanlıkları: kısa «altın kurallar»

  • Noktalı virgül: İfadeleri ; ile kapatın; GLSL’de satır sonu «otomatik ekleme» beklentisi yoktur.
  • Swizzling: vec4 üzerinde .rgba, .xyzw veya .xyzz gibi esnek erişim; aynı vektörden yeni vektör türetmek okunabilirliği artırır.
  • Yerleşik matematik: sin, cos, pow, mix (iki değer / vektör arasında geçiş), dot, cross, clamp, smoothstep — procedural görünümlerin çoğu bu küçük yapı taşlarıyla kurulur.
  • Düşük hassasiyet bilinci: precision mediump float; gibi bildirimler mobil donanımda performans / kalite dengesini etkiler; fragment kökünde sık görülür.

Bir sonraki adım olarak, Three.js’te ShaderMaterial ile bu kaynakları sahneye bağlama, uniform köprüleri ve derleyici hatalarını okuma pratiği; ayrıca TSL (Three.js Shading Language) ve motorun hazır chunk’ları üzerinden aynı matematiği daha sürdürülebilir yazma seçenekleri Holodepth haritasında genişletilecektir.

Holodepth teknik notu

GLSL öğrenirken en hızlı geri bildirim döngüsü, küçük bir sahneye tek bir uniform ekleyip her karede yalnızca onu değiştirmektir: renk değişiyorsa veri köprüsü çalışıyordur; sabit kaldıysa önce bağlama ve isim eşleşmesine bakın. Derleyici hatasının tam metni tarayıcı konsolunda — vertex ve fragment ayrı ayrı raporlanır.