Skip to content

Tepenin arkasındaki çimleri render etme: araziye duyarlı örtülme ayıklaması

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

28. Bölüm, Spike 57'yi tanıttı ve dört ayıklama yolunun kıyaslamasını birkaç paragrafta sundu. Bu, uzun versiyonu: neden bu dört yöntemi seçtiğimiz, diğer dört yöntemin ne olduğu ve yayınlayacağımız yöntemin arkasındaki gerekçeler.

Kamera ile bir çayır arasında bir tepe var. Çayırı render etmeniz gerekmez. Her modern AAA motoru bunu bilir. Geçen haftaya kadar bizimki de dahil olmak üzere çoğu tarayıcı tabanlı 3D motor bunu bilmez.

Bu yazı; araştırmayı gerektiği gibi yapmanın, onu gerçek kod tabanımıza dayandırmanın, çalışan bir spike geliştirmenin ve teknikleri bugün WebGL, yarın WebGPU ile yayın yaptığımızı göz önünde bulundurarak özellikle bize ne kadar yardımcı olduklarına göre değerlendirmenin sonucu.

Okumaya devam etmeden önce aşağıdaki canlı spike'ı kurcalayabilirsiniz. T/Y/U/I ayıklama yolları arasında geçiş yapar, C kamera ön ayarları arasında dolaşır, B ise testin neleri çıkardığını görebilmeniz için gizlenen chunk'ları kırmızı tel kafes olarak boyar. HUD, her aşamanın kaç instance'ı elediğini gösterir.

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

Motorumuz bugün ne yapıyor (ve ne yapmıyor)

Akış tabanlı chunk yöneticimiz, oyuncunun çevresinde üç LOD kademesinde 64 metrelik chunk'lardan oluşan bir halka yükler. Her chunk, yükseklik haritasına dağıtılmış instanced ağaçlara sahiptir. Chunk.updateObjectVisibility içinde bulunan ayıklama mantığı basit bir mesafe kontrolüdür: 60 metre içindeki ağaçlar tam mesh olarak, 60 ile 140 metre arasındakiler billboard olarak render edilir; daha uzaktakiler ise hiç render edilmez.

Bu ayıklama, iki büyük israf kategorisini gözden kaçırıyor:

  1. 60 metrelik diskin içinde ama kameranın arkasında olan her şey. Oyuncu 4 metreden fazla hareket ettiğinde instance buffer'ını yeniden sıralıyoruz, ancak görüş frustum'unu test etmiyoruz. Bu nedenle diskin ortalama yarısı, vertex dönüşümünden sonra kırpılmak üzere GPU'ya gönderiliyor.
  2. 60 metrelik diskin içinde ama bir tepenin arkasında olan her şey. Arazimizin dikey aralığı 80 metre ve çok sayıda vadisi var. Kamera bir vadideyken, ayıklama yarıçapındaki bitki örtüsünün çoğu en yakın sırt tarafından geometrik olarak gizleniyor. Yine de hepsini çiziyoruz.

Bu yazı ikinci kategoriye odaklanıyor. Bu aynı zamanda AAA motorlarının, ilk bakışta tarayıcıya kolayca uyarlanamayan bir dizi akıllı yöntemle çözdüğü kategori.

Guerrilla'dan Gilbert Sanders, Horizon Zero Dawn'ın açık dünya bitki örtüsünü nasıl render ettiğini anlatıyor. Son 20 dakika ayıklama yığınına ayrılmış ve "daha hızlı çizmek" yerine neden "daha az çizmenin" üstün olduğunu gösteren usta işi bir ders niteliğinde.

Bizim kullanım senaryomuz için değerlendirilen sekiz teknik

Modern örtülme ayıklama yığınını inceledim ve her parçayı prosedürel, yükseklik haritası tabanlı, tarayıcı içinde çalışan bir açık dünyaya ne kadar yardımcı olduğuna göre değerlendirdim. İki ölçüt kullandım: değer (sahnemiz düşünüldüğünde ne kadar israfı ortadan kaldırdığı) ve maliyet (mühendislik emeği ile yığınımızın ne kadarını değiştirmeye zorladığı). Tüm gerekçeler bu yazının arkasındaki araştırma dosyasında bulunuyor; kısa versiyonu şöyle.

1. Instance başına frustum ayıklaması. Yüksek değer, düşük maliyet. Şu anda instanced bitki örtümüz için frustum'u hiç test etmiyoruz. Bunu eklemek herhangi bir görünümde işi yaklaşık yarıya indiriyor, yaklaşık otuz satır kod gerektiriyor ve bugün WebGL ile yayınlanabiliyor. Bu, en büyük ve en ucuz kazanım; aylar önce yapmış olmalıydık.

