Skip to content

Tarayıcıda açık dünya oluşturmak, bölüm 16: Büyümeye devam eden bir dünya için yapı

Yazan: Oleg Sidorkin, Cinevva CTO'su ve Kurucu Ortağı

Burada yeni misiniz? Seri rehberine göz atın. Rehber, spike'ın ne olduğunu açıklıyor ve tüm bölümlere bağlantı veriyor.

Bölüm 13'ten bu yana her spike aynı tarifi izliyordu: önceki monoliti kopyala, bir özellik ekle. Spike 32'nin sonunda bu monolit, tek bir <script type="module"> içindeki 6.285 satırlık bir index.html dosyasına dönüşmüştü. Kod aramaları gereksiz sonuçlarla doluydu, bir özelliği nereye ekleyeceğimizi bulmak onu yazmaktan daha uzun sürüyordu ve herhangi bir mimari değişiklik, farklarını zihinde takip edemeyeceğiniz kadar büyük bir dosyaya dokunuyordu. Yeni bir özellik daha eklemeden önce yapısal borcu kapattık.

Davranışı hiç değiştirmeden monoliti parçalamak

Spike 33'ü yeni sekmede aç ↗ · Kaynak kodu görüntüle

Kısıt katıydı: her ayırma işlemi yeniden tasarım değil, yalnızca davranışı değiştirmeyen bir yeniden düzenleme olmalıydı. Monolit, 19 .mjs dosyasına ve 151 satırlık bir ana kabuğa dönüştü. Sahne, su, çim, karakter, fizik, çok oyunculu sistem ve kullanıcı arayüzü için üst düzey modüller; GPU tarafındaki tüm doğruların tek kaynağı olarak her WGSL kaynak dizesini barındıran bir wgsl.mjs; ayrıca yükseklik haritası, SDF, parçalar, GPU tampon sarmalayıcıları, fırça, LOD ve kalıcılık için bir terrain/ alt ağacı oluşturduk. Toplam kod 6.555 satır oldu; yani temelde monolite içe aktarma için gereken standart kodların eklenmiş hâliydi. Hacim neredeyse hiç değişmedi, ancak kod içinde gezinmek çok daha kolaylaştı.

Ardından sayfa siyah ekranla açıldı. İki hata mesajı ve birbiriyle ilgisiz iki temel neden vardı. İlki, sıfır baytlık bir tampon bağlamasıyla ilgili WebGPU şikâyetiydi. Monolitte SDF fırça tamponu, ilk marching cubes parçası göründüğünde tembel biçimde ayrılıyordu; bağlama grubu fabrikası ise tesadüfen daha sonra, tampon oluşturulduktan sonra çalışıyordu. terrain/gpu.mjs dosyasını terrain/brush.mjs dosyasından ayırmak modül değerlendirme sırasını değiştirdi. Böylece fabrika artık önce çalışıyor ve bir null yer tutucuyu bağlamaya çalışıyordu. Çözüm, bağlama grubu oluşturmayı ilk dispatch işlemine kadar erteleyen bir getOrCreateBindGroup(chunk) yardımcısı kullanmaktı. "Her şeyi baştan oluştur" kalıbı, monolitin tek başlatma yolundan kalma bir kalıntıydı.

İkinci mesaj, korkutucu görünen bir FBX iskelet uyarısıydı ama bunun yanlış iz olduğu ortaya çıktı. Spike 25'ten beri yazdırılıyordu ve zararsızdı. Karakterin kayıp olmasının tek nedeni ilk hatanın zincirleme etkisiydi: her compute dispatch hata veriyor, yükseklik haritasına hiçbir zaman yazılmıyor, yükseklik örnekleri 0 döndürüyor ve karakter başlangıç noktasında doğup dünyanın içinden aşağı düşüyordu. Tamponu düzeltince karakter, uyarıya rağmen sorunsuz biçimde canlandı.

