Dalam tutorial saya
"Membuat Shaders", saya terutama melihat fragmen shaders, yang cukup untuk menerapkan efek 2D dan contoh di
ShaderToy . Tetapi ada seluruh kategori teknik yang membutuhkan penggunaan vertex shaders. Dalam tutorial ini, saya akan berbicara tentang membuat shader air kartun bergaya dan memperkenalkan Anda ke shader vertex. Saya juga akan berbicara tentang penyangga kedalaman dan cara menggunakannya untuk mendapatkan informasi lebih lanjut tentang pemandangan dan untuk membuat garis busa laut.
Beginilah efek akhirnya akan terlihat. Demo interaktif dapat dilihat di
sini .
Efek ini terdiri dari unsur-unsur berikut:
- Jala air bening dengan poligon terbagi dan simpul offset untuk membuat gelombang.
- Garis air statis di permukaan.
- Simulasi daya apung perahu.
- Garis busa dinamis di sekitar batas benda di dalam air.
- Pasca pemrosesan untuk membuat distorsi dari segala sesuatu di bawah air
Dalam efek ini, saya menyukai kenyataan bahwa ia menyentuh banyak konsep berbeda dari grafik komputer, sehingga akan memungkinkan kita untuk menggunakan ide-ide dari tutorial sebelumnya, serta mengembangkan teknik yang dapat diterapkan dalam efek baru.
Dalam tutorial ini, saya akan menggunakan
PlayCanvas , hanya karena itu adalah web-IDE gratis yang nyaman, tetapi semuanya dapat diterapkan ke lingkungan WebGL lainnya tanpa masalah. Di akhir artikel, versi kode sumber untuk Three.js akan disajikan. Kami akan menganggap bahwa Anda sudah berpengalaman dalam fragmen shader dan antarmuka PlayCanvas. Anda dapat menyegarkan kembali pengetahuan Anda tentang shader di
sini , dan berkenalan dengan PlayCanvas di
sini .
Pengaturan lingkungan
Tujuan dari bagian ini adalah untuk mengkonfigurasi proyek PlayCanvas kami dan memasukkan ke dalamnya beberapa objek lingkungan yang akan dipengaruhi oleh air.
Jika Anda tidak memiliki akun PlayCanvas, maka
daftarkan dan buat
proyek kosong baru. Secara default, Anda harus memiliki beberapa objek dalam adegan, kamera dan sumber cahaya.
Masukkan Model
Sumber yang bagus untuk menemukan model 3D untuk web adalah proyek Google
Poly . Saya mengambil
model kapal dari sana. Setelah mengunduh dan membongkar arsip, Anda akan menemukan file
.obj
dan
.png
di dalamnya.
- Seret kedua file ke jendela Aset proyek PlayCanvas.
- Pilih bahan yang dihasilkan secara otomatis dan pilih file
.png
sebagai peta difusnya.
Sekarang Anda dapat menyeret
Tugboat.json ke tempat kejadian dan menghapus objek Kotak dan Pesawat. Jika kapal terlihat terlalu kecil, Anda dapat meningkatkan skalanya (saya menetapkan nilainya menjadi 50).
Demikian pula, Anda dapat menambahkan model lain ke TKP.
Mengorbit kamera
Untuk mengonfigurasi kamera yang terbang di orbit, kami akan menyalin skrip dari
contoh PlayCanvas ini . Ikuti tautan dan klik
Editor untuk membuka proyek.
- Salin konten
mouse-input.js
dan orbit-camera.js
dari proyek tutorial ini ke dalam file dengan nama yang sama dari proyek Anda. - Tambahkan komponen Script ke kamera.
- Lampirkan dua skrip ke kamera.
Petunjuk: untuk mengatur proyek, Anda dapat membuat folder di jendela Aset. Saya menempatkan dua skrip kamera ini dalam Skrip / Kamera / folder, model saya di Model /, dan materi dalam folder Bahan /.
Sekarang ketika Anda memulai permainan (tombol peluncuran di bagian kanan atas jendela pemandangan) Anda akan melihat sebuah kapal yang dapat Anda periksa dengan kamera dengan menggerakkannya di orbit dengan mouse.
Divisi Polygon Permukaan Air
Tujuan dari bagian ini adalah untuk membuat mesh yang terbagi lagi yang akan digunakan sebagai permukaan air.
Untuk membuat permukaan air, kami mengadaptasi bagian dari kode dari
tutorial generasi bantuan . Buat
Water.js
skrip
Water.js
baru. Buka skrip ini untuk mengedit dan membuat fungsi
GeneratePlaneMesh
baru yang akan terlihat seperti ini:
Water.prototype.GeneratePlaneMesh = function(options){
Sekarang kita dapat menyebutnya di fungsi
initialize
:
Water.prototype.initialize = function() { this.GeneratePlaneMesh({subdivisions:100, width:10, height:10}); };
Sekarang ketika Anda memulai permainan, Anda seharusnya hanya melihat permukaan datar. Tapi ini bukan hanya permukaan datar, itu adalah jaring yang terdiri dari ribuan puncak. Sebagai latihan, coba verifikasi ini sendiri (ini adalah alasan bagus untuk mempelajari kode yang baru saja disalin).
Masalah 1: menggeser koordinat Y dari setiap simpul dengan nilai acak sehingga bidang terlihat seperti gambar di bawah ini.
Ombaknya
Tujuan dari bagian ini adalah untuk menentukan permukaan air dari bahan Anda sendiri dan membuat gelombang animasi.
Untuk mendapatkan efek yang kami butuhkan, Anda perlu mengkonfigurasi materi Anda sendiri. Sebagian besar mesin 3D memiliki seperangkat shader yang telah ditentukan sebelumnya untuk merender objek dan cara untuk mendefinisikannya. Berikut ini
tautan bagus tentang cara melakukan ini di PlayCanvas.
Lampiran Shader
Mari kita membuat fungsi
CreateWaterMaterial
baru yang
CreateWaterMaterial
materi baru dengan shader yang diubah dan mengembalikannya:
Water.prototype.CreateWaterMaterial = function(){
Fungsi ini mengambil kode shader vertex dan fragmen dari atribut skrip. Jadi mari kita mendefinisikannya di bagian atas file (setelah baris
pc.createScript
):
Water.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Water.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' });
Sekarang kita dapat membuat file shader ini dan melampirkannya ke skrip kita. Kembali ke editor dan buat dua file shader:
Water.frag dan
Water.vert . Lampirkan shader ini ke skrip seperti yang ditunjukkan pada gambar di bawah ini.
Jika atribut baru tidak ditampilkan di editor, maka klik tombol
Parse untuk memperbarui skrip.
Sekarang tempel shader dasar ini ke
Water.frag :
void main(void) { vec4 color = vec4(0.0,0.0,1.0,0.5); gl_FragColor = color; }
Dan yang ini ada di
Water.vert :
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0); }
Akhirnya, kembalilah ke
Water.js untuk menggunakan materi baru kami dan bukannya materi standar. Artinya, alih-alih:
var material = new pc.StandardMaterial();
masukkan:
var material = this.CreateWaterMaterial();
Sekarang, setelah memulai permainan, pesawat seharusnya berwarna biru.
Hot reboot
Untuk saat ini, kami hanya menyiapkan blanko shader untuk materi baru kami. Sebelum saya mulai menulis efek nyata, saya ingin mengatur reload kode otomatis.
Setelah membatalkan fungsi
swap
di file skrip apa pun (misalnya, di Water.js), kami akan mengaktifkan pemuatan ulang hot. Nanti kita akan melihat bagaimana menggunakan ini untuk mempertahankan status bahkan ketika memperbarui kode secara real time. Tapi untuk saat ini, kami hanya ingin menerapkan kembali shader setelah melakukan perubahan. Sebelum berjalan di WebGL, shader dikompilasi, jadi untuk melakukan ini kita perlu membuat ulang materi kita.
Kami akan memeriksa apakah konten kode shader kami telah berubah, dan jika demikian, buat materi lagi. Pertama, simpan shader saat ini di
inisialisasi :
Dan dalam
pembaruan kami memeriksa apakah ada perubahan yang terjadi:
Sekarang, untuk memastikan ini berhasil, mulailah permainan dan ubah warna pesawat di
Water.frag menjadi biru yang lebih menyenangkan. Setelah menyimpan file, itu harus diperbarui bahkan tanpa reboot dan restart! Ini warna yang saya pilih:
vec4 color = vec4(0.0,0.7,1.0,0.5);
Vertex Shaders
Untuk membuat gelombang, kita harus memindahkan setiap simpul mesh kita di setiap frame. Tampaknya itu akan sangat tidak efisien, tetapi setiap simpul dari masing-masing model sudah ditransformasikan dalam setiap frame yang diberikan. Inilah yang dilakukan vertex shader.
Jika kita menganggap shader fragmen sebagai fungsi yang dieksekusi untuk setiap piksel, mendapatkan posisinya dan mengembalikan warna, maka
vertex shader adalah fungsi yang berjalan untuk setiap titik, mendapatkan posisinya dan mengembalikan posisinya .
Vertex shader secara default mendapatkan
posisi di dunia model dan mengembalikan
posisinya di layar . Adegan 3D kami diatur dalam koordinat x, y dan z, tetapi monitor adalah bidang dua dimensi yang datar, jadi kami memproyeksikan dunia 3D ke layar 2D. Matriks jenis, proyeksi dan model terlibat dalam proyeksi tersebut, oleh karena itu kami tidak akan mempertimbangkannya dalam tutorial ini. Tetapi jika Anda ingin memahami apa yang sebenarnya terjadi pada setiap tahap, maka di sini ada
panduan yang sangat bagus .
Yaitu, baris ini:
gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);
menerima
aPosition
sebagai posisi di dunia 3D dari titik tertentu dan mengubahnya menjadi
gl_Position
, yaitu, ke posisi akhir di layar 2D. Awalan "a" dalam posisi menunjukkan bahwa nilai ini adalah
atribut . Jangan lupa bahwa
seragam variabel adalah nilai yang dapat kita definisikan di CPU dan meneruskannya ke shader. Itu menyimpan nilai yang sama untuk semua piksel / simpul. Di sisi lain, nilai atribut diperoleh dari
array CPU yang ditentukan. Vertex shader dipanggil untuk setiap nilai array atribut ini.
Anda dapat melihat bahwa atribut ini dikonfigurasi dalam definisi shader yang kami atur di Water.js:
var shaderDefinition = { attributes: { aPosition: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0, }, vshader: vertexShader, fshader: fragmentShader };
PlayCanvas mengatur dan mentransmisikan array posisi vertex untuk posisi saat melewati enumerasi ini, tetapi dalam kasus umum, kita dapat meneruskan array data apa pun ke vertex shader.
Gerakan verteks
Misalkan kita ingin mengompres seluruh bidang dengan mengalikan semua nilai
x
dengan 0,5. Apakah kita perlu mengubah
aPosition
atau
gl_Position
?
Ayo coba
aPosition
dulu. Kami tidak dapat mengubah atribut secara langsung, tetapi kami dapat membuat salinan:
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition; pos.x *= 0.5; gl_Position = matrix_viewProjection * matrix_model * vec4(pos, 1.0); }
Sekarang pesawat akan terlihat lebih seperti persegi panjang. Dan tidak ada yang aneh tentang itu. Tetapi apa yang terjadi jika kita mencoba mengubah
gl_Position
?
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition;
Sampai Anda mulai menggerakkan kamera, mungkin terlihat sama. Kami mengubah koordinat ruang layar, yaitu gambar akan tergantung pada
bagaimana kami melihatnya .
Jadi kita bisa memindahkan simpul, dan pada saat yang sama penting untuk membedakan antara pekerjaan di dunia dan ruang layar.
Tugas 2: dapatkah Anda memindahkan seluruh permukaan pesawat beberapa unit ke atas (sepanjang sumbu Y) di vertex shader tanpa mengubah bentuknya?
Tugas 3: Saya mengatakan bahwa gl_Position adalah dua dimensi, tetapi gl_Position.z juga ada. Bisakah Anda memeriksa untuk melihat apakah nilai ini mempengaruhi sesuatu, dan jika demikian, untuk apa ini digunakan?
Menambah waktu
Hal terakhir yang kita butuhkan sebelum mulai membuat gelombang bergerak adalah variabel seragam yang dapat digunakan sebagai waktu. Nyatakan seragam dalam vertex shader:
uniform float uTime;
Sekarang, untuk meneruskannya ke shader,
mari kembali ke
Water.js dan tentukan variabel waktu dalam inisialisasi:
Water.prototype.initialize = function() { this.time = 0;
Sekarang, untuk mentransfer variabel ke shader, kami menggunakan
material.setParameter
. Pertama, kami menetapkan nilai awal di akhir fungsi
CreateWaterMaterial
:
Sekarang, dalam fungsi
update
, kita dapat melakukan penambahan waktu dan mengakses materi menggunakan tautan yang dibuat untuk ini:
this.time += 0.1; this.material.setParameter('uTime',this.time);
Akhirnya, dalam fungsi swap, kami menyalin nilai waktu lama sehingga bahkan setelah mengubah kode, itu terus meningkat tanpa mengatur ulang ke 0.
Water.prototype.swap = function(old) { this.time = old.time; };
Sekarang semuanya sudah siap. Jalankan game untuk memastikan tidak ada kesalahan. Sekarang mari kita pindahkan pesawat kita menggunakan fungsi waktu di
Water.vert
:
pos.y += cos(uTime)
Dan pesawat kita harus mulai bergerak naik dan turun! Karena kita sekarang memiliki fungsi swap, kita juga dapat memperbarui Water.js tanpa harus memulai ulang. Untuk memastikan ini berhasil, coba ubah kenaikan waktu.
Tugas 4: bisakah Anda memindahkan simpul sehingga terlihat seperti gelombang pada gambar di bawah ini?
Izinkan saya memberi tahu Anda bahwa saya memeriksa secara rinci topik berbagai cara menciptakan gelombang di
sini . Artikel ini terkait dengan 2D, tetapi perhitungan matematis berlaku untuk kasus kami. Jika Anda hanya ingin melihat solusinya, maka
inilah intinya .
Tembus cahaya
Tujuan dari bagian ini adalah untuk membuat permukaan air yang tembus cahaya.
Anda mungkin memperhatikan bahwa warna yang dikembalikan ke Water.frag memiliki nilai kanal alfa 0,5, tetapi permukaannya tetap buram. Dalam banyak kasus, transparansi masih menjadi masalah yang tidak terselesaikan dalam grafik komputer. Cara murah untuk mengatasinya adalah dengan menggunakan pencampuran.
Biasanya, sebelum menggambar piksel, ia memeriksa nilai dalam
buffer kedalaman dan membandingkannya dengan nilai kedalamannya sendiri (posisinya di sepanjang sumbu Z) untuk menentukan apakah akan menggambar ulang piksel layar saat ini atau tidak. Inilah yang memungkinkan Anda untuk membuat adegan dengan benar tanpa harus menyortir objek kembali ke depan.
Saat mencampur, alih-alih sekadar menolak piksel atau menimpa, kita dapat menggabungkan warna piksel (target) yang sudah dirender dengan piksel yang akan kita gambar (sumber). Daftar semua fungsi pencampuran yang tersedia di WebGL dapat ditemukan di
sini .
Agar saluran alfa bekerja sesuai dengan harapan kami, kami ingin warna hasil gabungan menjadi sumber yang dikalikan dengan saluran alfa ditambah piksel tujuan dikalikan dengan satu alfa dikurangi. Dengan kata lain, jika alpha = 0,4, maka warna akhir harus memiliki nilai:
finalColor = source * 0.4 + destination * 0.6;
Di PlayCanvas, ini adalah operasi
pc.BLEND_NORMAL melakukan .
Untuk mengaktifkannya, cukup setel properti material di dalam
CreateWaterMaterial
:
material.blendType = pc.BLEND_NORMAL;
Jika Anda sekarang memulai permainan, maka air akan menjadi tembus! Namun, itu masih belum sempurna. Masalah muncul ketika permukaan bening ditumpangkan pada dirinya sendiri, seperti yang ditunjukkan di bawah ini.
Kita bisa menghilangkannya dengan menggunakan
alpha to coverage , teknik multisampling untuk transparansi, alih-alih memadukan:
Tetapi ini hanya tersedia di WebGL 2. Dalam tutorial selanjutnya, demi kesederhanaan, saya akan menggunakan pencampuran.
Untuk meringkas
Kami mengatur lingkungan dan menciptakan permukaan air yang tembus cahaya dengan gelombang animasi dari vertex shader. Di bagian kedua tutorial, kita akan mempertimbangkan daya apung benda, menambahkan garis ke permukaan air dan membuat garis busa di sepanjang batas benda yang bersinggungan dengan permukaan.
Pada bagian ketiga (terakhir), kami akan mempertimbangkan penerapan efek pasca pemrosesan dari distorsi bawah air dan mempertimbangkan gagasan untuk perbaikan lebih lanjut.
Kode sumber
Proyek PlayCanvas yang sudah selesai dapat ditemukan di
sini . Repositori kami juga memiliki
port proyek di bawah Three.js .