Skip to content

Tarayıcıda açık dünya inşa etmek, bölüm 20: Düz bir yüzeyde sahte derinlik oluşturmak

Cinevva Teknoloji Direktörü ve Kurucu Ortağı Oleg Sidorkin tarafından

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.

19. Bölüm, uzaktaki bütün bir ağacı taklit etmek için düz bir dörtgen kullandı. Bu bölüm ise yakından derinlik yanılsaması yaratmak için düz bir dörtgen kullanıyor: tek bir ek tepe noktası bile harcamadan, parke taşlı bir yolu 5 cm derinliğinde derzlere sahipmiş gibi gösteren paralaks örtüşme eşleme tekniği. Amaç, bu tekniği üretim teknoloji yığınına (Three.js r184, WebGPU, TSL) taşımaktı; böylece arazi ayrıntı malzemeleri, önemli olduğu yerlerde bu derinlik yanılsamasını taşıyabilecek, diğer her yerdeyse düz doku maliyeti ödeyecekti.

Derinliği taklit etmenin üç yolu, yan yana

Spike 39'u yeni sekmede aç ↗ · Kaynak kodunu görüntüle

Spike, yan yana üç düz 5×5 m düzlem yerleştiriyor. Hepsi aynı malzeme iskeletini kullanıyor ve yalnızca örnekleyicileri besleyen UV bakımından farklılaşıyor. Düz yöntem, referans taban çizgisi olarak dokuyu doğrudan örnekliyor. Tek örnekli paralaks, UV'yi o noktadaki yüksekliğe göre görüş doğrultusunda bir kez kaydırıyor; bu yöntem ucuzdur ve düşük genlikte yeterince iyidir, ancak yatık açılarda kayar. POM ise teğet uzayında ışın yürütür: görüş ışını boyunca ilerler, ışının yükseklik alanının altına geçtiği ilk katmanı bulur ve kesişimi hassaslaştırır. Her test düzlemi eksenlerle hizalı olduğundan teğet uzayı basit kalır; böylece görüş yönü, tepe noktası başına tam bir TBN matrisi yerine birkaç işaret çevirmesiyle teğet uzayına aktarılır. Doku seti, 17. Bölüm'deki model aramasının kullandığı yolla aynı olan Polyhaven dosya API'sinden canlı olarak alınır.

WebGPU'da iki engel ve dallanmasız ışın yürütme

Klasik POM döngüsü, ilk kesişmede aramadan çıkar. r184'te bu, birbirinden ayrı iki nedenle çalışmıyor. If(...).and(...) hatasız derlendi ancak döngü gövdesinin hiç çalışmadığı bir WGSL üretti; dolayısıyla döngü sonrası hassaslaştırma çöp veri üzerinde çalıştı ve düzlem neredeyse beyaz görüntülendi. Ayrıca bağımsız bir düğüm olarak Break() henüz r184 derlemesine eklenmemişti. Bu yüzden çalışan bir If olsa bile "ilk kesişmede dur" ifadesini oluşturmanın bir yolu yoktu. Her iki sorun da three.js'in bu sürüm aralığında If ve Loop sınırları boyunca TSL kontrol akışını aşırı optimize etmesiyle ilgili bilinen sorunlara dayanıyor.

