Instancing (Tekrarlı Render)

Aynı vertex verilerini içeren çok sayıda nesne çizdiğiniz bir sahneyi düşünün; yalnızca dünya dönüşümleri farklı. Çim yapraklarıyla dolu bir sahneyi hayal edin: her yaprak yalnızca birkaç üçgenden oluşan küçük bir modeldir. Her frame binlerce hatta on binlerce yaprak render etmeniz gerekebilir. Her yaprak yalnızca birkaç üçgenden oluştuğundan neredeyse anında render edilir; ancak yapacağınız binlerce render çağrısı performansı dramatik biçimde düşürür.

Çok sayıda nesne render ederken şuna benzer bir kod döngüsüyle karşılaşırsınız:

for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{
    DoSomePreparations(); // VAO bağla, doku bağla, uniform ayarla vb.
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

Bu şekilde modelinizin birçok instance'ını çizerken hızla bir performans darboğazına ulaşırsınız; çünkü glDrawArrays veya glDrawElements gibi fonksiyonlarla GPU'ya vertex verisinin nasıl render edileceğini söylemek, görece yavaş olan CPU→GPU otobüsü üzerinden gerçekleştiğinden ciddi bir ek yük oluşturur.

Veriyi GPU'ya bir kez gönderip ardından OpenGL'e tek bir çizim çağrısıyla birden fazla nesne çizmesini söyleyebilseydik çok daha verimli olurdu. Instancing tam da bunu yapan bir teknik.

Instancing, aynı mesh verisini tek bir render çağrısıyla çok sayıda nesne render etmemizi sağlayarak her nesneyi render ettiğimizde yapılan tüm CPU→GPU iletişimlerini ortadan kaldırır. Instanced rendering kullanmak için glDrawArrays ve glDrawElements çağrılarını sırasıyla glDrawArraysInstanced ve glDrawElementsInstanced olarak değiştiririn. Bu instanced versiyonlar, kaç tane instance render etmek istediğimizi belirleyen ekstra bir instance count parametresi alır.

Bu fonksiyon tek başına çok kullanışlı değildir; her render edilen nesne aynı konumda render edileceğinden yalnızca tek bir nesne görünür. GLSL bu sorunu çözmek için vertex shader'a gl_InstanceID adında başka bir yerleşik değişken ekledi.

Instanced render çağrılarından biriyle çizerken gl_InstanceID, 0'dan başlayarak her instance için artırılır. 43. instance'ı render ederken gl_InstanceID, vertex shader'da 42 değerine sahip olur. Her instance için benzersiz bir değere sahip olmak, her instance'ı farklı bir dünya konumuna konumlandırmak için büyük bir konum değerleri dizisini indekslemek gibi işlemlere olanak tanır.

Buna aşinalık kazanmak için yalnızca tek bir render çağrısıyla normalize edilmiş cihaz koordinatlarında yüz adet 2D quad render eden basit bir örnek göstereceğiz:

Quad instancing sonucu

Her quad 6 vertex içerir. Vertex verisinin her biri 2B NDC konum ve renk vektörü içerir. Vertex shader:

Burada gl_InstanceID ile offsets uniform dizisini indeksleyerek her instance için bir offset vektörü alırız. Render döngüsüne girmeden önce 10x10 grid için 100 offset pozisyonu hesaplarız:

Ardından verileri vertex shader'ın uniform dizisine aktarırız:

Instanced rendering ile çizmek için glDrawArraysInstanced çağırırız:

glDrawArraysInstanced'ın parametreleri glDrawArrays ile tamamen aynıdır; yalnızca sonuna çizmek istediğimiz instance sayısı eklenir.


Instanced Array'ler

Önceki uygulama bu özel kullanım durumu için iyi çalışsa da 100'den fazla instance render etmek istediğimizde shader'lara gönderebileceğimiz uniform veri miktarının sınırına çabucak çarparız.

Alternatif bir seçenek instanced array'dir. Instanced array, vertex başına değil instance başına güncellenen bir vertex attribute olarak tanımlanır; bu sayede çok daha fazla veri depolayabiliriz.

Normal vertex attribute'larda vertex shader'ın her çalıştırılmasında GPU, mevcut vertex'e ait sonraki vertex attribute kümesini alır. Bir vertex attribute instanced array olarak tanımlandığında ise vertex shader, vertex attribute içeriğini yalnızca instance başına günceller. Bu, per-vertex veriler için standart vertex attribute'ları, per-instance benzersiz veriler için ise instanced array kullanmamızı sağlar.

Önceki örneği kullanarak offset uniform dizisini instanced array'e dönüştürelim. Vertex shader'a başka bir vertex attribute ekleriz:

gl_InstanceID'yi artık kullanmıyoruz ve offset attribute'unu büyük bir uniform dizisini indekslemeden doğrudan kullanabiliriz.

Instanced array de bir vertex attribute olduğundan içeriğini bir VBO'da saklamamız ve attribute pointer'ını yapılandırmamız gerekir:

Vertex attribute pointer'ını ayarlayıp etkinleştiririz:

Son satırdaki glVertexAttribDivisor çağrısının ne işe yaradığına dikkat edin. Bu fonksiyon OpenGL'e bir vertex attribute'un içeriğini ne zaman bir sonraki elemana güncelleyeceğini söyler. İlk parametresi söz konusu vertex attribute, ikincisi ise attribute divisor'dır. Varsayılan olarak attribute divisor 0'dır; bu vertiex shader'ın her iterasyonunda vertex attribute içeriğinin güncelleneceği anlamına gelir. 1'e ayarlandığında her yeni instance için güncellenir; 2'ye ayarlandığında her 2 instance'da bir güncellenir vb. Attribute divisor'ı 1'e ayarlayarak, location 2'deki vertex attribute'un bir instanced array olduğunu OpenGL'e etkin biçimde belirtmiş oluruz.

glDrawArraysInstanced ile quad'ları tekrar çizersek öncekiyle aynı görüntüyü elde ederiz; ancak bu sefer çok daha fazla veri (bellek izin verdiği ölçüde) iletebiliyoruz.

Eğlence olsun diye gl_InstanceID'yi kullanarak her quad'ı sağ-üstten sol-alta doğru kademeli olarak küçültebiliriz:

Sonuç olarak ilk instance'lar son derece küçük çizilir; instance çizme süreci ilerledikçe gl_InstanceID 100'e yaklaştıkça quad'lar orijinal boyutlarına kavuşur:

Instanced array quad'ları

Tam kaynak kodunu buradanarrow-up-right bulabilirsiniz.


Bir Asteroit Sahası

Instancing'in gerçek gücünü görmek için uzayda bir gezi yapalım! Büyük bir asteroit halkasının merkezinde büyük bir gezegen bulunan bir sahneyi düşünün. Böyle bir asteroit halkası binlerce hatta on binlerce kaya oluşumunu içerebilir ve herhangi bir grafik kartında render edilmesi imkânsız hale gelir. Instanced rendering bu senaryo için son derece uygundur; tüm asteroidler tek bir model olarak temsil edilebilir.

Instanced rendering'in etkisini göstermek için önce instanced rendering olmadan bir gezegen etrafında asteroidlerin süzdüğü bir sahneyi render edeceğiz. Her asteroid için bir model dönüşüm matrisi üretiriz:

Planet ve kaya modellerini yükleyip shader'ları derledikten sonra render kodu şuna benzer:

Sonuç, bir gezegen etrafında doğal görünen bir asteroit halkasıdır:

Asteroit sahası

Bu sahne frame başına 1001 render çağrısı içerir; bunların 1000 tanesi kaya modelinedir. amount'u 2000'e yaklaştırdığımızda GPU'da sahne o kadar yavaşlar ki hareket etmek güçleşir.

Şimdi aynı sahneyi instanced rendering ile çizmeye çalışalım. Vertex shader'ı biraz güncelleriz:

Model uniform değişkeni yerine mat4 bir vertex attribute kullanıyoruz. Ancak vec4'ten büyük bir veri türü vertex attribute olarak tanımlandığında işler biraz farklı çalışır. Bir vertex attribute için maksimum veri boyutu vec4'e eşittir. mat4 temelde 4 adet vec4 olduğundan bu matris için 4 vertex attribute rezerve etmemiz gerekir. Ona 3 konumunu atadığımızdan, matrisin sütunları 3, 4, 5 ve 6 vertex attribute konumlarında yer alır.

Bu 4 vertex attribute'un her biri için attribute pointer'ları ayarlayıp instanced array olarak yapılandırırız:

Ardından glDrawElementsInstanced ile çizeriz:

Öncekiyle aynı sayıda asteroidi çiziyoruz; ancak bu sefer instanced rendering kullanıyoruz. Sonuçlar tamamen aynı görünmeli; ancak amount'u artırdığınızda instanced rendering'in gerçek gücünü görmeye başlarsınız. Instanced rendering olmadan 1000-1500 asteroid arasında akıcı bir render elde edebiliyorduk. Instanced rendering ile bu değeri 100000'e çıkarabiliriz. 576 vertex'e sahip kaya modeliyle bu, frame başına yaklaşık 57 milyon vertex demek; üstelik yalnızca 2 çizim çağrısıyla!

100000 asteroit
circle-info

Farklı makinelerde 100000 asteroit biraz fazla gelebilir; kabul edilebilir bir kare hızına ulaşana kadar değerleri biraz deneyin.

Görüldüğü gibi doğru ortamlarda instanced rendering, uygulamanızın render kapasitelerinde büyük bir fark yaratabilir. Bu nedenle instanced rendering, çim, bitki örtüsü, partiküller ve bu gibi sahnelerde, yani çok sayıda tekrarlayan şekle sahip her sahnede yaygın olarak kullanılır.

Tam kaynak kodunu buradanarrow-up-right bulabilirsiniz.

Last updated