Yeniden düzenlemenin asıl dersi buydu. Monolit, tüm "Y oluşturulmadan önce X mevcut olmalı" ilişkilerini betiğin yukarıdan aşağıya çalışma sırasının içine gizliyordu. Modülerleştirme bu sırayı değiştirdi ve bekleyen üç sıralama hatasını daha açığa çıkardı: yükseklik haritası yüklenmeden önce çimlerin dağıtılması, ortam haritasının kod çözme işlemi bitmeden önce su düzleminin eklenmesi ve kalıcılık yüklemesinin ilk kareden sonra tamamlanması. Üçünün de çözümü tek satırdı ve ayırma işlemi olmasaydı hiçbiri yakalanmayacaktı.

Hiçbir şeyi büyütmeden yüz nesne eklemek

Spike 34'ü yeni sekmede aç ↗ · Kaynak kodu görüntüle

Spike 34, bu yapının işe yarayıp yaramadığını sınayan testti. Amaç, kontrollerden ayrılmadan bir CC0 model paketindeki ağaçları, kayaları, çalıları, mantarları ve yolları yerleştirmek için birinci şahıs bir palet oluşturmaktı. Buna araziye duyarlı hizalama, kalıcılık, çok oyunculu eşitleme ve fizik çarpıştırıcıları da dâhildi. Yeni kodun her satırı src/props/ altındaki beş yeni dosyaya yerleşti ve mevcut modüllerin hiçbiri on satırdan fazla bağlantı koduyla büyümedi.

Varlık süreci, kayda değer bir sapma yaşadı; çünkü bu, bir günü kolayca tüketebilecek türden bir meseleydi. İşe, gömülü dokular içermeyen bir FBX kütüphanesi olan Quaternius Ultimate Nature paketiyle başladık. FBX malzemeleri, haritasız MeshPhong olarak geliyordu. Bu nedenle malzeme adlarını PNG'lerle eşleştiren manuel bir tablo kurduk, Phong'u Standard'a dönüştürdük ve renk uzaylarını elle ayarladık. Malzemelerin yaklaşık %30'unun eşleşen PNG'si yoktu ve benzer ağaçlar arasında bazı adlar belirsizdi. İkinci bir FBX paketinde de aynı eksik vardı. Çözüm daha fazla eşleme tablosu değil, daha iyi hazırlanmış bir paketti: Quaternius Stylized Nature MegaKit, gömülü PBR malzemeleri ve fırınlanmış normalleri olan 116 eksiksiz glTF ile geliyor. FBXLoader yerine GLTFLoader kullanmak; santimetreden metreye ölçeklemeyi, doku tablosunu ve Phong dönüşümünü ortadan kaldırdı, ayrıca library.mjs dosyasını yaklaşık 80 satır küçülttü. Çıkarım şu: glTF sunan CC0 paketler için PBR destekli glTF doğru işlem hattıdır; manuel doku eşlemeli FBX ise yarı kalite için iki kat kod gerektiriyordu.

glTF yolu da birkaç keskin köşeyle geldi. Palet 116 küçük resim oluşturuyor ve WebGPU'nun canvas.toDataURL() işlevi, GPUCanvasContext yüzeyi için boş sonuç döndürüyor. Bu nedenle küçük resimler bir RenderTarget içine işleniyor, readRenderTargetPixelsAsync ile geri okunuyor ve WebGPU'nun 256 baytlık satır hizalaması dikkate alınarak 2B bir tuvale kopyalanıyor. Hayalet önizlemeler, her malzemeyi yeşile boyamak için klonluyor; ancak material değeri bir dizi olan mesh'lerde bu bozuluyordu. Sorunu bir Array.isArray dalıyla çözdük. Ayrıca yaklaşık 200 MB boyutunda gelen 16 bit normal haritaları, tek seferlik bir mogrify -depth 8 işlemiyle yaklaşık 32 MB'a indirildi. Tarayıcılar yükleme sırasında zaten alt örnekleme yaptığı için görsel sonuç tamamen aynıydı.

İşlenen geometri yalnızca GPU'da mevcut olduğunda

