Pengantar
Baca pengantar Three.js terlebih dahulu, karena ini adalah dasar untuk dokumen 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 mengabstraksi kesulitan untuk Anda. Namun, akan ada saatnya Anda ingin mencapai efek tertentu, atau Anda ingin menggali lebih dalam tentang bagaimana hal-hal menakjubkan itu muncul di layar, dan shader hampir pasti akan menjadi bagian dari persamaan tersebut. Anda mungkin ingin beralih dari hal-hal dasar dalam tutorial terakhir ke sesuatu yang sedikit lebih rumit.
Saya akan mengasumsikan bahwa Anda menggunakan Three.js, karena library ini melakukan banyak pekerjaan untuk kita dalam hal menjalankan shader. Anda akan mempelajari konteks shader terlebih dahulu, diikuti dengan langkah-langkah lanjutan nanti dalam dokumen ini.
1. Dua shader kita
WebGL tidak menawarkan penggunaan Fixed Pipeline, yang merupakan cara singkat untuk mengatakan bahwa WebGL tidak memberi Anda sarana apa pun untuk merender konten secara langsung. Yang ditawarkannya adalah Pipeline yang Dapat Diprogram, yang lebih canggih, tetapi juga lebih sulit dipahami dan digunakan. Singkatnya, Pipeline yang Dapat Diprogram berarti sebagai programmer, Anda bertanggung jawab untuk merender verteks dan sebagainya ke layar. Shader adalah bagian dari pipeline ini, dan ada dua jenis shader:
- Shader vertex
- Shader fragmen
Keduanya, kami yakin Anda akan setuju, tidak berarti apa-apa jika berdiri sendiri. Yang perlu Anda ketahui tentang keduanya adalah bahwa keduanya berjalan sepenuhnya di GPU kartu grafis Anda. Artinya, kita ingin mengalihkan semua tugas yang bisa dialihkan ke GPU, sehingga CPU kita bisa melakukan pekerjaan lain. GPU modern sangat dioptimalkan untuk fungsi yang diperlukan shader, sehingga sangat bagus untuk dapat menggunakannya.
2. Shader vertex
Ambil bentuk primitif standar, seperti bola. Terdiri dari titik sudut, bukan? Vertex shader diberikan setiap vertex ini secara bergantian dan dapat mengutak-atiknya. Terserah shader vertex apa yang sebenarnya dilakukannya dengan masing-masing, tetapi shader vertex memiliki satu tanggung jawab: pada titik tertentu, shader vertex 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 (puncak dengan x,y,z) ke, atau diproyeksikan, ke layar 2D. Untungnya, jika kita menggunakan sesuatu seperti Three.js, kita memiliki cara singkat untuk menetapkan gl_Position tanpa membuat semuanya menjadi terlalu berat.
3. Shader fragmen
Jadi, kita memiliki objek dengan verteksnya, dan kita telah memproyeksikannya ke layar 2D, tetapi bagaimana dengan warna yang kita gunakan? Bagaimana dengan tekstur dan pencahayaan? Itulah fungsi shader fragmen. Sama seperti shader verteks, shader fragmen juga hanya memiliki satu tugas yang harus dilakukan: shader ini harus menetapkan atau menghapus variabel gl_FragColor, vektor float 4D lainnya, yang merupakan warna akhir fragmen kita.
Namun, apa itu fragmen? Bayangkan tiga titik yang membentuk segitiga. Setiap piksel dalam segitiga tersebut perlu digambar. Fragmen adalah data yang disediakan oleh ketiga verteks tersebut untuk tujuan menggambar setiap piksel dalam segitiga tersebut. Oleh karena itu, fragmen menerima nilai interpolasi dari verteks penyusunnya. Jika satu verteks berwarna merah, dan tetangganya berwarna biru, kita akan melihat nilai warna berinterpolasi dari merah, melalui ungu, hingga biru.
4. Variabel shader
Ada tiga deklarasi variabel yang dapat Anda buat: Uniform, Attribute, dan Varying. Hal ini mungkin membingungkan, karena tidak cocok dengan bahasa umum lainnya. Berikut cara Anda dapat memahaminya:
Uniform dikirim ke shader vertex dan shader fragmen, serta berisi nilai yang tetap sama di seluruh frame yang dirender. Contoh yang baik untuk hal ini mungkin adalah posisi lampu.
Atribut adalah nilai yang diterapkan ke setiap verteks. Atribut hanya tersedia untuk shader vertex. Misalnya, setiap vertex memiliki warna yang berbeda. Atribut memiliki hubungan satu-ke-satu dengan verteks.
Varying adalah variabel yang dideklarasikan dalam vertex shader yang ingin kita bagikan dengan fragment shader. Untuk melakukannya, kita harus memastikan bahwa kita mendeklarasikan variabel bervariasi dengan jenis dan nama yang sama di shader vertex dan shader fragmen. Penggunaan klasik untuk hal ini adalah normal verteks karena dapat digunakan dalam penghitungan pencahayaan.
Nanti, kita akan menggunakan ketiga jenis tersebut sehingga Anda dapat memahami penerapannya yang sebenarnya.
Tidak, lihat salah satu shader paling sederhana yang dapat Anda buat.
5. Bonjourno World
Berikut adalah Halo Dunia 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 berikut adalah hal yang sama untuk shader fragmen:
/**
* Set the color 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?
Di shader vertex, kita dikirimi beberapa uniform oleh Three.js. Kedua uniform ini adalah matriks 4D, yang disebut Matriks Model-View dan Matriks Proyeksi. Anda tidak perlu tahu persis cara kerjanya, meskipun sebaiknya pahami cara kerja sesuatu jika Anda bisa. Singkatnya, matriks ini adalah cara posisi 3D verteks diproyeksikan ke posisi 2D akhir di layar.
Saya sengaja tidak menyertakannya dalam cuplikan contoh karena Three.js menambahkannya ke bagian atas kode shader itu sendiri, sehingga Anda tidak perlu repot melakukannya. Sebenarnya, shader verteks menambahkan lebih banyak hal dari itu, seperti data cahaya, warna verteks, dan normal verteks. Jika Anda melakukannya tanpa Three.js, Anda harus membuat dan menetapkan semua uniform dan atribut tersebut sendiri. Kisah nyata.
6. Menggunakan MeshShaderMaterial
Oke, kita sudah menyiapkan shader, tetapi bagaimana cara menggunakannya dengan Three.js?
/**
* 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 yang Anda beri material tersebut. Sangat mudah, bukan? Mungkin memang demikian, tetapi kita berbicara tentang 3D yang berjalan di browser Anda, jadi saya rasa Anda mengharapkan tingkat kerumitan tertentu.
Kita sebenarnya dapat menambahkan dua properti lagi ke MeshShaderMaterial: uniform dan atribut. Keduanya dapat menggunakan vektor, bilangan bulat, atau float, tetapi seperti yang saya sebutkan sebelumnya, uniform sama untuk seluruh frame, atau untuk semua verteks, sehingga cenderung berupa nilai tunggal. Namun, atribut adalah variabel per vertex, 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 meluangkan sedikit waktu untuk menambahkan loop animasi, atribut vertex, dan uniform. Kita juga akan menambahkan variabel bervariasi sehingga shader vertex dapat mengirim beberapa data ke shader fragmen. Hasil akhirnya adalah bola kita yang berwarna merah muda akan tampak menyala dari atas dan samping serta akan berdenyut. Mungkin agak membingungkan, tetapi semoga ini akan membantu Anda memahami ketiga jenis variabel tersebut serta hubungannya satu sama lain dan geometri yang mendasarinya.
8. Cahaya palsu
Sekarang, perbarui pewarnaan agar tidak menjadi objek berwarna datar. Kita bisa melihat cara Three.js menangani pencahayaan, tetapi seperti yang Anda ketahui, hal ini lebih kompleks daripada yang kita butuhkan, jadi kita akan memalsukannya. Anda harus melihat shader fantastis yang merupakan bagian dari Three.js, dan juga shader dari project WebGL yang luar biasa oleh Chris Milk dan Google, Rome. Kembali ke shader kita. Kita akan memperbarui Vertex Shader untuk memberikan setiap normal verteks ke Fragment Shader. 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 Fragment Shader, kita akan menyiapkan nama variabel yang sama, lalu menggunakan produk titik dari normal verteks dengan vektor yang mewakili cahaya yang bersinar dari atas dan ke kanan bola. Hasil akhirnya memberi kita efek yang mirip dengan cahaya direksional 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 color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
Jadi, alasan perkalian titik berfungsi adalah karena dengan dua vektor, perkalian titik akan menghasilkan angka yang memberi tahu Anda seberapa 'mirip' kedua vektor tersebut. Dengan vektor yang dinormalisasi, jika keduanya 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 tersebut dan menerapkannya pada pencahayaan kita. Jadi, verteks di kanan atas akan memiliki nilai mendekati atau sama dengan 1 (menyala sepenuhnya), sedangkan verteks di samping akan memiliki nilai mendekati 0 dan di belakang akan bernilai -1. Kami membatasi nilai menjadi 0 untuk nilai negatif, tetapi saat Anda memasukkan angka, Anda akan mendapatkan pencahayaan dasar yang kita lihat.
Apa langkah selanjutnya? Mungkin akan menarik untuk mencoba mengubah beberapa posisi verteks.
9. Atribut
Sekarang, kita akan melampirkan angka acak ke setiap verteks menggunakan atribut. Kita akan menggunakan angka ini untuk mendorong vertex keluar di sepanjang normalnya. Hasil akhirnya adalah semacam bola berduri aneh yang akan berubah setiap kali Anda memuat ulang halaman. Animasi belum akan ditampilkan (hal ini akan terjadi berikutnya), tetapi beberapa kali pemuatan ulang halaman akan menunjukkan bahwa animasi diacak.
Mulailah dengan menambahkan atribut ke vertex shader:
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. Hal ini karena atribut belum disiapkan di MeshShaderMaterial sehingga shader menggunakan nilai nol. Saat ini, ini seperti placeholder. Sebentar lagi, kita akan menambahkan atribut ke MeshShaderMaterial di JavaScript dan Three.js akan otomatis mengaitkan keduanya untuk kita.
Perlu diperhatikan juga bahwa saya harus menetapkan posisi yang diperbarui ke variabel vec3 baru karena atribut asli, seperti semua atribut, bersifat hanya baca.
10. Mengupdate MeshShaderMaterial
Langsung perbarui MeshShaderMaterial dengan atribut yang diperlukan untuk menggerakkan pergeseran. Pengingat: atribut adalah nilai per verteks, jadi kita memerlukan satu nilai per verteks di 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 hal yang keren adalah semua pergeseran terjadi di GPU.
11. Animasikan
Kita harus membuat animasi ini. Bagaimana cara melakukannya? Ada dua hal yang perlu kita siapkan:
- Uniform untuk menganimasikan seberapa banyak pergeseran yang harus diterapkan di setiap frame. Kita bisa menggunakan sinus atau kosinus untuk itu karena keduanya berjalan dari -1 hingga 1
- 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. Namun, saat ini kita tampaknya telah mengambil langkah mundur. Hal ini sebagian besar karena nilai amplitudo kita berada di angka 0 dan karena kita mengalikannya dengan pergeseran, kita tidak melihat perubahan apa pun. Kita juga belum menyiapkan loop animasi, jadi kita tidak akan pernah melihat angka 0 itu berubah menjadi angka lain.
Dalam 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);
Kesimpulan
Dan selesai! Sekarang Anda dapat melihat animasinya berdenyut dengan cara yang aneh (dan sedikit membingungkan).
Ada banyak hal lain yang dapat kita bahas tentang shader sebagai topik, dan semoga pengantar ini bermanfaat bagi Anda. Sekarang Anda akan dapat memahami shader saat melihatnya, dan memiliki keyakinan untuk membuat beberapa shader yang luar biasa.