Skip to content

Tarayıcıda açık dünya inşa etmek, bölüm 24: Bir dünyayı kaydetmek ve görünür rüzgâr

Cinevva CTO'su ve Kurucu Ortağı Oleg Sidorkin tarafından

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

Bölüm 23, insanları dünyaya yerleştirdi ve onlara ses verdi. Bu bölüm, dünyanın insanların yaptığı değişiklikleri hatırlamasını ve kimse dokunmadığında bile canlı hissettirmesini ele alıyor. Spike 47 kalıcılıkla ilgili: Bir içerik üreticisi araziyi şekillendirip nesneler yerleştiriyor; bu düzenlemeler sayfa yeniden yüklendiğinde korunuyor, diğer tüm eşlerle eşitleniyor ve iki kişi aynı anda düzenleme yaptığında çakışmalar düzgün biçimde çözümleniyor. Spike 49 ise rüzgârla ilgili: Stilize bir doğa paketindeki bitki örtüsü gölgelendiricilerini, ağaçların, çalıların ve çimenlerin sanatçının amaçladığı şekilde hareket etmesi için taşımak. Bu süreç, matematikten çok gölgelendirici derleyicisiyle mücadeleye dönüştü.

Hatırlayan bir dünya

Spike 47'yi yeni sekmede açın ↗ · Kaynak kodunu görüntüleyin

Kurulum, paylaşılan bir içerik üretme oturumundan oluşuyor. Oyuncular dünyada dolaşıyor, tıklayarak nesneler yerleştiriyor, sağ tıklayarak bunları kaldırıyor ve bir fırçayı sürükleyerek zemini şekillendiriyor. Bunu yaparken yükseklik haritasını yükseltip alçaltabiliyor veya bir parçayı marching cubes yöntemine geçiren SDF arazisinde hacimsel mağaralar oyabiliyorlar. Yaptıkları her şey, kendi SQLite depolamasıyla desteklenen, sunucu otoriteli bir Durable Object olan Cloudflare WorldChunkDO içinde kalıcılaştırılıyor. İstemciler durumu doğrudan yazmıyor. Niyetlerini gönderiyorlar; DO kararı verip sonucu yayınlıyor. DO, tek doğruluk kaynağı olduğundan yeni katılan biri bir anlık görüntü alıyor ve diğer herkesin gördüğüyle birebir aynı dünyaya geliyor.

İki kalıcılık ayrıntısı özellikle değerini kanıtladı. Arazi, katılım sırasında yeniden oynatılacak bir olay günlüğü olarak saklanmıyor; doğruluk kaynağı olan parça başına ikili blob'lar hâlinde tutuluyor ve fırça darbesi sona erdiğinde yükleniyor. Böylece yeni katılan biri binlerce fırça örneğini yeniden çalıştırmak yerine doğrudan kaydedilmiş baytları yüklüyor. Ayrıca bu blob'lar tek bir büyük satırda değil, parça başına bir depolama anahtarında tutuluyor; çünkü tek bir SDF parçası 168 KB ve DO'nun satır başına 2 MB sınırı var. Hangi anahtarların mevcut olduğunu izleyen bir parça dizini, DO'nun uyandığında tüm haritayı yeniden yüklemesini sağlıyor. Kimlik, istemcide iki depolama alanıyla yönetiliyor: Oyuncu kimliği sessionStorage içinde tutuluyor; böylece iki sekme, DO'nun oyuncu haritasında kendisinin üzerine yazan tek bir eş yerine iki ayrı eş oluyor. Görünen ad ise localStorage içinde tutuluyor; böylece bir sekmede yapılan ad değişikliği diğer tüm sekmelere aktarılıyor.

Eşzamanlı düzenlemelerin aynı sonuca ulaşmasını sağlamak

Çok oyunculu içerik üretiminin gerçekten zor kısmı, iki kişinin aynı anda örtüşen zemin bölgelerini düzenlediğinde ne olduğudur. Spike, düzenlemeleri cebirsel özelliklerine göre ayırıyor. Toplamsal işlemler — yükseklik haritasında yükseltme ve alçaltma ile SDF üzerinde ekleme ve çıkarma — değişmelidir: Hangi sırayla uygulanırlarsa uygulansınlar aynı sonucu verirler. Bu nedenle, her istemcinin işlemi yerel olarak uygulayıp damgayı gönderdiği iyimser damga yolunu kullanırlar; DO da hiçbir koordinasyon gerektirmeden bunu herkese yayınlar. Sıralama gerçekten önemsiz olduğundan koordine edilecek bir şey de yoktur.

