- Berangkat dari animasi CSS
border-radius sederhana di beranda, implementasi animasi blob yang halus secara langsung dengan beberapa baris fragment shader
- Shader adalah program kecil di GPU yang mengubah koordinat piksel menjadi warna; berkat eksekusi paralel ia cepat, tetapi punya banyak batasan dalam berbagi state dan abstraksi tingkat tinggi
- Contoh dasar GLSL memetakan koordinat
vUv ke nilai RGBA gl_FragColor, sekaligus memperkenalkan sintaks seperti varying, uniform, tipe vektor, dan swizzling
- Lingkaran dan blob dibuat dengan
distance(), step(), smoothstep(), serta Signed Distance Function (SDF), lalu beberapa SDF digabungkan dengan min() atau smooth minimum
- Dengan
u_time, gerakan bola dibuat; jika u_mouse ditambahkan ke array pusat, shader berkembang menjadi shader interaktif yang sebagian dapat dimanipulasi pengguna dengan mouse
Apa yang Dilakukan Shader dan Batasannya
- Shader adalah program kecil yang dijalankan di GPU, dan setidaknya menerima koordinat piksel sebagai input lalu menghasilkan warna
- Dalam gim video, shader digunakan untuk efek visual seperti pencahayaan, efek khusus, dan rendering bergaya kartun, serta menjadi fondasi penting grafis gim modern
- Kunci kecepatannya ada pada paralelisasi, yaitu dijalankan secara bersamaan di banyak piksel
- Pengantar ini berfokus pada OpenGL Shading Language yang ramah browser, yaitu GLSL
- Sebagai kompensasi atas performa, shader harus ditulis secara kecil dan low-level
- Sulit mengandalkan abstraksi tingkat tinggi atau impor library
- Karena sifat eksekusi paralel, shader bekerja secara stateless dan tanpa memori, sehingga tidak memungkinkan penyimpanan atau berbagi data antar-piksel
Contoh GLSL Pertama: Mengubah Koordinat Menjadi Warna
- Shader mengubah koordinat ternormalisasi menjadi warna RGBA
- Koordinat biasanya dinormalisasi di antara 0 dan 1
(0, 0) adalah kiri bawah, (1, 1) adalah kanan atas
- Koordinat ini secara konvensi disebut
st atau uv
- Contoh paling sederhana adalah gradien: semakin besar koordinat x, semakin meningkat warna merah; semakin besar koordinat y, semakin meningkat warna hijau
varying vec2 vUv;
void main() {
vec2 st = vUv;
gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}
- Gradien biru dibuat dengan memasukkan koordinat x ke kanal biru, seperti
gl_FragColor = vec4(0.0, 0.0, st.x, 1.0);
- Elemen penting dalam sintaksnya adalah sebagai berikut
varying: input yang nilainya berbeda untuk setiap piksel
uniform: input yang sama untuk semua piksel
vec2, vec3, vec4, mat2, mat3: tipe vektor dan matriks yang tipenya dinyatakan eksplisit seperti C
- swizzling: notasi untuk mengambil sebagian vektor, seperti
vec4(1, 2, 3, 4).xy
gl_FragColor: output yang menentukan warna tiap piksel di akhir main()
Membuat Lingkaran dengan distance(), step(), dan smoothstep()
- Bahkan saat menggambar bentuk tegas seperti lingkaran, yang digunakan bukan fungsi seperti
drawCircle(), melainkan jarak matematis
- Jarak antara piksel saat ini dan pusat lingkaran dapat dihitung dengan
distance(vec2 p1, vec2 p2)
- Jika jarak dipetakan langsung menjadi warna, hasilnya adalah gradien melingkar; lingkaran solid dibuat dengan
step(float threshold, float value)
- Jika jarak melewati nilai ambang, hasilnya 1
- Jika tidak, hasilnya 0
step() memiliki transisi tajam, sehingga dapat menimbulkan aliasing di tepi lingkaran
- Tepi yang lebih halus dapat dibuat dengan
smoothstep(float t_start, float t_end, float x)
Merepresentasikan Bentuk dengan Signed Distance Function
- Signed Distance Function (SDF) merepresentasikan seberapa jauh sebuah titik di ruang dari suatu bentuk dengan jarak bertanda
- Bagian dalam bentuk bernilai negatif
- Bagian luar bernilai positif
- Batasnya bernilai 0
- SDF untuk lingkaran dapat dibuat dengan mengurangkan radius dari jarak relatif terhadap pusat
float circleSDF(vec2 p, float r) {
return length(p) - r;
}
- Dengan menggeser titik berdasarkan posisi piksel
uv, kita dapat menghitung jarak terhadap lingkaran yang berada di posisi mana pun dalam ruang UV
- Jika
d < 0.0, piksel berada di dalam lingkaran sehingga dapat diwarnai dengan warna lain
- Bentuk SDF 2D yang lebih beragam dapat dilihat di daftar komprehensif milik Inigo Quilez
Menggabungkan Beberapa SDF untuk Membuat Blob
- SDF memudahkan pembuatan bentuk baru melalui operasi boolean
- Union dari dua SDF cukup dibuat dengan mengambil
min() dari kedua jarak
- Jika salah satu dari dua bentuk berada di bagian dalam, jarak minimum menjadi negatif
- Jika keduanya berada di luar, jarak minimum menjadi positif
- Alasan menggunakan
1. - smoothstep() adalah karena step() dan smoothstep() menghasilkan 1 untuk jarak yang lebih besar dari nilai ambang, yaitu di luar bentuk
min() sederhana menciptakan diskontinuitas tajam di titik pertemuan dua lingkaran
- Untuk mencampurnya secara halus seperti blob, gunakan smooth minimum
- Argumen tambahan
k mengendalikan tingkat kehalusannya
- Contoh ini menggunakan fungsi polynomial smooth min
float smin(float a, float b, float k)
{
float h = max( k-abs(a-b), 0.0 )/k;
return min( a, b ) - h*h*k*(1.0/4.0);
}
Membuat Animasi Metaball dengan Uniform Waktu
- Shader dapat menerima nilai arbitrer sebagai uniform, mirip slider
- Animasi dibuat dengan memasukkan
u_time dari JavaScript sebagai input shader, lalu menggunakannya dalam perhitungan koordinat pusat lingkaran
- Secara default, shader diperbarui 60 kali per detik dengan nilai
u_time baru, menghasilkan gerakan yang halus
- Pusat lingkaran dapat dibuat berayun dengan fungsi periodik seperti
sin dan cos
- Saat menggabungkan beberapa lingkaran sebagai metaball, koordinat pusat disimpan dalam array dan SDF diakumulasi dengan loop
vec2 centers[4] = vec2[4](c1,c2,c3,c4);
float d = 99.;
for (int i = 0; i < 4; i++) {
vec2 c = centers[i];
float sdf = circleSDF(uv, c, .1*u_slider);
d = smin(d, sdf, K);
}
- Blob yang dihasilkan berfungsi dengan baik, tetapi masih satu warna, sehingga kemudian ditambahkan warna dan interaksi
Interaksi Akhir yang Dikendalikan Mouse
- Tahap akhir adalah menerima koordinat mouse sebagai uniform
u_mouse agar pengguna dapat mengendalikan langsung salah satu bola di dalam blob
uniform vec2 u_mouse;
- Jika koordinat mouse ditambahkan ke array pusat bola, interaksi masuk hanya dengan satu baris
vec2 centers[5] = vec2[5](c1,c2,c3,c4,u_mouse);
- Shader akhir membalik sumbu y mouse agar sesuai dengan koordinat kanvas, lalu menggabungkan empat pusat yang bergerak bersama pusat dari mouse
- Warna dikombinasikan dengan beberapa kali menggunakan
mix(colorA, colorB, percent)
- Jika
percent digunakan seperti boolean, perilakunya mirip if/else
- Nilai
metaball, jarak titik pusat, shine, dan perhitungan membrane tercermin dalam warna akhir
- Jika memahami prinsip yang sama, Anda juga dapat lebih memahami cara kerja internal editor shader berbasis node seperti Blender’s shader nodes atau Unity’s Shader Graph
Materi Lain yang Layak Dilihat
1 komentar
Komentar Hacker News
Akhirnya memberanikan diri untuk menulis di internet dan menunjukkan diri. Sudah lama ingin belajar shader, dan kupikir akan bagus jika proses belajarnya didokumentasikan lalu dibagikan ke orang lain
Blog yang jujur sedang mati perlahan tanpa terlihat, di bawah bayang-bayang algoritma mahakuasa, dan rasanya internet sudah sampai di tengah-tengah Star Wars Episode IV. Umpan balikku cuma satu: terima kasih, dan semoga menulis lebih banyak lagi
Jika ingin melihat apa yang bisa dilakukan para ahli dengan shader, aku ingin memperkenalkan Inigo Quilez dan seni shader buatannya: https://www.youtube.com/watch?v=BFld4EBO2RE
Tambahan: tadi aku tidak melihat bahwa kamu adalah penulisnya. Tulisannya sangat bagus, dan aku memang sedang mencari tutorial yang membahas seni pemrograman shader dengan cara yang lebih mudah diakses dan interaktif
smoothstep(0.0f, 0.01f, dist);, cukup gunakansmoothstep(fwidth(dist), -fwidth(dist), dist);Tulisannya cukup bagus, tetapi terlalu menyepelekan masalah inti shader.
Shader adalah kerumitan yang sebenarnya tidak diinginkan oleh sebagian besar program dan aplikasi. 3D menyukai segitiga dan GPU juga cocok dengan abstraksi itu, dan shader berguna untuk interpolasi di atas segitiga-segitiga tersebut
Namun, di hampir semua hal yang bukan 3D, segitiga kurang cocok. Rendering 2D menginginkan path, rendering font menginginkan path atau pixmap, GUI jauh lebih cocok dengan path dan pixmap, compositor menginginkan pixmap, dan decoder video menginginkan pixmap serta rendering paralel
Yang diinginkan sisi non-3D adalah pixmap persegi panjang dan akses komputasi langsung ke pixmap itu, tetapi GPU tidak terlalu menyukainya dan shader juga tidak terlalu cocok untuk itu
Memang benar bahwa ini bukan bentuk yang diinginkan di level lebih tinggi, tetapi cukup baik sebagai fondasi untuk membangun abstraksi level lebih tinggi. Bisa saja didukung langsung oleh hardware, tetapi keuntungannya tidak begitu jelas. Tidak ada yang mengeluh bahwa CPU tidak mendukung loop
fordi tingkat arsitekturSetelah kubaca lagi, aku kurang paham masalah apa yang sebenarnya dimaksud tentang GPU. Pipeline pemrosesan vertex bisa diabaikan sepenuhnya, cukup gambar satu quad layar penuh atau gunakan compute shader, dan GPU sangat bagus dalam menangani itu. Tulisan yang ditautkan juga membahas jenis seperti ini
Setidaknya begitulah cara berpikirnya pada era 2000-an. Seiring perangkat lunak makin kompleks, GPU memang bisa menjadi optimisasi yang diinginkan, tetapi pipeline GPU selalu ketat dan tertutup, dan pergeseran paradigma dari pemrograman single-core ke multi-core sering kali menuntut algoritma yang berbeda
Pemrograman GPU umum memang membebaskan kita dari keterikatan pada segitiga, tetapi untuk menikmati paralelismenya, tetap dibutuhkan pendekatan yang sepenuhnya berbeda
Keren! Belakangan ini aku sedang cukup dalam menyusuri rabbit hole SDF. Senang melihat tautan ke situs iq; materinya memang sangat bagus
Rasanya mustahil tidak menautkan shader “happy bouncing”-nya. Menurutku pribadi ini menakjubkan: https://www.shadertoy.com/view/3lsSzf
Ada juga video YouTube berdurasi 6 jam yang menunjukkan proses pembuatannya. Kodenya padat dan mantap, sekitar 500 baris
Aku sudah beberapa kali mencoba tertarik pada topik ini, tetapi tidak pernah menemukan pintu masuk yang mudah diakses, dan baru menemukannya lewat tulisan pengantar ini. Aku suka karena pendekatannya benar-benar menyenangkan dan playful, dan aku tidak sabar membaca tulisan berikutnya
Koreksi kecil saja, tetapi saat menyebut cel shading, tertulis
cell. Istilah ini berasal dari cel yang digunakan dalam animasi gambar tangan dan dari tone kuantisasi pada shading tersebutSangat bagus. Sebagai seseorang yang tadinya seniman lalu menjadi programmer, kadang aku tergoda untuk mendalami pemrograman grafis
Aku pernah menulis beberapa shader yang sangat dasar, tetapi begitu masuk ke matematika, yang sebenarnya cukup awal, aku langsung mentok. Aku kuliah seni, bukan ilmu komputer, jadi kemampuan matematikaku praktis nyaris tidak ada
Bagaimanapun, ini ditulis dengan baik dan aku sangat menyukai tulisannya
Tulisan pengantar yang luar biasa, dan semoga terus berlanjut. Tulisan seperti ini terlalu sering dimulai dengan pembuka yang keren lalu berhenti begitu saja
Saya belum pernah bekerja dengan shader, jadi ini mungkin pertanyaan yang sangat mendasar. Dalam satu frame game, apakah pada dasarnya shader yang menggambar semuanya?
Atau apakah ada bentuk dasar seperti segitiga, persegi, lingkaran, lalu shader menambahkan hal-hal seperti bayangan atau anti-aliasing di atasnya?
Dari contohnya, terlihat seperti kita bisa membuat shader yang menggambar objek apa pun dalam sebuah scene, lalu menggabungkan shader lain untuk mendapatkan bayangan dan pencahayaan. Dari pengalaman saya yang sangat terbatas, saya menggambar bentuk, bukan menggambar dengan shader. Saya selama ini mengira shader tidak menggambar objek itu sendiri
GPU mengubah bentuk vektor abstrak seperti segitiga, yang didefinisikan oleh tiga vertex dan data seperti normal yang terkait ke tiap vertex, menjadi aliran fragmen, satu untuk tiap piksel yang dicakup bentuk itu di buffer output, atau lebih jika menggunakan multisampling. Bagian ini seluruhnya ditangani oleh perangkat keras
Fragmen terdiri dari koordinat piksel dan data yang diberikan pengguna. Datanya bisa berupa uniform yang konstan, atau varying yaitu data vertex yang disebut sebelumnya yang diinterpolasikan ke seluruh permukaan segitiga. Interpolasi ini juga ditangani perangkat keras dan tidak bisa diprogram
Fragment shader menerima fragmen sebagai input, menghitung warna berdasarkan data itu, lalu setelah beberapa tahap lagi mengeluarkannya sebagai warna piksel yang sesuai di layar atau buffer offscreen. Bisa berupa warna polos atau perhitungan pencahayaan yang kompleks
Dalam rendering GPU, semua ini berlangsung secara paralel dalam skala besar sehingga sangat banyak fragmen diproses sekaligus. Shader adalah fungsi murni tanpa status, jadi hanya bisa mengakses input dan efek yang bisa dilakukannya juga hanya mengembalikan beberapa hal seperti warna dan nilai kedalaman
Singkatnya, perangkat keras GPU menghitung piksel mana yang harus diisi untuk menggambar tiap segitiga, dan fragment shader menentukan nilai warna untuk masing-masing piksel itu
Sebelum fragment shader menggambar “di atas” scene, ada tahap lain seperti vertex dan tessellation yang menggambar bentuk dasar
Bahkan untuk fragment shader pun, masih ada jauh lebih banyak hal daripada yang sudah dijelaskan. Misalnya deferred rendering[2], dan itu sendiri kalau dibahas juga topik besar
1: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipe...
2: https://learnopengl.com/Advanced-Lighting/Deferred-Shading
Ada graphics pipeline yang merupakan campuran tahap perangkat keras fungsi tetap dan tahap yang bisa diprogram. Pada level tinggi, 1) GPU menerima kumpulan segitiga 3D dari CPU, 2) vertex shader mengubah vertex segitiga 3D menjadi vertex segitiga 2D dengan koordinat piksel sehingga menjadi datar, 3) GPU melakukan rasterisasi segitiga 2D untuk menentukan secara tepat piksel mana yang tercakup segitiga, 4) untuk tiap piksel yang tercakup, pixel shader dijalankan untuk menentukan warna piksel, dan 5) warna piksel hasilnya disimpan di frame buffer. Pada tahap ini warnanya juga bisa di-blend dengan warna yang sudah ada
Pipeline ini diulang berkali-kali dengan mesh segitiga dan shader yang berbeda sampai seluruh frame selesai digambar
Tugas seperti warna, bayangan, shading, efek gambar, dan pemrosesan gambar umum semuanya dilakukan sebagai komputasi paralel yang menggabungkan banyak array data. Ini mencakup vertex dan atributnya, tekstur sumber, fungsi yang sudah dipra-hitung, tekstur target, buffer, dan sebagainya
Misalnya, untuk mendapatkan cahaya dan bayangan, shader harus bisa mengakses hal-hal seperti posisi dan arah spotlight, mungkin sebagai variabel global. Pencahayaan komposit sering diperoleh dengan menggabungkan beberapa shader pass. Misalnya satu pass dasar untuk pencahayaan global dan satu pass untuk tiap cahaya, lalu tiap pass secara harfiah menambahkan cahaya
Sekarang, agar tidak menambahkan cahaya pada piksel yang tertutup dari sumber cahaya, yaitu bayangan, teknik yang paling umum adalah menggunakan Z-buffer. Pada dasarnya ini adalah tekstur floating-point. Karena kita ingin tahu seberapa jauh tiap cahaya menjangkau scene, sebelum menerapkan semua pencahayaan kita menyiapkan satu shader pass tunggal yang menggabungkan seluruh geometri solid scene, menggunakan posisi dan arah cahaya seperti transformasi kamera, dan memakai shader khusus yang hanya menulis jarak ke objek ke Z-buffer
Setelah itu, setiap kali ingin tahu apakah cahaya mencapai suatu titik di ruang, setelah sedikit perhitungan geometri kita melakukan sampling pada Z-buffer ini, lalu membandingkan nilai yang tersimpan pada arah tersebut dengan jarak titik itu. Ini bisa penuh bug dan kesalahan presisi juga umum terjadi. Engine yang matang biasanya sudah menangani ini, tetapi tetap memungkinkan Anda memodifikasi prosesnya
Sisanya pada dasarnya hanyalah variasi dari topik ini. Deferred rendering adalah cara merender data ke tekstur perantara alih-alih warna, lalu memprosesnya nanti untuk mendapatkan warna. Efek blur dilakukan sebagai konvolusi 2D pada tekstur render, misalnya dengan kernel Gaussian. Tessellation shader berkaitan dengan pembuatan geometri baru dari vertex shader. Bahkan menggambar teks pun dilakukan melalui atlas font dan persegi panjang kecil
Karena itu, para artis berbakat mendorong batas kemampuan di dalam fragment shader sambil bergulat dengan kompromi performa
Fragment shader lebih umum dipakai untuk efek filter layar penuh, misalnya koreksi warna
Shader juga digunakan untuk membuat tekstur dan material objek dasar. Para material artist sering menghasilkan tekstur dengan matematika shader
Banyak efek visual dibuat dengan penggunaan shader yang kreatif
Shader dijalankan di GPU dengan cara paralel seperti gelombang. Sangat banyak thread berjalan dalam satu wave pada data yang sama
Dalam beberapa kasus, shader bisa jauh lebih cepat daripada kode bercabang di CPU. Shader juga bisa mengakses sebagian data rendering dengan lebih mudah
Jadi, ini adalah ruang yang cocok untuk membuat efek khusus yang kreatif. Dalam game, objek dengan detail permukaan yang tinggi adalah target umum untuk memindahkan detail tersebut ke shader. Contohnya permukaan laut, mesh tessellation, dan masih banyak kegunaan lain karena GPU kuat dan fleksibel.
Jika ada yang melihat gambar tampak seperti noise yang berkedip, saya mengatasinya dengan menyalin gambar dari browser lalu menempelkannya ke tempat lain. Setelah itu gambarnya bisa terlihat dengan benar.
Ini tautan Imgur. Gambar pertama adalah tangkapan layar dari yang terlihat di browser, dan sisanya adalah gambar sebenarnya setelah ditempel ke Imgur.
https://imgur.com/a/F4203rz
Pemahaman saya tentang shader seperti ini.
Saat menggambar garis di CPU, itu adalah fungsi yang menelusuri setiap piksel di antara titik A dan titik B lalu menggambarnya satu per satu secara berurutan. Ada tepat sejumlah langkah sesuai jumlah piksel pada garis, dan dijalankan satu kali.
Saat menggambar garis di GPU, itu adalah fungsi yang memeriksa apakah piksel tersebut berada di atas garis, lalu jika ya maka digambar. Fungsi ini dijalankan secara bersamaan pada semua piksel di layar, termasuk piksel yang jauh dari garis.
Apakah ini benar?
Kedua, GPU juga tidak perlu menjalankan pixel shader untuk seluruh layar. Dengan menggunakan segitiga, shader bisa dijalankan hanya untuk bentuk arbitrer yang diinginkan. Jadi cara yang efisien untuk menggambar garis adalah mengirim dua segitiga ke GPU yang sesuai dengan geometri garis yang diinginkan, lalu menjalankan pixel shader hanya pada piksel yang ditutupi segitiga tersebut. Jauh lebih efisien.