Pengantar shader

Pengantar

Sebelumnya, kami telah memberikan pengantar Three.js kepada Anda. Jika belum membacanya, sebaiknya Anda melakukannya karena ini adalah dasar yang akan saya bangun selama artikel ini.

Yang ingin saya lakukan adalah membahas shader. WebGL sangat bagus, dan seperti yang telah saya katakan sebelumnya, Three.js (dan library lainnya) melakukan pekerjaan yang luar biasa dalam memisahkan kesulitan untuk Anda. Namun, ada kalanya Anda ingin mendapatkan efek tertentu, atau Anda ingin mempelajari lebih dalam cara hal-hal menakjubkan itu muncul di layar, dan shader hampir pasti akan menjadi bagian dari persamaan tersebut. Selain itu, jika Anda seperti saya, Anda mungkin ingin beralih dari hal-hal dasar dalam tutorial terakhir ke sesuatu yang sedikit lebih rumit. Saya akan bekerja berdasarkan bahwa Anda menggunakan Three.js, karena metode ini melakukan banyak hal yang perlu dilakukan untuk mengaktifkan shader. Saya juga akan sampaikan bahwa di awal, saya akan menjelaskan konteks untuk shader, dan bagian terakhir dari tutorial ini akan membahas wilayah yang sedikit lebih maju. Alasannya adalah shader terlihat tidak biasa pada pandangan pertama dan perlu sedikit penjelasan.

1. Dua Shader Kami

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

  1. Shader vertex
  2. Shader fragmen

Saya yakin Anda akan setuju bahwa keduanya tidak berarti apa-apa. Yang harus Anda ketahui tentang keduanya adalah keduanya berjalan sepenuhnya di GPU kartu grafis Anda. Artinya, kita ingin memindahkan semua yang dapat kita lakukan ke GPU, sehingga CPU dapat melakukan pekerjaan lain. GPU modern sangat dioptimalkan untuk fungsi yang diperlukan shader, sehingga sangat bagus untuk dapat menggunakannya.

2. Vertex Shader

Ambil bentuk primitif standar, seperti bola dunia. Terdiri dari verteks, kan? Vertex shader diberikan setiap satu dari vertex ini secara bergantian dan dapat mengacaukan vertex tersebut. Vertex shader memiliki tanggung jawab untuk menentukan apa yang sebenarnya dilakukannya dengan setiap vertex, tetapi memiliki satu tanggung jawab: pada suatu saat, vertex shader harus menetapkan sesuatu yang disebut gl_Position, vektor float 4D, yang merupakan posisi akhir vertex di layar. Proses ini cukup menarik, karena kita sebenarnya berbicara tentang mendapatkan posisi 3D (sudut dengan x,y,z) ke, atau diproyeksikan, ke layar 2D. Untungnya, jika menggunakan sesuatu seperti Three.js, kita akan memiliki cara singkat untuk menetapkan gl_Position tanpa terlalu membebani.

3. Shader Fragmen

Jadi, kita memiliki objek dengan vertex-nya, dan kita telah memproyeksikan objek tersebut ke layar 2D, tetapi bagaimana dengan warna yang kita gunakan? Bagaimana dengan tekstur dan pencahayaan? Itulah tujuan shader fragmen. Sangat mirip dengan shader verteks, shader fragmen juga hanya memiliki satu tugas yang harus dilakukan: shader fragmen harus menyetel atau menghapus variabel gl_FragColor, vektor float 4D lainnya, yang merupakan warna akhir fragmen kita. Namun, apa yang dimaksud dengan fragmen? Pikirkan tiga verteks yang membentuk segitiga. Setiap piksel dalam segitiga tersebut harus digambar. Fragmen adalah data yang disediakan oleh ketiga vertex tersebut untuk tujuan menggambar setiap piksel dalam segitiga tersebut. Oleh karena itu, fragmen menerima nilai yang diinterpolasi dari vertex penyusunnya. Jika satu vertex berwarna merah, dan tetangganya berwarna biru, kita akan melihat nilai warna diinterpolasikan dari merah, melalui ungu, hingga biru.