Sıraya bağlı işlemler olan yumuşatma ve düzleştirme ise ilginç vakaydı. İlk tasarım bunlara bölge kilidi veriyordu: İstemci, işaretçiye basıldığında kilit istiyor; DO isteği kabul veya reddediyor; istemci basılı tutma süresince örnekleri tamponluyor ve işaretçi bırakıldığında DO tüm fırça darbesini atomik olarak uyguluyordu. Bu yöntem çalışıyor, ancak kendi kilit TTL'si ve kabul/ret gidiş dönüşü olan ayrı bir protokol gerektiriyordu. Bunun yerini alan daha temiz çözüm, önceden hesaplanmış delta oldu: İşlemi başlatan istemci yumuşatma veya düzleştirme fırçasını yerel olarak çalıştırıyor, ardından hücre başına elde edilen delta listesini gönderiyor. Diğer tüm eşler de hiçbir şeyi yeniden türetmeden bu deltaları kendi hücrelerine ekliyor. Böylece sıraya bağlı bir işlem, sonucu kaynağında sabitlenerek değişmeli hâle geliyor; tüm düzenleme sistemi aynı yakınsamayı sağlayan tek ve tutarlı bir değişmeli protokol üzerinde, hiçbir kilit olmadan çalışıyor. Nesne kilitleri farklı bir nedenle varlığını sürdürüyor: Kayıt başına kilitleme, yalnızca sahibin silebildiği modelin yerini aldı. Böylece kilitli olmadığı sürece herhangi bir eş herhangi bir nesneyi silebiliyor ve kilidi yalnızca kilitleyen kişi kaldırabiliyor. Geri alma ve yineleme, istemcinin bir nesneyi yerleştirmeden önce kimliğini belirleyebilmesiyle çalışıyor. Böylece istemci, sunucunun yanıtı gelmeden kimliği biliyor ve kendi eylemlerini deterministik biçimde geri alabiliyor. Hazırda bekleyen WebSocket'ler, boşta duran bir odayı tüm süre boyunca ücretsiz tutuyor; önceki bölümde avatar aktarımını ucuz kılan da aynı özellikti.

Rüzgârı aslına sadık biçimde taşıdık, ardından onunla mücadele ettik

Spike 49'u yeni sekmede açın ↗ · Kaynak kodunu görüntüleyin

Spike 49, Quaternius'un Stylized Nature MegaKit paketini alıp rüzgâr sistemini kullandığımız teknoloji yığınına taşıyor. Paket, dört özgün Godot gölgelendiricisiyle birlikte geliyor ve doğru yaklaşım bunları yeniden icat etmek yerine aslına sadık biçimde çevirmekti. Kabuk gölgelendiricisinin verteks işlevi boş; dolayısıyla gövdeler sabit kalıyor. Daha önce denenmiş prosedürel maske, gövdelerin de sallanmasına neden oluyordu ve çözüm, rüzgârı kabuğa hiç uygulamamaktı. Yapraklar, üçgensel darbe hash'inden verteks başına kaotik bir sallanma alıyor. Bu hareket yüksekliğe göre maskelendiğinden ağaç tepeleri hareket ederken dip kısım yerinde kalıyor. Temel bitki örtüsü, dünya uzayında gürültüyle modüle edilen sin/cos sallanması alıyor. Çimen, temel bitki örtüsü hareketine ek olarak rüzgâr çizgisi salınımına sahip. Kayan bir gürültü dokusu kuvvet eğrisi üzerinden örnekleniyor; böylece yalnızca dokunun parlak bantları katkıda bulunuyor ve çayır boyunca ilerleyen görünür dalgalar oluşuyor. Yönlendirme, paketin kendi malzeme adlandırma kuralını izliyor. Bu nedenle Leaves_Birch adlı bir malzeme yaprak yoluna, Grass_Common ise çimen yoluna yönlendiriliyor; tahmin yürütmek gerekmiyor.

