Per quanto riguarda Java vs C ++, ho scritto un motore voxel in entrambi (versione C ++ mostrata sopra). Scrivo anche motori voxel dal 2004 (quando non erano di moda). :) Posso dire con poca esitazione che le prestazioni del C ++ sono di gran lunga superiori (ma è anche più difficile da programmare). Dipende meno dalla velocità di calcolo e dalla gestione della memoria. Giù le mani, quando stai allocando / deallocando tutti i dati di ciò che è in un mondo voxel, C (++) è il linguaggio da battere. però, dovresti pensare al tuo obiettivo. Se le prestazioni sono la tua massima priorità, scegli C ++. Se vuoi solo scrivere un gioco senza prestazioni all'avanguardia, Java è sicuramente accettabile (come evidenziato da Minecraft). Esistono molti casi banali / marginali, ma in generale ci si può aspettare che Java funzioni circa 1,75-2,0 volte più lentamente del C ++ (ben scritto). Puoi vedere una versione del mio motore scarsamente ottimizzata in azione qui (EDIT: versione più recente qui ). Mentre la generazione di blocchi può sembrare lenta, tieni presente che sta generando volumetricamente diagrammi 3D di voronoi, calcolando normali di superficie, illuminazione, AO e ombre sulla CPU con metodi a forza bruta. Ho provato varie tecniche e riesco a ottenere una generazione di blocchi circa 100 volte più veloce utilizzando varie tecniche di memorizzazione nella cache e istanziazione.
Per rispondere al resto della domanda, ci sono molte cose che puoi fare per migliorare le prestazioni.
- Caching. Ovunque sia possibile, è necessario calcolare i dati una volta. Ad esempio, creo l'illuminazione nella scena. Potrebbe utilizzare l'illuminazione dinamica (nello spazio dello schermo, come post-processo), ma cuocere l'illuminazione significa che non devo passare le normali per i triangoli, il che significa ...
Passa il minor numero di dati possibile alla scheda video. Una cosa che le persone tendono a dimenticare è che più dati passi alla GPU, più tempo ci vuole. Passo in un unico colore e in una posizione di vertice. Se voglio fare cicli diurni / notturni, posso semplicemente eseguire la classificazione dei colori, oppure posso ricalcolare la scena mentre il sole cambia gradualmente.
Poiché il trasferimento di dati alla GPU è così costoso, è possibile scrivere un motore nel software che è più veloce per alcuni aspetti. Il vantaggio del software è che può fare tutti i tipi di manipolazione dei dati / accesso alla memoria che semplicemente non è possibile su una GPU.
Gioca con la dimensione del lotto. Se si utilizza una GPU, le prestazioni possono variare notevolmente in base alla dimensione di ciascun array di vertici che si passa. Di conseguenza, gioca con le dimensioni dei pezzi (se usi pezzi). Ho scoperto che i blocchi 64x64x64 funzionano abbastanza bene. Non importa cosa, mantieni i tuoi cubi cubici (senza prismi rettangolari). Ciò renderà la codifica e le varie operazioni (come le trasformazioni) più facili e, in alcuni casi, più performanti. Se memorizzi un solo valore per la lunghezza di ogni dimensione, tieni presente che si tratta di due registri in meno che vengono scambiati durante il calcolo.
Considera gli elenchi di visualizzazione (per OpenGL). Anche se sono alla "vecchia" maniera, possono essere più veloci. Devi inserire un elenco di visualizzazione in una variabile ... se chiami le operazioni di creazione dell'elenco di visualizzazione in tempo reale, sarà ungodly lento. Come è più veloce un elenco di visualizzazione? Aggiorna solo lo stato, rispetto agli attributi per vertice. Questo significa che posso passare fino a sei facce, quindi un colore (contro un colore per ogni vertice del voxel). Se stai usando GL_QUADS e voxel cubici, questo potrebbe risparmiare fino a 20 byte (160 bit) per voxel! (15 byte senza alfa, anche se di solito si desidera mantenere le cose allineate a 4 byte.)
Uso un metodo a forza bruta per il rendering di "blocchi" o pagine di dati, che è una tecnica comune. A differenza di octrees, è molto più facile / veloce leggere / elaborare i dati, anche se molto meno compatibile con la memoria (tuttavia, in questi giorni è possibile ottenere 64 gigabyte di memoria per $ 200- $ 300) ... non che l'utente medio abbia questo. Ovviamente, non è possibile allocare un enorme array per tutto il mondo (un set di voxel 1024x1024x1024 è 4 gigabyte di memoria, supponendo che per voxel venga utilizzato un int a 32 bit). Quindi allocare / deallocare molti piccoli array, in base alla loro vicinanza al visualizzatore. È inoltre possibile allocare i dati, ottenere l'elenco di visualizzazione necessario, quindi scaricare i dati per risparmiare memoria. Penso che la combinazione ideale potrebbe essere quella di utilizzare un approccio ibrido di ocre e array: archiviare i dati in un array durante la generazione procedurale del mondo, l'illuminazione, ecc.
Rendering vicino al lontano ... un pixel tagliato viene risparmiato tempo. La gpu genererà un pixel se non supera il test del buffer di profondità.
Rendering solo blocchi / pagine nella finestra (autoesplicativo). Anche se la gpu sa come tagliare i poligoni al di fuori della finestra, passare questi dati richiede ancora tempo. Non so quale sarebbe la struttura più efficiente per questo ("vergognosamente", non ho mai scritto un albero BSP), ma anche un semplice raycast su una base per blocco potrebbe migliorare le prestazioni, e ovviamente testare contro il frustum di visualizzazione sarebbe risparmia tempo.
Informazioni ovvie, ma per i neofiti: rimuovi ogni singolo poligono che non si trova in superficie, ovvero se un voxel è composto da sei facce, rimuovi le facce che non vengono mai visualizzate (toccano un altro voxel).
Come regola generale di tutto ciò che fai in programmazione: CACHE LOCALITY! Se riesci a mantenere le cose nella cache locale (anche per un breve periodo di tempo, ciò farà una differenza enorme. Ciò significa mantenere i tuoi dati congruenti (nella stessa area di memoria) e non cambiare le aree di memoria per elaborarle troppo spesso. , idealmente, lavora su un blocco per thread e mantieni quella memoria esclusiva per il thread. Questo non si applica solo alla cache della CPU. Pensa alla gerarchia della cache in questo modo (dalla più lenta alla più veloce): rete (cloud / database / ecc.) -> disco rigido (ottieni un SSD se non ne hai già uno), ram (ottieni un canale tripple o RAM maggiore se non lo hai già), cache (s) della CPU, registri. Cerca di mantenere i tuoi dati attivi quest'ultimo fine, e non scambiarlo più del necessario.
Threading. Fallo. I mondi Voxel sono adatti per il threading, poiché ogni parte può essere calcolata (principalmente) indipendentemente dagli altri ... Ho visto letteralmente un miglioramento quasi 4x (su un Core i7 a 4 core, 8 thread) nella generazione procedurale mondiale quando ho scritto il routine per l'infilatura.
Non utilizzare tipi di dati char / byte. O pantaloncini. Il tuo consumatore medio avrà un moderno processore AMD o Intel (come probabilmente tu). Questi processori non hanno registri a 8 bit. Calcolano i byte mettendoli in uno slot a 32 bit, quindi riconvertendoli (forse) in memoria. Il tuo compilatore può fare ogni sorta di voodoo, ma l'uso di un numero a 32 o 64 bit ti darà i risultati più prevedibili (e più veloci). Allo stesso modo, un valore "bool" non richiede 1 bit; il compilatore utilizzerà spesso 32 bit completi per un bool. Potrebbe essere allettante eseguire determinati tipi di compressione sui dati. Ad esempio, è possibile memorizzare 8 voxel come numero singolo (2 ^ 8 = 256 combinazioni) se fossero tutti dello stesso tipo / colore. Tuttavia, devi pensare alle conseguenze di questo: potrebbe risparmiare molta memoria, ma può anche ostacolare le prestazioni, anche con un piccolo tempo di decompressione, perché anche quella piccola quantità di tempo extra si ridimensiona cubicamente con le dimensioni del tuo mondo. Immagina di calcolare un raycast; per ogni fase del raycast, dovresti eseguire l'algoritmo di decompressione (a meno che non ti venga in mente un modo intelligente di generalizzare il calcolo per 8 voxel in una fase del raggio).
Come menziona Jose Chavez, il modello di design dei pesi mosca può essere utile. Proprio come useresti una bitmap per rappresentare una tessera in un gioco 2D, puoi costruire il tuo mondo da diversi tipi di tessere 3D (o blocchi). Il rovescio della medaglia a questo è la ripetizione delle trame, ma puoi migliorarlo usando trame varianza che si incastrano. Come regola generale, si desidera utilizzare l'istanza ovunque sia possibile.
Evita l'elaborazione dei vertici e dei pixel nello shader quando emetti la geometria. In un motore voxel avrai inevitabilmente molti triangoli, quindi anche un semplice pixel shader può ridurre notevolmente i tempi di rendering. È meglio eseguire il rendering su un buffer, quindi fare pixel shader come post-processo. Se non riesci a farlo, prova a fare calcoli nel tuo shader di vertice. Altri calcoli dovrebbero essere inseriti nei dati del vertice ove possibile. Passaggi aggiuntivi diventano molto costosi se è necessario eseguire nuovamente il rendering di tutta la geometria (come la mappatura dell'ombra o la mappatura dell'ambiente). A volte è meglio rinunciare a una scena dinamica a favore di dettagli più ricchi. Se il tuo gioco ha scene modificabili (ad es. Terreno distruttibile) puoi sempre ricalcolare la scena man mano che le cose vengono distrutte. La ricompilazione non è costosa e dovrebbe richiedere meno di un secondo.
Rilassa i loop e mantieni le matrici piatte! Non farlo:
for (i = 0; i < chunkLength; i++) {
for (j = 0; j < chunkLength; j++) {
for (k = 0; k < chunkLength; k++) {
MyData[i][j][k] = newVal;
}
}
}
//Instead, do this:
for (i = 0; i < chunkLengthCubed; i++) {
//figure out x, y, z index of chunk using modulus and div operators on i
//myData should have chunkLengthCubed number of indices, obviously
myData[i] = newVal;
}
EDIT: Attraverso test più approfonditi, ho scoperto che questo può essere sbagliato. Usa la custodia che funziona meglio per il tuo scenario. Generalmente, le matrici dovrebbero essere piatte, ma l'utilizzo di loop multi-indice può spesso essere più veloce a seconda del caso