Buat shader air kartun untuk web. Bagian 2

Pada bagian pertama, kami melihat pengaturan lingkungan dan permukaan air. Pada bagian ini, kita akan memberikan daya apung objek, menambahkan garis air pada permukaan dan membuat garis busa dengan penyangga kedalaman di sekitar batas objek yang bersinggungan dengan permukaan.

Untuk membuat adegan terlihat sedikit lebih baik, saya membuat sedikit perubahan padanya. Anda dapat menyesuaikan adegan dengan cara yang Anda inginkan, tetapi saya melakukan yang berikut:

  • Menambahkan model mercusuar dan gurita.
  • Menambahkan model dasar dengan warna #FFA457 .
  • Menambahkan warna langit #6CC8FF .
  • Menambahkan warna latar #FFC480 ke adegan (parameter ini dapat ditemukan di pengaturan adegan).

Adegan asli saya sekarang terlihat seperti ini.


Daya apung


Cara termudah untuk menciptakan daya apung adalah dengan menggunakan skrip untuk mendorong objek ke atas dan ke bawah. Buat skrip Buoyancy.js baru dan tetapkan yang berikut ini di inisialisasi:

 Buoyancy.prototype.initialize = function() { this.initialPosition = this.entity.getPosition().clone(); this.initialRotation = this.entity.getEulerAngles().clone(); //     ,  //        //     this.time = Math.random() * 2 * Math.PI; }; 

Sekarang dalam pembaruan, kami menjalankan penambahan waktu dan memutar objek:

 Buoyancy.prototype.update = function(dt) { this.time += 0.1; //      var pos = this.entity.getPosition().clone(); pos.y = this.initialPosition.y + Math.cos(this.time) * 0.07; this.entity.setPosition(pos.x,pos.y,pos.z); //    var rot = this.entity.getEulerAngles().clone(); rot.x = this.initialRotation.x + Math.cos(this.time * 0.25) * 1; rot.z = this.initialRotation.z + Math.sin(this.time * 0.5) * 2; this.entity.setLocalEulerAngles(rot.x,rot.y,rot.z); }; 

Terapkan skrip ini ke perahu dan lihat bagaimana lompatannya ke atas dan ke bawah air! Anda dapat menerapkan skrip ini ke beberapa objek (termasuk kamera - coba)!

Tekstur permukaan


Sementara kita bisa melihat ombaknya, kita melihat ujung-ujung permukaan air. Menambahkan tekstur akan membuat gerakan permukaan lebih terlihat. Selain itu, ini adalah cara berbiaya rendah untuk mensimulasikan pantulan dan kaustik.

Anda dapat mencoba menemukan beberapa tekstur kaustik atau membuatnya sendiri. Saya menggambar tekstur di Gimp yang bisa Anda gunakan dengan bebas. Tekstur apa pun cocok, asalkan itu bisa ubin tanpa sendi yang terlihat.

Setelah memilih tekstur yang Anda suka, seret ke jendela Aset proyek Anda. Kita perlu mereferensikan tekstur ini dari skrip Water.js, jadi mari kita buat atribut untuk itu:

 Water.attributes.add('surfaceTexture', { type: 'asset', assetType: 'texture', title: 'Surface Texture' }); 

Dan kemudian menempatkannya di editor:


Sekarang kita harus meneruskannya ke shader. Pergilah ke Water.js dan atur fungsi CreateWaterMaterial parameter baru:

 material.setParameter('uSurfaceTexture',this.surfaceTexture.resource); 

Sekarang kembali ke Water.frag dan nyatakan seragam baru:

 uniform sampler2D uSurfaceTexture; 

Kami hampir selesai. Untuk membuat tekstur pada bidang, kita perlu tahu di mana setiap piksel berada di jala. Artinya, kita perlu mentransfer data dari vertex shader ke fragmen.

Variabel yang Bervariasi


Variabel yang bervariasi memungkinkan Anda untuk mentransfer data dari vertex shader ke yang fragmen. Ini adalah tipe ketiga dari variabel khusus yang dapat digunakan dalam shader (dua yang pertama adalah seragam dan atribut ). Variabel diatur untuk setiap simpul dan setiap piksel dapat mengaksesnya. Karena terdapat lebih banyak piksel daripada titik, nilainya diinterpolasi di antara titik (karena itu nama "bervariasi" - itu menyimpang dari nilai yang diteruskan ke sana).

Untuk mengujinya dalam operasi, deklarasikan variabel baru di Water.vert sebagai beragam:

 varying vec2 ScreenPosition; 

Dan kemudian berikan nilai gl_Position setelah menghitungnya:

 ScreenPosition = gl_Position.xyz; 

Sekarang kembali ke Water.frag dan deklarasikan variabel yang sama. Kami tidak bisa mendapatkan output data debug dari shader, tetapi kami bisa menggunakan warna untuk debugging visual. Inilah cara melakukannya:

 uniform sampler2D uSurfaceTexture; varying vec3 ScreenPosition; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); //    varying- color = vec4(vec3(ScreenPosition.x),1.0); gl_FragColor = color; } 

Pesawat sekarang akan terlihat hitam dan putih, dan garis pemisah warna akan pergi ke tempat ScreenPosition.x = 0. Nilai warna hanya berubah dari 0 ke 1, tetapi nilai-nilai di ScreenPosition mungkin berada di luar kisaran ini. Mereka secara otomatis terbatas, oleh karena itu, ketika Anda melihat hitam, itu bisa 0 atau angka negatif.

Apa yang baru saja kita lakukan adalah untuk melewatkan posisi layar dari setiap simpul ke setiap piksel. Anda dapat melihat bahwa garis yang memisahkan sisi hitam dan putih akan selalu lewat di tengah layar, terlepas dari di mana permukaan sebenarnya di dunia.

Tugas 1: buat variabel baru yang bervariasi untuk mentransfer posisi di dunia alih-alih posisi layar. Visualisasikan dengan cara yang sama. Jika warnanya tidak berubah dengan pergerakan kamera, maka semuanya dilakukan dengan benar.

Menggunakan UV


UV adalah koordinat 2D dari setiap simpul dalam mesh, dinormalisasi dari 0 hingga 1. Mereka diperlukan untuk pengambilan sampel yang benar dari tekstur ke pesawat, dan kami sudah mengkonfigurasi mereka di bagian sebelumnya.

Kami akan mendeklarasikan atribut baru di Water.vert (nama ini diambil dari definisi shader di Water.js):

 attribute vec2 aUv0; 

Dan sekarang kita hanya perlu meneruskannya ke fragmen shader, jadi buat saja yang berbeda-beda dan tetapkan nilai atributnya:

 //  Water.vert //        varying vec2 vUv0; // .. //        //  varying,        vUv0 = aUv0; 

Sekarang kita akan mendeklarasikan variabel berbeda yang sama di shader fragmen. Untuk memastikan semuanya bekerja, kita dapat memvisualisasikan debugging seperti sebelumnya, dan kemudian Water.frag akan terlihat seperti ini:

 uniform sampler2D uSurfaceTexture; varying vec2 vUv0; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); //  UV color = vec4(vec3(vUv0.x),1.0); gl_FragColor = color; } 

