Membangun dunia terbuka di browser, bagian 26: Tiga cara membuat air
Oleh Oleg Sidorkin, CTO dan Co-Founder Cinevva
Baru di sini? Gunakan panduan seri. Panduan tersebut menjelaskan apa itu spike dan menautkan semua bagian.
Bagian 25 memberi pakaian pada avatar. Bagian ini membahas air, dan terdiri dari tiga spike karena air adalah permukaan tempat jalan pintas murah dan jawaban yang tepat tampak identik dalam tangkapan layar, tetapi sangat berbeda saat bergerak. Spike 51 membangun pantulan dengan cara ruang layar, pilihan yang menggoda, lalu langsung terbentur keterbatasan bawaannya. Spike 52 beralih ke metode yang benar-benar digunakan oleh setiap game yang dirilis. Spike 53 memasang pustaka air yang sudah jadi untuk melihat seberapa jauh posisi kita dari kondisi "selesai". Ketiganya memakai satu lapisan refraksi yang sama, jadi satu-satunya variabel yang berubah di antara dua spike pertama adalah cara menghitung pantulan.
Pantulan dari layar yang sudah Anda miliki
Buka Spike 51 di tab baru ↗ · Lihat sumber
Pantulan ruang layar menggunakan kembali frame yang sudah dirender. Untuk setiap piksel air, Anda memantulkan sinar pandang dari permukaan, menelusuri sinar pantulan tersebut melalui buffer kedalaman, lalu ketika sinar melewati bagian belakang permukaan yang tercatat, Anda telah menemukan apa yang dipantulkan air, yang langsung diambil sampelnya dari buffer warna. Port ini mengikuti SSRNode milik three.js baris demi baris, yang pada gilirannya mengikuti pengantar SSR dari lettier. Penelusurannya berupa langkah DDA dalam ruang layar: proyeksikan titik awal dan akhir sinar ke koordinat piksel, lalu melangkahlah di sepanjang sumbu yang lebih panjang, satu sampel per piksel. Kedalaman sinar pantulan pada setiap langkah memerlukan interpolasi yang tepat secara perspektif,
Dua hal membuatnya layak digunakan, bukan sekadar tayangan slide. Penelusuran kasar dibatasi hingga 64 langkah, karena sinar panjang yang diproyeksikan melintasi seribu piksel akan menjalankan ratusan iterasi per fragmen jika tidak dibatasi, dan bidang air dengan sejuta fragmen dikalikan ratusan iterasi serta beberapa sampel tekstur akan menghasilkan adegan 30 fps. Kualitas mengendalikan stride efektif di dalam batas tersebut, bukan jumlah iterasinya. Selain itu, karena penelusuran kasar yang dibatasi meninggalkan pola bertangga yang terlihat, penyempurnaan biner enam iterasi membagi dua interval antara titik meleset terakhir dan titik perpotongan. Hasilnya adalah akurasi sublangkah 64x, cukup agar fragmen air yang berdekatan tidak lagi terkunci pada posisi perpotongan kasar yang sama. Pemeriksaan akhir jarak titik-ke-garis memastikan kandidat benar-benar berada pada sinar pantulan, bukan sekadar memiliki kedalaman yang sama, dengan toleransi ketebalan yang otomatis menyesuaikan lebar satu piksel dalam ruang pandang pada kedalaman tersebut—lebih ketat dari dekat dan lebih longgar dari jauh.
Bagian yang jujur dari spike ini tertulis dalam komentarnya sendiri: SSR tidak dapat memantulkan apa yang tidak pernah diambil sampelnya oleh kamera utama. Bagian bawah pohon, apa pun yang berada di luar layar, dan apa pun yang terhalang tidak ada dalam buffer, jadi semuanya tidak dapat muncul dalam pantulan. Inilah "hilangnya informasi dari sisi yang salah" yang tidak dapat diperbaiki oleh kualitas penelusuran setinggi apa pun, dan inilah alasan pasti keberadaan spike berikutnya.
Cermin yang tidak bisa berbohong
Buka Spike 52 di tab baru ↗ · Lihat sumber
Pantulan planar merender adegan untuk kedua kalinya dari kamera yang dicerminkan melintasi bidang air ke sebuah target di luar layar, lalu shader air mengambil sampel dari target tersebut. Ini adalah pola kanonis yang digunakan oleh UE5 Water, WaterMesh milik three.js, ABZÛ, dan Sea of Thieves, karena tersedia setiap piksel adegan sebagai sumber, termasuk geometri yang tidak akan pernah dapat dilihat SSR. Dalam TSL, penerapannya hampir antiklimaks: reflector() mengalokasikan kamera pembantu yang dicerminkan beserta target rendernya, Anda menambahkan targetnya ke mesh agar diperbarui pada setiap frame, lalu mengambil sampel warnanya. Saat gelombang ditambahkan nanti, pantulan dibuat bergoyang dengan menambahkan offset distorsi ke node UV milik reflektor, persis seperti baris yang digunakan WaterMesh.
Bug yang patut dicatat berada di jaring pengaman, bukan di cerminnya. Versi sebelumnya mencampurkan keluaran reflektor dengan langit prosedural sebagai fallback, dengan bobot berdasarkan magnitudo warna pantulan, berlandaskan teori bahwa pantulan yang mendekati nol berarti target tidak memiliki apa pun di sana. Namun, bayangan kanopi yang gelap juga memiliki magnitudo rendah, sehingga clamp tidak pernah mencapai kekuatan penuh dan piksel yang memang benar-benar gelap itu tercampur dengan langit cerah. Gejala yang ditemukan pengguna sangat spesifik: target cermin mentah dalam mode debug menampilkan pepohonan gelap dengan sempurna, sementara hasil render komposit memiliki pantulan yang pudar, dan diagnosisnya adalah shader tersebut "menghilangkan warna gelap". Perbaikannya adalah penghapusan. Target reflektor dapat diandalkan setelah frame pertama, jadi fallback sama sekali tidak diperlukan. Kedua spike memakai refraksi yang sama di bawahnya: ambil sampel adegan di belakang permukaan, rekonstruksi seberapa jauh setiap piksel berada di bawah garis air, lalu terapkan ekstingsi Beer-Lambert per kanal agar warna merah lenyap dalam beberapa meter sementara biru bertahan, dengan mask langit agar latar belakang bidang jauh tidak terkena kabut. Fresnel Schlick mencampurkan refraksi saat Anda melihat lurus ke bawah ke dalam air dengan pantulan saat Anda memandang melintasi permukaannya.
Seperti apa hasil yang sudah selesai
Buka Spike 53 di tab baru ↗ · Lihat sumber
Spike 53 adalah pemeriksaan buat-sendiri-versus-beli. Spike ini memasang threejs-water-pro sebagaimana pustaka tersebut didistribusikan, lengkap dengan preset tropis, clipmap laut bawaan, pelacakan kamera, dan langit Rayleigh, serta memuat glTF pulau yang sama dengan yang digunakan demo pustaka tersebut. Tujuannya adalah melihat jarak antara shader air datar buatan sendiri dan sistem laut lengkap, dan jaraknya memang besar: sistem ini memiliki daya apung yang mengambil sampel tinggi gelombang di beberapa titik di bawah lambung kapal agar kapal mengangguk dan berguling alih-alih sekadar naik-turun, generator jejak kapal, buih garis pantai dan permukaan, serta pass mask yang mencegah air dirender di dalam lambung kapal agar riak tidak menembus geladak.
Integrasinya mengungkap jenis detail yang hanya dapat dipelajari dengan menggunakan pustaka, bukan dengan membaca README-nya. Tekstur buih dirujuk berdasarkan nama file di dalam preset, tetapi pemuatannya menjadi tanggung jawab pengguna pustaka, dan tanpa tekstur tersebut garis pantai tampak seperti tepian garis air yang keras alih-alih ombak pecah. Pulau diposisikan sedemikian rupa sehingga geometri bawah airnya turun melewati dasar laut, sehingga mesh dasar milik pustaka menutupi cincin luar model tanpa tepian bidang yang terlihat. Inilah pola yang dimaksudkan pustaka tersebut: bawa model 3D, jangan menyintesis medan. Antialiasing juga sengaja dinonaktifkan pada renderer dan digantikan dengan pass FXAA pascaproses, karena MSAA mencampurkan fragmen tepi dengan latar belakang sebelum kabut atmosfer yang memperhitungkan kedalaman dijalankan, sehingga meninggalkan garis gelap tipis di setiap pertemuan geometri dengan kabut. Menangani aliasing setelah kabut, bukan sebelumnya, menghilangkan garis tersebut. Hal yang diamankan oleh spike ini adalah keputusannya sendiri: laut siap produksi merupakan sistem khusus yang besar, dan untuk kasus yang memerlukannya, mengadopsi pustaka yang dipelihara lebih baik daripada membangun ulang jejak kapal, daya apung, dan buih dari nol. Sementara itu, shader cermin planar dari spike 52 tetap menjadi jawaban yang tepat untuk perairan darat yang lebih kecil yang ditempatkan kreator di dunianya sendiri.
Teknologi yang dirujuk dalam bab ini
Air dengan pantulan ruang layar. Sinar pandang yang dipantulkan menelusuri buffer kedalaman dalam ruang layar melalui DDA, menggunakan interpolasi 1/z yang tepat secara perspektif, batas langkah tetap untuk membatasi biaya per fragmen, dan pass penyempurnaan biner untuk menghilangkan pola garis yang ditinggalkan oleh penelusuran terbatas. Konfirmasi titik-ke-garis dengan ketebalan yang diskalakan berdasarkan kedalaman menolak perpotongan palsu. Batas mutlak metode ini adalah bahwa ia hanya dapat memantulkan geometri yang sudah diambil sampelnya oleh kamera utama, sehingga permukaan di luar layar dan di sisi yang salah tidak pernah muncul. Lihat material medan.
Pantulan cermin planar. Kamera pembantu yang dicerminkan melintasi bidang air merender adegan ke target di luar layar yang diambil sampelnya oleh shader air, sehingga menghasilkan pantulan yang presisi hingga tingkat piksel, termasuk geometri yang tidak dapat dilihat SSR. Inilah pola yang digunakan oleh UE5 Water dan WaterMesh three.js, dengan distorsi gelombang yang diterapkan sebagai offset pada node UV milik reflektor. Fallback langit berbobot magnitudo secara keliru menghapus piksel pantulan gelap; target reflektor dapat diandalkan setelah frame pertama, jadi menghapus fallback tersebut merupakan perbaikannya.
Refraksi rona kedalaman Beer-Lambert. Kedua shader mengambil sampel adegan di belakang permukaan, merekonstruksi kedalaman setiap piksel latar di bawah garis air, lalu menerapkan ekstingsi eksponensial per kanal (merah lenyap dalam hitungan meter, biru bertahan) yang dikomposisikan menuju warna kabut air, dengan mask langit agar bidang jauh tidak terkena kabut. Fresnel Schlick mencampurkan refraksi pada insidensi normal dengan pantulan pada sudut datang landai.
Mengadopsi pustaka air siap produksi. threejs-water-pro menyediakan clipmap laut, langit Rayleigh, daya apung multititik untuk gerakan angguk dan guling kapal, jejak kapal, buih, serta pass mask lambung. Detail di sisi pengguna pustaka itu penting: tekstur buih harus dimuat secara eksplisit, geometri bawah air pulau harus turun melewati dasar laut agar dasar tersebut menutupi tepiannya, dan antialiasing harus dijalankan sebagai pass FXAA pascaproses, bukan MSAA, untuk menghindari garis kabut gelap pada tepi geometri.
Bagian 26 dari 29. Sebelumnya: Bagian 25 - Satu kerangka, semua pakaian Berikutnya: Bagian 27 - Pulau dari noise, tanah yang benar-benar tampak seperti tanah Panduan seri: /id/blog/2026-02-25-open-world-browser-series-guide