Giriş
Daha önce Three.js'ye giriş yapmıştık. Bu makalede temel alacağım bilgiler bu makalede yer aldığından, henüz okumadıysanız okumanızı öneririm.
Gölgelendiricilerden bahsetmek istiyorum. WebGL mükemmel bir teknoloji. Daha önce de belirttiğim gibi Three.js (ve diğer kitaplıklar) zorluklardan sizi kurtarmak için harika bir iş çıkarıyor. Ancak belirli bir efekti elde etmek veya ekranınızda bu muhteşem efektlerin nasıl göründüğünü biraz daha ayrıntılı incelemek isteyebilirsiniz. Bu durumda gölgelendiriciler neredeyse kesinlikle bu denklemin bir parçası olacaktır. Ayrıca benim gibiyseniz son eğitimdeki temel bilgilerden biraz daha karmaşık bir konuya geçmek isteyebilirsiniz. Üç boyutlu gölgelendiriciyi çalıştırma konusunda bizim için çok fazla iş yaptığından Three.js'yi kullandığınızı varsayacağım. Başlangıçta gölgelendiricilerin bağlamını açıklayacağımı ve bu eğitimde biraz daha ileri seviye konulara değineceğimizi de belirtmek isterim. Bunun nedeni, gölgelendiricilerin ilk bakışta alışılmadık olması ve biraz açıklama gerektirmesidir.
1. İki Gölgelendiricimiz
WebGL, sabit ardışık düzenin kullanılmasını sunmaz. Bu, öğelerinizi kutudan çıkarıp oluşturmanıza olanak tanımadığı anlamına gelir. Ancak daha güçlü olan ve aynı zamanda anlaşılması ve kullanılması daha zor olan Programlanabilir Ardışık Düzenleme özelliğini sunar. Kısacası, Programlanabilir Ardışık Düzen, programcı olarak köşe noktalarının ve diğer öğelerin ekranda oluşturulmasından sorumlu olduğunuz anlamına gelir. Bu ardışık düzenin bir parçası olan iki tür gölgelendirici vardır:
- Köşe noktası gölgelendiricileri
- Parça gölgelendiricileri
Bu ikisinin de tek başına hiçbir şey ifade etmediğini kabul edeceğinizi düşünüyorum. Bu iki işlem hakkında bilmeniz gereken şey, her ikisinin de tamamen grafik kartınızın GPU'sunda çalıştığıdır. Bu, mümkün olduğunca fazla işi GPU'ya aktarmak ve CPU'muzu başka işlerle uğraşmaya bırakmak istediğimiz anlamına gelir. Modern GPU'lar, gölgelendiricilerin gerektirdiği işlevler için yoğun şekilde optimize edildiğinden bunları kullanabilmek çok faydalıdır.
2. Köşe noktası gölgelendiricileri
Küre gibi standart bir ilkel şekli alın. Köşelerden oluşuyor, değil mi? Bir köşe üstü gölgelendiricisine bu köşelerin her biri sırayla verilir ve bunlarla oynayabilir. Her biriyle ne yapacağı düğüm gölgelendiriciye bağlıdır ancak bir sorumluluğu vardır: Bir noktada gl_Position adlı bir değer ayarlamalıdır. Bu değer, düğümün ekrandaki nihai konumu olan 4D kayan vektördür. Bu süreç başlı başına oldukça ilginçtir. Çünkü aslında 3D bir konumu (x, y, z içeren bir köşe noktası) 2D bir ekrana yansıtmaktan bahsediyoruz. Neyse ki Three.js gibi bir şey kullanıyorsak gl_Position'u çok fazla ağırlaşmadan ayarlamak için kısa bir yöntemimiz var.
3. Parça Gölgelendiricileri
Böylece, köşe noktaları olan nesnemizi 2D ekrana yansıttık. Peki kullandığımız renkler ne olacak? Peki doku ve ışıklandırma? Tam da bu nedenle, kırıntı gölgelendirici kullanılır. Düğüm gölgelendiriciye çok benzer şekilde, parçacık gölgelendiricinin de yalnızca bir zorunlu görevi vardır: Parçacığın son rengini oluşturan başka bir 4D kayan vektör olan gl_FragColor değişkenini ayarlamalı veya atmalıdır. Peki, fragman nedir? Üçgenin köşelerini düşünün. Bu üçgen içindeki her pikselin çizilmesi gerekir. Parça, bu üç köşe noktası tarafından üçgenin her bir pikseli çizmek amacıyla sağlanan verilerdir. Bu nedenle, parçalar, bileşen köşelerinden oluşturulan değerleri alır. Bir köşe kırmızı, komşusu maviyse renk değerlerinin kırmızıdan mor ve maviye doğru ara değerlerle gösterildiğini görürüz.
4. Gölgelendirici Değişkenleri
Değişkenler söz konusu olduğunda üç tür beyan yapabilirsiniz: Tümler, Özellikler ve Değişkenler. Bu üç terimi ilk duyduğumda, daha önce çalıştığım hiçbir şeyle uyuşmadıkları için çok kafam karışmıştı. Ancak bunları şu şekilde düşünebilirsiniz:
Üniformalar, hem köşe birimi gölgelendiricilere hem de parça gölgelendiricilere gönderilir ve oluşturulan çerçevenin tamamında aynı kalan değerler içerir. Buna iyi bir örnek, ışığın konumu olabilir.
Özellikler, bağımsız köşe noktalarına uygulanan değerlerdir. Özellikler yalnızca köşe üstü gölgelendirici tarafından kullanılabilir. Örneğin, her köşenin farklı bir rengi olabilir. Özelliklerin köşelerle bire bir ilişkisi vardır.
Değişkenler, köşe üstü gölgelendiricide tanımlanan ve parçacık gölgelendiriciyle paylaşmak istediğimiz değişkenlerdir. Bunu yapmak için hem köşe gölgelendiricide hem de parçacık gölgelendiricide aynı türde ve ada sahip değişkeni değişken olarak tanımladığımızdan emin oluruz. Bu işlevin klasik bir kullanımı, ışıklandırma hesaplamalarında kullanılabileceği için bir köşenin normal değeridir.
Daha sonra, bunların gerçekte nasıl uygulandığını anlayabilmeniz için üç türün de kullanılacağı bir örnek vereceğiz.
Köşe üstü gölgelendiricileri ve parça gölgelendiricileri ile bu gölgelendiricilerin kullandığı değişken türleri hakkında konuştuk. Şimdi, oluşturabileceğimiz en basit gölgelendiricilere göz atalım.
5. Bonjourno World
İşte tepe noktası gölgelendiricilerinin "Merhaba Dünya"sı:
/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
Parçacık gölgelendirici için de aynısı geçerlidir:
/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
Çok karmaşık değil, değil mi?
Üçgen gölgelendiricide Three.js tarafından bize birkaç üniforma gönderilir. Bu iki üniforma, Model-Görüntü Matrisi ve Projeksiyon Matrisi olarak adlandırılan 4D matrislerdir. Bu özelliklerin tam olarak nasıl çalıştığını bilmeniz gerekmez ancak mümkünse bu özelliklerin nasıl çalıştığını anlamak her zaman en iyisidir. Özetlemek gerekirse, bu noktalar, köşenin 3D konumunun ekrandaki nihai 2D konuma nasıl yansıtıldığını gösterir.
Three.js bunları gölgelendirici kodunuzun üst kısmına eklediği için bunları yukarıdaki snippet'ten çıkardım. Bu nedenle, bunları ekleme konusunda endişelenmenize gerek yoktur. Aslında ışık verileri, köşe renkleri ve köşe normalleri gibi çok daha fazlasını ekler. Bunu Three.js olmadan yapıyorsanız tüm bu forma ve özellikleri kendiniz oluşturup ayarlamanız gerekir. Gerçek hikaye.
6. MeshShaderMaterial kullanma
Tamam, bir gölgelendirici oluşturduk. Peki bunu Three.js ile nasıl kullanacağız? Bu işlemin çok kolay olduğu ortaya çıktı. Daha çok şu şekildedir:
/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader: $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});
Ardından Three.js, bu malzemeyi verdiğiniz ağa bağlı gölgelendiricilerinizi derleyip çalıştırır. Bu işlemden daha kolay bir yöntem yoktur. Muhtemelen öyledir. Ancak tarayıcınızda 3D çalıştırmaktan bahsediyoruz. Bu nedenle, belirli bir düzeyde karmaşıklık beklediğinizi tahmin ediyorum.
MeshShaderMaterial'ımıza iki özellik daha ekleyebiliriz: üniformalar ve özellikler. Her ikisi de vektör, tam sayı veya kayan nokta alabilir ancak daha önce de belirttiğim gibi, uniformlar tüm çerçeve için (yani tüm köşeler için) aynıdır. Bu nedenle, tek değer olma eğilimindedirler. Ancak özellikler, köşe başına değişken olduğundan bir dizi olması beklenir. Özellikler dizisindeki değer sayısı ile ağdaki köşe sayısı arasında bire bir ilişki olmalıdır.
7. Sonraki Adımlar
Şimdi bir animasyon döngüsü, köşe noktası özellikleri ve üniforma eklemek için biraz zaman harcayacağız. Ayrıca, köşe üstü gölgelendiricinin parça gölgelendiriciye bazı veriler gönderebilmesi için değişen bir değişken de ekleriz. Sonuç olarak, pembe olan küremiz yukarıdan ve yandan aydınlatılmış gibi görünecek ve titreşecek. Bu biraz kafa karıştırıcı olabilir ancak umarım üç değişken türünü ve bunların birbirleriyle ve temel geometriyle olan ilişkilerini iyice anlamışsınızdır.
8. Sahte Işık
Renkleri, düz renkli bir nesne olmaması için güncelleyelim. Three.js'in ışığı nasıl işlediğine bakabiliriz ancak bunun şu anda ihtiyacımız olandan daha karmaşık olduğunu tahmin edebileceğiniz gibi, bunu taklit edeceğiz. Three.js'in bir parçası olan harika gölgelendiricileri ve Chris Milk ile Google'ın yakın zamanda yayınladığı muhteşem WebGL projesi Rome'daki gölgelendiricileri kesinlikle incelemelisiniz. Şimdi gölgelendiricilerimize dönelim. Her bir köşe için normal değeri Fragment Shader'a sağlayacak şekilde Vertex Shader'ımızı güncelleyeceğiz. Bunu çeşitli yöntemlerle yaparız:
// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;
void main() {
// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
ve Fragment Shader'da aynı değişken adını ayarlayacağız ve ardından köşe normalinin, kürenin üstünden ve sağ tarafından gelen bir ışığı temsil eden bir vektörle nokta çarpımını kullanacağız. Bunun net sonucu, 3D paketteki yönlü ışığa benzer bir efekt elde etmemizi sağlar.
// same name and type as VS
varying vec3 vNormal;
void main() {
// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
// ensure it's normalized
light = normalize(light);
// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));
// feed into our frag colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
Nokta çarpımının işe yaramasının nedeni, iki vektör verildiğinde iki vektörün ne kadar "benzer" olduğunu belirten bir sayı elde edilmesidir. Normalleştirilmiş vektörler tam olarak aynı yönü gösteriyorsa 1 değerini alırsınız. Oklar zıt yönlere işaret ediyorsa -1 alırsınız. Bu sayıyı alıp ışıklandırmamıza uyguluyoruz. Bu nedenle, sağ üstteki bir köşe noktasının değeri 1'e yakın veya 1'e eşit olur (yani tamamen aydınlatılmış olur), kenardaki bir köşe noktasının değeri 0'a yakın olur ve arka taraftaki köşe noktasının değeri -1 olur. Negatif değerler için değeri 0 olarak sınırlandırırız ancak sayıları girdiğinizde gördüğümüz temel aydınlatmayı elde edersiniz.
Sırada ne var? Bazı köşe konumlarını değiştirmeyi denemek iyi olabilir.
9. Özellikler
Şimdi her bir köşeye bir özellik aracılığıyla rastgele bir sayı eklemek istiyoruz. Bu sayıyı, köşe noktasını normali boyunca dışarı itmek için kullanırız. Net sonuç, sayfayı her yenilediğinizde değişecek tuhaf bir diken topu olacaktır. Henüz animasyonlu değildir (bu daha sonra gerçekleşir) ancak birkaç sayfayı yenilediğinizde rastgele olduğunu görebilirsiniz.
Özelliği köşe düğümü gölgelendiricisine ekleyerek başlayalım:
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
normal *
vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
Nasıl görünüyor?
Pek bir fark yok. Bunun nedeni, MeshShaderMaterial'da özelliğin ayarlanmamış olmasıdır. Bu nedenle, gölgelendirici bunun yerine sıfır değerini kullanır. Şu anda yer tutucusu gibi. Bir saniye içinde özelliği JavaScript'teki MeshShaderMaterial'a ekleyeceğiz ve Three.js bu ikisini otomatik olarak birbirine bağlayacak.
Ayrıca, tüm özellikler gibi orijinal özellik salt okunur olduğu için güncellenen konumu yeni bir vec3 değişkenine atamamız gerektiğini de belirtmek isteriz.
10. MeshShaderMaterial'ı güncelleme
Hemen MeshShaderMaterial'ı yer değiştirmemizi desteklemek için gereken özellikle güncelleyelim. Hatırlatma: Özellikler köşe başına değerlerdir. Bu nedenle, küremizde köşe başına bir değere ihtiyacımız vardır. Aşağıdaki gibi:
var attributes = {
displacement: {
type: 'f', // a float
value: [] // an empty array
}
};
// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}
Şimdi de bozuk bir küre görüyoruz. Ancak tüm yer değiştirme işleminin GPU'da gerçekleşmesi çok güzel.
11. Animating That Sucker
Bunu animasyonlu hale getirmemiz gerekiyor. Bunu nasıl yapıyoruz? Bunun için iki şey yapmamız gerekiyor:
- Her karede ne kadar yer değiştirmenin uygulanacağını animasyonlu olarak gösteren bir üniforma. -1 ile 1 arasında oldukları için bunun için sinüs veya kosinüs kullanabiliriz.
- JS'de animasyon döngüsü
Üniformayı hem MeshShaderMaterial'a hem de Vertex Shader'a ekleyeceğiz. Öncelikle Vertex Shader:
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
Ardından MeshShaderMaterial'ı güncelleriz:
// add a uniform for the amplitude
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
};
// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
Şu an için gölgelendiricilerimiz hazır. Ancak şu anda geriye doğru bir adım atmış gibiyiz. Bunun başlıca nedeni, genlik değerimizin 0 olması ve bunu yer değiştirmeyle çarptığımız için hiçbir değişiklik görmememizdir. Ayrıca animasyon döngüsünü, 0 değerinin hiçbir zaman başka bir değere dönüşmeyeceği şekilde ayarladık.
Artık JavaScript'imizde render çağrısını bir işleve sarmalayıp ardından requestAnimationFrame kullanarak bu işlevi çağırmamız gerekiyor. Burada üniformanın değerini de güncellememiz gerekiyor.
var frame = 0;
function update() {
// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;
renderer.render(scene, camera);
// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);
12. Sonuç
Hepsi bu kadar! Artık garip (ve biraz da kafa karıştırıcı) bir şekilde titreşen bir animasyon görebilirsiniz.
Gölgelendiriciler hakkında daha birçok konu ele alabiliriz ancak bu girişin faydalı olduğunu umuyoruz. Artık gölgelendiricileri gördüğünüzde anlayabilir ve kendi muhteşem gölgelendiricilerinizi oluşturabilirsiniz.