Taşıma sırasında karşılaştığımız sürprizlerden biri, yaprak renginin dokuda bulunmamasıydı. Quaternius, yaprak görünümünü tamamen dikey bir gradyan ve Fresnel kenar ışığıyla oluşturuyor: Albedo, yüksekliğe bağlı olarak ağaç tepesinin altındaki ek renkten üstteki yaprak rengine doğru karıştırılıyor. Buna, (1NV)3 Fresnel terimiyle ölçeklenen emisyon biçiminde bir yüzey altı saçılım tonu ekleniyor. Bu nedenle yalnızca doku kullanan sade bir geçiş düz görünüyordu. Renkler boyanmış haritadan değil, gradyandan ve kenar ışığından geliyor. Taşıma, ham yerel Y yerine önceden hesaplanmış bir heightFactor verteks özniteliğini okuyor. Bu öznitelik yükleme sırasında her yaprak grubu için dünya uzayındaki Y değerinden normalize ediliyor; böylece FBX içe aktarma işlemi her ağın yerel eksenlerini nasıl döndürmüş olursa olsun hem gradyan hem de rüzgâr maskesi doğru davranıyor. Godot renk sabitleri sRGB olarak etiketleniyor ve gölgelendiriciye ulaşmadan önce doğrusal uzaya dönüştürülüyor. Dolayısıyla taşıma da parlak sRGB sayılarını doğrusal değerler olarak verip bitki örtüsünü soluklaştırmak yerine aynı dönüşümü uyguluyor.

Kare hızını yiyip bitiren yeniden derleme

Bunun önce bir FBX temel sürümü olarak yayımlanmasının ve eksiksiz rüzgâr malzemesi setinin bir .bak dosyasında kenara alınmasının nedeni, sahneyi yaklaşık 1 fps'ye düşüren kare başına yeniden derleme hatasıydı. FBX ile yüklenmiş malzemelerin üzerine özel TSL eklemek, needsUpdate fiilen takılı kalmış gibi Three.js'in gölgelendirici programlarını her karede yeniden oluşturmasını tetikliyordu. Tanılama yaklaşımı, her bitki örtüsü malzemesini özel düğümler içermeyen sade, dokulu bir geçişe indirgemek ve yeniden derleme döngüsünün sürüp sürmediğini gözlemlemekti. Döngü durursa sorun özel grafikteydi; devam ederse neden FBX malzeme kurulumunun üst katmanlarında veya Three.js'in kendisindeydi. Gerçek gölgelendiricilerin geri gelmesini sağlayan çözüm, malzeme başına değişen her değeri — yaprak rengi, SSS rengi, kuvvet ve karışım — uniform olarak bağlamaktı. Böylece derleyicinin her benzersiz renk kombinasyonu için yeni bir gölgelendirici üretip derleme kuyruğunu altüst etmesi yerine tüm yaprak varlıkları tek bir derlenmiş programı paylaşıyor.

İki ayrıntı daha korunmaya değer. Çimen, kaynak dosya başına bir InstancedMesh olarak işleniyor ve sinüs fazları dünya konumuna bağlı olduğundan rüzgârı dünya uzayında hesaplanıyor. Ancak örnek matrisi uygulanmadan önce yer değiştirme yerel uzayda uygulanmalı ve WGSL'de çağrılabilecek bir inverse() bulunmuyor. Öteleme, Y dönüşü ve tekdüze ölçekten oluşan bir örnek matrisi için üst 3×3 bölümün tersi, basitçe transpozunun ölçeğin karesine bölünmesidir. Bu nedenle gölgelendirici, dünya uzayındaki yer değiştirmeyi transpoze model matrisiyle çarpıyor ve matrisin ilk sütununun karesi alınmış uzunluğuna bölüyor; böylece karekök kullanmadan ölçeği elde ediyor. Verteks dönüşümü matrisi yeniden uyguladıktan sonra hareket, her çimen öbeğinin dönüşünden veya ölçeğinden bağımsız olarak tam tasarlandığı biçimde dünya uzayına yerleşiyor. Ayrıca her öbek, verteks başına GPU görüş hacmi elemesi çalıştırıyor: Örnek merkezi klip uzayına yansıtılıyor ve pay bırakılmış görüş hacminin dışına düşerse her verteks yerel orijine daraltılıyor. Böylece her üçgenin üç verteksi de çakışıyor, rasterleştirici dejenere üçgeni atıyor ve ekran dışındaki çimenler için parça, alfa testi veya gölge işlemi yapılmıyor. Bu işlem, Three.js'in zaten yaptığı parça başına kaba sınırlayıcı küre elemesinin üzerine ekleniyor ve konum karışımıyla doğrudan çarpılabilmesi için boolean değerler yerine kayan noktalı step kullanılarak oluşturuluyor.