Anda akan melihat gradien yang mengonfirmasi bahwa kami memiliki nilai 0 dari satu ujung dan 1 dari yang lain. Sekarang untuk mencicipi tekstur nyata, yang perlu kita lakukan adalah:

 color = texture2D(uSurfaceTexture,vUv0); 

Setelah itu, kita akan melihat tekstur di permukaan:


Penataan tekstur


Alih-alih hanya mengatur tekstur sebagai warna baru, mari kita gabungkan dengan warna biru yang ada:

 uniform sampler2D uSurfaceTexture; varying vec2 vUv0; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); vec4 WaterLines = texture2D(uSurfaceTexture,vUv0); color.rgba += WaterLines.r; gl_FragColor = color; } 

Ini berfungsi karena warna teksturnya hitam (0) di mana-mana kecuali garis air. Menambahkannya, kita tidak mengubah warna biru awal, dengan pengecualian tempat dengan garis-garis di mana ia menjadi lebih terang.

Namun, ini bukan satu-satunya cara untuk menggabungkan warna.

Tugas 2: Bisakah Anda menggabungkan warna untuk mendapatkan efek yang lebih lemah seperti yang ditunjukkan di bawah ini?


Tekstur bergerak


Sebagai efek terakhir, kami ingin garis-garis bergerak di sepanjang permukaan dan itu tidak terlihat begitu statis. Untuk melakukan ini, kita akan mengambil keuntungan dari fakta bahwa nilai apa pun di luar interval dari 0 hingga 1, diteruskan ke fungsi texture2D , akan ditransfer (misalnya, 1,5 dan 2,5 menjadi sama dengan 0,5). Oleh karena itu, kita dapat meningkatkan posisi kita dengan variabel waktu seragam yang telah kita atur untuk menambah atau mengurangi kerapatan garis pada permukaan, yang akan memberikan fragmen akhir berupa bentuk ini:

 uniform sampler2D uSurfaceTexture; uniform float uTime; varying vec2 vUv0; void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); vec2 pos = vUv0; //      1 //     pos *= 2.0; //   ,      pos.y += uTime * 0.02; vec4 WaterLines = texture2D(uSurfaceTexture,pos); color.rgba += WaterLines.r; gl_FragColor = color; } 

