Membangun dunia terbuka di browser, bagian 30: Kamera yang menghormati dinding
Oleh Oleg Sidorkin, CTO dan Co-Founder Cinevva
Baru mengikuti seri ini? Gunakan panduan seri. Panduan tersebut menjelaskan apa itu spike dan menautkan semua bagian.
Bagian 29 memberi kita satu controller yang dapat mengendalikan tubuh apa pun. Tubuh itu sekarang bergerak dengan benar. Ia berjalan, meluncur, berenang, melayang, memanjat masuk ke gua, dan merunduk di bawah overhang. Masalahnya adalah apa yang mengamatinya. Selama dua puluh sembilan bagian, kameranya berupa rig orbit standar yang mengikuti pemain dan hanya melakukan satu hal cerdas untuk menghindari masalah: kamera menolak dimiringkan ke bawah garis cakrawala agar tidak dapat menyelinap ke bawah tanah datar. Clamp itu adalah petunjuknya. Clamp tersebut ada karena kamera sama sekali tidak mengetahui letak geometri dunia, sehingga satu-satunya pertahanan terhadap clipping adalah melarang sudut-sudut yang paling mungkin menyebabkan clipping. Berjalanlah mendekati bukit dan kamera akan berada di dalam bukit. Masuklah ke salah satu gua marching cubes dari Bagian 7 dan Anda akan melihat bagian dalam batu. Bangunlah rumah dengan alat authoring dari Bagian 16, lalu berdirilah di dalam ruangan, dan kamera akan melayang di luar dinding sambil menghadap pelapis luarnya. Bagian ini membuat kamera menghormati geometri sebagaimana tubuh pemain sudah melakukannya.
Bentuk masalah dan bentuk solusinya
Kamera orang ketiga memiliki satu tugas yang sulit dan belasan tugas yang mudah. Tugas mudahnya adalah mengikuti, memperhalus gerakan, dan menangani input orbit, dan semuanya sudah kita miliki. Tugas sulitnya disebut sebagai batasan visibilitas dalam literatur akademis, dan survei yang dirujuk semua orang, Camera Control in Computer Graphics karya Christie dan Olivier, membingkai seluruh bidang ini di sekitarnya: pertahankan subjek di dalam bingkai dan jangan sampai terhalang, sambil tetap menghormati dunia. Saat runtime, persoalan itu dapat dipersempit menjadi pertanyaan yang tampak sederhana dan diajukan setiap frame. Pemain adalah pivot. Pengguna telah memutar dan memperbesar atau memperkecil kamera ke posisi yang diinginkan pada jarak tertentu di belakang pemain. Seberapa jauh kamera benar-benar dapat ditempatkan di sepanjang garis tersebut sebelum menembus benda padat? Jawab pertanyaan itu dengan akurat, maka kamera akan merapat ke depan bukit, bergeser masuk sepanjang boom saat Anda mundur ke sudut, dan berhenti di langit-langit gua alih-alih menembusnya.
Pola yang menjawabnya sudah lama digunakan dan terbukti andal. Unreal menyebutnya spring arm, Godot menyediakan node SpringArm3D, dan Cinemachine milik Unity membaginya antara rig pengikut orang ketiga dan ekstensi deoccluder. Gagasannya selalu sama. Gantungkan kamera di ujung boom yang ditambatkan pada pivot. Pertahankan kamera pada panjang yang diinginkan ketika jalurnya bebas, tarik kamera mendekati pivot ketika ada sesuatu yang menghalangi, lalu pegas kembali ke luar ketika jalurnya kembali bebas. Real-Time Cameras karya Mark Haigh-Hutchinson, yang ditulis oleh pemimpin kamera Metroid Prime, mencurahkan beberapa bab penuh untuk membahas berbagai mode kegagalan yang dapat mengubah implementasi naif menjadi sesuatu yang membuat pemain mual. Kita mengambil pola tersebut dan membangun versi kita sendiri, cukup ringkas untuk dibaca dalam sekali duduk, di public/world/src/camera-rig.mjs.
Boom yang hanya mengendalikan panjangnya
Aturan desain yang menjaga rig ini tetap ringkas dipinjam dari pekerjaan controller di Bagian 29: kendalikan satu hal secara penuh dan tolak sisanya. Rig ini mengendalikan panjang boom dan tidak mengendalikan hal lain. Yaw, pitch, damping orbit, gestur sentuh dan roda gulir, semuanya tetap ditangani OrbitControls, yang sudah menyelesaikannya dengan baik dan sama sekali tidak ingin kita tulis ulang. Jadi, rig ini bukan controller kamera. Ini adalah pascaproses yang berjalan setelah perhitungan orbit dan mengoreksi tepat satu angka, yaitu jarak dari pivot ke kamera.
Keputusan itu terdengar rapi dan nyaris langsung merusak semuanya karena cara berpikir OrbitControls. Pada awal setiap pembaruan, OrbitControls membaca posisi kamera saat ini dan memperoleh radius orbit darinya. Biasanya proses itu tidak terlihat. Namun, begitu rig kita memperpendek posisi kamera untuk menghindari dinding, pada frame berikutnya OrbitControls membaca posisi yang telah diperpendek tersebut, menyimpulkan bahwa pengguna pasti telah memperbesar tampilan, lalu menetapkan pemendekan itu sebagai zoom yang diinginkan pengguna. Setelah beberapa frame, kamera pun merapat ke kepala pemain dan tidak mau kembali menjauh. Solusinya adalah dua pemanggilan yang mengapit pembaruan orbit, dan hanya itulah seluruh integrasinya. Sebelum OrbitControls berjalan, beforeControls() mengembalikan kamera ke jarak penuh dari frame sebelumnya yang belum diperpendek, sehingga perhitungan orbit selalu membaca zoom pengguna yang sebenarnya. Setelah OrbitControls berjalan, afterControls(dt) membaca posisi target yang baru selesai diorbitkan tersebut, menyelesaikan tabrakan, menerapkan damping pada panjangnya, dan menempatkan kamera di posisi yang seharusnya benar-benar dirender. Maksud pengguna dan koreksi tabrakan tidak pernah saling memengaruhi, dan dolly tetap seresponsif sebelum rig ini ada. Pengujian headless yang kita tulis untuk rig ini mengunci perilaku tersebut secara tepat: dorong boom ke dinding selama enam puluh frame, singkirkan dindingnya, dan panjang boom akan kembali memantul ke zoom penuh pengguna sejauh sepuluh meter, bukan tertahan pada jarak tabrakan.
Satu kontrak untuk collider apa pun
Rig tidak pernah menanyakan dunia terbuat dari apa. Collider hanyalah objek dengan metode probe, dan rig memberinya sebuah ray, jarak maksimum, serta radius probe kamera, lalu menerima satu angka sebagai hasilnya, yaitu jarak terjauh yang dapat ditempuh kamera sebelum collider tersebut menghalanginya. Rig memeriksa setiap collider yang terdaftar dan mengambil benturan terdekat. Itulah keseluruhan kontraknya, dan pendekatan ini sama dengan yang digunakan character controller ketika mengubah lokomosi menjadi perilaku yang dapat dipasang. Medan dapat dipasang, properti dapat dipasang, selubung bangunan dapat dipasang, masing-masing di balik probe yang sama, sementara rig tetap tidak perlu mengetahui apa pun tentang semuanya. Menambahkan jenis penghalang baru berarti menambahkan collider ke daftar, bukan mengedit kamera.
Satu detail dalam kontrak ini terbukti sangat berguna, yaitu radius probe. Kita tidak menembakkan ray tipis dari pivot ke kamera, tetapi menyapu bola yang cukup besar untuk menampung near plane kamera. Satu ray menghentikan pusat kamera pada dinding, tetapi near plane memiliki lebar, sehingga sudut-sudutnya sudah akan tertanam di dalam dinding sebelum ray di pusat melaporkan benturan. Menyapu bola kecil alih-alih ray adalah pendekatan yang digunakan setiap implementasi produksi; Unreal mengeksposnya sebagai ukuran probe. Inilah perbedaan antara kamera yang bersandar rapi pada permukaan dan kamera yang memungkinkan Anda melihat menembus permukaan di tepi layar.
Kita tidak menebak radius tersebut, melainkan menghitungnya. Titik near plane yang terjauh dari kamera adalah salah satu sudutnya, dan jaraknya dapat langsung diturunkan dari proyeksi. Dengan near plane
Radius probe adalah jarak sudut tersebut dikalikan margin keamanan kecil, dengan batas bawah tetap agar tidak pernah turun di bawah nilai minimum yang masuk akal pada frustum yang sangat sempit. Setiap kali field of view, rasio aspek, atau near plane berubah, radius dihitung ulang, sehingga perubahan ukuran jendela atau zoom yang memengaruhi proyeksi tidak dapat diam-diam membuat probe terlalu kecil untuk mencakup sudut-sudut yang seharusnya dilindunginya.
Collider yang memahami gua
Collider medan menjadi bagian yang menarik, karena medan di engine kita bukanlah heightmap. Sejak Bagian 7, medan tersebut berupa signed distance field, sebuah fungsi yang mengembalikan seberapa jauh titik mana pun di ruang dari permukaan padat terdekat dan apakah titik tersebut berada di dalam atau di luar batu. Nilai positif berarti udara, nilai negatif berarti batu, dan satu fakta itulah yang memungkinkan kamera kita melakukan sesuatu yang secara struktural tidak dapat dilakukan kamera berbasis heightmap. Heightmap mengetahui ketinggian tanah pada koordinat x dan z. Heightmap tidak memiliki konsep langit-langit karena hanya ada satu permukaan di atas setiap titik. Jadi, kamera berbasis heightmap dapat mencegah Anda berjalan menembus bukit, tetapi sama sekali tidak mengetahui bahwa bibir overhang atau atap gua menggantung di atas pivot, sehingga kamera melaju menembus keduanya. Distance field mengetahui setiap permukaan dalam tiga dimensi, sehingga probe yang sama yang menghentikan kamera pada lereng bukit juga menghentikannya pada langit-langit gua tanpa satu pun kasus khusus.
Menjalankan probe di sepanjang field merupakan teknik yang memiliki nama dan makalah yang mendasarinya. Sphere Tracing karya John Hart dari tahun 1996 adalah cara standar untuk melakukan marching ray terhadap distance field, dan triknya adalah bahwa field tersebut tidak hanya memberi tahu apakah Anda telah menabrak sesuatu, tetapi juga memberi tahu jarak aman yang dapat ditempuh tanpa menabrak apa pun. Jadi, alih-alih bergerak perlahan dalam langkah-langkah kecil yang tetap, Anda mengambil sampel field, melangkah maju sejauh ruang aman yang dilaporkannya, lalu mengulanginya, dengan langkah panjang melalui udara terbuka dan langkah pendek yang hati-hati saat mendekati permukaan. Namun, ada satu kendala yang dipaksakan oleh medan kita. Medan jarak sejati melaporkan jarak Euklides sebenarnya ke permukaan terdekat, dan melangkah sejauh seluruh ruang bebas tersebut selalu aman. Namun, di wilayah tempat medan masih berupa heightmap alih-alih voxel yang dipahat, medan yang dapat kita sampel dengan murah bukanlah jarak sejati, melainkan jarak bebas vertikal, yaitu celah lurus ke bawah menuju tanah. Pada lereng, angka tersebut melaporkan jarak gerak kamera secara berlebihan karena batu terdekat berada di samping, bukan tepat di bawah. Jarak sebenarnya lebih kecil dengan faktor yang bertambah seiring gradien,
Jika dituliskan, boom tersebut adalah sinar
Di sini,
Collider memiliki satu metode tambahan sebagai jaring pengaman, yaitu depenetrasi. Collision seharusnya sejak awal mencegah kamera masuk ke benda padat, tetapi beberapa situasi masih dapat lolos: pivot yang mengangkangi dinding sangat tipis sehingga boom dimulai di dalamnya, gua yang baru saja dipahat oleh alat medan ketika kamera berada di dalam batu yang kini telah hilang, atau bagian bangunan yang ditempatkan mengelilingi kamera. Untuk situasi tersebut, collider memeriksa apakah kamera mengakhiri frame di dalam benda padat, yaitu ketika
Beberapa iterasi saja akan konvergen ke isopermukaan
Menyentak masuk, melonggar keluar, dan tidak tersentak oleh tiang pagar
Boom yang sekadar melompat ke jarak collision setiap frame justru lebih buruk daripada tidak memakai boom sama sekali, karena dunia dipenuhi benda tipis yang dilewati kamera dari belakang hanya selama satu frame: tiang pagar, lampu, batang pohon. Kamera yang menerjang untuk menghindari setiap benda lalu menerjang kembali akan membuat mual. Buku Haigh-Hutchinson dan presentasi Itay Keren yang sangat disukai tentang gerakan kamera sama-sama sampai pada intuisi yang sama: kamera harus bereaksi terhadap ancaman dan bahaya lebih cepat daripada saat kembali rileks setelah ancaman berlalu. Karena itu, damping sengaja dibuat asimetris. Saat occluder muncul dan boom perlu memendek, boom menyentak masuk hampir seketika karena clipping selama satu frame terlihat buruk dan pemain akan memaklumi gerakan masuk yang cepat. Saat occluder menghilang dan boom ingin memanjang, boom melonggar keluar secara perlahan, dan hanya setelah timer jeda singkat dengan ruang bebas terus-menerus telah berlalu. Jeda tersebut adalah histeresis yang menghilangkan sentakan. Putar kamera dengan cepat melewati tiang tipis dan tiang itu tidak pernah menghilang cukup lama untuk memicu pemanjangan lambat, sehingga kamera meluncur melewatinya seolah-olah tiang itu tidak ada, tepat seperti yang diinginkan mata Anda. Cinemachine menggunakan gagasan yang sama sebagai nilai damping terpisah untuk memasuki dan keluar dari collision, dan asimetri inilah yang membuatnya terasa seperti operator kamera alih-alih pegas.
Dalam kode, mekanismenya hanya satu baris exponential smoothing dengan laju yang diganti berdasarkan tanda perubahan. Jika
dan cabang pelonggaran keluar hanya berjalan setelah ruang bebas bertahan selama waktu jeda
Perilaku terakhir ditujukan untuk interior sempit yang mengawali seluruh bagian ini. Ketika boom memendek sedemikian rupa sehingga kamera berada tepat di atas pemain, kita menyembunyikan avatar pemain sendiri dan membiarkan tampilan berada dekat dengan sudut pandang orang pertama. Inilah yang dilakukan Breath of the Wild di shrine sempit dan yang menjadi pilihan cadangan sebagian besar game orang ketiga ketika berada di sudut, karena alternatifnya, kamera yang terjepit ke dinding sambil menatap belakang kepala, tidak berguna. Rig menyediakan satu flag untuk hal ini, lalu loop dunia membaca flag tersebut dan mengubah visibilitas avatar lokal. Ketika boom lebih pendek dari satu meter, Anda pada dasarnya berada dalam sudut pandang orang pertama, dinding tetap dipatuhi, dan begitu Anda mundur ke ruangan yang lebih lapang, avatar perlahan muncul kembali dan boom memanjang.
Yang akan dipasang berikutnya
Collider medan dirilis hari ini dan merupakan separuh yang sulit, karena medan ada di mana-mana dan distance field merupakan hal yang merepotkan untuk diperiksa. Prop dan selubung bangunan dari eksperimen authoring adalah separuh yang mudah, dan kontraknya sudah siap menunggu. Collider kedua, yaitu raycast collider, memancarkan sinar dari pivot menuju kamera terhadap daftar mesh dan melaporkan hit terdekat dengan cara yang sama seperti collider medan. Versi murah memancarkan satu sinar, yang memadai hingga jumlah prop meningkat, sedangkan peningkatannya adalah mengganti sinar tersebut dengan bola yang disapu menggunakan three-mesh-bvh, pustaka Garrett Johnson yang membungkus mesh dalam hierarki volume pembatas agar kueri spasial berjalan dalam waktu logaritmik alih-alih melalui brute force. Apa pun pilihannya, rig tidak berubah. Rig mengueri daftar collider yang lebih panjang dan mengambil hit terdekat, yang memang merupakan tujuan utama membangun kontrak terlebih dahulu dan collider setelahnya.
Teknologi yang dirujuk dalam bab ini
Boom yang hanya memiliki satu angka. Rig kamera adalah post-process di atas OrbitControls, bukan penggantinya. Rig hanya memiliki panjang boom dan menyerahkan yaw, pitch, zoom, serta penanganan gestur kepada orbit controller yang sudah kita percayai. Integrasinya terdiri dari dua pemanggilan yang mengapit proses: beforeControls() memulihkan jarak penuh frame sebelumnya agar matematika orbit membaca zoom sebenarnya dari pengguna, bukannya salah mengira pemendekan akibat collision sebagai dolly-in, lalu afterControls(dt) menyelesaikan collision dan menulis posisi yang dirender. Tanpa pasangan tersebut, kamera akan runtuh mendekati pemain dalam beberapa frame.
Kontrak collider yang dapat dipasang-lepas. Collider adalah objek apa pun dengan probe yang menjawab, "seberapa jauh kamera dapat bergerak dari pivot sepanjang sinar ini sebelum Anda menghalanginya." Rig mengueri setiap collider dan mengambil hit terdekat tanpa perlu mengetahui apakah rintangannya adalah medan, prop, atau dinding. Ini adalah disiplin hanya-memiliki-satu-hal yang sama seperti yang digunakan controller karakter yang dapat dipasang-lepas untuk pergerakan. Probe tersebut berupa bola yang disapu dengan ukuran yang mencakup near plane, bukan sinar tipis, sehingga sudut-sudut frustum tidak pernah menembus permukaan yang luput dari sinar tengah. Radiusnya diturunkan dari proyeksi, yaitu jarak ke sudut near plane
Probe medan jarak bertanda yang mematuhi overhang dan gua. Karena medan merupakan signed distance field, bukan heightmap, probe yang menghentikan kamera di lereng bukit juga menghentikannya di langit-langit gua, kasus yang secara struktural tidak dapat dilihat oleh raycast heightmap. Proses marching menggunakan sphere tracing ala Hart, melangkah sebesar bagian yang direlaksasi-bawah dari ruang bebas yang dilaporkan medan dan dijepit di kedua ujung. Ini diperlukan karena wilayah heightmap melaporkan jarak bebas vertikal alih-alih jarak sejati dan melebih-lebihkan seberapa jauh kamera dapat bergerak di lereng, sehingga langkah penuh akan melompati punggung bukit. Pass depenetrasi yang digerakkan gradien menjadi jaring pengaman yang memulihkan tampilan ketika tanah dipahat dari bawah kamera.
Damping asimetris dengan timer jeda. Boom menyentak masuk dengan cepat saat occluder muncul dan melonggar keluar secara perlahan setelah occluder hilang, serta hanya setelah ruang bebas terus tersedia selama jendela waktu singkat. Dengan begitu, gerakan cepat melewati tiang pagar tidak pernah membuat kamera menerjang. Di bawah ambang keruntuhan, rig menandai kondisi mendekati sudut pandang orang pertama dan loop dunia menyembunyikan avatar lokal, solusi cadangan standar untuk interior sempit alih-alih membiarkan kamera terkubur di dalam dinding.
Referensi
Perumusan kontrol kamera sebagai batasan visibilitas berasal dari Marc Christie dan Patrick Olivier, Kontrol Kamera dalam Grafika Komputer (Computer Graphics Forum, 2008). Proses marching distance field berasal dari John C. Hart, Sphere Tracing: Metode Geometris untuk Ray Tracing Berantialias pada Permukaan Implisit (The Visual Computer, 1996). Pola spring-arm dan collision bola probe-nya didokumentasikan dalam Komponen Spring Arm dari Epic serta Cinemachine Deoccluder dan Third Person Follow dari Unity. Intuisi gerakan dan damping berasal dari Mark Haigh-Hutchinson, Kamera Waktu Nyata (Morgan Kaufmann, 2009), dan Itay Keren, Gulir Kembali: Teori dan Praktik Kamera dalam Game Side-Scroller (GDC 2015). Jalur peningkatan mesh collider menggunakan three-mesh-bvh karya Garrett Johnson.
Bagian 30 dari 30. Sebelumnya: Bagian 29 - Satu controller, tubuh apa pun Panduan seri: /blog/2026-02-25-open-world-browser-series-guide