Bu bölümde değinilen teknolojiler

Sunucu otoriteli dünya kalıcılığı. Bir WorldChunkDO Durable Object, nesne yerleştirme ve arazi düzenlemeleri hakkında karar veriyor; parça başına ikili blob'ları doğruluk kaynağı olarak kalıcılaştırıyor. Böylece katılanlar olay günlüğünü yeniden oynatmak yerine kaydedilmiş baytları yüklüyor. Ayrıca DO'nun satır başına 2 MB sınırının altında kalmak için her parça ayrı bir depolama anahtarında saklanıyor. Oyuncu kimliği sessionStorage içinde tutulduğundan sekmeler ayrı eşler oluyor; görünen ad ise localStorage içinde tutulduğundan ad değişiklikleri sekmeler arasında aktarılıyor.

Değişmeli düzenlemelerin yakınsaması. Toplamsal arazi işlemleri (yükseltme/alçaltma, SDF ekleme/çıkarma) değişmeli olduğundan koordinasyon gerektirmeyen iyimser damga yolunu kullanıyor. Sıraya bağlı işlemler (yumuşatma, düzleştirme), bölge kilidi almak yerine önceden hesaplanmış hücre başına deltalar gönderilerek değişmeli hâle getiriliyor. Böylece tüm sistem, hiçbir kilit olmadan tek ve tutarlı bir protokol altında aynı sonuca ulaşıyor. Kayıt başına nesne kilitleri yalnızca sahibin silebildiği modelin yerini alıyor; istemci tarafından adlandırılan nesne kimlikleri ise sunucunun yanıtı gelmeden deterministik geri alma/yineleme sağlıyor. Bkz. GPU güdümlü LOD.

Godot'tan TSL'ye aslına sadık gölgelendirici taşıma. Quaternius'un dört özgün rüzgâr gölgelendiricisi satır satır çevriliyor: sabit kabuk, yüksekliğe göre maskelenmiş yaprak sallanması, dünya uzayında bitki örtüsü sallanması ve kayan rüzgâr çizgisi salınımına sahip çimen. Yollar, paketin malzeme adlandırma kuralına göre seçiliyor. Yaprak rengi dokudan değil, yükseklik gradyanı ile Fresnel güdümlü SSS kenar ışığından geliyor. Görünümün referans render'larla eşleşmesi için sRGB üretim sabitleri doğrusal uzaya dönüştürülüyor.

Kare başına gölgelendirici yeniden derlemelerini önleme. FBX malzemelerinin üzerine özel TSL eklemek, needsUpdate değerini takılı bırakıp programların her karede yeniden oluşturulmasına yol açarak performansı yaklaşık 1 fps'ye düşürebilir. Malzeme başına değişen her değeri uniform olarak bağlamak, her benzersiz parametre kümesi için yeni gölgelendirici üretmek yerine tüm varyantların tek bir derlenmiş programı paylaşmasını sağlar. Sade, dokulu bir tanılama geçişi de sorunun özel grafikten mi yoksa üst katmandaki kurulumdan mı kaynaklandığını ayırır.

Örneklenmiş bitki örtüsünde dünya uzayı rüzgârı. Çimen rüzgârı dünya uzayında hesaplanıyor ve WGSL'de inverse() bulunmadığı için elle türetilmiş bir ters matrisle (transpoz bölü ölçeğin karesi) yerel uzaya geri dönüştürülüyor. Verteks başına GPU görüş hacmi elemesi, ekran dışındaki öbekleri dejenere üçgenlere daraltıyor; böylece parça veya gölge işlemi yapılmıyor. Bu işlem, Three.js'in parça başına sınırlayıcı küre elemesinin üzerine ekleniyor.


29 bölümün 24. bölümü. Önceki: Bölüm 23 - Elli avatar ve odada bir ses Sonraki: Bölüm 25 - Tek iskelet, her kıyafet Seri rehberi: /blog/2026-02-25-open-world-browser-series-guide