Jak mogę poprawić szybkość renderowania gry typu Voxel / Minecraft?


35

Piszę własny klon Minecrafta (również napisany w Javie). Teraz działa świetnie. Dzięki odległości oglądania wynoszącej 40 metrów mogę z łatwością osiągnąć 60 FPS na moim MacBooku 8,1. (Intel i5 + Intel HD Graphics 3000). Ale jeśli ustawię odległość oglądania na 70 metrów, osiągnę tylko 15-25 FPS. W prawdziwym Minecrafcie mogę bez problemu ustawić odległość oglądania na odległość (= 256 m). Więc moje pytanie brzmi: co powinienem zrobić, aby moja gra była lepsza?

Optymalizacje, które wdrożyłem:

  • Przechowuj tylko lokalne fragmenty w pamięci (w zależności od odległości oglądania gracza)
  • Ubijanie Frustum (najpierw na kawałki, a następnie na bloki)
  • Rysowanie tylko widocznych powierzchni bloków
  • Używanie list na porcję, które zawierają widoczne bloki. Fragmenty, które staną się widoczne, dodadzą się do tej listy. Jeśli staną się niewidoczne, zostaną automatycznie usunięte z tej listy. Bloki stają się (nie) widoczne przez zbudowanie lub zniszczenie sąsiedniego bloku.
  • Używanie list na porcję, które zawierają bloki aktualizacji. Ten sam mechanizm, co widoczne listy bloków.
  • Nie używaj prawie żadnych newinstrukcji w pętli gry. (Moja gra działa około 20 sekund do momentu wywołania Garbage Collector)
  • Obecnie korzystam z list połączeń OpenGL. ( glNewList(), glEndList(), glCallList()) Dla każdej strony w rodzaju bloku.

Obecnie nawet nie używam żadnego systemu oświetlenia. Słyszałem już o VBO. Ale nie wiem dokładnie, co to jest. Jednak zrobię o nich trochę badań. Czy poprawią wydajność? Przed wdrożeniem VBO chcę spróbować użyć glCallLists()i przekazać listę list połączeń. Zamiast tego tysiąc razy glCallList(). (Chcę tego spróbować, ponieważ uważam, że prawdziwy MineCraft nie używa VBO. Prawda?)

Czy istnieją inne sztuczki mające na celu poprawę wydajności?

Profilowanie VisualVM pokazało mi to (profilowanie tylko 33 klatek, z odległości oglądania 70 metrów):

wprowadź opis zdjęcia tutaj

Profilowanie za pomocą 40 metrów (246 ramek):

wprowadź opis zdjęcia tutaj

Uwaga: synchronizuję wiele metod i bloków kodu, ponieważ generuję porcje w innym wątku. Myślę, że uzyskanie blokady dla obiektu jest problemem z wydajnością podczas robienia tak dużo w pętli gry (oczywiście mówię o czasie, w którym jest tylko pętla gry i nie są generowane żadne nowe fragmenty). Czy to jest poprawne?

Edycja: Po usunięciu niektórych synchronisedbloków i innych drobnych usprawnień. Wydajność jest już znacznie lepsza. Oto moje nowe wyniki profilowania z 70 metrami:

wprowadź opis zdjęcia tutaj

Myślę, że jest to całkiem jasne, o selectVisibleBlocksto tutaj chodzi.

Z góry dziękuję!
Martijn

Aktualizacja : Po kilku dodatkowych ulepszeniach (takich jak użycie pętli zamiast dla każdego, buforowanie zmiennych poza pętlami itp.), Mogę teraz całkiem dobrze wyświetlać odległość 60.

Myślę, że zamierzam jak najszybciej wdrożyć VBO.

PS: Cały kod źródłowy jest dostępny na GitHub:
https://github.com/mcourteaux/CraftMania


2
Czy możesz podać nam zdjęcie profilowe z odległości 40 m, abyśmy mogli zobaczyć, co może się powiększać szybciej niż inne?
James

Być może zbyt szczegółowe, ale jeśli weźmiesz pod uwagę, to po prostu pyta techniki, jak przyspieszyć grę 3D, brzmi interesująco. Ale tytuł może przestraszyć ppl.
Gustavo Maciel

@Gtoknu: Co sugerujesz jako tytuł?
Martijn Courteaux,

