Pengantar shader

Pengantar

Sebelumnya saya telah memberikan pengantar Three.js. Jika Anda belum membaca bahwa Anda mungkin ingin melakukannya, karena ini adalah fondasi yang akan saya bangun dalam artikel ini.

Saya ingin membahas shader. WebGL brilian, dan seperti yang saya sampaikan sebelumnya Three.js (dan library lainnya) berhasil mengabstraksikan masalah Anda dengan luar biasa. Namun, ada kalanya Anda ingin mencapai efek tertentu, atau ingin menggali lebih dalam tentang bagaimana hal-hal luar biasa itu muncul di layar Anda, dan shader hampir pasti akan menjadi bagian dari persamaan tersebut. Jika Anda seperti saya, sebaiknya beralih dari hal dasar di tutorial terakhir ke sesuatu yang sedikit lebih rumit. Saya akan bekerja berdasarkan bahwa Anda menggunakan Three.js, karena ia melakukan banyak hal untuk menjalankan shader. Saya juga akan mengatakan bahwa di awal, saya akan menjelaskan konteks untuk shader, dan bagian terakhir dari tutorial ini adalah tempat kita akan membahas wilayah yang sedikit lebih canggih. Alasannya adalah shader tidak biasa pada awalnya dan perlu sedikit menjelaskan.

1. Dua Shader Kami

WebGL tidak menawarkan penggunaan Pipeline Tetap, yang merupakan cara singkat untuk mengatakan bahwa WebGL tidak memberi Anda cara untuk merender item Anda secara langsung. Namun, yang disediakan adalah Programmable Pipeline, yang lebih canggih tetapi lebih sulit untuk dipahami dan digunakan. Singkatnya, Programmable Pipeline berarti sebagai programmer, Anda bertanggung jawab untuk mendapatkan verteks dan sebagainya yang dirender ke layar. Shader adalah bagian dari pipeline ini, dan ada dua jenisnya:

  1. Shader vertex
  2. Shader fragmen

Keduanya, saya yakin Anda setuju, sama sekali tidak berarti. Yang harus Anda ketahui tentang keduanya adalah keduanya berjalan sepenuhnya di GPU kartu grafis Anda. Ini berarti kita ingin melimpahkan semampu kita kepada mereka, dan menyerahkan CPU untuk melakukan pekerjaan lain. GPU modern sangat dioptimalkan untuk fungsi yang diperlukan oleh shader, sehingga sangat bagus untuk dapat menggunakannya.

2. Shader Vertex

Ambil bentuk primitif standar, seperti bola dunia. Terdiri dari titik sudut, bukan? Sebuah shader verteks diberikan pada setiap verteks ini secara bergantian dan dapat mengacaukannya. Shader verteks dapat melakukan apa dengan setiap shader verteks, tetapi memiliki satu tanggung jawab: pada titik tertentu, shader verteks harus menetapkan sesuatu yang disebut gl_Position, vektor float 4D, yang merupakan posisi akhir dari vertex di layar. Prosesnya cukup menarik karena sebenarnya kita sedang berbicara tentang mendapatkan posisi 3D (verteks dengan x,y,z) ke, atau diproyeksikan, ke layar 2D. Untungnya, jika menggunakan fitur seperti Three.js, kita akan memiliki cara cepat untuk menyetel gl_Position tanpa menjadi terlalu berat.

3. Shader Fragmen

Kita punya objek beserta verteksnya, dan telah memproyeksikan ke layar 2D, tapi bagaimana dengan warna yang kita gunakan? Bagaimana dengan pewarnaan tekstur dan pencahayaan? Itulah kegunaan shader fragmen. Mirip dengan shader verteks, shader fragmen juga hanya memiliki satu tugas yang harus dilakukan: shader fragmen harus menetapkan atau menghapus variabel gl_FragColor, vektor float 4D lainnya, yang merupakan warna akhir fragmen. Namun, apa yang dimaksud dengan fragmen? Bayangkan tiga verteks yang membentuk segitiga. Setiap piksel dalam segitiga tersebut perlu digambar. Fragmen adalah data yang disediakan oleh ketiga verteks tersebut untuk menggambar setiap piksel dalam segitiga tersebut. Oleh karena itu, fragmen menerima nilai interpolasi dari verteks konstituennya. Jika satu verteks berwarna merah, dan tetangganya berwarna biru, kita akan melihat nilai warna berinterpolasi dari merah, hingga ungu, hingga biru.

4. Variabel Shader