Yeniden yazılan sürüm dallanmasız. Her yineleme dokuyu koşulsuz olarak örnekliyor; böylece doku erişimi, WGSL belirtiminin istediği gibi tekdüze kontrol akışında kalıyor. Ardından yeni durum, kayan noktalı sayı olarak tutulan bir done bayrağı aracılığıyla birleştiriliyor. done değeri 1'e geçtiğinde, yineleme başına yapılan mix çağrıları "durumu değiştirmeden koru" işlemine dönüşüyor; bu da break işleminin dallanmasız karşılığı. done bayrağı, 0.5 + 0.5 × sign(x + ε) biçiminde uygulanan bir step yardımcısıyla oluşturuluyor; çünkü boolean değerlerden kayan noktalı sayılara dönüştürme r18x serisinde zaman zaman sorunlu oldu ve sign() her yerde güvenli. Bunun bedeli, gerçekte nerede kesiştiğine bakılmaksızın her parçanın 64 yinelemenin tamamını çalıştırması. Ancak parça ölçeğinde doğru tercih bu: çalışma zamanı zaten azami adım sayısını sınırlıyor ve gerçek bir GPU da "gerçek" bir break sonrasındaki işlemleri spekülatif olarak yürütebilir. Burada temiz bir geri dönüş önemli: son bir mix(baseUV, refined, done). Böylece sıfır genlikte, yani mesafe azalmasının uzak ucunda, hiçbir parça kesişmez, done 0 olarak kalır ve POM malzemesi bit düzeyinde düz yöntemle aynı olur. Mesafeye dayalı LOD numarasının amacı da tam olarak bu: etki zaten piksel altı düzeydeyken maliyeti düz doku maliyetine indirmek.

Hata matematikte değil, disiplin eksikliğindeydi

Dallanmasız sürüm çalıştı ancak bozuk görünüyordu: orta düzey genlikte çizgili yatay bozulmalar, düşük genlikteyse belli belirsiz yanlış ama net olmayan bir sonuç vardı. Düzeltme, tek satırlık bir istemden geldi: git ve standart referansı oku. Buna ilham veren LlamAcademy öğreticisi yalnızca bir Unity ShaderGraph düğümüydü; dolayısıyla gerçek uygulama Unity'nin PerPixelDisplacement.hlsl dosyasında bulunuyordu. Dosyayı satır satır okumak, farkında olmadan ortaya çıkardığım üç anlamsal farkı açığa çıkardı: ışın yüksekliği taban çizgisinde bir birimlik kayma (Unity döngüden önce ilk ilerlemeyi yapar; dolayısıyla benim referans çerçevem tam bir adım faz dışındaydı ve kesişimleri yaklaşık yarı yarıya yanlış katmana yerleştiriyordu), hassaslaştırma adımının dayandığı azami ofset işareti kuralı ve hassaslaştırma matematiğimi gereksiz yere zorlaştırıp işareti karmaşıklaştıran kümülatif ofset ile kümülatif UV kayıt yöntemi farkı.

Temel neden tek bir hata değil, iki referansı birbirine karıştırmaktı. Kılavuz olarak benzer ama farklı işaret kuralları ve farklı bir hassaslaştırma formülü kullanan LearnOpenGL POM öğreticisini almıştım. Sonuçta matematiğin üçte ikisinin bir kaynakla, üçte birinin diğer kaynakla eşleştiği melez bir durum ortaya çıktı. Yeniden yazılan sürüm, Unity'nin HLSL kodunun TSL'ye neredeyse birebir aktarımı: aynı değişken adları, aynı ilk ilerleme, aynı hassaslaştırma ve bunların üzerine eklenmiş dallanmasız done bayrağı. Çıkarılacak ders önemli: başka bir teknoloji yığınındaki çalıştığı bilinen bir gölgelendiriciyi aktarırken önce aynı adları kullanarak satır satır aktarın, ardından yerel stile göre yeniden düzenleyin. Aktarımın ortasında ikinci bir referansa bakarak formülü yeniden türetmeyin.

Yalan söyleyemeyen bir referans düzlemi

