Memorizzazione di voxel per un motore voxel in C ++


9

Sto cercando di scrivere un piccolo motore voxel perché è divertente, ma faccio fatica a trovare il modo migliore per memorizzare i voxel reali. Sono consapevole che avrò bisogno di pezzi di qualche tipo, quindi non ho bisogno di avere l'intero mondo in memoria, e sono consapevole di aver bisogno di renderli con prestazioni ragionevoli.

Ho letto di ocre e da quello che ho capito inizia con 1 cubo, e in quel cubo possono esserci altri 8 cubi, e in tutti quegli 8 cubi possono esserci altri 8 cubi ecc. Ma non penso che questo si adatti al mio motore voxel perché i miei cubi / oggetti voxel avranno tutti la stessa identica dimensione.

Quindi un'altra opzione è quella di creare un array di dimensioni 16 * 16 * 16 e avere un pezzo, e lo riempi di elementi. E le parti in cui non ci sono articoli avranno 0 come valore (0 = aria). Ma temo che questo sprecherà molta memoria e non sarà molto veloce.

Quindi un'altra opzione è un vettore per ogni blocco e riempilo con cubetti. E il cubo mantiene la sua posizione nel blocco. Ciò consente di risparmiare memoria (senza blocchi d'aria), ma rende molto più lento la ricerca di un cubo in una posizione specifica.

Quindi non riesco davvero a trovare una buona soluzione e spero che qualcuno mi possa aiutare in questo. Quindi cosa useresti e perché?

Ma un altro problema è il rendering. Basta leggere ogni blocco e inviarlo alla GPU usando OpenGL è facile, ma molto lento. Generare una mesh per blocco sarebbe meglio, ma ciò significa che ogni volta che rompo un blocco, devo ricostruire l'intero blocco che potrebbe richiedere un po 'di tempo causando un piccolo ma evidente singhiozzo, che ovviamente non voglio neanche. Quindi sarebbe più difficile. Quindi come renderei i cubi? Basta creare tutti i cubi in un buffer di vertici per blocco e renderlo e magari provare a inserirlo in un altro thread o c'è un altro modo?

Grazie!


1
Dovresti usare instancing per rendere i tuoi cubi. Puoi trovare un tutorial qui learnopengl.com/Advanced-OpenGL/Instancing . Per memorizzare i cubi: hai forti vincoli di memoria sull'hardware? 16 ^ 3 cubi non sembrano troppa memoria.
Turms,

@Turms Grazie per il tuo commento! Non ho forti vincoli di memoria, è solo un normale PC. Ma ho pensato, se ogni pezzo più grosso è il 50% di aria e il mondo è molto grande, allora ci deve essere un bel po 'di memoria sprecata. Ma probabilmente non è molto come dici tu. Quindi dovrei scegliere solo 16 * 16 * 16 pezzi con una quantità statica di blocchi? E inoltre, dici che dovrei usare l'istanza, è davvero necessario? La mia idea era quella di generare una mesh per ogni blocco, perché in questo modo posso tralasciare tutti i triangoli invisibili.

6
Non consiglio di usare l'istanziamento per i cubi come descrive Turms, ciò ridurrà solo le tue chiamate di disegno, ma non farà nulla per il superamento e le facce nascoste - in effetti ti lega le mani dalla risoluzione di quel problema, poiché per istanziare per lavorare tutti i cubi deve essere lo stesso: non è possibile eliminare le facce nascoste di alcuni cubi o unire le facce complanari in singoli poligoni più grandi.
DMGregory

Scegliere il miglior motore voxel può essere una sfida. La grande domanda da porsi è "quali operazioni devo fare sui miei voxel?" Questo guida le operazioni. Ad esempio, sei preoccupato di quanto sia difficile capire quale voxel è dove si trova un ottetto. Gli algoritmi dell'albero di ott sono ottimi per problemi che possono generare queste informazioni quando necessario mentre cammina sull'albero (spesso in modo ricorsivo). Se hai problemi specifici in cui questo è troppo costoso, puoi guardare altre opzioni.
Cort Ammon,

Un'altra grande domanda è quanto spesso i voxel vengono aggiornati. Alcuni algoritmi sono fantastici se possono pre-elaborare i dati per archiviarli in modo efficiente, ma meno efficienti se i dati vengono costantemente aggiornati (poiché i dati potrebbero in una simulazione fluida basata sulle particelle)
Cort Ammon,

Risposte:


23