Garis Busa dan Buffer Kedalaman


Rendering garis busa di sekitar benda-benda di air membuatnya lebih mudah untuk melihat seberapa terbenam benda-benda itu dan di mana mereka melintasi permukaan. Selain itu, dengan cara ini air kita menjadi jauh lebih dapat dipercaya. Untuk mewujudkan garis-garis busa, kita perlu mencari tahu di mana batas-batas masing-masing objek, dan melakukannya secara efektif.

Trick


Kita perlu belajar untuk menentukan apakah suatu piksel pada permukaan air dekat dengan objek. Jika demikian, maka kita bisa mengecatnya dengan warna busa. Tidak ada cara sederhana untuk menyelesaikan masalah ini (sejauh yang saya tahu). Oleh karena itu, untuk menyelesaikannya, saya menggunakan teknik yang berguna untuk memecahkan masalah: Saya akan mengambil contoh yang kita tahu jawabannya dan melihat apakah kita dapat menggeneralisasikannya.

Lihatlah gambar di bawah ini.


Pixel apa yang harus menjadi bagian dari busa? Kita tahu itu akan terlihat seperti ini:


Jadi mari kita lihat dua piksel spesifik. Di bawah ini saya menandainya dengan tanda bintang. Hitam akan berada di busa, dan merah tidak akan. Bagaimana kita membedakan mereka dalam shader?


Kita tahu bahwa meskipun kedua piksel dalam ruang layar ini berdekatan satu sama lain (keduanya berada di atas mercusuar), sebenarnya keduanya sangat jauh di ruang dunia. Kami dapat memverifikasi ini dengan melihat pemandangan yang sama dari sudut yang berbeda.


Perhatikan bahwa bintang merah tidak terletak di mercusuar, seperti yang terlihat bagi kita, tetapi bintang hitam sebenarnya ada di sana. Kita dapat membedakan dari menggunakan jarak ke kamera, yang biasanya disebut "kedalaman". Kedalaman 1 berarti titik tersebut sangat dekat dengan kamera, kedalaman 0 berarti sangat jauh. Tapi ini bukan hanya masalah jarak absolut di dunia, kedalaman atau kamera. Kedalaman relatif terhadap piksel di belakangnya adalah penting.

Lihat lagi pada tampilan pertama. Katakanlah mercusuar memiliki nilai kedalaman 0,5. Kedalaman bintang hitam akan sangat dekat dengan 0,5. Artinya, dan piksel di bawahnya memiliki nilai kedalaman yang sangat dekat. Di sisi lain, tanda bintang merah akan memiliki kedalaman yang jauh lebih besar, karena lebih dekat ke kamera, katakanlah 0,7. Dan meskipun piksel di belakangnya masih di mercusuar, ia memiliki nilai kedalaman 0,5, yaitu, ada lebih banyak perbedaan.