Saat membahas variabel, ada tiga deklarasi yang dapat Anda buat: Uniforms, Attributes, dan Variasi. Ketika pertama kali mendengar tentang ketiganya, saya sangat bingung karena tidak cocok dengan hal lain yang pernah saya gunakan. Namun, inilah cara Anda memahaminya:

  1. Uniform dikirim ke kedua shader verteks dan shader fragmen, serta berisi nilai yang tetap sama di seluruh frame yang dirender. Contoh yang cocok adalah posisi lampu.

  2. Atribut adalah nilai yang diterapkan ke setiap verteks. Atribut hanya tersedia untuk shader verteks. Ini bisa berupa setiap verteks memiliki warna yang berbeda. Atribut memiliki hubungan one-to-one dengan verteks.

  3. Memvariasikan adalah variabel yang dideklarasikan dalam shader verteks yang ingin kita bagikan dengan shader fragmen. Untuk melakukannya, kami memastikan bahwa kami mendeklarasikan variabel yang berbeda dari jenis dan nama yang sama di shader verteks dan shader fragmen. Penggunaan klasik ini adalah penggunaan verteks yang normal karena dapat digunakan dalam perhitungan pencahayaan.

Selanjutnya kita akan menggunakan ketiga jenis tersebut sehingga Anda dapat merasakan bagaimana ketiganya diterapkan secara nyata.

Setelah membahas shader verteks dan shader fragmen, serta jenis variabel yang ditangani, sekarang sebaiknya lihat shader verteks dan shader yang paling sederhana yang dapat kita buat.

5. Dunia Bonjourno

Selanjutnya, inilah Hello World dari shader verteks:

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

dan inilah yang sama untuk shader fragmen:

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

Tidak terlalu rumit, bukan?

Dalam shader verteks, kita dikirim beberapa uniform oleh Three.js. Kedua uniform ini adalah matriks 4D, yang disebut Matriks Model-View dan Matriks Proyeksi. Anda tidak perlu mengetahui secara persis cara kerjanya, meskipun sebaiknya Anda memahami cara kerjanya jika memungkinkan. Versi singkatnya adalah bahwa posisi 3D verteks ini sebenarnya diproyeksikan ke posisi 2D akhir di layar.

Saya tidak menyertakannya dalam cuplikan di atas karena Three.js menambahkannya ke bagian atas kode shader itu sendiri, sehingga Anda tidak perlu khawatir untuk melakukannya. Sesungguhnya, kode ini sebenarnya menambahkan lebih banyak dari hal tersebut, seperti data cahaya, warna verteks, dan normal verteks. Jika melakukannya tanpa Three.js, Anda harus membuat dan menetapkan sendiri semua seragam dan atribut tersebut. Kisah nyata.

6. Menggunakan MeshShaderMaterial

Oke, jadi kita telah menyiapkan shader, tetapi bagaimana cara menggunakannya dengan Three.js? Ternyata itu sangat mudah. Kodenya seperti ini:

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

Dari sana, Three.js akan mengompilasi dan menjalankan shader Anda yang dilampirkan ke mesh tempat Anda memberikan materi tersebut. Ini tidak menjadi jauh lebih mudah dari sebenarnya. Mungkin memang benar, namun kita berbicara tentang berjalannya 3D di browser, jadi saya rasa Anda mengharapkan sejumlah kerumitan.

Sebenarnya, kita dapat menambahkan dua properti lagi ke MeshShaderMaterial: seragam dan atribut. Keduanya dapat mengambil vektor, bilangan bulat, atau float, tetapi seperti yang saya sebutkan sebelumnya, uniform sama untuk seluruh frame, yaitu untuk semua verteks, sehingga cenderung berupa nilai tunggal. Namun, atribut adalah variabel per verteks, sehingga diharapkan berupa array. Harus ada hubungan one-to-one antara jumlah nilai dalam array atribut dan jumlah verteks dalam mesh.

7. Langkah Berikutnya

Sekarang kita akan menghabiskan sedikit waktu untuk menambahkan loop animasi, atribut verteks, dan uniform. Kita juga akan menambahkan variabel yang bervariasi sehingga shader verteks dapat mengirim beberapa data ke shader fragmen. Hasil akhirnya adalah bola dunia kita yang berwarna merah muda akan tampak menyala dari atas dan ke samping dan akan berdenyut. Ini agak rumit, tetapi semoga ini dapat membuat Anda memahami ketiga jenis variabel tersebut dengan baik serta hubungannya satu sama lain dan geometri yang mendasarinya.

8. Lampu Palsu

Mari kita perbarui pewarnaan sehingga ini bukan objek berwarna datar. Kita dapat melihat cara Three.js menangani pencahayaan, tetapi karena saya yakin Anda mengerti bahwa hal ini lebih kompleks daripada yang kita perlukan saat ini, jadi kita akan memalsukannya. Anda harus benar-benar melihat shader fantastis yang merupakan bagian dari Three.js, dan juga shader dari project WebGL yang menakjubkan baru-baru ini oleh Chris milk dan Google, Rome. Kembali ke shader. Kita akan mengupdate Vertex Shader untuk menyediakan setiap verteks normal ke Shader Fragmen. Kami melakukannya dengan berbagai:

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

