Lanjutan Three.js: bahan shader dan pasca-pemrosesan


Ada beberapa pengantar tentang dasar-dasar bekerja dengan Three.js di web, tetapi Anda mungkin melihat kekurangan bahan pada topik yang lebih maju. Dan salah satu topik ini adalah kombinasi shader dan adegan dengan model tiga dimensi. Di mata banyak pengembang pemula, ini tampaknya hal-hal yang tidak sesuai dari dunia yang berbeda. Hari ini, dengan contoh sederhana dari “bola plasma”, kita akan melihat apa itu ShaderMaterial dan dimakan dengan apa, Efek Efek apa itu, dan seberapa cepat mungkin untuk melakukan pasca-pemrosesan untuk adegan yang diberikan.


Diasumsikan bahwa pembaca akrab dengan dasar-dasar bekerja dengan Three.js dan memahami cara kerja shader. Jika Anda belum pernah mengalami ini sebelumnya, maka saya sangat merekomendasikan membaca ini dulu:



Tapi mari kita mulai ...


ShaderMaterial - apa itu?


Kita telah melihat bagaimana tekstur datar digunakan dan bagaimana ia direntangkan pada objek tiga dimensi. Karena tekstur ini adalah gambar biasa. Ketika kami memeriksa penulisan fragmen shader, semuanya juga datar di sana. Jadi: jika kita dapat menghasilkan gambar datar menggunakan shader, lalu mengapa tidak menggunakannya sebagai tekstur?


Ide inilah yang membentuk dasar untuk bahan shader. Saat membuat bahan untuk objek tiga dimensi, kami menunjukkan shader alih-alih tekstur untuknya. Dalam bentuk dasarnya, tampilannya seperti ini:


const shaderMaterial = new THREE.ShaderMaterial({ uniforms: { // ... }, vertexShader: '...', fragmentShader: '...' }); 

Shader fragmen akan digunakan untuk membuat tekstur material, dan Anda, tentu saja, bertanya, apa yang akan dilakukan vertex shader? Apakah dia akan lagi melakukan penghitungan ulang koordinat yang dangkal? Ya, kami akan mulai dengan opsi sederhana ini, tetapi kami juga dapat mengatur offset atau melakukan manipulasi lain untuk setiap titik objek tiga dimensi - sekarang tidak ada batasan pada pesawat. Tetapi lebih baik untuk melihat semua ini dengan sebuah contoh. Dengan kata-kata, sedikit yang dipahami. Buat adegan dan buat satu bola di tengah.



Sebagai bahan untuk bola, kami akan menggunakan ShaderMaterial:


 const geometry = new THREE.SphereBufferGeometry(30, 64, 64); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: { // . . . }, vertexShader: document.getElementById('sphere-vertex-shader').textContent, fragmentShader: document.getElementById('sphere-fragment-shader').textContent }); const sphere = new THREE.Mesh(geometry, shaderMaterial); SCENE.add(sphere); 

Vertex shader akan menjadi netral:


 void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } 

Perhatikan bahwa Three.js melewati variabel seragamnya. Kami tidak harus melakukan apa pun, mereka tersirat. Dalam dirinya sendiri, mereka mengandung semua jenis matriks, yang sudah dapat kita akses dari JS, serta posisi kamera. Bayangkan bahwa di awal shaders sendiri ada sesuatu yang dimasukkan:


 // = object.matrixWorld uniform mat4 modelMatrix; // = camera.matrixWorldInverse * object.matrixWorld uniform mat4 modelViewMatrix; // = camera.projectionMatrix uniform mat4 projectionMatrix; // = camera.matrixWorldInverse uniform mat4 viewMatrix; // = inverse transpose of modelViewMatrix uniform mat3 normalMatrix; // = camera position in world space uniform vec3 cameraPosition; 

Selain itu, beberapa variabel atribut diteruskan ke vertex shader:


 attribute vec3 position; attribute vec3 normal; attribute vec2 uv; 

Dengan namanya jelas apa itu - posisi puncak saat ini, normal ke permukaan pada titik ini, dan koordinat pada tekstur yang sesuai dengan titik tersebut.


Secara tradisional, koordinat dalam ruang ditetapkan sebagai (x, y, z), dan koordinat pada bidang tekstur sebagai (u, v). Karenanya nama variabel. Anda akan sering bertemu dengannya dalam berbagai contoh. Secara teori, kita perlu mentransfer koordinat ini ke shader fragmen untuk bekerja dengannya di sana. Kami akan melakukannya.


 varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } 

Sebagai permulaan, shader fragmen harus seperti ini:


 #define EPSILON 0.02 varying vec2 vUv; void main() { if ((fract(vUv.x * 10.0) < EPSILON) || (fract(vUv.y * 10.0) < EPSILON)) { gl_FragColor = vec4(vec3(0.0), 1.0); } else { gl_FragColor = vec4(1.0); } } 

Cukup buat mesh. Jika Anda berpikir sedikit, maka di pesawat itu hanya akan menjadi kotak kotak, tetapi karena kita menempatkannya pada bola, itu terdistorsi, berubah menjadi bola dunia. Ada gambar bagus di Wikipedia yang menggambarkan apa yang terjadi:



Yaitu, dalam fragmen shader kami membuat tekstur rata, seperti di tengah dalam ilustrasi ini, dan Three.js kemudian menariknya pada bola. Sangat nyaman



Tentu saja, untuk model yang lebih kompleks, sapuan akan lebih rumit. Tetapi biasanya ketika membuat berbagai situs desain kami bekerja dengan bentuk geometris yang sederhana dan mudah untuk membayangkan sapuan di kepala Anda.


Oke, apa yang bisa Anda lakukan?


Fitur utamanya adalah bahan shader dapat berubah seiring waktu. Ini bukan hal statis yang kita gambar sekali dan lupa, kita bisa menghidupkannya. Apalagi dalam warna (dalam fragmen shader) dan dalam bentuk (di vertex). Ini adalah alat yang sangat kuat.


Dalam contoh kita, kita akan membuat api yang menyelimuti sebuah bola. Akan ada dua bola - satu biasa (di dalam), dan yang kedua dari bahan shader (di luar, dengan jari-jari besar). Menambahkan bola lain tidak akan berkomentar.



Pertama, tambahkan waktu sebagai variabel seragam untuk shader dalam materi kami. Tidak ada tempat tanpa waktu. Kami sudah melakukan ini dalam JS murni, tetapi dalam Three.js itu sama sederhana. Biarkan waktu di shader disebut uTime, dan disimpan dalam variabel TIME:


 function updateUniforms() { SCENE.traverse((child) => { if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') { child.material.uniforms.uTime.value = TIME; child.material.needsUpdate = true; } }); } 

Kami memperbarui semuanya dengan setiap panggilan ke fungsi animate:


 function animate() { requestAnimationFrame(animate); TIME += 0.005; updateUniforms(); render(); } 

Api


Menciptakan api pada dasarnya sangat mirip dengan menghasilkan lanskap, tetapi bukannya ketinggian, warna. Atau transparansi, seperti dalam kasus kami.


Fungsi untuk keacakan dan kebisingan yang telah kita lihat, kita tidak akan menganalisisnya secara rinci. Yang perlu kita lakukan adalah membuat suara pada frekuensi yang berbeda untuk menambah variasi, dan membuat masing-masing suara ini bergerak dengan kecepatan yang berbeda. Anda akan mendapatkan sesuatu seperti api, yang besar bergerak perlahan, yang kecil bergerak lebih cepat:


 uniform float uTime; varying vec2 vUv; float rand(vec2); float noise(vec2); void main() { vec2 position1 = vec2(vUv.x * 4.0, vUv.y - uTime); vec2 position2 = vec2(vUv.x * 4.0, vUv.y - uTime * 2.0); vec2 position3 = vec2(vUv.x * 4.0, vUv.y - uTime * 3.0); float color = ( noise(position1 * 5.0) + noise(position2 * 10.0) + noise(position3 * 15.0)) / 3.0; gl_FragColor = vec4(0.0, 0.0, 0.0, color - smoothstep(0.1, 1.3, vUv.y)); } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } float noise(vec2 position) { vec2 blockPosition = floor(position); float topLeftValue = rand(blockPosition); float topRightValue = rand(blockPosition + vec2(1.0, 0.0)); float bottomLeftValue = rand(blockPosition + vec2(0.0, 1.0)); float bottomRightValue = rand(blockPosition + vec2(1.0, 1.0)); vec2 computedValue = smoothstep(0.0, 1.0, fract(position)); return mix(topLeftValue, topRightValue, computedValue.x) + (bottomLeftValue - topLeftValue) * computedValue.y * (1.0 - computedValue.x) + (bottomRightValue - topRightValue) * computedValue.x * computedValue.y; } 

Agar nyala api tidak menutupi seluruh bola, kami bermain dengan parameter warna keempat - transparansi - dan mengikatnya ke koordinat y. Dalam kasus kami, opsi ini sangat nyaman. Dalam istilah yang lebih umum, kami menerapkan gradien dengan transparansi ke noise.


Pada saat-saat seperti ini, penting untuk mengingat fungsi smoothstep.

Secara umum, pendekatan untuk menciptakan api menggunakan shader adalah klasik. Anda akan sering bertemu dengannya di berbagai tempat. Ini akan berguna untuk bermain dengan angka-angka ajaib - mereka secara acak diatur dalam contoh, dan bagaimana plasma akan terlihat tergantung pada mereka.


Untuk membuat api lebih menarik, mari kita beralih ke vertex shader dan sedikit dukun ...


Bagaimana membuat nyala api sedikit "dituangkan" di ruang angkasa? Untuk pemula, pertanyaan ini dapat menyebabkan kesulitan besar, meskipun sederhana. Saya melihat pendekatan yang sangat kompleks untuk memecahkan masalah ini, tetapi pada dasarnya - kita perlu dengan lancar memindahkan simpul pada bola di sepanjang garis "dari pusatnya". Ke sana kemari, ke sana kemari. Three.js telah melewati kami pada posisi saat ini dan normal - kami akan menggunakannya. Untuk "bolak-balik" beberapa fungsi terbatas akan cocok, misalnya, sinus. Anda tentu saja dapat bereksperimen, tetapi sinus adalah opsi default.


Tidak tahu apa yang harus diambil - ambil sinus. Lebih baik lagi, jumlah sinus dengan frekuensi yang berbeda.

Kami menggeser koordinat normal ke nilai yang diperoleh dan menghitung ulang sesuai dengan rumus yang diketahui sebelumnya.


 uniform float uTime; varying vec2 vUv; void main() { vUv = uv; vec3 delta = normal * sin(position.x * position.y * uTime / 10.0); vec3 newPosition = position + delta; gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); } 

Apa yang kita dapatkan bukan lagi sebuah bidang. Ini ... Saya bahkan tidak tahu apakah ini memiliki nama. Tapi, sekali lagi, jangan lupa untuk bermain-main dengan peluang - mereka banyak mempengaruhi. Ketika membuat efek seperti itu, seringkali sesuatu dipilih dengan cara coba-coba dan sangat berguna untuk mengembangkan "intuisi matematika" dalam diri sendiri - kemampuan untuk sedikit banyak membayangkan bagaimana suatu fungsi berperilaku, bagaimana hal itu tergantung pada variabel mana.


Pada tahap ini, kita memiliki gambar yang menarik, tapi agak canggung. Jadi pertama-tama, mari kita lihat post-processing, dan kemudian beralih ke contoh hidup.


Pemrosesan pos


Kemampuan untuk melakukan sesuatu dengan gambar Three.js yang diberikan adalah hal yang sangat berguna, sementara dilupakan dalam berbagai pelajaran. Secara teknis, ini diimplementasikan sebagai berikut: gambar yang diberikan renderer kami dikirim ke EffectComposer (selama itu adalah kotak hitam), yang meredam sesuatu dalam dirinya sendiri dan menampilkan gambar akhir di kanvas. Yaitu, setelah renderer, satu modul lagi ditambahkan. Kami mentransfer parameter ke komposer ini - apa yang harus dilakukan dengan gambar yang diterima. Salah satu parameter tersebut disebut pass. Dalam arti tertentu, komposer bekerja seperti beberapa Gulp - tidak melakukan apa-apa, kami memberikan plugin yang sudah melakukan pekerjaan. Mungkin tidak sepenuhnya benar untuk mengatakannya, tetapi idenya harus jelas.


Segala sesuatu yang akan kita gunakan lebih lanjut tidak termasuk dalam struktur dasar Three.js, jadi kami menghubungkan beberapa dependensi dan dependensi dari dependensi itu sendiri:


 <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/EffectComposer.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/RenderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/ShaderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/CopyShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/LuminosityHighPassShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/UnrealBloomPass.js'></script> 

Ingat bahwa skrip ini termasuk dalam tiga paket dan Anda dapat memasukkan semua ini ke dalam satu bundel menggunakan paket web atau analog.

Dalam bentuk dasarnya, komposer dibuat seperti ini:


 COMPOSER = new THREE.EffectComposer(RENDERER); COMPOSER.setSize(window.innerWidth, window.innerHeight); const renderPass = new THREE.RenderPass(SCENE, CAMERA); renderPass.renderToScreen = true; COMPOSER.addPass(renderPass); 

RenderPass tidak benar-benar melakukan sesuatu yang baru. Itu hanya membuat apa yang kita dapatkan dari penyaji biasa. Bahkan, jika Anda melihat kode sumber RenderPass, maka Anda dapat menemukan renderer standar di sana. Karena sekarang rendering sedang terjadi di sana, kita perlu mengganti renderer dengan komposer dalam skrip kita:


 function render() { // RENDERER.render(SCENE, CAMERA); COMPOSER.render(SCENE, CAMERA); } 

Pendekatan ini menggunakan RenderPass sebagai pass pertama adalah praktik standar saat bekerja dengan EffectComposer. Biasanya kita perlu terlebih dahulu mendapatkan gambar yang diberikan dari adegan itu, kemudian melakukan sesuatu dengannya.


Pada contoh dari Three.js, di bagian postprocessing, Anda dapat menemukan sesuatu yang disebut UnrealBloomPass. Ini adalah skrip porting dari mesin Unreal. Ini menambahkan sedikit cahaya yang dapat digunakan untuk menciptakan pencahayaan yang lebih indah. Seringkali ini akan menjadi langkah pertama untuk meningkatkan gambar.


 const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 1, 0.1); bloomPass.renderToScreen = true; COMPOSER.addPass(bloomPass); 

Harap dicatat: opsi renderToScreen diatur hanya untuk Lulus terakhir yang kami lewati ke komposer.


Tapi mari kita lihat seperti apa cahaya yang diberikan bloomPass ini dan bagaimana ini cocok dengan bola:



Setuju, ini jauh lebih menarik daripada hanya bola dan sumber cahaya biasa, karena biasanya ditunjukkan dalam pelajaran awal di Three.js.


Tapi kita akan melangkah lebih jauh ...


Lebih banyak shader ke dewa shader!



Sangat berguna untuk menggunakan console.log dan melihat struktur komposer. Di dalamnya, Anda dapat menemukan beberapa elemen dengan nama renderTarget1, renderTarget2, dll., Di mana angka-angka tersebut sesuai dengan indeks pass yang dilewati. Dan kemudian menjadi jelas mengapa EffectComposer disebut. Ini bekerja berdasarkan prinsip filter dalam SVG. Ingat, di sana Anda dapat menggunakan hasil melakukan beberapa filter di orang lain? Di sini hal yang sama - Anda dapat menggabungkan efek.


Menggunakan console.log untuk memahami struktur internal objek Three.js dan banyak perpustakaan lainnya sangat berguna. Gunakan pendekatan ini lebih sering untuk lebih memahami apa itu apa.

Tambahkan pass lain. Kali ini adalah ShaderPass.


 const shader = { uniforms: { uRender: { value: COMPOSER.renderTarget2 }, uTime: { value: TIME } }, vertexShader: document.getElementById('postprocessing-vertex-shader').textContent, fragmentShader: document.getElementById('postprocessing-fragment-shader').textContent }; const shaderPass = new THREE.ShaderPass(shader); shaderPass.renderToScreen = true; COMPOSER.addPass(shaderPass); 

RenderTarget2 berisi hasil dari pass sebelumnya - bloomPass (itu yang kedua berturut-turut), kami menggunakannya sebagai tekstur (ini pada dasarnya adalah gambar yang diberikan datar) dan meneruskannya sebagai variabel seragam ke shader baru.


Mungkin perlu direm dan menyadari semua keajaiban di sini ...


Selanjutnya, buat shader vertex sederhana. Dalam kebanyakan kasus, pada tahap ini, kita tidak perlu melakukan apa pun dengan simpul, kita hanya meneruskan koordinat (u, v) ke shader fragmen:


 varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } 

Dan secara terpisah kita bisa bersenang-senang dengan selera dan warna kita. Misalnya, kita dapat menambahkan efek kesalahan cahaya, membuat semuanya hitam dan putih dan bermain dengan kecerahan / kontras:


 uniform sampler2D uRender; uniform float uTime; varying vec2 vUv; float rand(vec2); void main() { float randomValue = rand(vec2(floor(vUv.y * 7.0), uTime / 1.0)); vec4 color; if (randomValue < 0.02) { color = texture2D(uRender, vec2(vUv.x + randomValue - 0.01, vUv.y)); } else { color = texture2D(uRender, vUv); } float lightness = (color.r + color.g + color.b) / 3.0; color.rgb = vec3(smoothstep(0.02, 0.7, lightness)); gl_FragColor = color; } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } 

Mari kita lihat hasilnya:



Seperti yang Anda lihat, filter ditumpangkan di bola. Itu masih tiga dimensi, tidak ada yang rusak, tetapi di kanvas kami memiliki gambar yang diproses.


Kesimpulan


Bahan shader dan pasca-pemrosesan di Three.js adalah dua alat kecil namun sangat kuat yang pasti layak digunakan. Ada banyak opsi untuk penggunaannya - semuanya dibatasi oleh imajinasi Anda. Bahkan adegan paling sederhana dengan bantuan mereka dapat diubah tanpa bisa dikenali.

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


All Articles