2. Yükseklik haritasında ufuk ışını taraması. Yüksek değer, düşük–orta maliyet. Kameradan her aday instance'a doğru bir ışın ilerletin ve yol boyunca arazi yüksekliğini örnekleyin. Arazi herhangi bir noktada ışının üzerine çıkarsa instance örtülmüştür. Bunun düzgün çalışmasının nedeni, dünyamızın bir yükseklik haritası olması ve görünürlük problemini yatay yön boyunca 1B bir probleme indirgemesidir. Yoğun sürüm, her karede instance başına O(adım) maliyetlidir. Hızlandırılmış sürüm (bir sonraki madde) bunu O(log adım) seviyesine düşürür.

3. Maksimum yükseklik piramidiyle hızlandırma. Yüksek değer, orta maliyet. Her texel'in kendi kapsama alanındaki maksimum arazi yüksekliğini sakladığı, yükseklik haritasının 2B bir mipmap'i. Ufuk ışını taramasının çevre düzken kaba adımlarla ilerlemesini ve yalnızca tepelerin üzerinde hassaslaşmasını sağlar. 2 numaralı yöntemi üretimde ucuz hâle getiren yapı budur.

4. Hiyerarşik Z örtülmesi (Hi-Z / HZB). Yüksek değer, yüksek maliyet. Bir depth buffer mipmap'i oluşturun, her instance'ın sınırlarını ekran uzayına yansıtın ve doğru mip seviyesine karşı test edin. Bu, Unreal'ın Nanite'ı, Bevy'nin sanal geometrisi ve VTK'nın WebGPU portu tarafından kullanılan modern GPU tarafı standardıdır. Yalnızca arazi için değil, her şey için çalışır; ancak WebGPU, indirect draw'lar ve bir compute pass gerektirir. Binlerce ağaç değil, milyonlarca çim yaprağı render ettiğimizde karşılığını verir.

5. İki geçişli Hi-Z (Nanite tarzı). 4 numaraya kıyasla marjinal kazanç, yüksek maliyet. Bir karelik yeniden görünür olma artefaktını önlemek için 1. geçişten sonra bu karenin derinliğini yeniden render eder. Yalnızca GPU güdümlü yolda yeterince ilerlediğimizde ve maliyet artımlı hâle geldiğinde buna değer.

6. Yazılımsal örtülme rasterizer'ı (Frostbite/Intel MOC). Orta değer, yüksek maliyet. Büyük örtücülerin düşük çözünürlüklü depth buffer'ını CPU'da rasterize edin. Readback gecikmesi sıfırdır. Referans uygulamalar AVX/SSE C++ ile yazılmıştır; bunları WASM'a port etmek başlı başına gerçek bir projedir ve yükseklik haritası ışını taramamız, işin çok küçük bir bölümüyle aynı kazanımların çoğunu sağlar.

7. Önceden hesaplanmış PVS. Düşük değer, yüksek maliyet. Quake döneminin statik haritaları için harikadır. Arazimiz prosedürel ve sonsuz olduğundan her türlü ön işleme chunk akışı sırasında yapılmak zorunda; bunun maliyeti de çalışma zamanında görünürlük hesaplamak kadar yüksek. Atla.

8. Cesium tarzı ufuk ayıklaması. Bizim için değeri yok, orta maliyet. Gezegen elipsoidleri için tasarlanmıştır. Dünyamız nispeten düz ve sınırlıdır; matematik burada geçerli değildir ve ya hiçbir şey yapmaz ya da yanlış ayıklamaya yol açar. Atla.

Sonuç olarak: 1 ile 2+3'ü şimdi, CPU'da ve WebGL ile yapın. 4'ü WebGPU geçişi için planlayın. Geri kalanları atlayın.

Tarayıcı arazisi için yükseklik haritası ışını taraması neden kazanıyor?

Herhangi bir modern render sunumundaki standart tavsiye "bir Hi-Z buffer oluşturun" şeklindedir. Brian Karis'in Nanite üzerine SIGGRAPH 2021 derinlemesine incelemesi bu konudaki temel kaynaktır ve en az bir kez izlemelisiniz.

Her şeyi zaten GPU güdümlü indirect draw'lardan geçiren bir motor için doğru yanıt budur. Bizimki de dahil olmak üzere çoğu tarayıcı motoru böyle bir motor değildir. CPU tarafında instance buffer'larımız, WebGL draw call'larımız var ve compute aşamamız yok. Bu yığına Hi-Z eklemek; aynı anda WebGPU'ya port etmeyi, bitki örtüsü pipeline'ını indirect draw'larla yeniden yazmayı ve depth pyramid oluşturma pass'i eklemeyi gerektirir. Fikri doğrulayan ilk kare için üç aylık iş demektir.