Yan yana karşılaştırmada bariz bir şey eksikti: gerçek geometrili bir düzlem. O olmadan "POM oldukça iyi görünüyor" iddiası yanlışlanamaz. Neye kıyasla oldukça iyi? Bu nedenle spike'a dördüncü bir düzlem eklendi: aynı yükseklik haritası, gerçek tepe noktası konumlarına uygulandı. WebGPU'da donanımsal mozaikleme yoktur (Metal uyumluluğu için çıkarıldığından belirtimde hiç yer almaz). Bu yüzden yerine, tepe noktası aşamasında yer değiştirme uygulanan yoğun biçimde alt bölümlere ayrılmış bir düzlem (256×256 segment, 131.072 üçgen) kullanıldı. Aynı genlik uniform'u hem POM'u hem de geometri düzlemini yönetiyor; böylece ikisi birlikte azalıyor ve her mesafede adil bir karşılaştırma yapılıyor.

Gerçek referans ekranda gösterildiğinde, nitel iddialar ölçülebilir hâle geldi. 16°'lik yukarıdan bakan bir yörünge açısında POM ile mozaiklenmiş düzlem, iç gölgelendirme konusunda uyuşuyor. Yatık açılardaysa ayrılmaları gereken yerde ayrılıyorlar: POM, geometrinin kusursuz düz dikdörtgen kenarına sıkışırken gerçek ağ, ışığı yakalayan gerçek tepe ve vadilerin oluşturduğu engebeli bir ufuk profili gösteriyor. Böylece POM'un kenar "kaymasının" doku veya aydınlatma kaynaklı bir artefakt değil, algoritmanın özünde bulunan bir özellik olduğu kanıtlanabiliyor. İki maliyet profili de açıkça görülüyor: POM parça sınırlıdır (maliyet, kaplanan piksel sayısıyla ölçeklenir), mozaiklenmiş düzlem ise tepe noktası sınırlıdır (maliyet, kapsama alanından bağımsız olarak ağ yoğunluğuyla ölçeklenir). Zaten yükseklik haritasıyla yönetilen bir düzlemin tepe noktası maliyetini ödeyen arazi parçasında, ağ altı ayrıntılar için doğru çözüm POM'dur.

Referans düzlemi ayrıca ince bir kullanıcı deneyimi hatasını da yakaladı. Kullanıcı, genlik arttıkça yüzeyin aşağı çöktüğünü fark etti. Bunun nedeni, Unity'nin geometrik düzlemi yükseklik alanının üstü olarak kabul eden kuralıydı; böylece tepeler düzlemle aynı hizada sabit kalırken diğer her şey paralaksla aşağı kayıyor ve ortalama yüzeyi (1 − mean_h) × amplitude kadar düz taban çizgisinin altına çekiyordu. Düzeltme, kuralı h = 0.5 düzleme karşılık gelecek şekilde yeniden merkezliyor; tepeler kameraya doğru yükselirken vadiler içeri çekiliyor. Algoritma tam olarak Unity'nin öngördüğü şekilde çalışıyor; spike yalnızca çıktıyı yarım ofset kadar sonradan işleyerek "genlik" kavramını kaydırıcıyı sürükleyen bir insanın bekleyeceği anlama getiriyor.

