Skip to content

Membangun dunia terbuka di browser, bagian 12: Cincin, kabut langit, dan apa yang akan kami lakukan lagi

Oleh Oleg Sidorkin, CTO dan Co-Founder Cinevva

Baru di sini? Gunakan panduan seri. Panduan ini menjelaskan apa itu spike dan menyediakan tautan ke semua bagian.

Spike 24 awalnya seharusnya hanya "menambahkan cincin clipmap ke medan." Namun, spike ini berubah menjadi penutup besar yang sekaligus menyentuh rendering, shader, infrastruktur modul, dan integrasi visual.

Tugas utama untuk medan adalah menghasilkan cincin clipmap konsentris di vertex shader. Setiap cincin merupakan mesh grid datar yang berpusat pada kamera, dengan vertex yang digeser berdasarkan sampel heightmap. Cincin terdalam menggunakan resolusi penuh. Setiap cincin berikutnya menggandakan jarak antar-vertex dan mencakup area yang lebih luas. Bagian tersulitnya adalah batas antarcincin: ketika cincin beresolusi tinggi bertemu dengan cincin beresolusi rendah, vertex tepi pada mesh yang lebih rapat harus menempel ke titik tengah tepi mesh yang lebih kasar. Kami melakukan morphing tepi 2:1 dengan mendeteksi vertex batas (vertex dengan koordinat grid ganjil di sepanjang tepi cincin), lalu menempelkan ketinggiannya ke titik tengah dua tetangga genapnya, h=12(hleft+hright). Itulah tepatnya posisi tepi tunggal dari cincin kasar, sehingga tepi halus dan kasar menggambarkan garis yang sama. Hasilnya adalah sambungan kedap tanpa geometri transisi.

Buka Spike 24 di tab baru ↗ · Lihat kode sumber

Berikutnya adalah integrasi kabut dan langit. Kami ingin medan yang jauh memudar ke warna langit sebenarnya, bukan ke warna konstan yang datar. Itu berarti shader kabut perlu mengetahui warna langit pada arah setiap fragmen. Kami memuat tekstur skybox HDR equirectangular dan mengambil sampelnya di fragment shader menggunakan arah pandang dari kamera ke fragmen, yang dikonversi menjadi koordinat UV equirectangular melalui node equirectUV milik TSL. Faktor kabut dihitung berdasarkan jarak menggunakan positionView.z.negate() untuk kedalaman dalam ruang kamera, lalu dipadukan dengan smoothstep antara jarak dekat dan jauh.

Pengkabelan modul ternyata lebih menjengkelkan daripada bagian geometri mana pun. Kami meningkatkan versi ke Three.js 0.183.1, yang merombak keluaran build-nya. Import three/tsl harus mengarah ke three.tsl.js, sementara secara internal TSL mengimpor three/webgpu sebagai bare specifier. Kedua pemetaan tersebut harus dituliskan secara eksplisit di import map HTML. Jika salah satunya tidak ada, muncul galat samar "does not provide an export" atau "failed to resolve module specifier" tanpa petunjuk pemetaan mana yang salah. Setelah keduanya dimasukkan ke import map, graph shader berhasil dimuat dengan benar.

Kami juga mengalami masalah orientasi skybox yang membuat tekstur dirender terbalik. Solusinya adalah mengatur flipY = true pada tekstur equirectangular. Ini merupakan nilai default Three.js untuk tekstur yang dimuat, tetapi di kode awal kami nilainya diatur ke false.

Implementasi kabut awal mengambil sampel langit dari arah yang hampir konstan, sehingga menghasilkan pita tipis berwarna cakrawala alih-alih gradasi alami. Solusinya adalah menghitung arah dunia aktual dari kamera ke fragmen per piksel menggunakan positionWorld.sub(cameraPosition).normalize(), lalu meneruskannya ke equirectUV untuk mengambil warna kabut. Dengan demikian, fragmen medan memudar ke warna langit yang benar-benar berada di belakangnya, sehingga terlihat tepat dari sudut kamera mana pun.

Di balik semua perbaikan individual itu, hasil utamanya tetap tercapai. Kini kami memiliki sistem medan yang menggabungkan pengeditan volumetrik jarak dekat (marching cubes dengan sambungan Transvoxel), chunk heightmap jarak menengah, dan cincin clipmap jarak jauh, yang semuanya diatur oleh lapisan kebijakan untuk menentukan mode, LOD, dan perilaku transisi.

