Come posso migliorare la velocità di rendering di un gioco di tipo Voxel / Minecraft?


35

Sto scrivendo il mio clone di Minecraft (anche scritto in Java). Funziona benissimo in questo momento. Con una distanza di visione di 40 metri posso facilmente raggiungere 60 FPS sul mio MacBook Pro 8,1. (Intel i5 + Intel HD Graphics 3000). Ma se metto la distanza di visione su 70 metri, raggiungo solo 15-25 FPS. Nel vero Minecraft, posso mettere la distanza di visualizzazione lontana (= 256m) senza problemi. Quindi la mia domanda è: cosa devo fare per migliorare il mio gioco?

Le ottimizzazioni che ho implementato:

  • Mantieni solo blocchi locali in memoria (a seconda della distanza di visione del giocatore)
  • Abbattimento del frustum (prima sui pezzi, poi sui blocchi)
  • Disegnando solo facce davvero visibili dei blocchi
  • Utilizzo di elenchi per blocco che contengono i blocchi visibili. I pezzi che diventano visibili si aggiungeranno a questo elenco. Se diventano invisibili, vengono automaticamente rimossi da questo elenco. I blocchi diventano (in) visibili costruendo o distruggendo un blocco vicino.
  • Utilizzo di elenchi per blocco che contengono i blocchi di aggiornamento. Stesso meccanismo delle liste di blocchi visibili.
  • Usa quasi nessuna newistruzione all'interno del ciclo di gioco. (Il mio gioco dura circa 20 secondi fino a quando viene richiamato il Garbage Collector)
  • Al momento sto usando gli elenchi di chiamate OpenGL. ( glNewList(), glEndList(), glCallList()) Per ciascun lato di un tipo di blocco.

Attualmente non sto nemmeno usando alcun tipo di sistema di illuminazione. Ho già sentito parlare dei VBO. Ma non so esattamente di cosa si tratti. Tuttavia, farò alcune ricerche su di loro. Miglioreranno le prestazioni? Prima di implementare i VBO, voglio provare a utilizzare glCallLists()e passare un elenco di elenchi di chiamate. Invece usando migliaia di volte glCallList(). (Voglio provare questo, perché penso che il vero MineCraft non usi i VBO. Corretto?)

Ci sono altri trucchi per migliorare le prestazioni?

Il profilo VisualVM mi ha mostrato questo (profilo per soli 33 frame, con una distanza di visione di 70 metri):

enter image description here

Profilatura con 40 metri (246 cornici):

enter image description here