Memorizzare i blocchi come posizioni e valori è in realtà molto inefficiente. Anche senza alcun sovraccarico causato dalla struttura o dall'oggetto in uso, è necessario memorizzare 4 valori distinti per blocco. Avrebbe senso usarlo solo con il metodo di "memorizzazione dei blocchi in array fissi" (quello che hai descritto in precedenza) quando solo un quarto dei blocchi sono solidi, e in questo modo non prendi nemmeno altri metodi di ottimizzazione in account.

Gli Octrees sono in realtà fantastici per i giochi basati su voxel, poiché sono specializzati nella memorizzazione di dati con funzionalità più grandi (ad es. Patch dello stesso blocco). Per illustrare questo, ho usato un quadrifoglio (fondamentalmente octrees in 2d):

Questo è il mio set iniziale contenente 32x32 tessere, che equivarrebbe a 1024 valori: inserisci qui la descrizione dell'immagine

Memorizzare questo come 1024 valori separati non sembra così inefficiente, ma una volta raggiunte dimensioni della mappa simili ai giochi, come Terraria , il caricamento delle schermate richiederebbe più secondi. E se lo aumenti alla terza dimensione, inizia a consumare tutto lo spazio nel sistema.

Quadtrees (o octrees in 3d) possono aiutare la situazione. Per crearne uno, puoi andare dalle tessere e raggrupparle insieme, oppure passare da una cella enorme e dividerla fino a raggiungere le tessere. Userò il primo approccio, perché è più facile da visualizzare.

Quindi, nella prima iterazione raggruppa tutto in celle 2x2 e se una cella contiene solo riquadri dello stesso tipo, elimini i riquadri e ne memorizzi il tipo. Dopo una ripetizione, la nostra mappa sarà simile a questa:

inserisci qui la descrizione dell'immagine

Le linee rosse segnano ciò che memorizziamo. Ogni quadrato ha solo 1 valore. Ciò ha portato la dimensione da 1024 valori a 439, con una riduzione del 57%.

Ma conosci il mantra . Facciamo un passo avanti e raggruppiamo questi in celle:

inserisci qui la descrizione dell'immagine

Ciò ha ridotto la quantità di valori memorizzati a 367. Questo è solo il 36% delle dimensioni originali.

Ovviamente devi fare questa divisione fino a quando ogni 4 celle adiacenti (8 blocchi adiacenti in 3d) all'interno di un blocco vengono archiviate all'interno di una cella, essenzialmente convertendo un blocco in una cella di grandi dimensioni.

Questo ha anche alcuni altri vantaggi, principalmente quando si fa una collisione, ma potresti voler creare un ottetto separato per quello, che si preoccupa solo del fatto che un singolo blocco sia solido o meno. In questo modo, invece di verificare la collisione per ogni blocco all'interno di un blocco, puoi semplicemente farlo contro le celle.


Grazie per la tua risposta! Sembra che gli ocree siano la strada da percorrere (dal momento che il mio motore voxel sarà in 3D) Ho una domanda frew che vorrei porre però: la tua ultima foto mostra le parti nere con quadrati più grandi, dal momento che ho intenzione di avere un minecraft come motore in cui è possibile modificare il terreno voxel, preferirei mantenere tutto ciò che ha un blocco della stessa dimensione, perché altrimenti renderebbe le cose molto complicate, è possibile giusto? (Semplificherei comunque le fessure di vuoto / aria del corso.) Secondo , c'è una sorta di tutorial su come si potrebbe programmare un ottetto? Grazie!

7
@ appmaker1358 non è affatto un problema. Se il giocatore prova a modificare un blocco di grandi dimensioni, allora lo spezzi in blocchi più piccoli in quel momento . Non è necessario memorizzare i valori 16x16x16 di "rock" quando si potrebbe dire invece "tutto questo pezzo è solido rock" fino a quando non sarà più vero.
DMGregory

1
@ appmaker1358 Come diceva DMGregory, l'aggiornamento dei dati archiviati in un octree è relativamente semplice. Tutto quello che devi fare è dividere la cella in cui è avvenuta la modifica fino a quando ogni sotto-cella contiene solo un singolo tipo di blocco. Ecco un esempio interattivo con un quadrifoglio . Anche generarne uno è semplice. Si crea una cella grande, che contiene completamente il blocco, quindi si ricorre in modo ricorsivo a ogni cella foglia (celle che non hanno figli), si controlla se la parte del terreno che rappresenta contiene più tipi di blocchi, se sì, si suddivide il cell
Bálint,

