Gölgelendiricilere giriş

Paul Lewis

Giriş

Daha önce size Three.js'ye giriş sunmuştuk. Henüz okumadıysanız, okumak isteyebilirsiniz. Çünkü bu makale boyunca temel üzerinde oluşturacağım.

Ben gölgelendiricileri tartışmak istiyorum. WebGL müthiş bir şey. Three.js'den (ve diğer kitaplıklardan) önce söylediğim gibi, zorlukları soyutlayarak harika bir iş çıkarıyor. Ancak belirli bir etkiye ulaşmak istediğiniz zamanlar olabilir veya bu şaşırtıcı öğenin ekranınızda nasıl göründüğünü biraz daha ayrıntılı olarak incelemek isteyebilirsiniz. Gölgelendiriciler, neredeyse kesinlikle bu denklemin bir parçası olacaktır. Ayrıca, eğer siz de benim gibiyseniz son eğitimdeki temel bilgilerden biraz daha zor bir yaklaşıma geçmek isteyebilirsiniz. Gölgelendiriciyi çalıştırma açısından eşek işinin büyük bir kısmını üstlendiğinden Three.js'yi temel alarak çalışacağım. Başlangıçta gölgelendiricilerin bağlamını açıklayacağımı ve bu eğiticinin ikinci kısmında biraz daha ileri düzeylere gireceğimizi söyleyebilirim. Bunun nedeni, gölgeleyicilerin ilk bakışta alışılmadık olması ve biraz açıklama gerektirmesidir.

1. Gölgelendiricilerimiz

WebGL, Sabit Ardışık Düzen kullanımını sunmaz. Bu, size içeriğinizi kutusundan çıkarırken göstermenin hiçbir yolu olmadığını söylemenin kestirme yoludur. Ancak daha güçlü ancak anlaşılması ve kullanılması daha zor olan Programlanabilir Ardışık Düzen sunar. Kısacası Programlanabilir Ardışık Düzen, programcı olarak köşe noktalarının ekrana yansıtılmasından sorumludur. Gölgelendiriciler bu ardışık düzenin bir parçasıdır ve iki tür mevcuttur:

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

Eminim her ikisi de kendi başlarına kesinlikle hiçbir şey ifade etmiyor. Bunlarla ilgili bilmeniz gereken nokta, her ikisinin de tamamen grafik kartınızın GPU'suyla çalıştığıdır. Bu, mümkün olan her şeyi onlara boşaltmak istediğimiz anlamına gelir ve CPU'muzdan başka işler yapmaya çalışırız. Modern bir GPU, gölgelendiricilerin gerektirdiği işlevler açısından yoğun bir şekilde optimize edilmiştir. Bu nedenle, bu GPU'yu kullanmak harikadır.

2. Köşeli Gölgelendiriciler

Küre gibi standart bir ilkel şekil alın. Köşelerden oluşuyor, değil mi? Bu köşelerin her birine sırayla bir tepe gölgelendirici verilir ve bunları karıştırabilir. Köşe gölgelendiricisinin her birinde gerçekte ne yaptığına bağlıdır, ancak bir sorumluluğu vardır: Bir noktada tepe noktasının ekrandaki son konumu olan gl_Position adlı bir 4D kayan vektör vektörü olmalıdır. Bu, başlı başına oldukça ilginç bir süreç, çünkü aslında 3D bir konumu (x,y,z içeren bir köşe noktası) 2D ekrana almak ya da yansıtmaktan bahsediyoruz. Neyse ki, Three.js gibi bir dosya kullanıyorsak işler çok fazla ağırlaşmadan gl_Position değerini belirlemenin kısa bir yolunu bulabiliyoruz.

3. Parça Gölgelendiriciler

Köşeleri olan bir nesnemiz var ve bunları 2D ekrana yansıttık. Peki kullandığımız renkler ne olacak? Doku oluşturma ve ışıklandırma ne olacak? Parça gölgelendiricinin işlevi de tam olarak budur. Köşe gölgelendiricisine çok benzer şekilde, parça gölgelendiricinin de yapılması gereken tek bir iş vardır: Parçamızın son rengi olan başka bir 4D kayan vektör gl_FragColor değişkenini ayarlamalı veya kaldırmalıdır. Peki, parça nedir? Bir üçgeni oluşturan üç köşe noktasını düşünün. Bu üçgen içindeki her bir pikselin çizilmesi gerekir. Parça, her bir pikseli söz konusu üçgenin içinde çizmek amacıyla bu üç köşe tarafından sağlanan verilerdir. Bu nedenle, parçalara ait bileşenlerin köşelerinden interpole edilmiş değerler alırlar. Bir köşe kırmızı, komşusu maviyse renk değerlerinin kırmızıdan mora ve maviye doğru hesaplandığını görürüz.

4. Gölgelendirici Değişkenleri