En öğretici hata, imleç hareket ederken hayalet önizlemenin 1 ila 2 metrelik adımlarla sıçramasıydı. Arazi mesh'leri, compute işlem hattı konumları doğrudan GPU'ya yazdığı için köşe konumlarını bir StorageBufferAttribute içinde saklıyor. Bu nedenle three.js'in CPU tarafındaki Raycaster bileşeni bunları göremiyor ve hiçbir sonuç döndürmüyor. Yedek yöntem, analitik yükseklik haritasına karşı 1,5 metrelik kaba bir ışın yürüyüşüydü ve kullanıcının gördüğü ızgarayı bu sabit adım oluşturuyordu. Bunu uyarlanabilir bir yürüyüşle değiştirdik: yüzeyin çok üzerindeyken 2,5 m adım, yüzeye 5 m kala 0,4 m adım ve (rayyterrainy) işareti değiştiğinde 14 kez ikiye bölme. Bu yöntem, ışın başına yaklaşık 30 geniş adım ve 14 ikiye bölme ile milimetrenin altında hassasiyet sağlıyor. İşlenen geometri yalnızca GPU'da yaşıyorsa raycaster ile mücadele etmeyin; analitik kaynağa karşı yürüyün.

Düzenlemeler boyunca doğruluğunu koruyan çarpıştırıcılar

Dışbükey gövdeler veya mesh çarpıştırıcıları yerine ilkel şekil vekillerini seçtik. Quaternius nesneleri belirgin içbükeylikleri olmayan, yuvarlak hatlı ve düşük poligonlu modeller. Bu nedenle gövdeler aynı oynanış için yaklaşık 50 kat fazla kod ve çalışma zamanında 10 kat fazla maliyet getirirdi. Her nesne, sınırlayıcı kutusundan türetilen tek bir şekle indirgeniyor: ağaçlar ve kaktüsler dikey kapsüle, kayalar küreye, kütükler uzun eksen boyunca yatay kapsüle dönüşüyor; dekoratif çalılar ve çiçekler ise hiçbir çarpıştırıcı almıyor. Kayaların ve kütüklerin üzerinde yürünebiliyor; üzerlerinde durabilmeniz için yalnızca dikey itme uygulanıyor. Ağaçlar ve kaktüsler ise engelleyici; gövdeye tırmanamamanız için tam 3B itme uygulanıyor. 8 metrelik bir uzamsal karma, kare başına testi oyuncunun 3×3'lük komşuluğuyla sınırlıyor; burada genellikle sıfır ila altı nesne bulunuyor.

İki tasarım kararı sistemin tutarlı kalmasını sağladı. Arazi hizalaması, kod içine sabitlenmiş bir kategori enum'u değil, manifest içindeki bir bayraktır. Böylece hayalet önizleme ile kesinleştirilmiş yerleşim aynı placement.alignToTerrain değerini okur ve birbiriyle çelişemez. Ayrıca yerleştirilen nesneler, arazi düzenlemelerine tek bir yardımcı üzerinden tepki verir: bir fırça darbesinden sonra —ister yerel olsun ister eşten yeniden oynatılsın— refreshPlacementsInRadius, etkilenen disk içindeki her nesnenin altındaki zemini yeniden örnekler, hizalamayı yeniden uygular ve çarpıştırıcı uç noktalarını yeniden türetir. Bir ağacın altında tepe şekillendirdiğinizde ağaç da onunla birlikte yükselir. Kalıcılık ve çok oyunculu sistem, spike 31'in kalıbını aynen yeniden kullanıyor; düz bir {uid, propId, x, y, z, rotY, scale} listesi saklıyor ve yerleştirme, kaldırma ve küçük hareket olaylarını BroadcastChannel üzerinden yansıtıyor.

Bu bölümde değinilen teknolojiler