Ini triknya. Ketika kedalaman piksel pada permukaan air cukup dekat dengan kedalaman piksel tempat gambar itu diambil, maka kita cukup dekat dengan batas beberapa objek dan dapat membuat piksel seperti busa.

Artinya, kami membutuhkan informasi lebih banyak daripada yang kami miliki di piksel mana pun. Entah bagaimana, kita perlu mencari tahu kedalaman piksel tempat gambar itu harus ditarik. Dan di sini buffer kedalaman berguna bagi kita.

Penyangga Kedalaman


Anda dapat menganggap penyangga bingkai atau penyangga bingkai sebagai render atau tekstur target di luar layar. Ketika kita perlu membaca data, kita perlu membuat di luar layar. Teknik ini digunakan dalam efek asap .

Buffer kedalaman adalah render target khusus yang berisi informasi tentang nilai kedalaman setiap piksel. Jangan lupa bahwa nilai dalam gl_Position dihitung dalam vertex shader adalah nilai ruang layar, tetapi juga memiliki koordinat ketiga - nilai Z. Nilai Z ini digunakan untuk menghitung kedalaman, yang ditulis ke buffer kedalaman.

Buffer kedalaman dimaksudkan untuk rendering adegan yang benar tanpa perlu menyortir objek dari belakang ke depan. Setiap piksel yang akan ditarik terlebih dahulu memeriksa buffer kedalaman. Jika nilai kedalamannya lebih besar dari nilai dalam buffer, maka itu ditarik, dan nilainya sendiri menimpa nilai buffer. Kalau tidak, itu dibuang (karena itu berarti ada objek lain di depannya).

Bahkan, Anda dapat menonaktifkan penulisan ke buffer kedalaman untuk melihat bagaimana segala sesuatu akan terlihat tanpa itu. Mari kita coba melakukannya di Water.js:

 material.depthTest = false; 

Anda akan melihat bahwa air sekarang akan selalu diambil dari atas, bahkan jika itu di belakang benda buram.

Visualisasi Penyangga Kedalaman


Mari kita tambahkan cara untuk membuat buffer kedalaman untuk keperluan debugging. Buat skrip DepthVisualize.js baru. Pasang ke kamera.

Untuk mengakses buffer kedalaman di PlayCanvas, cukup tulis yang berikut ini:

 this.entity.camera.camera.requestDepthMap(); 

Jadi kami secara otomatis menyuntikkan variabel seragam ke semua shader kami, yang dapat kami gunakan dengan mendeklarasikannya sebagai berikut:

 uniform sampler2D uDepthMap; 

Di bawah ini adalah contoh skrip yang meminta peta mendalam dan merendernya di atas layar. Dia telah mengkonfigurasi restart panas.

 var DepthVisualize = pc.createScript('depthVisualize'); //  initialize,       DepthVisualize.prototype.initialize = function() { this.entity.camera.camera.requestDepthMap(); this.antiCacheCount = 0; //    ,         this.SetupDepthViz(); }; DepthVisualize.prototype.SetupDepthViz = function(){ var device = this.app.graphicsDevice; var chunks = pc.shaderChunks; this.fs = ''; this.fs += 'varying vec2 vUv0;'; this.fs += 'uniform sampler2D uDepthMap;'; this.fs += ''; this.fs += 'float unpackFloat(vec4 rgbaDepth) {'; this.fs += ' const vec4 bitShift = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0);'; this.fs += ' float depth = dot(rgbaDepth, bitShift);'; this.fs += ' return depth;'; this.fs += '}'; this.fs += ''; this.fs += 'void main(void) {'; this.fs += ' float depth = unpackFloat(texture2D(uDepthMap, vUv0)) * 30.0; '; this.fs += ' gl_FragColor = vec4(vec3(depth),1.0);'; this.fs += '}'; this.shader = chunks.createShaderFromCode(device, chunks.fullscreenQuadVS, this.fs, "renderDepth" + this.antiCacheCount); this.antiCacheCount ++; //     ,        this.command = new pc.Command(pc.LAYER_FX, pc.BLEND_NONE, function () { pc.drawQuadWithShader(device, null, this.shader); }.bind(this)); this.command.isDepthViz = true; //    ,      this.app.scene.drawCalls.push(this.command); }; //  update,     DepthVisualize.prototype.update = function(dt) { }; //  swap,      //      DepthVisualize.prototype.swap = function(old) { this.antiCacheCount = old.antiCacheCount; //      for(var i=0;i<this.app.scene.drawCalls.length;i++){ if(this.app.scene.drawCalls[i].isDepthViz){ this.app.scene.drawCalls.splice(i,1); break; } } //    this.SetupDepthViz(); }; //      ,  : // http://developer.playcanvas.com/en/user-manual/scripting/ 

