Gölgelendiricilere giriş

Paul Lewis

Giriş

Daha önce Three.js'ye giriş yapmıştık. Bu makalede temel alacağım bilgiler bu makalede yer aldığından, makaleyi 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) zorluklarla ilgili tüm ayrıntıları sizin adınıza üstleniyor. 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. Ayrıca başlangıçta gölgelendiricilerin bağlamını açıklayacağım, bu eğiticinin ikinci bölümünde ise biraz daha gelişmiş bir alana gireceğimiz yer olacaktır. 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 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öşeleri alma ve diğer işlerin ekranda açısından sorumlu olma sorumluluğunu üstlenirsiniz. Bu ardışık düzenin bir parçası olan iki tür gölgelendirici vardır:

  1. Köşe gölgelendiricileri
  2. Parça gölgelendiricileri

Bunların ikisi de eminim ki siz de kabul edeceksiniz. Bu uygulamalar tamamen grafik kartınızın GPU'sunda çalışır. Bu, mümkün olduğunca fazla işi GPU'ya aktarıp CPU'muzu başka işlerle uğraşmaya bırakacağımız 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ölgelendiriciler

Böylece, köşe noktaları olan nesnemizi elde ettik ve bunları 2D ekrana yansıttık. Peki kullandığımız renkler ne olacak? Peki ya 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ığımızı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 ara değerler 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şkenlerden bahsederken yapabileceğiniz üç beyan vardır: Üniformalar, Özellikler ve Varyantlar. Bu üç terimi ilk duyduğumda, daha önce çalıştığım hiçbir şeyle uyuşmadıkları için çok kafam karışmıştı. Bunları şu şekilde düşünebilirsiniz:

  1. Ü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 olarak ışığın konumu gösterilebilir.

  2. Özellikler, bağımsız köşe noktalarına uygulanan değerlerdir. Özellikler yalnızca köşe gölgelendiricide kullanılabilir. Örneğin, her köşenin farklı bir rengi olabilir. Özelliklerin köşelerle bire bir ilişkisi vardır.

  3. Değişkenler, köşe düğümü gölgelendiricisinde 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 gölgelendiricileri, parça gölgelendiricileri ve ilgilendikleri değişken türlerinden bahsettik. Şimdi, oluşturabileceğimiz en basit gölgelendiricilere bakmakta fayda var.

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);
}

Ama ç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ı. Bu, 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()
});

Oradan Three.js, ilgili materyali verdiğiniz ağa bağlı gölgelendiricilerinizi derler ve ç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çacık 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

Renklendirmeyi, düz renkli bir nesne olmayacak şekilde güncelleyelim. Three.js'in ışığı nasıl işlediğine bakabiliriz ancak bunun şu anda ihtiyacımız olandan daha karmaşık olduğunu anlayacağınızı umuyorum. Bu nedenle, ışığı taklit edeceğiz. Üç Ş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, bize 3D paketteki yönlü ışığa benzer bir efekt 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);

}

Bu nedenle nokta çarpımı, iki vektör elde edildiğinde iki vektörün ne kadar "benzer" olduğunu belirten bir sayı elde etmenizi sağlar. 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 köşe noktası 1'e yakın veya 1'e eşit bir değere sahip, yani tamamen aydınlatılmış, kenardaki tepe noktasının değeri 0'a yakın, arkayı yuvarlayan ise -1 olur. Negatif her şey için değeri 0'a sınırlarız, ancak sayıları taktığınızda sonuçta gördüğümüz temel ışıklandırma elde edersiniz.

Sırada ne var? Birkaç köşe konumuyla uğraşmayı denemek güzel olurdu.

9. Özellikler

Şimdi her bir köşeye bir özellik aracılığıyla rastgele bir sayı eklemek istiyorum. 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 animasyon oluşturulmayacak (daha sonra gerçekleşecek), ancak birkaç sayfa yenilendiğinde rastgele hale getirildiği gösterilir.

Ö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ür?

Pek bir fark yok. Bunun nedeni, MeshShaderMaterial'da özelliğin ayarlanmamış olmasıdır. Bu nedenle, shader bunun yerine sıfır değerini kullanır. Şu an için bu bir yer tutucu gibi bir şey. Birazdan, özelliği JavaScript'teki MeshShaderMaterial'a ekleyeceğiz, Three.js bu iki öğeyi bizim için otomatik olarak birbirine bağlayacaktır.

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

Şimdi, MeshShaderMaterial'ımızı, yer değiştirme sistemimize güç vermek 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 karışık bir küre görüyoruz. İşin güzel yanı, tüm yer değiştirmenin GPU'da gerçekleşmesi.

11. Animating That Sucker

Bunu kesinlikle animasyonlu hale getirmeliyiz. Bunu nasıl yaparız? Bunun için iki şey yapmamız gerekiyor:

  1. Her karede ne kadar yer değiştirmenin uygulanacağını animasyonlu olarak gösteren bir üniforma. -1'den 1'e kadar çıktığından bunun için sinüs veya kosinüs kullanabiliriz
  2. JS'de bir 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 attığımızı görüyoruz. 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 başka bir değere değiş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! Şimdi tuhaf (ve biraz titrek) bir şekilde titreştiğini 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.