5
W zależności od tego, kogo zapytasz, niektórzy twierdzą, że Minecraft też nie jest tak szybki.
thedaian

Myślę, że coś w stylu „Które techniki mogą przyspieszyć grę 3D” powinno być znacznie lepsze. Pomyśl o czymś, ale staraj się nie używać słowa „najlepszy” lub spróbuj porównać do innej gry. Nie możemy dokładnie powiedzieć, czego używają w niektórych grach.
Gustavo Maciel

Odpowiedzi:


15

Wspominasz o robieniu frustum na poszczególnych blokach - spróbuj to wyrzucić. Większość fragmentów renderowania powinna być całkowicie widoczna lub całkowicie niewidoczna.

Minecraft odbudowuje tylko listę wyświetlania / bufor wierzchołków (nie wiem, którego używa), gdy blok jest modyfikowany w danym fragmencie, podobnie jak ja . Jeśli modyfikujesz listę wyświetlania za każdym razem, gdy zmienia się widok, nie zyskujesz korzyści z list wyświetlania.

Wygląda na to, że używasz kawałków wysokości świata. Pamiętaj, że Minecraft używa sześciennych kawałków 16 × 16 × 16 do swoich list wyświetlania, w przeciwieństwie do ładowania i zapisywania. Jeśli to zrobisz, będzie jeszcze mniej powodów, aby odrzucić poszczególne fragmenty.

(Uwaga: nie zbadałem kodu Minecrafta. Wszystkie te informacje to albo słyszenie, albo moje własne wnioski z obserwacji renderowania Minecrafta podczas gry.)


Bardziej ogólne porady:

Pamiętaj, że renderowanie odbywa się na dwóch procesorach: CPU i GPU. Gdy częstotliwość klatek jest niewystarczająca, jeden lub drugi jest ograniczającym zasobem - twój program jest albo związany z procesorem, albo z GPU (zakładając, że nie zamienia lub nie ma problemów z planowaniem).

Jeśli twój program działa na 100% procesorze (i nie ma żadnego nieograniczonego innego zadania do wykonania), oznacza to, że twój procesor wykonuje zbyt dużo pracy. Powinieneś spróbować uprościć jego zadanie (np. Zrobić mniej ubijania) w zamian za to, że GPU robi więcej. Podejrzewam, że to twój problem, biorąc pod uwagę twój opis.

Z drugiej strony, jeśli GPU jest limitem (niestety zwykle nie są wygodne monitory obciążenia 0% -100%), powinieneś pomyśleć o tym, jak wysłać go mniej danych lub wymagać, aby wypełnił mniej pikseli.


2
Świetne referencje, twoje badania na ten temat wspomniane na twojej wiki były dla mnie bardzo pomocne! +1
Gustavo Maciel

@OP: renderuj tylko widoczne twarze (nie bloki ). Patologiczny, ale monotoniczny fragment 16 x 16 x 16 będzie miał prawie 800 widocznych ścian, a zawarte w nim bloki będą miały 24 000 widocznych ścian. Gdy to zrobisz, odpowiedź Kevina zawiera kolejne najważniejsze ulepszenia.
AndrewS,

@KevinReid Istnieje kilka programów ułatwiających debugowanie wydajności. Na przykład AMD GPU PerfStudio informuje, czy jest związany procesor lub GPU, a na GPU, który komponent jest związany (tekstura vs fragment vs wierzchołek itp.) I jestem pewien, że Nvidia też ma coś podobnego.
akaltar

3

Jak bardzo nazywa się Vec3f.set? Jeśli budujesz to, co chcesz renderować od zera w każdej klatce, to zdecydowanie na tym miejscu chcesz zacząć ją przyspieszać. Nie jestem zbytnio użytkownikiem OpenGL i nie wiem wiele o tym, jak renderuje Minecraft, ale wydaje się, że funkcje matematyczne, których używasz, zabijają cię teraz (po prostu zobacz, ile czasu spędzasz w nich i ile razy są nazywani - nazywają ich śmiercią tysiącem cięć).