Yükseklik haritası ışını taraması CPU'da, WebGL ile ve zaten sahip olduğumuz verilerle çalışır. Dünyamızla ilgili AAA motorlarının yararlanamadığı tek bir olguyu kullanır: örtücülerimiz 1B bir yükseklik fonksiyonuyla tanımlanıyor. Bu fonksiyonu bir ışın boyunca örneklemek, iki dizi erişimi ve bir çarpma işleminden ibarettir. Hi-Z buffer'ın aynı olguyu piksel piksel keşfetmesi gerekirdi.

Teknik ilk olarak IEEE Visualization 2002'de "Hiyerarşik Araziler için Ufuk Örtülme Ayıklaması" adıyla yayımlandı (PDF). Yirmi yıldır araç kutusunda kalmasının nedeni doğru yapıya sahip olmasıdır: arazi düzken ucuz, yalnızca gerçekten tepelerin bulunduğu yerlerde pahalı ve kolayca paralelleştirilebilir.

Çizimle maksimum yükseklik piramidi

Naif ışın taraması, her ışın boyunca yaklaşık 24 noktada terrainHeight değerini örnekler ve arazi ışını keserse erkenden sonlandırılır. Bu, binlerce ağaç için yeterlidir. Yüz binlerce çim yaprağında çöker.

Çözüm, her texel'in kendi kapsama alanındaki maksimum yüksekliği sakladığı bir yükseklik haritası mipmap'idir:

seviye 0 (256×256, texel başına 2.25m): 4×4 sapmalı örneğin maks. h değeri
seviye 1 (128×128, texel başına 4.5m):  maks(0,0), maks(1,0), maks(0,1), maks(1,1)
seviye 2 ( 64×64, texel başına 9.0m):  bir üst seviyede aynı indirgeme
...
seviye 8 (   1×1, texel başına 576m):  global maksimum

Işın segmenti uzun ve düz olduğunda kaba bir seviyeyi örnekleyin: tek bir erişim size "bu 9 metrelik karede hiçbir arazi 12 metre rakımı aşmıyor ve ışın burada 30 metrede, dolayısıyla devam et" bilgisini verir. Yalnızca kaba bir texel "arazi ışının üzerinde olabilir" dediğinde bir seviye aşağı iner ve hassaslaştırırsınız. Yapının tamamı birkaç yüz KB boyutundadır ve onlarca milisaniyede oluşturulur.

Şematik olarak:

                                            gözden çıkan ışın
        göz 1.7m                          o─────────────────────►
              o─────────────────────────·─·─·─·─·─·──────────────
              │                          \                       │
              │   seviye 3 (dev adım)     \   seviye 0 (hassas)  │
              │   "8m üstü arazi yok"      \   "burada 9m tepe!" │
              │                              \                   │
        ──────┴────────────/▔▔▔\─────────────/▔▔▔▔▔\─────────────
                                 tepe A (8m)   tepe B (12m)

                                                burada engeller

A tepesinin çevresinden geçen ışın segmentinde, seviye 3 erişimi ("18 metre genişliğindeki bu kutunun maksimum yüksekliği 8 metre") bize 1,7 metre rakımdaki ve birkaç metre yükselen ışının önünün açık olduğunu zaten söyler. Tek bir sorguyla 36 metrelik ilerlemeyi atlarız. B tepesi üzerinde seviye 3 "buradaki maksimum 12 metre" der; aşağı ineriz, seviye 0 "evet, tam olarak bu texel'de 12 metre" der ve instance'ı eleriz.

Piramit oluşturma kodu height-pyramid.mjs, onu kullanan ayıklama yolları ise cull.mjs içindedir. Her ikisini de spike kaynak tarayıcısında okuyabilirsiniz.

Spike gerçekte ne gösteriyor?

Yukarıdaki Spike 57'yi açın ve dört yolu deneyin:

  • T0, üretimde bugün yaptığımız şeydir. Yalnızca mesafe. C1'den (vadi tabanı) bakıldığında HUD, yaklaşık 12.000 çim yaprağının görünür olduğunu bildirir.
  • T1, instance başına frustum ayıklaması ekler. Kameranın arkasında veya yanlarda kalan her şey GPU'ya ulaşmadan elendiği için görünür öğe sayısı yaklaşık yarıya düşer.
  • T2, kaba kuvvet kullanan yükseklik haritası ışını taramasını ekler. C1'de sayı bir %60-80 daha azalır; çünkü alanın çoğu en yakın sırtın diğer tarafındadır. Instance başına yaklaşık 24 kez terrainHeight örneklediğimiz için Cull-ms sütunundaki değer yükselir.
  • T3, kaba kuvvet yönteminin yerine maksimum yükseklik piramidini koyar. Görünürlük kazanımını korurken Cull-ms tekrar T1'e yakın bir seviyeye düşer. Gerçekte yayınlayacağınız yol budur.