@ appmaker1358 Il problema più grande è in realtà il contrario: assicurarsi che l'octree non si riempia di foglie con un solo blocco, cosa che può facilmente accadere in un gioco in stile Minecraft. Tuttavia, ci sono molte soluzioni al problema: si tratta solo di scegliere quello che ritieni appropriato. E diventa un vero problema solo quando ci sono molti edifici in corso.
Luaan,

Gli ocre non sono necessariamente la scelta migliore. ecco una lettura interessante: 0fps.net/2012/01/14/an-analysis-of-minecraft-like-engines
Polygnome

7

Esistono octrees per risolvere esattamente il problema che descrivi, consentendo una densa memorizzazione di dati sparsi senza grandi tempi di ricerca.

Il fatto che i tuoi voxel abbiano le stesse dimensioni significa solo che il tuo octree ha una profondità fissa. per esempio. per un pezzo 16x16x16, sono necessari al massimo 5 livelli di albero:

  • chunk root (16x16x16)
    • ottante di primo livello (8x8x8)
      • ottante di secondo livello (4x4x4)
        • ottante di terzo livello (2x2x2)
          • voxel singolo (1x1x1)

Questo significa che hai al massimo 5 passaggi per scoprire se c'è un voxel in una particolare posizione nel blocco:

  • chunk root: l'intero chunk ha lo stesso valore (es. tutta l'aria)? Se è così, abbiamo finito. Altrimenti...
    • primo livello: l'ottante che contiene questa posizione ha lo stesso valore? Altrimenti...
      • secondo livello...
        • terzo livello ...
          • ora stiamo affrontando un singolo voxel e possiamo restituire il suo valore.

Molto più breve rispetto alla scansione anche dell'1% del percorso attraverso una serie di fino a 4096 voxel!

Si noti che questo ci consente di comprimere i dati ovunque ci sia un ottante completo dello stesso valore, indipendentemente dal fatto che quel valore sia tutta aria o tutta roccia o qualsiasi altra cosa. È solo dove gli ottanti contengono valori misti che dobbiamo suddividere ulteriormente, fino al limite dei nodi foglia a singolo voxel.


Per rivolgersi ai figli di un pezzo, in genere procediamo nell'ordine di Morton , qualcosa del genere:

  1. X- Y- Z-
  2. X- Y- Z +
  3. X- Y + Z-
  4. X- Y + Z +
  5. X + Y- Z-
  6. X + Y- Z +
  7. X + Y + Z-
  8. X + Y + Z +

Quindi, la navigazione del nostro nodo Octree potrebbe assomigliare a questa:

GetOctreeValue(OctreeNode node, int depth, int3 nodeOrigin, int3 queryPoint) {
    if(node.IsAllOneValue)
        return node.Value;

    int childIndex =  0;
    childIndex += (queryPoint.x > nodeOrigin.x) ? 4 : 0;
    childIndex += (queryPoint.y > nodeOrigin.y) ? 2 : 0;
    childIndex += (queryPoint.z > nodeOrigin.z) ? 1 : 0;

    OctreeNode child = node.GetChild(childIndex);

    return GetOctreeValue(
                child, 
                depth + 1,
                nodeOrigin + childOffset[depth, childIndex],
                queryPoint
    );
}

Grazie per la tua risposta! Sembra che gli ocree siano la strada da percorrere. Ma ho 2 domande, dici che gli ottici sono più veloci della scansione attraverso un array, il che è corretto. Ma non avrei bisogno di farlo poiché l'array potrebbe essere statico, il che significa che posso calcolare dove si trova il cubo di cui ho bisogno. Quindi perché dovrei scannerizzare? Seconda domanda, nell'ultimo livello dell'ottavo (il 1x1x1), come faccio a sapere quale cubo è dove, dal momento che se lo capisco correttamente, e il nodo octree ha altri 8 nodi, come fai a sapere quale nodo appartiene a quale posizione 3d ? (O dovrei ricordarmelo?)

Sì, hai già trattato il caso di un array esaustivo di voxel 16x16x16 nella tua domanda e sembra rifiutare l'impronta di memoria 4K per blocco (supponendo che ogni ID voxel sia un byte) eccessivo. La ricerca che hai citato arriva quando si memorizza un elenco di voxel con una posizione, costringendoti a scansionare l'elenco per trovare il voxel nella posizione di destinazione. 4096 qui è il limite superiore di quella lunghezza dell'elenco - in genere sarà più piccolo di quello, ma generalmente ancora più profondo di una corrispondente ricerca di ottici.
DMGregory
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.