Değişkenler hakkında konuşurken yapabileceğiniz üç bildirim vardır: Üniformalar, Özellikler ve Varyantlar. Bu üçünü ilk duyduğumda, birlikte çalıştığım başka hiçbir şeyle uyuşmadıkları için kafam karışmıştı. Bunları şöyle düşünebilirsiniz:

  1. Üniformalar, hem köşe 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 lambanın konumu olabilir.

  2. Özellikler, ayrı ayrı köşelere uygulanan değerlerdir. Özellikler yalnızca köşe gölgelendirici tarafından kullanılabilir. Bu, her bir köşenin farklı bir rengi olması gibi bir şey olabilir. Özelliklerin köşe noktalarıyla bire bir ilişkisi vardır.

  3. Varyantlar, köşe gölgelendiricide belirtilen ve parça gölgelendiriciyle paylaşmak istediğimiz değişkenlerdir. Bunu yapmak için hem köşe gölgelendiricide hem de parça gölgelendiricide aynı türde ve ada sahip farklı bir değişken tanımladığımızdan emin oluruz. Bunun klasik bir kullanımı, ışık hesaplamalarında kullanılabildiği için bir köşe noktasının normalidir.

Daha sonra bunların üçünü de kullanacağız. Böylece, bunların gerçek hayata nasıl uygulandıklarını anlayabilirsiniz.

Şimdi köşe gölgelendiricileri, parça gölgelendiriciler ve bunlarla çalışılan değişken türleri hakkında konuştuk. Şimdi oluşturabileceğimiz en basit gölgelendiricilere bakmaya değer.

5. Bonjourno Dünyası

Burada, köşe gölgelendiricilerinin Hello World uygulamasını görebilirsiniz:

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

Aynı durum parça gölgelendirici için de 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);
}

Yine de çok karmaşık değil, değil mi?

Köşe gölgelendiricisine, Three.js tarafından birkaç üniforma gönderilir. Bu iki biçim, Model Görüntüleme Matrisi ve Projeksiyon Matrisi olarak adlandırılan 4D matrislerdir. Bunların nasıl çalıştığını tam olarak bilmeniz gerekmez. Yine de mümkünse her şeyin nasıl çalıştığını anlamak en iyisidir. Kısacası, köşenin 3D konumunun aslında ekrandaki son 2D konumuna projeksiyonudur.

Three.js, bunları gölgelendirici kodunuzun en üstüne eklediği için bunları yukarıdaki snippet'in dışında bıraktım. Aslında ışık verileri, tepe noktası renkleri ve köşe normalleri gibi bundan çok daha fazlasını ekliyor. Bunu Three.js olmadan yapıyor olsaydınız, tüm bu formları ve özellikleri kendiniz oluşturmanız gerekirdi. Gerçek hikaye.

6. MeshShaderMaterial kullanma

Tamam, bir gölgelendirici kurulumumuz var ama bunu Three.js ile nasıl kullanacağız? Son derece kolay olduğu anlaşılıyor. Daha çok aşağıdaki gibidir:

/**
* 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()
});

Three.js, gölgelendiricilerinizi derler ve söz konusu malzemeyi verdiğiniz ağa bağlı olarak çalıştırır. Aslında bu, aslında bundan daha kolay değildir. Muhtemelen öyle, ancak tarayıcınızda 3D çalışmaktan söz ediyoruz. Bu yüzden biraz karmaşıklık bekliyorsunuz diye düşünüyorum.

Aslında MeshShaderMaterial malzememize iki özellik daha ekleyebiliriz: üniformalar ve özellikler. Bunların ikisi de vektör, tam sayı veya kayan noktalı değer alabilir, ancak daha önce de belirttiğim gibi, üniformalar tüm çerçeve için, yani tüm köşe noktaları için aynı olduğundan tek değerler olabilirler. Bununla birlikte, özellikler köşe başına değişkenlerdir, bu nedenle birer dizi olmaları beklenir. Özellikler dizisindeki değerlerin sayısı ile ağdaki köşe noktası sayısı arasında bire bir ilişki olmalıdır.

7. Sonraki adımlar

Şimdi, biraz zaman ayırıp bir animasyon döngüsü, köşe özellikleri ve bir tek tip oluşturacağız. Ayrıca, köşe gölgelendiricisinin parça gölgelendiriciye bir miktar veri gönderebilmesi için değişken bir değişken de ekleyeceğiz. Bunun sonucunda, pembe olan küremiz yukarıdan kenara doğru aydınlatılacak ve titreşecek. Biraz tuhaf ama üç değişken türünü iyi anlamanın yanı sıra birbirleriyle ve altta yatan geometriyle nasıl ilişki kurduğunu da umuyorum.

8. Sahte Işık

Nesnenin düz renkli olması için renklendirmeyi güncelleyelim. Three.js'nin ışıklandırmayı nasıl işlediğine bir göz atabiliriz, ama anlayacağınız üzere bu işin şu anda ihtiyacımız olandan daha karmaşık olduğunu anlayacağınızdan, bu konuda sahte bir şey yapacağız. Three.js'nin bir parçası olan harika gölgelendiricilere ve ayrıca Chris Milk ile Google'ın Roma'ya ait en son muhteşem WebGL projesindeki harika gölgelendiricilere göz atmalısınız. Gölgelendiricilerimize dönelim. Köşe Gölgelendiricimizi, her bir köşeyi Parça Gölgelendirici'ye uygun olacak şekilde güncelleyeceğiz. Bunu çeşitli şekillerde yapıyoruz:

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

Parça Gölgelendirici'de, aynı değişken adını ayarlayıp normal köşenin nokta çarpımını, kürenin yukarısından ve sağından gelen ışığı temsil eden bir vektörle birlikte kullanacağız. Bunun net sonucu bize 3D paketteki yönlü ışığa benzer bir efekt verir.

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

}

Dolayısıyla, nokta çarpımının çalışmasının nedeni, iki vektör verildiğinde, iki vektörün ne kadar "benzer" olduğunu gösteren bir sayıyla ortaya çıkmasıdır. Normalleştirilmiş vektörlerde tam olarak aynı yönü gösteriyorsa 1 değerini elde edersiniz. Karşıt yönü gösterirlerse -1 alırsınız. Yaptığımız şey bu sayıyı alıp ışıklandırmamıza uygulamak. Dolayısıyla, sağ üstteki bir köşe noktası 1'e yakın veya 1'e eşit bir değere, yani tamamen aydınlatılmışken, yan taraftaki köşe noktası 0'a yakın, arka kısmı -1 şeklinde yuvarlar. Negatif her şey için değeri 0'a sabitleriz, ancak sayıları eklediğinizde gördüğümüz temel ışıklandırmayı elde edersiniz.

Sonraki adım Bazı köşe konumları ile kafa karıştırmayı denemek güzel olurdu.

9. Özellikler

Şimdi yapmamız gereken, bir özellik aracılığıyla her bir köşe noktasına rastgele bir sayı eklemek. Bu sayıyı, tepe noktasını normal şekilde dışa itmek için kullanırız. Bunun sonucunda, sayfayı her yenilediğinizde değişecek olan bir tür tuhaf sivri top elde edilir. Yalnızca animasyon oluşturulmaz (bundan sonra gerçekleşir), ancak birkaç sayfa yenileme işlemide sayfanın rastgele hale getirildiğini gösterir.

Köşe noktası gölgelendiricisine özelliği 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?

Aslında pek farklı değil. Bunun nedeni, özelliğin MeshShaderMaterial'da ayarlanmamış olması gölgelendirmenin etkin bir şekilde bunun yerine sıfır değeri kullanmasıdır. Şu anda bir yer tutucu gibi bir şey. Birazdan özelliği JavaScript'teki MeshShaderMaterial'a ekleyeceğiz ve Three.js, ikisini bizim için otomatik olarak birbirine bağlayacaktır.

Ayrıca, tüm özellikler gibi orijinal özellik de salt okunur olduğundan güncellenen konumu yeni bir vec3 değişkenine atamam gerekti.

10. MeshShaderMaterial'ı güncelleme

Şimdi MeshShaderMaterial'ımızı yer değiştirmemizi güçlendirmek 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 parçalanmış bir küre görüyoruz, ancak işin güzel tarafı, tüm yer değiştirme işlemi GPU'da gerçekleşiyor.

11. Cansız Canlandırma

Tam anlamıyla bu animasyon oluşturmalıyız. Bunu nasıl yapıyoruz? Yerleştirmemiz gereken iki şey var:

  1. Her karede ne kadar yer değiştirme uygulanması gerektiğini gösteren tek tip. -1'den 1'e kadar çalıştıkları için sinüs veya kosinüs kullanabiliriz
  2. JS'de bir animasyon döngüsü

Üniformayı hem MeshShaderMaterial hem de Vertex Shader'a ekleyeceğiz. İlk olarak Köşe Gölgelendirici'yi kullanın:

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

Daha sonra MeshShaderMaterial'ı güncelliyoruz:

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

Gölgelendiricilerimiz şimdilik bitti. Fakat şimdi geriye bir adım atmış gibi duruyorduk. Bunun nedeni büyük ölçüde genlik değerimizin 0 olması ve bunu yer değiştirmeyle çarptığımız için hiçbir değişiklik olmamasıdır. Ayrıca, animasyon döngüsünü de kurmadığımız için 0'ın başka bir değerle değişmesini asla görmüyoruz.

JavaScript'mizde artık oluşturma çağrısını bir işleve sarmalamamız ve daha sonra, çağırmak için requestAnimationFrame işlevini kullanmamız gerekir. Burada formanı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 animasyonun garip (ve biraz titreşen) bir şekilde hareket ettiğini görebilirsiniz.

Gölgelendiriciler hakkında daha fazla konuyu ele alabiliriz. Ancak bu tanıtımı yararlı bulduğunuzu umuyorum. Artık gölgelendiricileri gördüğünüzde anlar, kendi başınıza mükemmel gölgelendiriciler oluşturabilirsiniz.