Cobalah untuk menyalin kode dan komentar keluar / batalkan komentar pada baris this.app.scene.drawCalls.push(this.command); untuk mengaktifkan / menonaktifkan rendering mendalam. Seharusnya terlihat seperti gambar di bawah ini.


Tugas 3: permukaan air tidak ditarik ke dalam buffer kedalaman. Mesin PlayCanvas sengaja melakukan ini. Bisakah Anda mencari tahu mengapa? Apa yang spesial dari material air? Dengan kata lain, mengingat aturan kami untuk memeriksa kedalaman, apa yang akan terjadi jika piksel air ditulis ke buffer kedalaman?

Petunjuk: Anda dapat mengubah satu baris di Water.js, yang memungkinkan Anda untuk menulis air ke buffer kedalaman.

Perlu juga dicatat bahwa dalam fungsi inisialisasi, saya mengalikan nilai kedalaman dengan 30. Ini perlu untuk melihatnya dengan jelas, karena jika tidak, rentang nilai akan terlalu kecil untuk menampilkan nada warna.

Penerapan Trick


Ada beberapa fungsi tambahan di mesin PlayCanvas untuk bekerja dengan nilai kedalaman, tetapi pada saat penulisan, mereka tidak dirilis dalam produksi, jadi kita harus mengonfigurasinya sendiri.

Kami mendefinisikan variabel seragam berikut di Water.frag :

 //   uniform-    PlayCanvas uniform sampler2D uDepthMap; uniform vec4 uScreenSize; uniform mat4 matrix_view; //      uniform vec4 camera_params; 

Kami mendefinisikan fungsi bantu ini di atas fungsi utama:

 #ifdef GL2 float linearizeDepth(float z) { z = z * 2.0 - 1.0; return 1.0 / (camera_params.z * z + camera_params.w); } #else #ifndef UNPACKFLOAT #define UNPACKFLOAT float unpackFloat(vec4 rgbaDepth) { const vec4 bitShift = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0); return dot(rgbaDepth, bitShift); } #endif #endif float getLinearScreenDepth(vec2 uv) { #ifdef GL2 return linearizeDepth(texture2D(uDepthMap, uv).r) * camera_params.y; #else return unpackFloat(texture2D(uDepthMap, uv)) * camera_params.y; #endif } float getLinearDepth(vec3 pos) { return -(matrix_view * vec4(pos, 1.0)).z; } float getLinearScreenDepth() { vec2 uv = gl_FragCoord.xy * uScreenSize.zw; return getLinearScreenDepth(uv); } 

Kami akan memberikan informasi shader tentang kamera di Water.js . Rekatkan ini ke tempat Anda meneruskan variabel seragam lainnya seperti uTime:

 if(!this.camera){ this.camera = this.app.root.findByName("Camera").camera; } var camera = this.camera; var n = camera.nearClip; var f = camera.farClip; var camera_params = [ 1/f, f, (1-f / n) / 2, (1 + f / n) / 2 ]; material.setParameter('camera_params', camera_params); 

