Bir Voxel / Minecraft tipi oyunun görüntü oluşturma hızını nasıl artırabilirim?


35

Kendi Minecraft klonumu yazıyorum (ayrıca Java'da yazılmıştır). Şu anda harika çalışıyor. 40 metrelik görüş mesafesiyle MacBook Pro 8,1'imde 60 FPS'ye kolayca vurabilirim. (Intel i5 + Intel HD Grafik 3000). Ancak görüntüleme mesafesini 70 metreye koyarsam, sadece 15-25 FPS'ye ulaşırım. Gerçek Minecraft'ta görüş mesafesini çok fazla (= 256m) uzağa koyabilirim. Öyleyse sorum şu: oyunumu daha iyi hale getirmek için ne yapmalıyım?

Uyguladığım optimizasyonlar:

  • Yerel parçaları yalnızca bellekte tutun (müzikçaların görüntüleme mesafesine bağlı olarak)
  • Frustum culling (Önce topaklarda, sonra bloklarda)
  • Sadece blokların gerçekten görünen yüzlerini çizerek
  • Görünür blokları içeren yığın başına listeleri kullanma. Görünen topaklar kendini bu listeye ekleyecektir. Görünmez hale gelirlerse, otomatik olarak bu listeden kaldırılırlar. Bloklar, bir komşu blok inşa ederek veya yok ederek görünürde olur.
  • Güncelleme bloklarını içeren yığın başına listeleri kullanma. Görünür blok listeleriyle aynı mekanizma.
  • newOyun döngüsünün içinde neredeyse hiç ifade kullanmayın. (Oyunum Çöp Toplayıcı çağrılıncaya kadar yaklaşık 20 saniye sürüyor)
  • Şu anda OpenGL çağrı listelerini kullanıyorum. ( glNewList(), glEndList(), glCallList()) Bloğun bir tür her bir tarafı için.

Şu anda bile herhangi bir aydınlatma sistemini kullanmıyorum. VBO'ları çoktan duydum. Ama tam olarak ne olduğunu bilmiyorum. Ancak, onlar hakkında biraz araştırma yapacağım. Performansı artıracak mı? VBO'ları uygulamadan önce glCallLists(), çağrı listelerinin bir listesini kullanmayı ve iletmeyi denemek istiyorum . Binlerce kez kullanmak yerine glCallList(). (Bunu denemek istiyorum çünkü gerçek MineCraft'ın VBO'ları kullanmadığını düşünüyorum.) Doğru mu?

Performansı arttırmak için başka püf noktaları var mı?

VisualVM profillemesi bana bunu gösterdi (70 metrelik bir görüş mesafesine sahip sadece 33 kare için profil oluşturma):

görüntü tanımını buraya girin

40 metrelik profil (246 kare):

görüntü tanımını buraya girin

Not: Başka bir iş parçasında topakları oluşturduğum için birçok yöntemi ve kod bloğunu senkronize ediyorum. Bir nesne için bir kilit elde etmenin bir oyun döngüsünde bu kadarını yaparken bir performans sorunu olduğunu düşünüyorum (tabii ki, sadece oyun döngüsünün olduğu ve yeni parçaların üretilmediği zamandan bahsediyorum). Bu doğru mu?

Düzenleme: Bazı synchronisedblokları ve diğer bazı küçük iyileştirmeleri yaptıktan sonra . Performans zaten çok daha iyi. İşte 70 metrelik yeni profil sonuçlarım:

görüntü tanımını buraya girin

Bence selectVisibleBlocksburada mesele oldukça açık .

Şimdiden teşekkürler!
Martijn

Güncelleme : Bazı ekstra geliştirmelerden sonra (her biri yerine döngüler için kullanmak, döngüler dışında değişkenleri tamponlamak vb ...), şimdi 60 görüntüleme mesafesini oldukça iyi çalıştırabilirim.

Sanırım en kısa zamanda VBO'ları uygulayacağım.

Not: Tüm kaynak kodları GitHub'da mevcuttur:
https://github.com/mcourteaux/CraftMania


2
Bize 40m'de bir profil resmi verebilir misiniz, böylece diğerinden daha hızlı ölçeklendirenin ne olduğunu görebiliriz?
James

Belki de belirtilmiş, ancak düşünürseniz, sadece bir 3D oyunu nasıl hızlandıracağınızla ilgili teknikler soruyor, ilginç geliyor. Ancak başlık ppl'yi korkutabilir.
Gustavo Maciel

@Gtoknu: Başlık olarak ne önerirsin?
Martijn Courteaux,

5
Kime sorduğunuza bağlı olarak, bazı insanlar Minecraft'ın gerçekten de o kadar hızlı olmadığını söyler.
thedaian

"Hangi teknikleri 3D oyunu hızlandırabilir" gibi bir şey daha iyi olmalı bence. Bir şey düşünün, ancak "en iyi" kelimesini kullanmamaya çalışın veya başka bir oyunla karşılaştırmayı deneyin. Bazı oyunlarda tam olarak ne kullandıklarını söyleyemiyoruz.
Gustavo Maciel

Yanıtlar:


15

Bireysel bloklara sinir krizi geçirmekten bahsediyorsun - bunu atmayı dene. Çoğu oluşturma parçası ya tamamen görünür olmalı ya da tamamen görünmez olmalıdır.

Minecraft sadece bir blok belirli bir yığın tadil edilmiş ve bir görüntüleme listesi / tepe tamponu (o kullanan bilgi yoktur) oluşturur , böylece I yapmak . Görünüm değiştiğinde ekran listesini değiştiriyorsanız, ekran listelerinden faydalanamazsınız.

Ayrıca, dünya yükseklikteki parçaları kullanıyor görünüyorsunuz. Minecraft kullandığı Not kübik yük için farklı olarak, kendi ekran listelerinin için 16 × 16 × 16 parçalar ve kaydedin. Bunu yaparsanız, tek tek parçaları kırmak için daha az sebep vardır.

(Not: Minecraft'ın kodunu incelemedim. Bu bilgilerin tümü ya Minecraft'ın oynadığı sırayı oynarken gözlemlemekten elde ettiğim sonuçlar ya da deneyimlerim.)


Daha genel tavsiye:

Renderinizin iki işlemci üzerinde çalıştığını unutmayın: CPU ve GPU. Kare hızınız yetersiz olduğunda, biri ya da diğeri sınırlayıcı kaynaktır - programınız CPU'ya bağlı ya da GPU'ya bağlıdır (değiştirilmediğini ya da zamanlama sorunları olduğunu varsayarak).

Programınız% 100 CPU'da çalışıyorsa (ve tamamlamak için sınırlandırılmamış başka bir görevi yoksa), CPU'nuz çok fazla iş yapıyor demektir. GPU'nun daha fazlasını yapması karşılığında görevini basitleştirmeye çalışmalısınız (örneğin daha az temizleme yapın). Açıklamanızdan dolayı, bunun sizin probleminiz olduğundan şüpheleniyorum.

Öte yandan, eğer GPU sınır ise (ne yazık ki, genellikle% 0 -% 100 yük monitörü uygun değildir), daha az veri göndermeyi düşünmelisiniz veya daha az piksel doldurmasını zorunlu tutmalısınız.


2
Harika referans, bu konudaki araştırmanız wikide çok yardımcı oldu! +1
Gustavo Maciel

@OP: yalnızca görünen yüzleri oluşturur ( bloklar değil ). Patolojik ama monotonik bir 16x16x16 öbek, yaklaşık 800 görülebilir yüze sahipken, içerilen bloklar 24.000 görülebilir yüze sahip olacaktır. Bunu yaptıktan sonra, Kevin'ın cevabı sonraki en önemli gelişmeleri içerir.
AndrewS,

@KevinReid Performans hata ayıklama konusunda yardımcı olacak bazı programlar vardır. Örneğin AMD GPU PerfStudio, eğer CPU'su veya GPU'su bağlıysa ve GPU'da hangi bileşenin bağlı olduğunu söyler (doku vs parça vs köşe vs) Ve Nvidia'da da benzer bir şey olduğundan eminim.
akaltar

3

Vec3f.set'e ne denir bu kadar? Her kareyi sıfırdan oluşturmak istediğiniz şeyi oluşturuyorsanız, o zaman kesinlikle hızlandırmak istediğiniz yer orasıdır. Çok fazla OpenGL kullanıcısı değilim ve Minecraft’ın işleyiş şeklini pek bilmiyorum, fakat kullandığınız matematik işlevleri şu anda sizi öldürüyor gibi görünüyor (sadece ne kadar zaman harcadığınıza bakın ve kaç kez çağrılırlar - onları arayarak binlerce kesimle ölüm).

İdeal olarak, dünyanız, birlikte işlemek üzere öğeleri gruplandırabilir, Vertex Tampon Nesneleri oluşturarak ve bunları birden fazla karede yeniden kullanabilecek şekilde bölünmüş olur. Bir VBO’yu yalnızca, temsil ettiği dünya bir şekilde değişirse (kullanıcının düzenlemesi gibi) değiştirmeniz gerekir. Daha sonra, bellek tüketimini düşük tutmak için görünür olma aralığına geldiğinde temsil ettiğin şeyler için VBO'lar yaratabilir / yok edebilirsin, sadece VBO her kareden ziyade yaratıldığı gibi etkilenecekti.

Profilinizde "çağrı" sayımı doğruysa, birçok kez çok fazla şey çağırıyorsunuz. (Vec3f.set'e 10 milyon çağrı ... ah!)


Bu yöntemi tonlarca şey için kullanıyorum. Basitçe vektör için üç değeri ayarlar. Bu, her seferinde yeni bir nesne tahsis etmekten çok daha iyidir.
Martijn Courteaux

2

Buradaki tarifim (kendi deneyimlerimden) geçerli:

Voksel oluşturma için daha verimli olanı: önceden yapılmış VBO veya bir geometri gölgelendiricisi?

Minecraft ve kodunuz muhtemelen sabit işlevli boru hattını kullanır; kendi gayretlerim GLSL ile oldu, fakat genel olarak uygulanabilir bir şey olduğunu düşünüyorum:

(Bellekten) Ekran frustumundan yarım blok daha büyük olan bir frustum yaptım. Daha sonra her bir yığının merkez noktalarını test ettim ( Minecraft'ta 16 * 16 * 128 blok var ).

Her birinin yüzleri VBO element dizisine yayılmıştır (topaklardan gelen birçok yüz, 'tam' olana kadar aynı VBO'yu paylaşır; düşünün malloc; aynı VBO'da aynı dokuya sahip olanlar mümkün olduğunca) ve kuzey için tepe indeksleri yüzler, güney yüzler vb. karışık olmaktan çok bitişiktir. Çizdiğimde, bir glDrawRangeElementsnormalde önceden yansıtılmış ve normalleştirilmiş, üniformalı, kuzey yüzleri için bir yapıyorum . Sonra güney yüzlerini ve benzerlerini yapıyorum, bu yüzden normaller herhangi bir VBO'da değil. Her öbek için, sadece görünecek olan yüzleri yaymak zorundayım - örneğin ekranın ortasındakilerin sol ve sağ taraflarını çizmesi gerekir; Bu GL_CULL_FACEuygulama düzeyinde basittir .

En büyük hızlanma iirc, her bir parçayı çok köşeli hale getirirken iç yüzleri ayırmaktı.

Ayrıca, doku-atlas yönetimi ve yüzleri dokuya göre sıralamak ve yüzleri aynı dokuya sahip yüzleri diğer parçalardan gelenlerle aynı vboya koymak önemlidir. Çok fazla doku değişiminden kaçınmak ve yüzleri dokuya göre sıralamak vb. İçindeki yayılma sayısını en aza indirir glDrawRangeElements. Komşu kiremit yüzlerini daha büyük dikdörtgenler ile birleştirmek de önemliydi. Yukarıda belirtilen diğer cevabın birleştirilmesi hakkında konuşuyorum.

Açıkça görülüyor ki, yalnızca görünür olan bu parçaları çokgenleştiriyorsunuz, uzun süredir görünmeyen bu parçaları atıyorsunuz ve düzenlenmiş olan parçaları yeniden çokgenleştiriyorsunuz (bu, bunları oluşturmaya kıyasla nadir bir durumdur).


Frustum optimizasyon fikrini seviyorum. Ama açıklamada "blok" ve "yığın" terimlerini karıştırmıyor musun?
Martijn Courteaux,

Muhtemelen evet. Bir blok öbek İngilizce'de bir blok bloğudur.
Will

1

Tüm karşılaştırmaların ( BlockDistanceComparator) nereden geliyor? Eğer bir sıralama fonksiyonundan geliyorsa, bu bir radix sıralama ile değiştirilebilir mi (asimptotik olarak daha hızlı ve karşılaştırma tabanlı değil)?

Zamanlamalarınıza bakmak, sıralamanın kendisi o kadar kötü olmasa bile, relativeToOriginişleviniz her compareişlev için iki kez çağrılıyor ; tüm bu veriler bir kez hesaplanmalıdır. Örneğin yardımcı bir yapıyı sıralamak daha hızlı olmalıdır.

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

ve sonra pseudoCode'da

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

Geçerli bir Java yapısı değilse (lisanstan beri Java’ya dokunmadım) üzgünüm ama umarım fikir edinirsiniz.


Bunu eğlenceli buluyorum. Java'nın yapıları yok. Şey, Java dünyasında buna benzer bir şey var ama veritabanları ile aynı şey yapmak zorunda. Kamu üyeleri ile son bir sınıf oluşturabilirler, sanırım işe yarıyor.
Theraot

1

Evet VBO'ları ve CULL'ları kullanın, ancak bu hemen hemen her oyunda geçerli. Yapmak istediğiniz şey sadece küpü oynatıcı tarafından görülebiliyorsa oluşturmaktır. VE eğer bloklar belirli bir şekilde dokunuyorsa (yeraltından dolayı göremediğiniz bir yığın diyelim) blokların köşelerini ekler ve neredeyse bir "büyük blok" gibi, ya da sizin durumunuzda - bir yığın. Buna açgözlü meshing denir ve performansı önemli ölçüde arttırır. Bir oyun (voksel tabanlı) geliştiriyorum ve açgözlü bir mesh algoritması kullanıyor.

Her şeyi böyle yapmak yerine:

kılmak

Bu şekilde işler:

render2

Bunun dezavantajı, ilk dünya yapısında yığın başına daha fazla hesaplama yapmanız gerekmesi veya oyuncu bir blok çıkardığında / ekliyorsa olmasıdır.

hemen hemen her türlü voksel motoru iyi performans için buna ihtiyaç duyar.

Yaptığı şey, blok yüzünün başka bir blok yüzüne temas edip etmediğini kontrol etmek ve eğer öyleyse: sadece bir (veya sıfır) blok yüz (ler) olarak işliyor. Topakları gerçekten hızlı bir şekilde oluştururken pahalı bir dokunuş.

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}

1
Ve buna değer mi? Bir LOD sistemi daha uygun olurdu gibi görünüyor.
MichaelHouse

0

Kodunuz nesnelerde ve işlev çağrısında boğuluyor gibi görünüyor. Rakamları ölçmek, içlerinde herhangi bir şey var gibi görünmüyor.

Farklı bir Java ortamı bulmayı deneyebilir ya da sahip olduğunuzun ayarlarıyla uğraşabilirsiniz, ancak kodunuzu hızlı ve basit yapmanın basit ve basit bir yolu, ancak en azından yavaş yavaş iyi bir işlem durdurmak için Vec3f'de kodlama OOO *. Her metodu kendi kendine içerir hale getirin, sadece bazı önemli görevleri yerine getirmek için diğer metotlardan herhangi birini arama.

Düzenleme: Her yerde ek yük varken, görünmeden önce blokları sıralamak en kötü performans yiyen kişi gibi görünüyor. Bu gerçekten gerekli mi? Eğer öyleyse muhtemelen bir döngüden geçerek başlamanız ve menşe noktasına olan her bir blok mesafesini hesaplamanız ve ardından buna göre sıralamanız gerekir.

* Aşırı Nesneye Dayalı


Evet, hafızadan tasarruf edersiniz, ancak CPU'yu kaybedersiniz! Bu yüzden OOO, gerçek zamanlı oyunlarda çok iyi değil.
Gustavo Maciel

Profil oluşturmaya başlar başlamaz (ve yalnızca örnekleme yapmaz), JVM'nin normalde yaptığı herhangi bir çizgi ortadan kaybolur. Bu tür bir kuantum teorisi, sonucu değiştirmeden bir şeyi ölçemez: p
Michael

@Gtoknu Bu evrensel olarak doğru değildir, OOO'nin bir düzeyinde işlev çağrıları satır içi koddan daha fazla bellek kullanmaya başlar. Söz konusu kodun iyi bir bölümü olduğunu söyleyebilirim ki bu, hafıza için kırılma noktasıdır.
aaaaaaaaaaaa

0

Ayrıca, Math işlemlerini bitsel operatörlere indirmeyi deneyebilirsiniz. Eğer varsa 128 / 16, bit tabanlı operatörü yapmaya: 128 << 4. Bu, sorunlarınız için çok yardımcı olacaktır. İşleri tam hızda çalıştırmaya çalışmayın. Oyun güncellemenizi 60 veya daha yüksek bir hızda yapın, hatta diğer şeyler için bunu bozun, ancak vokselleri yok edip veya yerleştirmek zorunda kalırsınız ya da fps'ınızı düşürecek bir yapılacaklar listesi yapmak zorunda kalırsınız. Varlıklar için yaklaşık 20'lik bir güncelleme yapabilirsiniz. Ve dünya güncellemeleri ve / veya üretimi için 10 gibi bir şey.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.