dan di Shader Fragmen, kita akan menyiapkan nama variabel yang sama, lalu menggunakan perkalian titik normal verteks dengan vektor yang mewakili cahaya yang bersinar dari atas dan kanan bola dunia. Hasil bersih dari ini memberi kita efek yang mirip dengan cahaya terarah dalam paket 3D.

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

}

Jadi, alasan kerja perkalian titik adalah karena diberikan dua vektor, tiap vektor akan muncul dengan angka yang menunjukkan seberapa 'serupa' kedua vektor tersebut. Dengan vektor yang dinormalkan, jika vektor tersebut mengarah ke arah yang sama persis, Anda akan mendapatkan nilai 1. Jika keduanya menunjuk ke arah yang berlawanan, Anda akan mendapatkan -1. Yang kita lakukan adalah mengambil angka itu dan menerapkannya ke pencahayaan kita. Jadi, verteks di kanan atas akan memiliki nilai mendekati atau sama dengan 1, yaitu terang sepenuhnya, sedangkan verteks di samping akan memiliki nilai mendekati 0 dan membulatkan belakang adalah -1. Kita membatasi nilai ke 0 untuk apa pun yang negatif, tetapi ketika Anda menghubungkan angka itu, kamu akhirnya dengan pencahayaan dasar yang kita lihat.

Apa langkah selanjutnya? Akan lebih baik jika Anda bisa mencoba mengotak-atik beberapa posisi verteks.

9. Atribut

Yang ingin saya lakukan sekarang adalah melampirkan angka acak ke setiap verteks melalui atribut. Kita akan menggunakan angka ini untuk mendorong verteks di sepanjang hal normal. Hasil akhirnya adalah semacam bola lonjakan aneh yang akan berubah setiap kali Anda memuat ulang halaman. Gambar tersebut belum akan dianimasikan (yang akan terjadi selanjutnya), tetapi refresh pada beberapa halaman akan menunjukkan bahwa elemen tersebut diacak.

Mari kita mulai dengan menambahkan atribut ke shader verteks:

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

Bagaimana tampilannya?

Tidak jauh berbeda! Ini karena atribut tersebut belum disiapkan di MeshShaderMaterial, sehingga shader tersebut menggunakan nilai nol sebagai gantinya. Saat ini, ini seperti {i>placeholder<i}. Berikutnya, kita akan menambahkan atribut tersebut ke MeshShaderMaterial di JavaScript, dan Three.js akan mengikat keduanya secara otomatis.

Yang perlu diperhatikan juga adalah fakta bahwa saya harus menetapkan posisi yang diperbarui ke variabel vec3 baru karena atribut asli, seperti semua atribut, bersifat hanya baca.

10. Mengupdate MeshShaderMaterial

Mari kita lanjutkan untuk mengupdate MeshShaderMaterial dengan atribut yang diperlukan untuk mendukung perpindahan kita. Pengingat: atribut adalah nilai per-verteks sehingga kita membutuhkan satu nilai per verteks dalam bola kita. Seperti ini:

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

Sekarang kita melihat bola yang hancur, tetapi yang kerennya adalah semua perpindahan ini terjadi di GPU.

11. Menganimasikan Pengisap Itu

Kita harus membuatnya menjadi animasi. Bagaimana kita melakukannya? Ada dua hal yang perlu kita persiapkan:

  1. Uniform untuk menganimasikan seberapa banyak perpindahan yang harus diterapkan di setiap frame. Kita dapat menggunakan sinus atau kosinus untuk itu karena urutannya dimulai dari -1 ke 1
  2. Loop animasi di JS

Kita akan menambahkan seragam ke MeshShaderMaterial dan Vertex Shader. Pertama, 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);
}

Selanjutnya, kita akan mengupdate MeshShaderMaterial:

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

Shader kita sudah selesai untuk saat ini. Tapi benar, kami akan tampak mengambil langkah mundur. Ini sebagian besar karena nilai amplitudo kita berada di 0 dan karena kita mengalikannya dengan perpindahan, kita tidak melihat perubahan apa pun. Kita juga belum menyiapkan loop animasi sehingga tidak pernah melihat bahwa 0 berubah ke hal lain.

Dalam JavaScript, kita sekarang harus menggabungkan panggilan render ke dalam fungsi, lalu menggunakan requestAnimationFrame untuk memanggilnya. Di sana kita juga perlu memperbarui nilai uniform.

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. Kesimpulan

Selesai. Sekarang Anda bisa melihat animasinya berdenyut dengan cara yang aneh (dan agak rumit).

Ada banyak hal yang dapat kami bahas tentang shader sebagai topik, tetapi kami harap pengantar ini bermanfaat bagi Anda. Sekarang Anda akan dapat memahami shader saat melihatnya, serta percaya diri untuk membuat shader Anda sendiri yang luar biasa.