Referans düzleminin açıklığa kavuşturduğu bir konu daha vardı. "Adımlar" kaydırıcısı hiçbir şey yapmıyormuş gibi görünüyordu ve bu bir bağlantı hatası izlenimi veriyordu, ancak öyle değildi. Doğrusal aramadan sonraki üç yinelemeli sekant hassaslaştırması o kadar başarılıdır ki (Tatarchuk'un 2006 tarihli POM makalesi, 4 adımlı arama artı 3 adımlı sekant yönteminin görsel olarak 64 adımlı aramadan ayırt edilemediğini belirtir), pürüzsüz bir yükseklik haritasında 4 ile 64 arasındaki her adım sayısı aynı piksel altı UV'ye yakınsar. Çözüm, tesisatı yeniden kurmak değil bir açma-kapama düğmesi eklemekti: sekantı kapattığınızda adım kaydırıcısı kesişim hassasiyeti üzerindeki tek denetim hâline gelir; böylece 4'e düşürmek parke taşlarında gözle görülür basamaklanma yaratırken 64'e çıkarmak yüzeyi yeniden pürüzsüzleştirir. Düğme, kapalıyken her sekant durum güncellemesini mix ile işlemsiz hâle getiren bir 0/1 uniform'dur. Böylece geçiş yapmak malzemeyi hiçbir zaman yeniden oluşturmaz ve takılmaya yol açmaz.

Bu bölümde değinilen teknolojiler

TSL'de paralaks örtüşme eşleme. POM, teğet uzayında görüş yönünü bir yükseklik alanı boyunca ışın yürüterek tarar, ışının yüzeyin altına düştüğü ilk katmanı bulur ve kesişimi hassaslaştırır. Böylece ek geometri kullanmadan düz bir dörtgen üzerinde içeri gömülü harç derinliği oluşturur. Son bir mix(baseUV, refined, done), hiçbir parça kesişmediğinde malzemeyi bit düzeyinde düz yöntemle aynı hâle getirir. Mesafeye dayalı LOD genlik azaltımının uzakta maliyeti düz doku maliyetine indirebilmesini sağlayan da budur. Bkz. arazi malzemeleri.

WebGPU kontrol akışı için dallanmasız döngüler. Three.js r184'te TSL If(...).and(...), döngü gövdesi hiç çalışmayan bir WGSL çıktısına derlenebilir ve bağımsız Break() kullanılamaz. Taşınabilir kalıp, her yinelemede koşulsuz bir doku örneklemesi (WGSL belirtimine uygun olarak doku erişimini tekdüze kontrol akışında tutar) ve ayarlandıktan sonra her durum güncellemesini mix aracılığıyla işlemsiz hâle getiren, kayan noktalı sayı olarak tutulan bir done bayrağıdır. sign(x + ε) ile oluşturulan bir step yardımcısı, güvenilmez boolean-kayan noktalı sayı dönüşümünü önler. Maliyet, erken çıkış noktasından bağımsız olarak sabit azami yineleme sayısıdır; parça ölçeğinde doğru tercih budur.

Gölgelendiricileri birebir aktarma. Başka bir motordaki çalıştığı bilinen bir gölgelendirici önce özgün değişken adlarıyla satır satır aktarılmalı, yerel stile göre yeniden düzenleme daha sonra yapılmalıdır. İki referansı (Unity'nin PerPixelDisplacement.hlsl dosyası ile LearnOpenGL öğreticisini) karıştırmak; bir birim kaymış ışın taban çizgisine, ters çevrilmiş ofset işaretine ve sınırlandırması aralık dışı ağırlıkları uzamsal süreksizlikler olarak gizleyen bir hassaslaştırma formülüne sahip melez bir sonuç üretti. Yeniden türetme değil, tek bir standart gerçeklik kaynağı kullanılmalı.

Tepe noktası yer değiştirmeli gerçek referans. WebGPU'da donanımsal mozaikleme olmadığından, tepe noktası aşamasında yer değiştirme uygulanan yoğun biçimde alt bölümlere ayrılmış bir düzlem (256² segment), parça aşamasındaki taklidi doğrulamak için gerçek geometri yerine geçer. Her ikisini de aynı genlik uniform'uyla yönetmek, mesafe boyunca karşılaştırmanın adil kalmasını sağlar. POM parça sınırlıdır (kaplanan piksel sayısıyla ölçeklenir), geometri düzlemi ise tepe noktası sınırlıdır (ağ yoğunluğuyla ölçeklenir). Bu nedenle tam olarak silüet kenarlarında ayrışırlar ve POM'un kenar kaymasının bir artefakt değil, tekniğin özünde bulunan bir özellik olduğunu kanıtlarlar.


29 bölümün 20.'si. Önceki: 19. Bölüm - Bir ormanda hayatta kalması gereken imposter Sonraki: 21. Bölüm - Daha hızlı olmayan daha hızlı bir işleyici Seri rehberi: /tr/blog/2026-02-25-open-world-browser-series-guide