Pengantar
Baca pengantar Three.js terlebih dahulu, karena merupakan 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, terkadang Anda ingin mencapai efek tertentu, atau Anda ingin mempelajari lebih dalam bagaimana hal-hal luar biasa tersebut muncul di layar Anda, dan shader hampir pasti menjadi bagian dari persamaan tersebut. Anda mungkin ingin beralih dari materi dasar dalam tutorial terakhir ke sesuatu yang sedikit lebih rumit.
Saya akan bekerja berdasarkan asumsi bahwa Anda menggunakan Three.js, karena Three.js melakukan banyak pekerjaan untuk kita dalam hal menjalankan shader. Anda akan mempelajari konteks shader terlebih dahulu, diikuti dengan langkah-langkah lanjutan di bagian selanjutnya dalam dokumen ini.
1. Dua Shader Kami
WebGL tidak menawarkan penggunaan Fixed Pipeline, yang merupakan cara singkat untuk mengatakan bahwa WebGL tidak memberi Anda cara untuk merender konten Anda secara langsung. Yang ditawarkan adalah Programmable Pipeline, yang lebih canggih tetapi juga lebih sulit dipahami dan digunakan. Singkatnya, Programmable Pipeline 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, saya yakin Anda akan setuju, tidak berarti apa pun dengan sendirinya. Yang perlu Anda ketahui tentang keduanya adalah bahwa keduanya berjalan sepenuhnya di GPU kartu grafis Anda. Artinya, kita ingin memindahkan semua yang bisa kita lakukan ke GPU, sehingga CPU kita 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. Terdiri dari titik sudut, kan? Vertex shader diberi setiap vertex ini secara bergantian dan dapat memanipulasinya. Vertex shader yang menentukan apa yang sebenarnya dilakukan dengan setiap vertex, tetapi memiliki satu tanggung jawab: pada suatu titik, vertex shader harus menetapkan sesuatu yang disebut gl_Position, yaitu vektor float 4D, yang merupakan posisi akhir vertex di layar. Proses itu sendiri cukup menarik, karena kita sebenarnya berbicara tentang mendapatkan posisi 3D (vertex dengan x,y,z) ke, atau diproyeksikan, ke layar 2D. Untungnya bagi kita, jika kita menggunakan sesuatu seperti Three.js, kita memiliki cara singkat untuk menyetel gl_Position tanpa 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 vertex shader, fragment shader juga hanya memiliki satu tugas yang harus dilakukan: yaitu menetapkan atau menghapus variabel gl_FragColor, vektor float 4D lainnya, yang merupakan warna akhir fragmen kita.
Namun, apa itu fragmen? Pikirkan tiga verteks yang membentuk segitiga. Setiap piksel dalam segitiga tersebut harus digambar. Fragmen adalah data yang disediakan oleh ketiga verteks tersebut untuk tujuan menggambar setiap piksel dalam segitiga tersebut. Oleh karena itu, fragmen menerima nilai terinterpolasi dari verteks penyusunnya. Jika satu verteks berwarna merah, dan verteks tetangganya berwarna biru, kita akan melihat nilai warna diinterpolasi dari merah, melalui ungu, hingga biru.
4. Variabel Shader
Ada tiga deklarasi variabel yang dapat Anda buat: Uniforms, Attributes, dan Varyings. Bahasa ini mungkin membingungkan karena tidak cocok dengan bahasa umum lainnya. Berikut cara Anda dapat memikirkannya:
Uniform dikirim ke shader vertex dan shader fragmen keduanya dan berisi nilai yang tetap sama di seluruh frame yang dirender. Contoh yang baik adalah posisi lampu.
Atribut adalah nilai yang diterapkan ke masing-masing verteks. Atribut hanya tersedia untuk shader vertex. Contohnya, setiap verteks memiliki warna yang berbeda. Atribut memiliki hubungan one-to-one dengan verteks.
Varying adalah variabel yang dideklarasikan di shader vertex yang ingin kita bagikan dengan shader fragmen. Untuk melakukannya, kita harus memastikan bahwa kita mendeklarasikan variabel varying dengan jenis dan nama yang sama di shader vertex dan shader fragmen. Penggunaan klasiknya adalah normalitas verteks karena dapat digunakan dalam perhitungan pencahayaan.
Selanjutnya, kita akan menggunakan ketiga jenis tersebut sehingga Anda dapat memahami cara penerapannya.
Tidak, lihat salah satu shader paling sederhana yang dapat Anda buat.
5. Bonjourno World
Berikut adalah Hello World 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);
}
Namun, tidak terlalu rumit, bukan?
Di shader vertex, kita dikirimi beberapa uniform oleh Three.js. Kedua uniform ini adalah matriks 4D, yang disebut Matriks Model-Tampilan dan Matriks Proyeksi. Anda tidak perlu mengetahui persis cara kerjanya, meskipun selalu lebih baik untuk memahami cara kerja sesuatu jika Anda bisa. Singkatnya, matriks ini adalah cara posisi 3D verteks diproyeksikan ke posisi 2D akhir di layar.
Saya sebenarnya tidak menyertakannya dalam cuplikan contoh karena Three.js menambahkannya ke bagian atas kode shader Anda sendiri sehingga Anda tidak perlu mengkhawatirkannya. Sebenarnya, banyak hal yang ditambahkan selain itu, seperti data cahaya, warna verteks, dan normal verteks. Jika Anda melakukannya tanpa Three.js, Anda harus membuat dan menyetel semua seragam dan atribut tersebut sendiri. Kisah nyata.
6. Menggunakan MeshShaderMaterial
Oke, kita telah 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 berikan material tersebut. Prosesnya sangat mudah. Mungkin saja, tetapi kita sedang membahas 3D yang berjalan di browser Anda, jadi saya rasa Anda mengharapkan tingkat kompleksitas tertentu.
Kita dapat menambahkan dua properti lagi ke MeshShaderMaterial: uniforms dan attributes. Keduanya dapat mengambil vektor, bilangan bulat, atau float, tetapi seperti yang saya sebutkan sebelumnya, seragam 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 verteks, dan seragam. Kita juga akan menambahkan variabel yang bervariasi agar shader vertex dapat mengirimkan beberapa data ke shader fragmen. Hasil akhirnya adalah bola kita yang berwarna merah muda akan tampak diterangi dari atas dan samping, serta akan berdenyut. Agak membingungkan, tetapi semoga hal ini akan membuat Anda memahami ketiga jenis variabel serta hubungannya satu sama lain dan geometri yang mendasarinya.
8. Cahaya palsu
Sekarang, perbarui pewarnaan agar tidak menjadi objek berwarna datar. Kita dapat melihat cara Three.js menangani pencahayaan, tetapi seperti yang Anda ketahui, hal ini lebih rumit 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 luar biasa oleh Chris Milk dan Google, Rome. Kembali ke shader kita. Kita akan memperbarui Vertex Shader untuk menyediakan normal setiap 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 normal vertex dengan vektor yang merepresentasikan cahaya yang bersinar dari atas dan ke kanan bola. Hasil bersih dari hal 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 color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
Jadi, alasan produk dot berfungsi adalah karena jika diberi dua vektor, produk dot 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 menunjuk ke arah yang berlawanan, Anda akan mendapatkan -1. Yang kita lakukan adalah mengambil angka tersebut dan menerapkannya pada pencahayaan. Jadi, verteks di kanan atas akan memiliki nilai yang mendekati atau sama dengan 1 (terang sepenuhnya), sedangkan verteks di samping akan memiliki nilai yang mendekati 0 dan di belakang akan menjadi -1. Kita 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 lebih baik jika kita mencoba mengubah 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 akan berupa bola berduri aneh yang akan berubah setiap kali Anda memuat ulang halaman. Gambar tidak akan dianimasikan (itu akan terjadi selanjutnya), tetapi beberapa kali memuat ulang halaman akan menunjukkan bahwa gambar diacak.
Mulai 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 terlalu berbeda. Hal ini karena atribut belum disiapkan di MeshShaderMaterial sehingga shader menggunakan nilai nol. Saat ini, ini seperti placeholder. Dalam beberapa detik, kita akan menambahkan atribut ke MeshShaderMaterial di JavaScript dan Three.js akan mengikat keduanya secara otomatis.
Perlu diperhatikan juga bahwa saya harus menetapkan posisi yang diperbarui ke variabel vec3 baru karena atribut asli, seperti semua atribut, bersifat hanya baca.
10. Memperbarui MeshShaderMaterial
Langsung saja perbarui MeshShaderMaterial dengan atribut yang diperlukan untuk menggerakkan perpindahan. Sebagai pengingat: atribut adalah nilai per verteks sehingga 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. Animasikan
Kita harus membuat ini beranimasi. Bagaimana cara melakukannya? Ada dua hal yang perlu kita siapkan:
- Uniform untuk menganimasikan seberapa besar perpindahan yang harus diterapkan di setiap frame. Kita dapat menggunakan sinus atau kosinus untuk itu karena keduanya berjalan dari -1 hingga 1
- Loop animasi di JS
Kita akan menambahkan seragam ke MeshShaderMaterial dan Vertex Shader. Pertama, Shader Verteks:
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 mundur selangkah. Hal ini sebagian besar karena nilai amplitudo kita adalah 0 dan karena kita mengalikannya dengan perpindahan, kita tidak melihat perubahan apa pun. Kita juga belum menyiapkan loop animasi sehingga kita tidak pernah melihat 0 berubah menjadi apa pun.
Di JavaScript, kita sekarang perlu membungkus panggilan render ke dalam fungsi, lalu menggunakan requestAnimationFrame untuk memanggilnya. Di sana, kita juga perlu memperbarui nilai seragam.
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
Selesai. Sekarang Anda dapat melihat animasinya berdenyut dengan cara yang aneh (dan sedikit trippy).
Masih banyak lagi yang bisa kita bahas tentang shader sebagai topik, dan semoga pengantar ini bermanfaat bagi Anda. Sekarang Anda dapat memahami shader saat melihatnya, dan memiliki keyakinan untuk membuat shader yang luar biasa sendiri.