Jika saya harus menyebutkan pola yang akan saya ulangi di proyek berikutnya, inilah daftarnya:

Mulailah dengan spike risiko sebelum mengerjakan fitur. Spike 1 menjawab pertanyaan "apakah kita bisa melakukan rendering dengan cukup cepat" bahkan sebelum kami berinvestasi dalam pipeline konten.

Bekukan baseline yang sudah terbukti baik sebelum melakukan lompatan integrasi. Spike 13 dan 14 menghemat waktu berhari-hari yang mungkin kami habiskan untuk melacak regresi dengan bisect.

Terapkan kebijakan dan observabilitas sebelum memulai maraton optimasi. Spike 23 mengubah bug misterius menjadi kondisi bernama dengan aturan pemicu.

Lakukan pengujian saat bergerak, bukan melalui tangkapan layar. Lompatan clipmap, kedipan sambungan, dan sendatan streaming tidak terlihat pada frame diam.

Ukur biaya waktu frame untuk setiap fitur, bukan FPS rata-rata. Nilai rata-rata menyembunyikan lonjakan yang benar-benar dirasakan pengguna.

Dan publikasikan bagian-bagian yang berantakan. Langkah-langkah yang salah, perburuan bayangan buffer usang, dua hari menyalahkan logika transisi ketika ternyata draw range yang salah. Justru dari bagian-bagian itulah orang benar-benar bisa belajar.

Pemeriksaan realitas eksternal: devlog Vuntra City

Setelah menyelesaikan seri ini, kami meninjau devlog @VuntraCity sebagai pemeriksaan implementasi eksternal terhadap asumsi kami sendiri mengenai dunia terbuka. Proyek tersebut merupakan proyek UE5 native, bukan stack browser, tetapi pola sistemnya cukup serupa sehingga perbandingannya tetap berguna.

Sinyal pertama adalah bahwa kecepatan pergerakan harus diperlakukan sebagai kontrol streaming, bukan sekadar bagian gameplay. Di Vuntra City, transportasi berkecepatan tinggi sengaja diarahkan melewati bagian atas sebagian besar interior, dan jangkauan detail disesuaikan dengan kecepatan gerakan untuk menghindari pergantian spawn yang berlebihan dan kemacetan (sistem transportasi, teknik performa). Ini sejalan dengan arah lapisan kebijakan kami: mode pergerakan harus secara langsung memengaruhi radius chunk, aktivasi interior, dan jumlah pekerjaan yang diizinkan per frame.

Sinyal kedua adalah arsitektur. Sistem peta dan alamat mereka memerlukan pemisahan topologi dunia dari objek yang dirender agar kueri global dapat dijalankan untuk wilayah yang belum dimuat (peta dan alamat). Pemisahan yang sama juga kami perlukan di browser untuk pencarian dunia, penentuan rute misi, pemindaian moderasi, dan pengindeksan POI tanpa memaksakan jalur data yang terikat pada rendering.

Sinyal ketiga adalah pembagian tingkat simulasi. Desain mereka untuk satu juta NPC menjaga state jadwal kasar tetap ringan dan global, lalu hanya mengalokasikan anggaran perilaku yang mahal di dekat pemain (ikhtisar satu juta NPC, pembahasan mendalam sistem). Hal ini memperkuat model simulasi kami sendiri yang mengutamakan AOI, dengan fidelitas jarak dekat dan determinisme jarak jauh sebagai kebutuhan terpisah yang memiliki anggaran masing-masing.

Sinyal keempat adalah kualitas desain, bukan skala mentah. Momen eksplorasi terbaik mereka berasal dari distribusi berbobot, pencilan langka, dan petunjuk navigasi diegetik, bukan dari overlay UI yang terus-menerus ditampilkan (catatan lingkungan prosedural, alur tanpa minimap). Bagi kami, ini menjadi pengingat bahwa sistem teknis harus disetel untuk menghasilkan variasi yang dapat ditemukan, bukan sekadar throughput maksimum.

Teknologi yang dirujuk dalam bab ini

Geometri cincin clipmap. Setiap cincin merupakan mesh grid datar yang berpusat pada kamera, dengan vertex yang digeser berdasarkan sampel heightmap. Cincin terdalam menggunakan resolusi penuh. Setiap cincin berikutnya menggandakan jarak antar-vertex dan mencakup area yang lebih luas. Bagian tersulitnya adalah batas: ketika cincin beresolusi tinggi bertemu dengan cincin beresolusi rendah, vertex tepi pada mesh yang lebih rapat menempel ke titik tengah tepi mesh yang lebih kasar. Teknik ini berasal dari makalah SIGGRAPH 2004 karya Losasso dan Hoppe (PDF) dan dijelaskan secara terperinci dalam GPU Gems 2, Bab 2. Lihat panduan lanskap kami mengenai geometry clipmap.