WebGPU altında ES modülü ayrıştırması. Monolitik bir <script type="module"> yapısını yalın yollu .mjs içe aktarmalarına bölmek, modüller statik varlıklar olarak sunulduğunda paketleyici gerektirmez; ayrıca three.js TSL modül sınırları arasında sorunsuz çalışır. Gizli maliyet, başlatma sırasıdır: monolit, "Y'den önce X'i oluştur" kuralını betiğin yukarıdan aşağıya sırasına kodlar. Modüller ise içe aktarma sırasına göre değerlendirilir ve bu, tamponu henüz mevcut değilken bir GPU bağlama grubu fabrikasını çalıştırabilir. Çözüm kalıbı, tembel başlatma (ilk kullanımda getOrCreate...) ve bildirim sırasına güvenmek yerine doğru promise'i beklemektir.

Gömülü PBR destekli glTF ile manuel eşlemeli FBX karşılaştırması. glTF metre cinsinden gelir, kendi dokularına başvurur ve doğrudan MeshStandardMaterial sağlar. Böylece glTF olarak hazırlanmış bir CC0 paketi doğrudan PBR işlem hattına girer. Doku bağlama meta verileri olmayan FBX paketleri, her paket güncellemesinde güncelliğini yitiren ve elle sürdürülen bir malzeme-adı-PNG tablosunun yanı sıra Phong'dan Standard'a dönüşüm ve manuel renk uzayı etiketleme gerektirir. Bitki örtüsü için kullanılan bir güvenlik ağı, alphaTest içermeyen transparent malzemeleri alphaTest: 0.5 kesme kartlarına yükseltir; böylece opak geometrinin arkasında doğru sıralanırlar.

WebGPU ekran dışı küçük resimleri. canvas.toDataURL(), GPUCanvasContext destekli bir tuval için boş sonuç döndürür; çünkü sunum yüzeyinden 2B bağlama geri dönen bir yol yoktur. WebGPU'nun 256 bayta hizalanmış geri okuma adımlarına uyulduğu sürece bir RenderTarget içine işlemek, pikselleri readRenderTargetPixelsAsync ile okumak ve 2B tuvale kopyalamak işe yarar. Sonuçlar, paket değişikliklerinin eski işlemeleri geçersiz kılabilmesi için sürümü artırılmış bir anahtar altında localStorage içinde önbelleğe alınır.

Analitik yükseklik haritasına karşı uyarlanabilir ışın yürüyüşü. Arazi köşeleri GPU'daki bir StorageBufferAttribute içinde yaşadığında CPU ışın izleyicisi bunları göremez. Analitik yükseklik işlevinde yüzeyden uzaktayken büyük, yüzeye yakınken küçük adımlarla yürümek ve (rayyterrainy) işaret değiştirdiğinde ikili aramayla inceltme yapmak, sınırlı sayıda örnekle milimetrenin altında imleç hassasiyeti sağlar. Aynı ilkel yöntem hem fırça imlecini hem de nesne hayaletini çalıştırır.

Uzamsal karma kullanan ilkel kapsül çarpıştırıcıları. Her nesne, sınırlayıcı kutusundan kategoriye göre türetilen bir kapsüle veya küreye indirgenir, {kind, walkable, radius, p1, p2} olarak kaydedilir ve kesiştiği her 8 metrelik karma bölmesine eklenir. Oyuncu, kare başına yalnızca 3×3'lük bölme komşuluğundaki nesneleri test eder ve her biri için tek bir kapsüle karşı kapsül çözümlemesi uygulanır. Üzerinde yürünebilen vekiller (kayalar, kütükler) yalnızca dikey itme, engelleyici vekiller (ağaçlar) ise tam 3B itme alır. Bunun temel aldığı kapsül matematiği için SDF arazi çarpışmalarına bakın.


29 bölümün 16.'sı. Önceki: Bölüm 15 - Temeli değiştir, ardından eşitle Sonraki: Bölüm 17 - Yeniden hedefleme gerektirmeyen animasyonlar ve canlı varlık araması Seri rehberi: /tr/blog/2026-02-25-open-world-browser-series-guide