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,.xyzwveya.xyzzgibi 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.