Pengenalan ke Shaders
Saatnya kita memulai perjalanan ke dunia shaders. Mereka sangat penting untuk menciptakan berbagai efek visual. Dalam bab ini, kita akan mempelajari tentang shaders, apa yang bisa kita capai dengannya, dan bagaimana menggunakannya di React Three Fiber.
Pendahuluan
Sebelum kita mulai, saya ingin menyebutkan bahwa shaders bisa memerlukan waktu untuk membiasakan diri. Ini bekerja berbeda dari kode lain yang telah kita tulis sejauh ini. Ini adalah cara berpikir dan menciptakan efek visual yang baru. Namun, jangan khawatir, saya akan memandu Anda melalui proses ini, kita akan mulai dengan dasar-dasarnya, dan secara bertahap bergerak ke topik yang lebih maju.
Jangan merasa berkecil hati jika Anda tidak memahami semuanya pada awalnya. Ini normal. Ini adalah konsep baru, dan memerlukan latihan untuk merasa lebih nyaman dengannya. Saya sarankan Anda untuk meluangkan waktu, bereksperimen, dan berlatih, tapi saya janji, itu sangat berharga! Shaders sangat kuat dan akan memberikan Anda kontrol untuk menciptakan efek visual apa pun yang Anda bayangkan.
Selain itu, kita semua memiliki gaya belajar yang berbeda. Beberapa orang belajar lebih baik dengan membaca, yang lain dengan menonton video, dan yang lain dengan melakukan. Untuk topik ini khususnya, merujuk silang berbagai sumber bisa sangat membantu. Saya akan membagikan sumber kepada Anda di akhir bab ini untuk mengkonsolidasikan pengetahuan Anda dan melampaui apa yang kita bahas di sini.
Saya harap saya tidak menakut-nakuti Anda dan bahwa Anda bersemangat untuk belajar tentang shaders. Mari kita mulai!
Apa itu Shaders?
Shaders adalah program kecil yang berjalan di GPU (Graphics Processing Unit). Mereka ditulis dalam bahasa yang disebut GLSL (OpenGL Shading Language), yang mirip dengan C.
Mereka digunakan untuk memposisikan verteks dari sebuah mesh (Vertex Shader) dan mewarnai setiap piksel dari wajah (Fragment Shader).
Sebenarnya, kita telah menggunakan shaders selama ini. Ketika kita membuat sebuah material, kita menggunakan sebuah shader. Misalnya, ketika kita membuat MeshBasicMaterial
, kita menggunakan sebuah shader yang mewarnai mesh dengan satu warna. Ketika kita membuat MeshStandardMaterial
, kita menggunakan sebuah shader yang mensimulasikan pencahayaan, bayangan, dan refleksi.
Vertex Shader
Sebuah vertex shader adalah program yang dieksekusi untuk setiap verteks dari sebuah geometri. Tanggung jawab utamanya adalah mengubah verteks dari ruang 3D (dunia 3D kita) ke ruang 2D (layar atau viewport kita). Transformasi ini dicapai dengan memanfaatkan beberapa matriks:
- View Matrix: Matriks ini mewakili posisi dan orientasi kamera dalam adegan. Ini mengubah verteks dari ruang dunia ke ruang kamera.
- Projection Matrix: Matriks ini, baik perspektif maupun ortografik, mengonversi verteks dari ruang kamera ke normalized device coordinates (NDC), mempersiapkan mereka untuk proyeksi akhir ke layar 2D.
- Model Matrix: Matriks ini mengenkapsulasi posisi, rotasi, dan skala dari setiap objek individual di dalam adegan. Ini mengubah verteks dari ruang objek ke ruang dunia.
Selain itu, vertex shader juga menggabungkan posisi asli verteks dan atribut lain yang terkait dengan itu.
Untuk setiap verteks dari geometri, vertex shader akan dieksekusi.
Akhirnya, posisi yang telah diubah dari verteks dalam ruang 2D dikembalikan melalui variabel pradidefinisikan gl_Position
. Setelah semua verteks diubah, GPU menginterpolasi nilai antara mereka untuk menghasilkan wajah-wajah dari geometri, yang kemudian di-rasterisasi dan dirender ke layar.
Fragment Shader
Fragment shader, juga dikenal sebagai pixel shader, adalah program yang dieksekusi untuk setiap fragmen (atau piksel) yang dihasilkan oleh proses rasterisasi. Tugas utamanya adalah menentukan warna akhir dari setiap piksel di layar.
Untuk setiap fragmen yang dihasilkan selama rasterisasi, fragment shader akan dieksekusi.
Fragment shader menerima nilai yang diinterpolasi dari vertex shader, seperti warna, koordinat tekstur, normal, dan atribut lain yang terkait dengan verteks dari geometri. Nilai-nilai yang diinterpolasi ini disebut varyings, dan menyediakan informasi tentang properti permukaan di setiap lokasi fragmen.
Selain nilai-nilai interpolasi, fragment shader juga dapat mengambil sampel tekstur dan mengakses variabel uniform, yang konstan di seluruh fragmen. Variabel uniform ini dapat mewakili parameter seperti posisi cahaya, properti material, atau data lain yang diperlukan untuk komputasi shading.
Kita akan kembali ke attributes dan uniforms nanti dalam pelajaran ini.
Dengan menggunakan data input, fragment shader melakukan berbagai komputasi untuk menentukan warna akhir dari fragmen. Ini dapat melibatkan perhitungan pencahayaan yang kompleks, tekstur mapping, efek shading, atau efek visual lain yang diinginkan dalam adegan.
Setelah komputasi warna selesai, fragment shader mengeluarkan warna akhir dari fragmen menggunakan variabel yang sudah ditentukan gl_FragColor
.
Saya berusaha sebisa mungkin menjelaskan shaders dengan cara yang sederhana dan sengaja menghilangkan beberapa detail teknis, tetapi saya mengerti bahwa mungkin tetap terasa abstrak. Mari kita buat shader sederhana untuk melihat bagaimana ini bekerja dalam praktik.
Shader Pertama Anda
Mari jalankan starter pack. Anda seharusnya melihat frame ini dengan bidang hitam di tengah layar:
Buka file ShaderPlane.jsx
, file ini berisi mesh sederhana dengan geometri bidang dan material dasar. Kita akan mengganti material ini dengan shader material kustom.
shaderMaterial
Untuk membuat shader material, kita gunakan fungsi shaderMaterial
dari Drei library.
Ini mengambil 3 parameter:
uniforms
: Objek yang berisi variabel uniform yang digunakan dalam shader. Biarkan kosong untuk sekarang.vertexShader
: String yang berisi kode GLSL untuk vertex shader.fragmentShader
: String yang berisi kode GLSL untuk fragment shader.
Di bagian atas file kita, mari deklarasikan shader material baru bernama MyShaderMaterial
:
import { shaderMaterial } from "@react-three/drei"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` );
Kita akan mendalami kode shader sebentar lagi.
Agar bisa menggunakannya secara deklaratif dengan React Three Fiber, kita gunakan metode extend
:
import { extend } from "@react-three/fiber"; // ... extend({ MyShaderMaterial });
Sekarang kita bisa mengganti <meshBasicMaterial>
dengan shader material baru kita:
import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` ); extend({ MyShaderMaterial }); export const ShaderPlane = ({ ...props }) => { return ( <mesh {...props}> <planeGeometry args={[1, 1]} /> <myShaderMaterial /> </mesh> ); };
Anda seharusnya melihat bidang hitam yang sama seperti sebelumnya. Kita belum mengubah apa pun, tetapi sekarang kita menggunakan shader material kustom.
Untuk memverifikasi bahwa ini berfungsi, mari kita ubah warna yang kita kembalikan dalam fragment shader. Ganti baris gl_FragColor
dengan yang berikut:
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor
adalah variabel yang sudah ditentukan yang mewakili warna dari fragmen. Ini adalah vec4
(vektor dengan 4 komponen) yang mewakili saluran merah, hijau, biru, dan alpha dari warna. Setiap komponen adalah float antara 0 dan 1.
Dengan mengatur komponen pertama ke 1.0
, kita mengatur saluran merah ke nilai maksimumnya, yang akan menghasilkan warna merah.
Anda seharusnya melihat bidang merah di tengah layar:
Selamat! Anda baru saja membuat shader material pertama Anda. Ini yang sederhana, tetapi ini awal yang baik.
Shader Code
Sebelum kita melanjutkan, mari kita siapkan lingkungan pengembangan kita untuk menulis shader dengan lebih nyaman.
Anda memiliki dua opsi untuk menulis kode shader:
- Inline: Anda dapat menulis kode shader langsung di dalam file JavaScript.
- Eksternal: Anda dapat menulis kode shader dalam file terpisah dengan ekstensi
.glsl
dan mengimpornya ke dalam file JavaScript Anda.
Saya biasanya lebih memilih pendekatan inline di dalam file material yang sesuai, dengan cara ini kode shader berada dekat dengan deklarasi material.
Tapi untuk memudahkan penulisan dan pembacaan, saya merekomendasikan menggunakan syntax highlighter untuk GLSL. Anda dapat menggunakan ekstensi Comment tagget templates untuk Visual Studio Code. Ini akan menyoroti kode GLSL di dalam template strings.
Setelah terpasang, untuk mengaktifkan syntax highlighter, Anda perlu menambahkan komentar berikut di awal kode shader:
const MyShaderMaterial = shaderMaterial( {}, /* glsl */ ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /* glsl */ ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } ` );
Anda seharusnya melihat kode GLSL disorot dalam template strings:
Shader vertex di atas sekarang memiliki penyorotan sintaks yang benar dan lebih mudah dibaca.
Ini semua yang Anda butuhkan untuk kode shader inline. Anda masih dapat memutuskan untuk menggunakan file eksternal jika Anda lebih suka memisahkan kode shader Anda. Mari kita lihat cara melakukannya.
Mengimpor File GLSL
Pertama, buat folder baru bernama shaders
di dalam folder src
. Di dalam folder ini, buat dua file: myshader.vertex.glsl
dan myshader.fragment.glsl
dan salin kode shader masing-masing ke dalam setiap file.
myshader.vertex.glsl
:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
myshader.fragment.glsl
:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
Silakan gunakan konvensi penamaan yang Anda sukai, dan untuk mengelompokkan shader Anda dalam subfolder jika Anda memiliki banyak shaders.
Kemudian, agar dapat mengimpor file-file ini dalam file JavaScript kita, kita perlu menginstal plugin vite-plugin-glsl sebagai dependency pengembangan:
yarn add vite-plugin-glsl --dev
Kemudian, di file vite.config.js
Anda, impor plugin tersebut dan tambahkan ke array plugins
:
import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import glsl from "vite-plugin-glsl"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), glsl()], });
Sekarang Anda dapat mengimpor file GLSL ke dalam file JavaScript Anda dan menggunakannya sebagai kode shader:
import myShaderFragment from "./shaders/myshader.fragment.glsl"; import myShaderVertex from "./shaders/myshader.vertex.glsl"; const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);
Sekarang kita memiliki cara yang nyaman untuk menulis dan mengimpor kode shader, kita dapat mulai mengeksplorasi bagian-bagian berbeda dari kode shader tersebut.
GLSL
Kode shader ditulis dalam GLSL (OpenGL Shading Language). Ini adalah bahasa mirip C, mari kita lihat dasar-dasarnya.
Tipe-Tipe
GLSL memiliki beberapa tipe, tetapi yang paling umum adalah:
- bool: Nilai boolean (
true
ataufalse
). - int: Angka integer.
- float: Angka floating-point.
- vectors: Sekumpulan angka.
vec2
adalah kumpulan dari 2 angka floating-point (x
dany
),vec3
adalah kumpulan dari 3 angka floating-point (x
,y
, danz
), danvec4
adalah kumpulan dari 4 angka floating-point (x
,y
,z
, danw
). Selain menggunakanx
,y
,z
, danw
, Anda juga dapat menggunakanr
,g
,b
, dana
untuk warna, karena keduanya dapat dipertukarkan. - matrices: Sekumpulan vektor. Misalnya,
mat2
adalah kumpulan dari 2 vektor,mat3
adalah kumpulan dari 3 vektor, danmat4
adalah kumpulan dari 4 vektor.
Swizzling dan Manipulasi
Anda dapat mengakses komponen dari sebuah vektor menggunakan swizzling. Misalnya, Anda dapat membuat vektor baru dengan menggunakan komponen dari vektor lain:
vec3 a = vec3(1.0, 2.0, 3.0); vec2 b = a.xy;
Dalam contoh ini, b
akan menjadi vektor dengan komponen x
dan y
dari a
.
Anda juga dapat menggunakan swizzling untuk mengubah urutan komponen:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = a.zyx;
Dalam contoh ini, b
akan sama dengan vec3(3.0, 2.0, 1.0)
.
Untuk membuat vektor baru dengan semua komponen yang sama, Anda dapat menggunakan konstruktor:
vec3 a = vec3(1.0);
Dalam contoh ini, a
akan sama dengan vec3(1.0, 1.0, 1.0)
.
Operator
GLSL memiliki operator aritmatika umum: +
, -
, *
, /
, +=
, /=
, *=
dan operator perbandingan umum: ==
, !=
, >
, <
, >=
, <=
.
Operator ini harus digunakan dengan tipe yang benar. Misalnya, Anda tidak bisa menambahkan integer ke float, Anda harus mengonversi integer ke float terlebih dahulu:
int a = 1; float b = 2.0; float c = float(a) + b;
Anda juga dapat melakukan operasi pada vector dan matriks:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = vec3(4.0, 5.0, 6.0); vec3 c = a + b;
Yang mana sama dengan:
vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);
Fungsi
Titik masuk dari vertex dan fragment shaders adalah fungsi main
. Ini adalah fungsi yang akan dieksekusi ketika shader dipanggil.
void main() { // Kode Anda di sini }
void adalah tipe pengembalian dari fungsi. Ini berarti bahwa fungsi tersebut tidak mengembalikan apapun.
Anda juga dapat mendefinisikan fungsi Anda sendiri:
float add(float a, float b) { return a + b; }
Kemudian Anda dapat memanggil fungsi ini dalam fungsi main
:
void main() { float result = add(1.0, 2.0); // ... }
GLSL menyediakan banyak fungsi built-in untuk operasi umum seperti sin
, cos
, max
, min
, abs
, round
, floor
, ceil
, dan banyak lainnya yang berguna seperti mix
, step
, length
, distance
, dan lainnya.
Kita akan menemukan dan berlatih dengan yang esensial dalam pelajaran berikutnya.
Loop dan Kondisi
GLSL mendukung for
loop dan if
statement. Keduanya bekerja mirip dengan JavaScript:
for (int i = 0; i < 10; i++) { // Kode Anda di sini } if (condition) { // Kode Anda di sini } else { // Kode Anda di sini }
Logging / Debugging
Karena program shader berjalan untuk setiap vertex dan fragmen secara paralel, tidak memungkinkan untuk menggunakan console.log
untuk mendebug kode Anda atau menambahkan breakpoint. Inilah yang membuat sulit untuk mendebug shader.
Cara umum untuk mendebug shader adalah menggunakan gl_FragColor
untuk memvisualisasikan nilai dari variabel Anda.
Kesalahan kompilasi
Jika Anda melakukan kesalahan dalam kode shader Anda, Anda akan melihat kesalahan kompilasi di konsol. Ini akan memberi tahu Anda baris dan jenis kesalahannya. Mungkin tidak selalu mudah dipahami, tetapi ini cara yang baik untuk mengetahui di mana mencari masalahnya.
Mari kita hapus saluran alpha dari gl_FragColor
dan lihat apa yang terjadi:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0); }
Anda seharusnya melihat kesalahan kompilasi di konsol:
Memberi tahu kita bahwa gl_FragColor
mengharapkan 4 komponen, tetapi kita hanya memberikan 3.
Jangan lupa untuk mengembalikan saluran alpha ke 1.0
untuk menghilangkan kesalahan.
Uniforms
Untuk mengirim data dari kode JavaScript ke shader, kita menggunakan uniforms. Mereka bersifat konstan di semua vertices dan fragmen.
projectionMatrix
, modelViewMatrix
, dan position
adalah contoh dari built-in uniforms yang secara otomatis dikirim ke shader.
Mari kita buat uniform kustom untuk mengirim warna ke shader. Kita akan menggunakannya untuk mewarnai pesawat. Kita akan menyebutnya uColor
. Adalah praktik yang baik untuk menambahkan prefix u
pada nama uniform untuk memperjelas bahwa itu adalah uniform dalam kode kita.
Pertama, mari kita deklarasikan dalam objek uniforms dari shaderMaterial
:
import { Color } from "three"; // ... const MyShaderMaterial = shaderMaterial( { uColor: new Color("pink"), } // ... ); // ...
Kemudian, kita dapat menggunakannya dalam fragmen shader:
uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 1.0); }
Anda seharusnya melihat pesawat berwarna merah muda:
Di sini, warna merah muda adalah nilai default dari uniform. Kita dapat mengubahnya langsung pada material:
<MyShaderMaterial uColor={"lightblue"} />
Pesawat sekarang berwarna biru muda.
Baik vertex maupun fragmen shader dapat mengakses uniforms. Mari tambahkan waktu sebagai uniform kedua ke vertex shader untuk menggerakkan pesawat naik dan turun:
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.