Idealnie byłoby, gdyby twój świat był podzielony na segmenty, abyś mógł grupować rzeczy do renderowania razem, budując obiekty bufora wierzchołków i ponownie je wykorzystując w wielu klatkach. Będziesz musiał zmodyfikować VBO tylko wtedy, gdy świat, który reprezentuje, w jakiś sposób się zmieni (tak jak użytkownik go edytuje). Następnie możesz utworzyć / zniszczyć VBO dla tego, co reprezentujesz, ponieważ jest to widoczne, aby ograniczyć zużycie pamięci, wystarczyłoby uderzenie, ponieważ VBO zostało utworzone, a nie każda klatka.

Jeśli liczba „wywołań” jest poprawna w Twoim profilu, nazywasz wiele rzeczy okropnie wiele razy. (10 milionów połączeń do Vec3f.set ... ouch!)


Używam tej metody do wielu rzeczy. Po prostu ustawia trzy wartości dla wektora. Jest to o wiele lepsze niż przydzielanie za każdym razem nowego obiektu.
Martijn Courteaux,

2

Obowiązuje mój opis (z własnego eksperymentu):

Co jest bardziej wydajne w renderowaniu wokseli: gotowe VBO lub moduł do cieniowania geometrii?

Minecraft i twój kod prawdopodobnie używają potoku funkcji stałej; moje własne wysiłki były z GLSL, ale sedno ma ogólne zastosowanie, czuję:

(Z pamięci) Zrobiłem frustum, które było o pół bloku większe od ekranu. Następnie przetestowałem punkty środkowe każdej części ( Minecraft ma 16 * 16 * 128 bloków ).

Ściany w każdej z nich mają rozpiętość w tablicy elementów VBO (wiele ścian z fragmentów dzieli to samo VBO, dopóki nie jest „pełne”; pomyśl malloc: te o tej samej teksturze w tym samym VBO, jeśli to możliwe) i indeksy wierzchołków dla północy twarze, twarze południowe itd. są przylegające, a nie mieszane. Kiedy rysuję, robię a glDrawRangeElementsdla ścian północnych, z normalną już rzutowaną i znormalizowaną, w mundurze. Potem robię twarze południowe i tak dalej, więc normalne nie są w żadnym VBO. Dla każdego fragmentu muszę tylko emitować twarze, które będą widoczne - tylko te na środku ekranu muszą rysować na przykład lewą i prawą stronę; jest to proste GL_CULL_FACEna poziomie aplikacji.

Największym przyspieszeniem, iirc, było wybijanie wewnętrznych powierzchni podczas poligonizacji każdego fragmentu.

Ważne jest również zarządzanie atlasem tekstur i sortowanie ścian według tekstury oraz umieszczanie ścian o tej samej teksturze w tym samym VBO, jak te z innych fragmentów. Chcesz uniknąć zbyt wielu zmian tekstury i sortowania ścian według tekstury itd., Aby zminimalizować liczbę zakresów w glDrawRangeElements. Dużym problemem było również łączenie sąsiadujących powierzchni tego samego kafelka w większe prostokąty. Mówię o połączeniu z drugą odpowiedzią cytowaną powyżej.

Oczywiście poligonizujesz tylko te fragmenty, które kiedykolwiek były widoczne, możesz odrzucić te fragmenty, które nie były widoczne przez długi czas, i ponownie poligonizujesz fragmenty, które są edytowane (ponieważ jest to rzadkie zjawisko w porównaniu do renderowania).


Podoba mi się pomysł twojej optymalizacji frustum. Ale czy w swoim objaśnieniu nie mieszasz terminów „blok” i „fragment”?
Martijn Courteaux,

prawdopodobnie tak. Część bloków to blok bloków w języku angielskim.
Czy

1

Skąd pochodzą wszystkie twoje porównania ( BlockDistanceComparator)? Jeśli pochodzi z funkcji sortowania, czy można ją zastąpić sortowaniem radix (która jest asymptotycznie szybsza i nie oparta na porównaniu)?

Patrząc na twoje czasy, nawet jeśli samo sortowanie nie jest takie złe, twoja relativeToOriginfunkcja jest wywoływana dwukrotnie dla każdej comparefunkcji; wszystkie te dane powinny być obliczone raz. Sortowanie struktury pomocniczej np. Powinno być szybsze

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

a następnie w pseudoCode

// 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 ]