4. Variabel Shader

Saat membahas variabel, ada tiga deklarasi yang dapat Anda buat: Seragam, Atribut, dan Variasi. Saat pertama kali mendengar ketiganya, saya sangat bingung karena tidak cocok dengan apa pun yang pernah saya kerjakan. Namun, berikut cara memahaminya:

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

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

  3. Varying adalah variabel yang dideklarasikan dalam shader vertex yang ingin kita bagikan dengan shader fragmen. Untuk melakukannya, kita memastikan bahwa kita mendeklarasikan variabel bervariasi dari jenis dan nama yang sama di shader vertex dan shader fragmen. Penggunaan klasiknya adalah normal vertex karena dapat digunakan dalam penghitungan pencahayaan.

Nanti, kita akan menggunakan ketiga jenis tersebut sehingga Anda dapat merasakan cara penerapannya yang sebenarnya.

Setelah kita membahas vertex shader dan fragment shader serta jenis variabel yang ditanganinya, sekarang saatnya melihat shader paling sederhana yang dapat kita buat.

5. Bonjourno World

Berikut adalah Hello World dari shader vertex:

/**
* 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 berikut ini hal 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 mengirimkan beberapa uniform oleh Three.js. Kedua seragam ini adalah matriks 4D, yang disebut Matriks Model-Tampilan dan Matriks Proyeksi. Anda tidak perlu mengetahui cara kerjanya secara persis, meskipun sebaiknya pahami cara kerja sesuatu jika Anda bisa. Versi singkatnya adalah cara posisi 3D verteks sebenarnya diproyeksikan ke posisi 2D akhir di layar.

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

6. Menggunakan MeshShaderMaterial

Oke, jadi kita telah menyiapkan shader, tetapi bagaimana cara menggunakannya dengan Three.js? Ternyata sangat mudah. Lebih 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 yang dilampirkan ke mesh tempat Anda memberikan material tersebut. Prosesnya tidak jauh lebih mudah daripada itu. Mungkin memang demikian, tetapi kita berbicara tentang cara kerja 3D di browser, jadi saya pikir Anda mengharapkan sejumlah kompleksitas tertentu.

Kita sebenarnya 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 bernilai tunggal. Namun, atribut adalah variabel per vertex, sehingga diharapkan berupa array. Harus ada hubungan satu-ke-satu antara jumlah nilai dalam array atribut dan jumlah vertex dalam mesh.

7. Langkah Berikutnya

Sekarang kita akan meluangkan waktu untuk menambahkan loop animasi, atribut vertex, dan seragam. Kita juga akan menambahkan variabel yang bervariasi sehingga vertex shader dapat mengirim beberapa data ke fragment shader. Hasil akhirnya adalah bola kita yang berwarna merah muda akan tampak menyala dari atas dan ke samping dan akan berdenyut. Ini agak membingungkan, tetapi semoga hal ini akan mengarah Anda pada pemahaman yang baik tentang ketiga jenis variabel serta cara variabel tersebut saling berhubungan dan geometri yang mendasarinya.

8. Lampu Palsu

Mari kita perbarui pewarnaan agar tidak menjadi objek berwarna datar. Kita dapat melihat cara Three.js menangani pencahayaan, tetapi seperti yang saya yakin Anda dapat menghargainya, proses ini lebih rumit daripada yang kita butuhkan saat ini, jadi kita akan membuatnya palsu. Anda harus melihat sepenuhnya shader fantastis yang merupakan bagian dari Three.js, dan juga shader dari project WebGL yang luar biasa baru-baru ini oleh Chris Milk dan Google, Rome. Kembali ke shader kita. Kita akan mengupdate Vertex Shader untuk menyediakan setiap verteks normal ke Fragment Shader. Kita 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 Fragment Shader, kita akan menyiapkan nama variabel yang sama, lalu menggunakan produk titik normal vertex dengan vektor yang mewakili cahaya yang bersinar dari atas dan ke kanan bola. Hasil bersihnya 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 produk titik berfungsi adalah karena diberikan dua vektor, produk titik akan menghasilkan angka yang memberi tahu Anda seberapa 'mirip' kedua vektor tersebut. Dengan vektor yang dinormalisasi, jika vektor tersebut mengarah ke arah yang sama persis, Anda akan mendapatkan nilai 1. Jika keduanya mengarah ke arah yang berlawanan, Anda akan mendapatkan -1. Yang kita lakukan adalah mengambil angka tersebut dan menerapkannya ke pencahayaan kita. Jadi, vertex di kanan atas akan memiliki nilai mendekati atau sama dengan 1, yaitu sepenuhnya terang, sedangkan vertex di samping akan memiliki nilai mendekati 0 dan di bagian belakang akan menjadi -1. Kita akan membatasi nilai ke 0 untuk nilai negatif, tetapi saat Anda memasukkan angka, Anda akan mendapatkan pencahayaan dasar yang kita lihat.

Apa langkah selanjutnya? Sebaiknya coba ubah beberapa posisi vertex.

9. Atribut

Yang ingin kita lakukan sekarang adalah melampirkan angka acak ke setiap vertex melalui atribut. Kita akan menggunakan angka ini untuk mendorong verteks tersebut keluar mengikuti kondisi normal. Hasil akhirnya adalah semacam bola lonjakan aneh yang akan berubah setiap kali Anda memuat ulang halaman. Ini belum akan dianimasikan (akan terjadi berikutnya), tetapi beberapa pemuatan ulang halaman akan menunjukkan bahwa ini acak.

Mari kita mulai dengan menambahkan atribut ke shader vertex:

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 terlalu berbeda. Hal ini karena atribut belum disiapkan di MeshShaderMaterial sehingga shader menggunakan nilai nol secara efektif. Saat ini, ini seperti placeholder. Dalam sekejap, kita akan menambahkan atribut ke MeshShaderMaterial di JavaScript dan Three.js akan menggabungkan keduanya secara otomatis.

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

10. Memperbarui MeshShaderMaterial

Mari kita langsung mengupdate MeshShaderMaterial dengan atribut yang diperlukan untuk mendukung perpindahan. Pengingat: atribut adalah nilai per verteks jadi kita memerlukan 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 rusak, tetapi yang keren adalah semua perpindahan terjadi di GPU.

11. Menganimasikan That Sucker

Kita harus membuat animasi ini. Bagaimana cara melakukannya? Nah ada dua hal yang perlu kita lakukan:

  1. Uniform untuk menganimasikan seberapa banyak perpindahan yang harus diterapkan di setiap frame. Kita dapat menggunakan sinus atau cosinus untuk itu karena keduanya berjalan dari -1 hingga 1
  2. Loop animasi di JS

Kita akan menambahkan uniform 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 memperbarui 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 kanan, kami tampak mengambil langkah mundur. Hal ini sebagian besar karena nilai amplitudo kita berada di 0 dan karena kita mengalikan nilai tersebut dengan pemindahan, kita tidak melihat perubahan apa pun. Kita juga belum menyiapkan loop animasi sehingga kita tidak pernah melihat perubahan 0 menjadi hal lain.

Di JavaScript, kita sekarang perlu 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 melihatnya dianimasikan dengan cara yang aneh (dan sedikit kaku).

Masih banyak hal yang dapat kita bahas tentang shader sebagai topik, tetapi semoga pengantar ini bermanfaat bagi Anda. Sekarang Anda dapat memahami shader saat melihatnya serta memiliki keyakinan untuk membuat beberapa shader Anda sendiri.