Morphing tepi 2:1. Pada batas antara dua cincin clipmap, cincin yang lebih rapat memiliki vertex di posisi yang tidak dimiliki cincin yang lebih kasar. Vertex batas dengan koordinat grid ganjil di sepanjang tepi cincin dideteksi, lalu ketinggiannya diinterpolasi di antara dua vertex genap di sebelahnya. Hasilnya adalah sambungan kedap tanpa geometri transisi khusus. Interpolasi dijalankan di vertex shader: morphedHeight = mix(heightLeft, heightRight, 0.5) untuk vertex batas, menggunakan kerangka geomorphing yang sama seperti yang dijelaskan dalam panduan kami.

Pemetaan skybox equirectangular. Satu gambar 2D yang memetakan seluruh bola arah langit menggunakan proyeksi garis bujur-garis lintang. Sumbu horizontal mencakup 0–360 derajat, sedangkan sumbu vertikal mencakup 0–180 derajat. Arah pandang ternormalisasi d^=(dx,dy,dz) dikonversi menjadi koordinat UV dengan membaca garis bujur dan garis lintangnya:

u=12+atan2(dz,dx)2π,v=12arcsin(dy)π

Di Three.js, pengaturan texture.mapping = EquirectangularReflectionMapping bersama SRGBColorSpace mengaktifkan tekstur tersebut sebagai latar scene. Di TSL, equirectUV(direction) menerapkan konversi yang sama, mengubah arah pandang 3D menjadi koordinat UV 2D untuk mengambil sampel tekstur.

Warna kabut per fragmen dari langit. Kabut standar memadukan fragmen menuju satu warna konstan. Untuk scene dengan skybox terperinci, hasilnya terlihat salah karena warna langit berbeda-beda berdasarkan arah. Solusinya adalah menghitung arah dunia dari kamera ke fragmen per piksel (positionWorld.sub(cameraPosition).normalize()), lalu mengambil sampel skybox pada arah tersebut sebagai warna kabut. Setiap fragmen memudar menuju warna langit yang benar-benar berada di belakangnya, menghasilkan perpaduan yang tepat dari sudut kamera mana pun. Faktor kabut menggunakan smoothstep(nearDist, farDist, viewDepth) dengan positionView.z.negate() untuk kedalaman dalam ruang kamera.

Import map untuk modul ES. Mekanisme bawaan browser (<script type="importmap">) yang memetakan bare module specifier (seperti three/tsl) ke URL sebenarnya. Ketika Three.js 0.183.1 merombak keluaran build-nya, three/tsl harus mengarah ke three.tsl.js, sementara secara internal TSL mengimpor three/webgpu sebagai bare specifier. Kedua pemetaan harus dituliskan secara eksplisit di import map, atau browser akan menghasilkan galat "does not provide an export" atau "failed to resolve module specifier".

Bacaan lebih lanjut

Untuk pembahasan lebih mendalam mengenai teknologi yang digunakan di sepanjang seri ini, lihat panduan pendamping kami:

Terima kasih telah mengikuti perjalanan dua belas bagian ini.

Bagian 1: Kami memulainya dengan mencoba menghancurkannya
Bagian 2: Fisika worker dan kekhawatiran akan input lag
Bagian 3: Spike yang tidak mencolok tetapi menyelamatkan kami
Bagian 4: Streaming sebelum medan yang canggih
Bagian 5: Mengatur anggaran untuk bagian yang indah
Bagian 6: Clipmap mengubah jalan cerita
Bagian 7: Marching cubes dan gua nyata pertama
Bagian 8: Integrasi tanpa kehilangan baseline kami
Bagian 9: Transvoxel dimulai dengan kerangka awal
Bagian 10: Kekacauan sambungan dan pertarungan bos di sudut
Bagian 11: Mode kebijakan, bukan mode hardcode


Bagian 12 dari 14.
Sebelumnya: Bagian 11 - Mode kebijakan, bukan mode hardcode
Berikutnya: Bagian 13 - Memahat medan dan matinya fungsi matematika
Panduan seri: /id/blog/2026-02-25-open-world-browser-series-guide