Akhirnya, kita membutuhkan posisi di dunia setiap piksel untuk shader fragmen kita. Kita harus mendapatkannya dari vertex shader. Oleh karena itu, kita akan mendefinisikan variabel yang bervariasi di Water.frag :

 varying vec3 WorldPosition; 

Tentukan variabel bervariasi yang sama di Water.vert . Kemudian kami menempatkannya pada posisi terdistorsi dari vertex shader sehingga kode lengkapnya terlihat seperti ini:

 attribute vec3 aPosition; attribute vec2 aUv0; varying vec2 vUv0; varying vec3 WorldPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; uniform float uTime; void main(void) { vUv0 = aUv0; vec3 pos = aPosition; pos.y += cos(pos.z*5.0+uTime) * 0.1 * sin(pos.x * 5.0 + uTime); gl_Position = matrix_viewProjection * matrix_model * vec4(pos, 1.0); WorldPosition = pos; } 

Kami menyadari triknya dengan nyata


Sekarang kita akhirnya siap untuk mengimplementasikan teknik yang dijelaskan di awal bagian ini. Kami ingin membandingkan kedalaman piksel di mana kami berada dengan kedalaman piksel di bawahnya. Pixel tempat kita berada diambil dari posisi di dunia, dan piksel di bawahnya diperoleh dari posisi layar. Karena itu, kami mengambil dua kedalaman ini:

 float worldDepth = getLinearDepth(WorldPosition); float screenDepth = getLinearScreenDepth(); 

Tugas 4: salah satu dari nilai-nilai ini tidak akan pernah lebih besar dari yang lain (dengan asumsi depthTest = true). Bisakah Anda menentukan yang mana?

Kita tahu bahwa busa akan berada di tempat jarak antara kedua nilai tersebut kecil. Karenanya, mari kita render perbedaan ini untuk setiap piksel. Tempel ini di akhir shader (dan matikan skrip visualisasi kedalaman dari bagian sebelumnya):

 color = vec4(vec3(screenDepth - worldDepth),1.0); gl_FragColor = color; 

Dan seharusnya terlihat seperti ini:


Artinya, kita dengan benar memilih batas-batas benda apa pun yang direndam dalam air secara real time! Tentu saja, Anda dapat mengukur perbedaannya untuk membuat busa lebih tebal atau kurang umum.

Kami sekarang memiliki banyak pilihan untuk menggabungkan hasil ini dengan permukaan air untuk membuat garis busa yang indah. Anda dapat meninggalkan mereka dengan gradien, gunakan untuk sampel dari tekstur yang berbeda, atau menetapkan warna tertentu jika perbedaannya kurang dari atau sama dengan nilai batas tertentu.

Yang paling saya sukai adalah penugasan warna yang mirip dengan garis-garis air statis, jadi fungsi utama saya yang sudah selesai terlihat seperti ini:

 void main(void) { vec4 color = vec4(0.0,0.7,1.0,0.5); vec2 pos = vUv0 * 2.0; pos.y += uTime * 0.02; vec4 WaterLines = texture2D(uSurfaceTexture,pos); color.rgba += WaterLines.r * 0.1; float worldDepth = getLinearDepth(WorldPosition); float screenDepth = getLinearScreenDepth(); float foamLine = clamp((screenDepth - worldDepth),0.0,1.0) ; if(foamLine < 0.7){ color.rgba += 0.2; } gl_FragColor = color; } 

Untuk meringkas


Kami menciptakan daya apung benda yang terbenam dalam air, menerapkan tekstur bergerak ke permukaan untuk mensimulasikan kaustik, dan mempelajari cara menggunakan buffer kedalaman untuk membuat garis-garis buih yang dinamis.

Pada bagian ketiga dan terakhir, kami akan menambahkan efek post-processing dan belajar bagaimana menggunakannya untuk menciptakan efek distorsi bawah air.

Kode sumber


Proyek PlayCanvas yang sudah selesai dapat ditemukan di sini . Repositori kami juga memiliki port proyek di bawah Three.js .

Source: https://habr.com/ru/post/id416983/


All Articles