Nota: sto sincronizzando molti metodi e blocchi di codice, perché sto generando blocchi in un altro thread. Penso che acquisire un lucchetto per un oggetto sia un problema di prestazioni quando si fa così tanto in un loop di gioco (ovviamente, sto parlando del tempo in cui c'è solo il loop di gioco e non vengono generati nuovi blocchi). È giusto?

Modifica: dopo aver rimosso alcuni synchronisedblocchi e altri piccoli miglioramenti. Le prestazioni sono già molto migliori. Ecco i miei nuovi risultati di profilazione con 70 metri:

enter image description here

Penso che sia abbastanza chiaro che questo selectVisibleBlocksè il problema qui.

Grazie in anticipo!
Martijn

Aggiornamento : dopo alcuni miglioramenti extra (come l'utilizzo di loop invece di ciascuno, il buffering delle variabili al di fuori dei loop, ecc ...), ora posso eseguire una distanza di visualizzazione 60 piuttosto buona.

Penso che implementerò i VBO il prima possibile.

PS: tutto il codice sorgente è disponibile su GitHub:
https://github.com/mcourteaux/CraftMania


2
Puoi darci un colpo di profilo a 40m in modo che possiamo vedere cosa potrebbe scalare più velocemente di un altro?
James,

Forse troppo specificato, ma se si considera, è solo chiedere tecniche su come velocizzare un gioco 3D, sembra interessante. Ma il titolo può spaventare ppl.
Gustavo Maciel,

@Gtoknu: cosa suggerisci come titolo?
Martijn Courteaux,

5
A seconda di chi chiedi, alcune persone direbbero che Minecraft non è nemmeno così veloce.
thedaian,

Penso che qualcosa come "Quali tecniche possono velocizzare un gioco 3D" dovrebbe essere molto meglio. Pensa a qualcosa, ma cerca di non usare la parola "migliore" o cerca di confrontarti con qualche altro gioco. Non possiamo dire esattamente cosa usano su alcuni giochi.
Gustavo Maciel,

Risposte:


15

Hai detto di fare l'abbattimento di frustum su singoli blocchi - prova a buttarlo fuori. La maggior parte dei blocchi di rendering dovrebbero essere completamente visibili o completamente invisibili.

Minecraft ricostruisce un buffer di elenco / vertice di visualizzazione (non so quale utilizzi) quando un blocco viene modificato in un determinato blocco, e anche io . Se si modifica l'elenco di visualizzazione ogni volta che la vista cambia, non si ottiene il vantaggio degli elenchi di visualizzazione.

Inoltre, sembra che tu stia usando blocchi di altezza mondiale. Nota che Minecraft utilizza blocchi cubici 16 × 16 × 16 per i suoi elenchi di visualizzazione, a differenza del caricamento e del salvataggio. Se lo fai, c'è ancora meno motivo per abbattere i singoli pezzi.

(Nota: non ho esaminato il codice di Minecraft. Tutte queste informazioni sono o sentito o le mie conclusioni osservando il rendering di Minecraft mentre gioco.)


Consigli più generali:

Ricorda che il rendering viene eseguito su due processori: CPU e GPU. Quando la frequenza dei fotogrammi è insufficiente, l' una o l'altra è la risorsa limitante : il programma è associato alla CPU o alla GPU (supponendo che non si stia scambiando o abbia problemi di pianificazione).

Se il tuo programma è in esecuzione al 100% della CPU (e non ha altre attività illimitate da completare), allora la tua CPU sta facendo troppo lavoro. Dovresti provare a semplificare il suo compito (ad es. Fare meno abbattimento) in cambio del fatto che la GPU faccia di più. Sospetto fortemente che questo sia il tuo problema, data la tua descrizione.

D'altra parte, se la GPU è il limite (purtroppo, di solito non ci sono comodi monitor di carico 0% -100%), dovresti pensare a come inviargli meno dati o richiedere che riempia meno pixel.


2
Grande riferimento, la tua ricerca menzionata sul tuo wiki mi è stata molto utile! +1
Gustavo Maciel,

@OP: rende solo le facce visibili (non i blocchi ). Un pezzo patologico ma monotonico 16x16x16 avrà quasi 800 facce visibili, mentre i blocchi contenuti avranno 24.000 facce visibili. Dopo averlo fatto, la risposta di Kevin contiene i prossimi miglioramenti più importanti.
AndrewS,

@KevinReid Ci sono alcuni programmi per aiutare con il debug delle prestazioni. La GPU AMD PerfStudio, ad esempio, ti dice se la sua CPU o GPU sono associate e sulla GPU quale componente è associato (trama vs frammento vs vertice, ecc.) E sono sicuro che anche Nvidia ha qualcosa di simile.
Akaltar

3

Cosa sta chiamando Vec3f.set così tanto? Se stai costruendo ciò che vuoi rendere da zero ogni fotogramma, è sicuramente lì che vorresti iniziare per accelerarlo. Non sono un utente OpenGL e non so molto su come viene eseguito il rendering di Minecraft, ma sembra che le funzioni matematiche che stai utilizzando ti stiano uccidendo proprio ora (guarda quanto tempo passi in loro e il numero di volte vengono chiamati - morte per mille tagli chiamandoli).

Idealmente, il tuo mondo verrebbe segmentato in modo tale da poter raggruppare elementi da renderizzare insieme, costruendo oggetti Buffer Vertex e riutilizzandoli su più frame. Dovresti modificare un VBO solo se il mondo che rappresenta cambia in qualche modo (come l'utente lo modifica). È quindi possibile creare / distruggere i VBO per ciò che si sta rappresentando in quanto è visibile per mantenere basso il consumo di memoria, si otterrebbe il colpo solo quando il VBO è stato creato anziché ogni fotogramma.

Se il conteggio delle "invocazioni" è corretto nel tuo profilo, stai chiamando moltissime cose moltissime volte. (10 milioni di chiamate a Vec3f.set ... ahi!)


Uso questo metodo per tonnellate di cose. Imposta semplicemente i tre valori per il vettore. È molto meglio che allocare ogni volta un nuovo oggetto.
Martijn Courteaux,

2

La mia descrizione (dalla mia sperimentazione) qui è applicabile:

Per il rendering voxel, cosa è più efficiente: VBO pre-made o uno shader di geometria?

Minecraft e il tuo codice probabilmente usano la pipeline a funzione fissa; i miei sforzi sono stati con GLSL ma l'essenza è generalmente applicabile, sento:

(Dalla memoria) ho creato un frustum che era mezzo blocco più grande del frustum dello schermo. Ho quindi testato i punti centrali di ogni blocco ( Minecraft ha 16 * 16 * 128 blocchi ).

Le facce in ciascuna hanno estensioni in un VBO di array di elementi (molte facce di blocchi condividono lo stesso VBO fino a quando non è "pieno"; pensa come malloc; quelli con la stessa trama nello stesso VBO dove possibile) e gli indici dei vertici per il nord le facce, le facce a sud e così via sono adiacenti anziché miste. Quando disegno, faccio una faccia a glDrawRangeElementsnord, con la normale già proiettata e normalizzata, in uniforme. Poi faccio le faccia a sud e così via, quindi le normali non sono in nessun VBO. Per ogni pezzo, devo solo emettere le facce che saranno visibili - solo quelle al centro dello schermo devono disegnare i lati sinistro e destro, per esempio; questo è semplice GL_CULL_FACEa livello di applicazione.

Il più grande speedup, iirc, stava abbattendo le facce interne quando poligonizzava ogni blocco.

È anche importante la gestione dell'atlante delle trame e l'ordinamento delle facce in base alle trame e l'inserimento delle facce con la stessa trama nello stesso vbo di quelle di altri blocchi. Volete evitare troppe modifiche alla trama e ordinare le facce per trama e così via minimizza il numero di span in glDrawRangeElements. Anche fondere facce della stessa piastrella adiacenti in rettangoli più grandi era un grosso problema. Parlo della fusione nell'altra risposta sopra citata.

Ovviamente poligonizzi solo quei pezzi che sono mai stati visibili, puoi scartare quei pezzi che non sono stati visibili per molto tempo e ripopoligilizzare i pezzi che vengono modificati (poiché si tratta di un evento raro rispetto al rendering).


Mi piace l'idea della tua ottimizzazione del frustum. Ma non stai confondendo i termini "blocco" e "pezzo" nella tua spiegazione?
Martijn Courteaux,

probabilmente sì. Un blocco di blocchi è un blocco di blocchi in inglese.
Sarà il

1

Da dove vengono tutti i tuoi confronti ( BlockDistanceComparator)? Se proviene da una funzione di ordinamento, potrebbe essere sostituito con un ordinamento radix (che è asintoticamente più veloce e non basato sul confronto)?

Guardando i tuoi tempi, anche se lo stesso ordinamento non è poi così male, la tua relativeToOriginfunzione viene chiamata due volte per ogni comparefunzione; tutti questi dati dovrebbero essere calcolati una volta. Dovrebbe essere più veloce ordinare una struttura ausiliaria, ad es

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

e quindi in pseudoCodice

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

Scusate se non è una struttura Java valida (non ho più toccato Java da quando ero studente) ma spero che abbiate capito.


Lo trovo divertente. Java non ha strutture. Bene, c'è qualcosa chiamato così nel mondo Java ma ha a che fare con i database, non è la stessa cosa. Possono creare una classe finale con membri pubblici, credo che funzioni.
Theraot,

1

Sì, usa i VBO e il CULL, ma questo vale praticamente per ogni gioco. Quello che vuoi fare è renderizzare il cubo solo se è visibile al giocatore, E se i blocchi si toccano in un modo specifico (diciamo un pezzo che non puoi vedere perché è sotterraneo) aggiungi i vertici dei blocchi e fai sembra quasi un "blocco più grande", o nel tuo caso, un pezzo. Questo è chiamato mesh avido e aumenta drasticamente le prestazioni. Sto sviluppando un gioco (basato su voxel) e utilizza un avido algoritmo di mesh.

Invece di rendere tutto così:

render

Lo rende così:

render2

L'aspetto negativo di questo è che devi fare più calcoli per pezzo sulla build del mondo iniziale, o se il giocatore rimuove / aggiunge un blocco.

praticamente ogni tipo di motore voxel ne ha bisogno per buone prestazioni.

Ciò che fa è verificare se la faccia del blocco sta toccando un'altra faccia del blocco, e in tal caso: renderizzare solo come una (o zero) faccia (e) di blocco. È un tocco costoso quando si esegue il rendering di blocchi molto velocemente.

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
E ne vale la pena? Sembra che un sistema LOD sarebbe più appropriato.
MichaelHouse

0

Sembrerebbe che il tuo codice stia annegando negli oggetti e nelle chiamate di funzione. Misurando i numeri, non sembra che stia accadendo alcun allineamento.

Potresti provare a trovare un diverso ambiente Java, o semplicemente fare confusione con le impostazioni di quello che hai, ma un modo chiaro e semplice di rendere il tuo codice, non veloce, ma molto meno lento è almeno internamente in Vec3f per fermarsi codifica OOO *. Rendi ogni metodo autonomo, non chiamare nessuno degli altri metodi solo per eseguire alcune attività umili.

Modifica: sebbene ci sia un sovraccarico ovunque, sembra che ordinare i blocchi prima del rendering sia il peggior mangiatore di prestazioni. È davvero necessario? In tal caso, dovresti probabilmente iniziare attraversando un ciclo e calcolare la distanza di ciascun blocco dall'origine, quindi ordinarlo per quello.

* Orientamento eccessivo agli oggetti


Sì, risparmierai memoria, ma perderai CPU! Quindi OOO non è troppo buono nei giochi in tempo reale.
Gustavo Maciel,

Non appena si avvia la profilazione (e non solo il campionamento), qualsiasi traccia che la JVM normalmente svanisce. È un po 'come la teoria quantistica, non può misurare qualcosa senza cambiare il risultato: p
Michael

@Gtoknu Questo non è universalmente vero, a un certo livello di OOO le chiamate di funzione iniziano a occupare più memoria di quanto non farebbe il codice inline. Direi che c'è una buona parte del codice in questione che riguarda il punto di pareggio della memoria.
aaaaaaaaaaaa

0

Potresti anche provare a suddividere le operazioni matematiche in operatori bit per bit. Se si dispone 128 / 16, provare a fare un operatore di bit a bit: 128 << 4. Questo aiuterà molto con i tuoi problemi. Non cercare di far funzionare le cose a tutta velocità. Aggiorna il tuo gioco a una velocità di 60 o qualcosa del genere, e analizzalo anche per altre cose, ma dovresti distruggere e posizionare i voxel o dovresti fare una lista di cose da fare, che farebbe cadere i tuoi fps. È possibile eseguire una frequenza di aggiornamento di circa 20 per le entità. E qualcosa come 10 per gli aggiornamenti e / o la generazione del mondo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.