Bu örüntü, üretim motorlarında görülenlerle örtüşüyor. Acerola'nın çim render etme üzerine iki bölümlük serisi (Oyunlar Bu Kadar Çok Çimi Nasıl Render Ediyor?, Oyunumdaki Çimleri Optimize Etmek İçin Ne Yaptım?), mühendislik emeğinizi shading aşamasına değil neden ayıklama aşamasına harcamanız gerektiğini anlatan YouTube'daki en anlaşılır içeriktir.

C3 kamerası ve "tepe üstü" başarısızlık senaryosu

Spike'ın üçüncü kamera ön ayarı, kamerayı oyun alanına bakan bir tepenin üzerine yerleştirir. Ufuk ayıklamasının neredeyse hiçbir şey yapmadığı senaryo budur; çünkü kamera ile dünyanın büyük bölümü arasında arazi yoktur. HUD, T2/T3'ün görünür öğe sayısını T1'e kıyasla yalnızca %5-10 kadar azalttığını gösterir.

Bu bir hata değil, özelliktir. Teknik tam da durması gereken yerde çalışmayı bırakır: örtecek hiçbir şey olmadığında. Frustum ayıklaması hâlâ gerçek iş yapar, mesafe ayıklaması hâlâ bütçeyi sınırlar ve ufuk testi sorunsuzca no-op davranışına geri döner. Bunu uygularsanız no-op durumunun da ucuz olduğunu doğrulamanız gerekir. Hiçbir ışın elenmeyecek olsa bile maksimum yükseklik piramidinin önemli olmasının nedeni budur: en kaba seviye çoğu zaman "hiçbir şey engellenmiyor" sonucunu doğrulamaya yeter.

Sırada neyi yayınlayacağız?

Sırayla yapılması gereken üç şey var.

İlk olarak T1 ve T3'ü üretimdeki Chunk.updateObjectVisibility içine port etmek. Piramidin chunk yöneticisinde, bir üst seviyede bulunması gerekiyor; çünkü birden fazla chunk'a yayılıyor. Mevcut chunk başına batching çalışmaya devam etsin diye ayıklama Chunk içinde kalacak. Tahmini süre: testler dahil bir gün. İkinci olarak, çimler hazır olduğunda aynısını onlar için de yapacağız. Mevcut prototip, 576 metrelik oyun alanına 12.000 çim yaprağı dağıtıyor; üretim sürümündeki yoğunluk bunun kabaca on katı olmalı. CPU ayıklama yolları 12.000 örneği bir milisaniyeden kısa sürede işliyor ve bunun 120.000 örnekte de geçerli kalmasını sağlayan piramit hızlandırmasıdır.

Üçüncü olarak, THREE.WebGPURenderer'a geçtiğimizde aynı döngüyü bir compute shader'a taşıyacağız. Meta veriler bir depolama tamponuna dönüşecek. Ayıklama işlemi drawIndirect argümanlarını yazacak. Piramit, maksimum indirgeme önceden uygulanmış bir 2D doku olarak yüklenecek. Kodun yapısı neredeyse tamamen aynı kalacak; zaten asıl amaç da bu: geçişi yeni bir algoritmaya bağlamıyoruz, hâlihazırda sahip olduğumuz bir algoritmayı daha hızlı bir çalışma ortamına taşıyoruz.

Guerrilla, Horizon Zero Dawn'ın prosedürel yerleştirme sistemi için bunun GPU tarafındaki sürümünü ele aldı; render işlem hattından çok veri yapıları önem taşıyor ve onlarınki de aynı yapıya sahip:

Binaları ve yükseklik haritasının tanımlayamadığı yönlerde örtme yapan yoğun dekoratif nesneleri yayınladığımızda Hi-Z yine yerini hak edecek. Ancak yükseklik haritası piramidi işlem hattında kalacak, çünkü zemine yakın örnekler için Hi-Z'den kesin biçimde daha ucuzdur ve bir sırtın silüetini aşan çimler, Hi-Z'nin en kötü başa çıktığı durumlardan biridir.

Kaynaklar

Önceliklendirilmiş gerekçelerin tamamı ve mimari kararlar yukarıda yer alıyor; aşağıda ise yığındaki yaklaşık görünme sıralarına göre her tekniğin temel kaynakları bulunuyor.