- Ana sayfadaki basit CSS
border-radius animasyonunu çıkış noktası alarak, birkaç satırlık fragment shader ile akıcı bir blob animasyonunu doğrudan uyguluyor
- Shader, GPU’da piksel koordinatlarını renge dönüştüren küçük bir programdır; paralel yürütme sayesinde hızlıdır, ancak durum paylaşımı ve üst düzey soyutlamalar konusunda büyük kısıtları vardır
- GLSL’in temel örneği,
vUv koordinatlarını gl_FragColor’ın RGBA değerlerine eşler; varying, uniform, vektör tipleri ve swizzling gibi söz dizimini öğrenme akışı sunar
- Daire ve blob’lar
distance(), step(), smoothstep() ve Signed Distance Function (SDF) ile oluşturulur; birden fazla SDF min() veya smooth minimum ile birleştirilir
u_time ile topların hareketi oluşturulup u_mouse merkez dizisine eklendiğinde, kullanıcıların fareyle bir kısmını kontrol edebildiği etkileşimli shader’a dönüşür
Shader’ların yaptığı iş ve kısıtları
- Shader, GPU’da çalışan küçük bir programdır ve en temel hâliyle piksel koordinatını girdi olarak alıp renk üretir
- Video oyunlarında ışıklandırma, özel efektler, çizgi film tarzı render gibi görsel efektler için kullanılır ve modern oyun grafiklerinin önemli temellerinden biridir
- Hızın anahtarı, birçok piksel üzerinde aynı anda çalışan paralelleştirmedir
- Bu giriş, tarayıcı dostu OpenGL Shading Language’a, yani GLSL’e odaklanır
- Performansın bedeli olarak shader’ların küçük ve düşük seviyeli yazılması gerekir
- Üst düzey soyutlamalara veya kütüphane içe aktarmaya yaslanmak zordur
- Paralel yürütmenin doğası nedeniyle pikseller arasında veri saklama veya paylaşma mümkün olmayan durumsuz ve belleksiz bir modeldir
Koordinatı renge dönüştüren ilk GLSL örneği
- Shader, normalize edilmiş koordinatları RGBA renge dönüştürür
- Koordinatlar genellikle 0 ile 1 arasında normalize edilir
(0, 0) sol altı, (1, 1) sağ üstü ifade eder
- Bu koordinatlar geleneksel olarak
st veya uv diye adlandırılır
- En basit örnek, x koordinatı büyüdükçe kırmızının, y koordinatı büyüdükçe yeşilin arttığı bir gradyandır
varying vec2 vUv;
void main() {
vec2 st = vUv;
gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}
- Mavi gradyan,
gl_FragColor = vec4(0.0, 0.0, st.x, 1.0); gibi mavi kanala x koordinatı verilerek oluşturulur
- Söz dizimindeki önemli unsurlar şunlardır
varying: değeri pikselden piksele değişen girdi
uniform: tüm piksellerde aynı olan girdi
vec2, vec3, vec4, mat2, mat3: C’de olduğu gibi tipi açıkça belirtilen vektör ve matris tipleri
- swizzling:
vec4(1, 2, 3, 4).xy gibi bir vektörün bir kısmını alma gösterimi
gl_FragColor: main() sonunda her pikselin rengini belirleyen çıktı
distance(), step(), smoothstep() ile daire oluşturma
- Daire gibi keskin şekiller çizerken de
drawCircle() benzeri bir fonksiyon değil, matematiksel mesafe kullanılır
- Geçerli piksel ile dairenin merkezi arasındaki mesafe
distance(vec2 p1, vec2 p2) ile hesaplanabilir
- Mesafeyi doğrudan renge eşlerseniz dairesel bir gradyan elde edilir; dolu daire ise
step(float threshold, float value) ile yapılır
- Mesafe eşik değeri aşarsa 1
- Aksi hâlde 0
step() geçişi keskin yaptığı için daire kenarında aliasing oluşabilir
- Daha yumuşak kenarlar
smoothstep(float t_start, float t_end, float x) ile oluşturulabilir
Signed Distance Function ile şekil ifade etme
- Signed Distance Function (SDF), uzaydaki bir noktanın bir şekilden ne kadar uzakta olduğunu işaretli mesafe olarak ifade eder
- Şeklin içi negatif
- Dışı pozitif
- Sınırı 0’dır
- Dairenin SDF’si, merkeze göre mesafeden yarıçap çıkarılarak oluşturulabilir
float circleSDF(vec2 p, float r) {
return length(p) - r;
}
- Piksel konumu
uv temel alınarak nokta kaydırıldığında, UV uzayındaki herhangi bir konumda bulunan daireye olan mesafe hesaplanabilir
d < 0.0 ise piksel dairenin içindedir; bu yüzden farklı bir renkle boyanabilir
- Daha çeşitli 2D SDF şekilleri Inigo Quilez’in kapsamlı listesinde görülebilir
Birden çok SDF’yi birleştirerek blob oluşturma
- SDF’lerle Boole işlemleri kullanarak yeni şekiller oluşturmak kolaydır
- İki SDF’nin birleşimi, iki mesafenin
min() değerini almakla elde edilir
- İki şekilden herhangi birinin içindeyse minimum mesafe negatif olur
- İkisi de dışındaysa minimum mesafe pozitif olur
1. - smoothstep() kullanılmasının nedeni, step() ve smoothstep()’in eşik değerinden büyük mesafelerde, yani şeklin dışında 1 üretmesidir
- Basit
min(), iki dairenin birleştiği noktada keskin bir süreksizlik oluşturur
- Blob gibi yumuşak biçimde karışmasını istiyorsanız smooth minimum kullanılır
- Ek argüman
k, yumuşaklığın şiddetini kontrol eder
- Örnek, polynomial smooth min fonksiyonunu kullanır
float smin(float a, float b, float k)
{
float h = max( k-abs(a-b), 0.0 )/k;
return min( a, b ) - h*h*k*(1.0/4.0);
}
Zaman uniform’u ile metaball animasyonu oluşturma
- Shader’a, slider gibi isteğe bağlı değerler uniform olarak aktarılabilir
- Animasyon, JavaScript’te oluşturulan
u_time değerinin shader girdisi olarak verilmesi ve bunun daire merkez koordinatlarının hesaplanmasında kullanılmasıyla yapılır
- Shader temelde saniyede 60 kez yeni
u_time değeriyle güncellenerek akıcı bir hareket üretir
- Daire merkezleri
sin, cos gibi periyodik fonksiyonlarla salındırılabilir
- Birden çok daireyi metaball olarak birleştirirken merkez koordinatları bir dizide tutulur ve SDF döngüyle biriktirilir
vec2 centers[4] = vec2[4](c1,c2,c3,c4);
float d = 99.;
for (int i = 0; i < 4; i++) {
vec2 c = centers[i];
float sdf = circleSDF(uv, c, .1*u_slider);
d = smin(d, sdf, K);
}
- Ortaya çıkan blob işlevsel olarak çalışır, ancak tek renklidir; bu yüzden sonrasında renk ve etkileşim eklenir
Fareyle kontrol edilen son etkileşim
- Son adım, fare koordinatlarını
u_mouse uniform’u olarak alıp blob içindeki toplardan birini kullanıcının doğrudan kontrol etmesini sağlamaktır
uniform vec2 u_mouse;
- Fare koordinatlarını top merkezleri dizisine eklemek, etkileşimi tek satırda devreye alır
vec2 centers[5] = vec2[5](c1,c2,c3,c4,u_mouse);
- Nihai shader, farenin y eksenini canvas koordinatlarına uyacak şekilde ters çevirir ve hareket eden 4 merkezi fare merkeziyle birlikte birleştirir
- Renkler
mix(colorA, colorB, percent) birkaç kez kullanılarak harmanlanır
percent boolean gibi kullanıldığında if/else’e benzer çalışır
metaball değeri, merkez noktası mesafesi, shine ve membrane hesaplaması nihai renge yansıtılır
- Aynı ilkeleri anladığınızda Blender’s shader nodes veya Unity’s Shader Graph gibi düğüm tabanlı shader editörlerinin içeride nasıl çalıştığını da daha iyi anlayabilirsiniz
Bakılabilecek diğer kaynaklar
1 yorum
Hacker News yorumları
Sonunda internette yazı yazıp kendimi ortaya koyacak cesareti topladım. Eskiden beri shader öğrenmek istiyordum ve öğrenme sürecimi belgeleyip başkalarıyla paylaşmanın iyi olacağını düşündüm
Samimi blog yazarlığı, her şeye gücü yeten algoritmanın gölgesinde görünmez şekilde ölüyor; internet de Star Wars Bölüm IV'ün tam ortasına gelmiş gibi hissettiriyor. Tek geri bildirimim var: teşekkürler, umarım daha çok yazarsın
Ustaların shader'larla neler yapabildiğini görmek istersen Inigo Quilez'i ve shader sanatını önermek isterim: https://www.youtube.com/watch?v=BFld4EBO2RE
Ek: Yazarı olduğunu fark etmemişim. Yazı çok iyi olmuş; shader kodlama sanatını daha erişilebilir ve interaktif biçimde ele alan eğitimler arıyordum
smoothstep(0.0f, 0.01f, dist);gibi sabitleri kabaca seçmek yerinesmoothstep(fwidth(dist), -fwidth(dist), dist);kullanman yeterliYazı oldukça iyi, ama shader'ların temel sorununu fazla hafife alıyor.
Shader'lar çoğu programın ve uygulamanın istemediği bir baş belası. 3D üçgenleri sever, GPU da bu soyutlamaya iyi uyar; shader'lar da bu üçgenlerin üzerinde enterpolasyon yapmakta faydalıdır.
Ama 3D olmayan neredeyse her yerde üçgenler pek iyi değildir. 2D render işlemleri path ister, font render işlemleri path ya da pixmap ister, GUI'ler için path ve pixmap çok daha uygundur, compositor pixmap ister, video decoder ise pixmap ve paralel render ister.
3D dışı tarafın istediği şey dikdörtgen pixmap'ler ve bu pixmap'lere doğrudan hesaplama erişimidir; GPU bunu pek sevmez, shader'lar da buraya pek iyi oturmaz
Üst düzeyde istenen biçim olmadığı doğru, ama daha yüksek seviyeli soyutlamaları uygulamak için temel olarak yeterince iyi. Donanımda doğrudan desteklenebilir belki, ama avantajı bariz değil. Kimse CPU'nun
fordöngüsünü mimari düzeyde desteklemediğinden şikayet etmiyor.Tekrar okuyunca GPU ile ilgili sorunun ne olduğunu tam anlayamadım. Vertex işleme hattını tamamen yok sayıp tek bir tam ekran dikdörtgen çizebilir ya da compute shader kullanabilirsin; GPU bunu gayet iyi işler. Bağlantı verilen yazı da bu tür şeylerden bahsediyor
En azından 2000'lerde düşünce biçimi buydu. Yazılım daha karmaşık hale geldikçe GPU istenen optimizasyon olabilir, ama GPU pipeline'ı her zaman katı ve kapalıydı; tek çekirdekten çok çekirdekli programlamaya geçişteki paradigma da çoğu zaman farklı algoritmalar gerektirir.
Genel amaçlı GPU programlama üçgenlere bağlı kalmaktan kurtarır, ama o paralellikten yararlanmak için hâlâ tamamen farklı bir yaklaşım gerekir
Harika! Son zamanlarda SDF tavşan deliğine biraz dalmıştım. iq sitesinin linkini koymana sevindim; oradaki materyaller gerçekten çok iyi.
Onun “happy bouncing” shader'ına link vermeden edemem. Bence olağanüstü: https://www.shadertoy.com/view/3lsSzf
Yapım sürecini gösteren 6 saatlik bir YouTube videosu da var. Yaklaşık 500 satırlık dopdolu bir kod
Bu konuya ilgi duymayı birkaç kez denedim ama erişilebilir bir giriş noktası bulamamıştım; bu tanıtım yazısında buldum. Gerçekten eğlenceli ve oyunbaz bir yaklaşım, hoşuma gitti; sonraki yazıyı da bir an önce okumak istiyorum
Küçük bir kusur arama olacak ama cel shading'den bahsederken
cellyazmışsın. Bu terim, elle çizilmiş animasyonda kullanılan cel'den ve bu gölgelendirmenin kuantize edilmiş tonlarından geliyorGerçekten iyi. Sanatçılıktan programcılığa geçmiş biri olarak bazen grafik programlamayı kurcalama isteği duyuyorum.
Birkaç çok temel shader yazdım, ama matematik işin içine girmeye başlayınca — aslında epey erken bir noktadan itibaren — duvara tosluyorum. Bilgisayar bilimi değil güzel sanatlar okuduğum için matematik becerim fiilen yok denecek kadar az.
Her neyse, iyi yazılmış ve yazıyı sevdim
Harika bir giriş yazısı; umarım devamı gelir. Böyle yazılar çok sık güzel bir girişle başlayıp sonra yarıda kalıyor
Shader’larla hiç uğraşmadığım için çok temel bir soru olabilir. Bir oyunun tek bir karesinde, özünde her şeyi shader mı çiziyor?
Yoksa üçgen, dörtgen, daire gibi temel şekiller var da shader bunların üzerine gölge ya da kenar düzeltme gibi şeyler mi ekliyor?
Örneklere bakınca sahnedeki herhangi bir nesneyi çizen bir shader yazılabiliyor, sonra başka shader’lar birleştirilerek gölge ve aydınlatma gibi şeyler elde ediliyor gibi görünüyor. Benim çok sınırlı çizim deneyimimde şekiller çiziyordum, shader ile çizmiyordum. Shader’ın nesnenin kendisini çizmediğini düşünmüştüm
GPU, üç tepe noktası ve her tepe noktasına bağlı normal gibi verilerle tanımlanan üçgen benzeri soyut vektör şekilleri, çıktı tamponunda o şeklin kapladığı her piksel için bir tane, multisampling varsa daha da fazla olacak şekilde bir fragment akışına dönüştürür. Bu kısmın tamamını donanım yapar
Fragment, piksel koordinatları ve kullanıcı tarafından sağlanan verilerden oluşur. Bu veri sabit olan uniform ya da az önce bahsedilen tepe noktası verisinin üçgen yüzeyi boyunca enterpole edilmiş hâli olan varying olabilir. Bu enterpolasyonu da donanım yapar ve programlanamaz
Fragment shader, fragment’ı girdi olarak alır, o verilere göre rengi hesaplar ve birkaç aşamadan sonra ekrandaki ya da offscreen buffer’daki ilgili piksel rengi olarak çıktı verir. Bu tek renk de olabilir, karmaşık bir aydınlatma hesabı da olabilir
GPU rendering’de tüm bunlar büyük ölçekte paralel gerçekleşir; çok sayıda fragment aynı anda işlenir. Shader saf ve durumsuz bir fonksiyondur; yalnızca girdilerine erişebilir ve yapabildiği etki de renk ve derinlik değeri gibi birkaç şeyi döndürmekten ibarettir
Özetle GPU donanımı, her üçgeni çizmek için hangi piksellerin doldurulması gerektiğini hesaplar; fragment shader ise bu piksellerin her birinin renk değerini belirler
Fragment shader sahnenin “üzerine” çizmeden önce vertex, tessellation gibi başka aşamalar temel şekilleri çizer
Fragment shader konusunda da anlatılandan çok daha fazlası var. Örneğin deferred rendering[2] buna girer ve o da kendi başına büyük bir konudur
1: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipe...
2: https://learnopengl.com/Advanced-Lighting/Deferred-Shading
Sabit işlevli donanım aşamaları ile programlanabilir aşamaların karıştığı bir graphics pipeline vardır. Üst düzeyde bakarsak: 1) GPU, CPU’dan bir 3B üçgen kümesi alır, 2) vertex shader 3B üçgen tepe noktalarını piksel koordinatlarına sahip 2B üçgen tepe noktalarına dönüştürüp düzleştirir, 3) GPU 2B üçgeni rasterize ederek tam olarak hangi piksellerin üçgen tarafından kaplandığını belirler, 4) kaplanan her piksel için pixel shader çalıştırılır ve piksel rengini belirler, 5) ortaya çıkan piksel rengi framebuffer’a kaydedilir. Bu sırada mevcut renkle blend edilebilir
Bu pipeline, tüm frame çizilene kadar farklı üçgen mesh’leri ve shader’larla birçok kez tekrarlanır
Renk, gölge, shading, görüntü efektleri, genel görüntü işleme gibi işler, birden fazla veri dizisini birleştiren paralel hesaplamalarla yapılır. Tepe noktaları ve öznitelikleri, kaynak texture’lar, önceden hesaplanmış fonksiyonlar, hedef texture’lar, buffer’lar vb. buna dahildir
Örneğin ışık ve gölge elde etmek için shader’ın spotlight’ın konumu ve yönü gibi, muhtemelen global değişkenlere erişebilmesi gerekir. Kompozit aydınlatma çoğu zaman birden fazla shader pass’inin birleştirilmesiyle elde edilir. Global aydınlatma için bir temel pass ve her ışık için bir pass gibi yapılandırılır; her pass kelimenin tam anlamıyla ışık ekler
Şimdi, ışık kaynağının engellendiği piksellere, yani gölgelere ışık eklememek için en yaygın teknik Z-buffer denen şeyi kullanmaktır. Aslında bu bir floating-point texture’dır. Sahnedeki her ışığın nereye kadar ulaştığını bilmek istediğimiz için, tüm aydınlatmayı uygulamadan önce sahnedeki tüm katı geometriyi birleştiren tek bir shader pass ayarlarız; ışığın konumunu ve yönünü kamera dönüşümü gibi kullanır ve yalnızca nesneye olan mesafeyi Z-buffer’a yazan özel bir shader kullanırız
Daha sonra uzaydaki bir noktaya ışığın ulaşıp ulaşmadığını bilmek istediğimiz her seferinde, biraz geometri hesabından sonra bu Z-buffer’ı sample eder ve o yönde kaydedilmiş değerle söz konusu noktanın mesafesini karşılaştırırız. Bu bol hatalı olabilir ve hassasiyet hataları da yaygındır. İyi bir engine bunu zaten sizin için yapar, ama süreci değiştirmenize de izin verir
Geri kalan her şey bu konunun varyasyonlarıdır. Deferred rendering, rengi değil veriyi ara texture’lara render edip daha sonra işleyerek rengi elde etme yöntemidir. Blur efekti, render texture üzerinde 2B konvolüsyonla, örneğin Gaussian kernel ile yapılır. Tessellation shader, vertex shader’da yeni geometri üretmekle ilgilidir. Metin çizimi bile font atlası ve küçük dikdörtgenler üzerinden yapılır
Bu yüzden yetenekli sanatçılar fragment shader içinde sınırları zorlar ve performans ödünleriyle mücadele eder
Fragment shader daha yaygın olarak tam ekran filtre efektlerinde, örneğin renk düzeltmede kullanılır
Shader’lar temel nesnelerin texture ve material’larını oluşturmak için de kullanılır. Material artist’ler çoğu zaman shader matematiğiyle texture üretir
Pek çok görsel efekt, shader’ların yaratıcı kullanımıyla yapılır
Shader’lar GPU’da paralel, dalga benzeri bir biçimde çalıştırılır. Çok sayıda thread aynı wave içinde aynı veri üzerinde çalışır
Bazı durumlarda shader, CPU’daki dallanmalı koddan çok daha hızlıdır. Shader’lar bazı rendering verilerine de daha kolay erişebilir
Bu yüzden yaratıcı özel efektler oluşturmak için iyi bir alan. Oyunlarda yüzey ayrıntısı yüksek nesneler, bu ayrıntıyı shader’a taşımak için yaygın hedeflerdir. Deniz yüzeyi, tessellation mesh’leri vb. buna örnektir; GPU güçlü ve esnek olduğu için başka pek çok kullanım alanı da vardır.
Görüntü titreşen bir gürültü gibi görünüyorsa, tarayıcıda görüntüyü kopyalayıp başka bir yere yapıştırarak çözdüm. Böylece görüntü düzgün görülebiliyor.
Bu bir Imgur bağlantısı. İlk görüntü tarayıcıda görünen ekranın ekran görüntüsü; diğerleri ise Imgur'a yapıştırıldıktan sonraki gerçek görüntüler.
https://imgur.com/a/F4203rz
Shader'ları ben şöyle anlıyorum.
CPU'da bir çizgi çizerken, A noktası ile B noktası arasındaki her pikselin üzerinden geçip pikselleri tek tek sırayla çizen bir fonksiyon çalışır. Çizgideki piksel sayısı kadar tam olarak adım vardır ve bir kez çalıştırılır.
GPU'da bir çizgi çizerken ise, ilgili pikselin çizginin üzerinde olup olmadığını kontrol eden ve öyleyse çizen bir fonksiyon çalışır. Çizgiden epey uzaktaki pikseller de dahil olmak üzere ekrandaki tüm piksellerde aynı anda çalıştırılır.
Bu doğru mu?
İkincisi, GPU'nun tüm ekran için piksel shader'ını çalıştırması da gerekmez. Üçgenler kullanarak shader'ı yalnızca istediğiniz herhangi bir şekil için çalıştırabilirsiniz. Bu yüzden bir çizgiyi verimli çizmenin yolu, istediğiniz çizgi geometrisine karşılık gelen iki üçgeni GPU'ya göndermek ve piksel shader'ını yalnızca bu üçgenlerin üst üste geldiği pikseller için çalıştırmaktır. Bu çok daha verimlidir.