Przepraszam, jeśli to nie jest poprawna struktura Java (nie dotknąłem Javy od czasów licencjackich), ale mam nadzieję, że rozumiesz.


Uważam to za zabawne. Java nie ma struktur. Cóż, w świecie Java jest coś takiego, ale ma to związek z bazami danych, a nie z tym samym. Mogą stworzyć ostateczną klasę z członkami publicznymi, tak myślę, że to działa.
Theraot

1

Tak, używaj VBO i CULL, ale dotyczy to praktycznie każdej gry. To, co chcesz zrobić, to renderować sześcian tylko wtedy, gdy jest widoczny dla gracza, ORAZ jeśli bloki dotykają się w określony sposób (powiedzmy, że fragment nie jest widoczny, ponieważ jest pod ziemią) dodajesz wierzchołki bloków i tworzysz to prawie jak „większy blok” lub, w twoim przypadku, kawałek. Nazywa się to chciwym tworzeniem siatki i drastycznie zwiększa wydajność. Tworzę grę (opartą na wokselach), która wykorzystuje chciwy algorytm tworzenia siatki.

Zamiast renderować wszystko w ten sposób:

renderowanie

Renderuje to tak:

render2

Minusem tego jest to, że musisz wykonać więcej obliczeń na porcję na początkowej kompilacji świata lub jeśli gracz usunie / doda blok.

prawie każdy typ silnika wokselowego potrzebuje tego do dobrych osiągów.

Sprawdza, czy powierzchnia bloku dotyka innej powierzchni bloku, a jeśli tak: renderuje tylko jako jedną (lub zero) powierzchni bloku. Jest to drogi dotyk, gdy renderujesz fragmenty naprawdę szybko.

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
A czy warto? Wydaje się, że bardziej odpowiedni byłby system LOD.
MichaelHouse

0

Wygląda na to, że twój kod tonie w obiektach i wywołaniach funkcji. Po wyliczeniu liczb nie wydaje się, aby miało to miejsce w środku.

Możesz spróbować znaleźć inne środowisko Java lub po prostu zadzierać z ustawieniami tego, co masz, ale prosty i prosty sposób, aby twój kod nie był szybki, ale znacznie wolniejszy jest przynajmniej wewnętrzny Vec3f, aby zatrzymać kodowanie OOO *. Spraw, aby każda metoda zawierała się w sobie, nie wywołuj żadnej z innych metod tylko w celu wykonania jakiegoś zadania służebnego.

Edycja: Podczas gdy w całym miejscu jest narzut, wydawałoby się, że zamówienie bloków przed renderowaniem jest najgorszym zjadaczem wydajności. Czy to naprawdę konieczne? Jeśli tak, prawdopodobnie powinieneś zacząć od przejścia przez pętlę i obliczyć odległość każdego bloku do początku, a następnie posortować według tego.

* Zbyt zorientowany obiektowo


Tak, zaoszczędzisz pamięć, ale stracisz procesor! Więc OOO nie jest zbyt dobra w grach w czasie rzeczywistym.
Gustavo Maciel

Gdy tylko zaczniesz profilować (a nie tylko próbkowanie), wszelkie wstawki, które JVM zwykle robi, znikają. To trochę jak teoria kwantowa, nie można czegoś zmierzyć bez zmiany wyniku: p
Michael

@Gtoknu To nie jest prawdą, na pewnym poziomie OOO wywołania funkcji zaczynają zajmować więcej pamięci niż wbudowany kod. Powiedziałbym, że jest spora część tego kodu, która znajduje się wokół progu rentowności pamięci.
aaaaaaaaaaaa

0

Możesz także spróbować podzielić operacje matematyczne na operatory bitowe. Jeśli masz 128 / 16, spróbuj zrobić operatory bitowe: 128 << 4. To bardzo pomoże w twoich problemach. Nie staraj się, aby wszystko działało z pełną prędkością. Dokonaj aktualizacji gry w tempie 60 lub coś, a nawet zepsuć to dla innych rzeczy, ale będziesz musiał zniszczyć lub umieścić woksele lub musisz zrobić listę rzeczy do zrobienia, co obniży twoje fps. Możesz wykonać aktualizację około 20 dla podmiotów. I coś w rodzaju 10 do aktualizacji i / lub generacji na świecie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.