Come posso implementare l'illuminazione basata su voxel con l'occlusione in un gioco in stile Minecraft?


13

Sto usando C # e XNA. Il mio attuale algoritmo per l'illuminazione è un metodo ricorsivo. Tuttavia, è costoso , al punto in cui un pezzo 8x128x8 calcolato ogni 5 secondi.

  • Esistono altri metodi di illuminazione che renderanno ombre a oscurità variabile?
  • O il metodo ricorsivo è buono e forse sto solo sbagliando?

Sembra che le cose ricorsive siano fondamentalmente costose (costrette a passare circa 25k blocchi per blocco). Stavo pensando di utilizzare un metodo simile al ray tracing, ma non ho idea di come funzionerebbe. Un'altra cosa che ho provato è stata la memorizzazione di sorgenti luminose in un elenco e per ogni blocco ottenere la distanza da ciascuna sorgente luminosa e usarla per illuminarla al livello corretto, ma poi l'illuminazione passava attraverso i muri.

Il mio attuale codice di ricorsione è sotto. Questo viene chiamato da qualsiasi punto del blocco che non ha un livello di luce pari a zero, dopo aver eliminato e aggiunto nuovamente luce solare e fiaccolata.

world.get___atè una funzione che può ottenere blocchi al di fuori di questo blocco (questo è all'interno della classe di blocco). Locationè la mia struttura che è simile a Vector3, ma utilizza numeri interi anziché valori in virgola mobile. light[,,]è la mappa luminosa per il pezzo.

    private void recursiveLight(int x, int y, int z, byte lightLevel)
    {
        Location loc = new Location(x + chunkx * 8, y, z + chunky * 8);
        if (world.getBlockAt(loc).BlockData.isSolid)
            return;
        lightLevel--;
        if (world.getLightAt(loc) >= lightLevel || lightLevel <= 0)
            return;
        if (y < 0 || y > 127 || x < -8 || x > 16 || z < -8 || z > 16)
            return;
        if (x >= 0 && x < 8 && z >= 0 && z < 8)
            light[x, y, z] = lightLevel;

        recursiveLight(x + 1, y, z, lightLevel);
        recursiveLight(x - 1, y, z, lightLevel);
        recursiveLight(x, y + 1, z, lightLevel);
        recursiveLight(x, y - 1, z, lightLevel);
        recursiveLight(x, y, z + 1, lightLevel);
        recursiveLight(x, y, z - 1, lightLevel);
    }

1
Qualcosa di orribilmente sbagliato se stai facendo 2 milioni di blocchi per blocco - specialmente perché ci sono solo 8.192 blocchi in realtà in un blocco 8 * 128 * 8. Cosa potresti fare che stai attraversando ogni blocco ~ 244 volte? (potrebbe essere 255?)
doppelgreener,

1
Ho sbagliato la mia matematica. Scusa: P. Mutevole. Ma il motivo per cui devi andare anche se così tanti è che devi "sparare" da ogni blocco fino a raggiungere un livello di luce maggiore di quello che hai impostato. Ciò significa che ogni blocco potrebbe essere sovrascritto 5-10 volte prima di raggiungere il livello di luce reale. 8x8x128x5 = molto

2
Come stai conservando i tuoi Voxels? Questo è importante per ridurre i tempi di attraversamento.
Samaursa,

1
Puoi pubblicare il tuo algoritmo di illuminazione? (chiedi se lo stai facendo male, non ne abbiamo idea)
doppelgreener

Li sto archiviando in una matrice di "Blocchi" e un Blocco è costituito da un enum per materiale, oltre a un byte di metadati per uso futuro.

Risposte:


6
  1. Ogni luce ha una posizione precisa (virgola mobile), e delimita sfera definito da un valore di raggio di luce scalare, LR.
  2. Ogni voxel ha una posizione precisa (in virgola mobile) al suo centro, che puoi facilmente calcolare dalla sua posizione nella griglia.
  3. Attraversa ciascuno degli 8192 voxel una sola volta, e per ciascuno, vedi se rientra in ciascuno dei volumi di delimitazione sferica di N luci controllando |VP - LP| < LR, dove VP è il vettore di posizione del voxel rispetto all'origine ed LPè il vettore di posizione della luce rispetto all'origine. Per ogni luce il cui raggio voxel corrente è risultato essere in, incrementa è fattore chiaro dalla distanza dal centro luce |VP - LP|. Se normalizzi quel vettore e poi ottieni la sua grandezza, questo sarà compreso nell'intervallo 0,0-> 1,0. Il livello di luce massimo che un voxel può raggiungere è 1.0.

Il tempo di esecuzione è O(s^3 * n), dov'è sla lunghezza laterale (128) della tua regione voxel en è il numero di sorgenti luminose. Se le tue sorgenti luminose sono statiche, questo non è un problema. Se le tue sorgenti luminose si muovono in tempo reale, puoi lavorare esclusivamente sui delta anziché ricalcolare l'intero shebang ad ogni aggiornamento.

Potresti persino memorizzare i voxel che ogni luce influenza, come riferimenti all'interno di quella luce. In questo modo, quando la luce si sposta o viene distrutta, puoi passare attraverso quell'elenco, regolando i valori della luce di conseguenza, piuttosto che dover attraversare di nuovo l'intera griglia cubica.


Se ho capito bene il suo algoritmo, prova a fare una sorta di pseudo-radiosity, permettendo alla luce di raggiungere luoghi lontani anche se ciò significa che deve "aggirare" alcuni angoli. O, in altre parole, un algoritmo di riempimento dell'inondazione degli spazi "vuoti" (non solidi) con una distanza massima limitata dall'origine (la sorgente luminosa) e la distanza (e da essa, attenuazione della luce) calcolata in base a il percorso più breve verso l'origine. Quindi, non proprio quello che proponi attualmente.
Martin Sojka,

Grazie per il dettaglio @MartinSojka. Sì, sembra più un riempimento intelligente. Con qualsiasi tentativo di illuminazione globale, i costi tendono ad essere elevati anche con ottimizzazioni intelligenti. Quindi è bene tentare prima questi problemi in 2D, e se sono anche lontanamente costosi, sappi che avrai una sfida definitiva per te in 3D.
Ingegnere

4

Minecraft stesso non fa la luce del sole in questo modo.

Basta riempire la luce del sole dall'alto verso il basso, ogni strato sta raccogliendo luce dai voxel vicini nel livello precedente con attenuazione. Molto veloce - passaggio singolo, nessun elenco, nessuna struttura di dati, nessuna ricorsione.

Devi aggiungere torce e altre luci non allaganti in un passaggio successivo.

Ci sono molti altri modi per farlo, inclusa la propagazione della luce direzionale, ecc., Ma sono ovviamente più lenti e devi capire se vuoi investire nel realismo aggiuntivo date quelle penalità.


Aspetta, come fa esattamente Minecraft? Non riuscivo a capire esattamente cosa stavi dicendo ... Cosa significa "ogni livello sta raccogliendo luce dai voxel vicini nel livello precedente con attenuazione"?

2
Inizia con il livello più in alto (sezione di altezza costante). Riempilo di luce solare. Quindi vai al livello sottostante e ogni voxel lì ottiene la sua illuminazione dai voxel più vicini nel livello precedente (sopra di esso). Metti la luce zero in voxel solidi. Hai un paio di modi per decidere il "kernel", i pesi dei contributi dai voxel sopra, Minecraft usa il valore massimo trovato, ma lo diminuisce di 1 se la propagazione non è diretta. Questa è l'attenuazione laterale, quindi le colonne verticali sbloccate di voxel otterranno la piena propagazione della luce solare e curve di luce intorno agli angoli.
Bjorn Wesen,

1
Si noti che questo metodo non è in alcun modo basato su alcuna fisica reale :) Il problema principale è che stai essenzialmente cercando di approssimare una luce non direzionale (la dispersione atmosferica) E di far rimbalzare la radiosità con una semplice euristica. Sembra abbastanza buono.
Bjorn Wesen,

3
Che dire di un "labbro" che pende, come fa la luce ad arrivare lassù? In che modo la luce viaggia verso l'alto? Quando vai solo dall'alto verso il basso, non puoi tornare indietro verso l'alto per riempire gli strapiombi. Anche torce / altre fonti luminose. come lo faresti? (potevano solo scendere!)

1
@Felheart: è da un po 'che lo guardo, ma in sostanza c'è un livello minimo di luce ambientale che di solito è sufficiente per la parte inferiore degli sbalzi, quindi non sono completamente neri. Quando l'ho implementato da solo, ho aggiunto un secondo passaggio da down-> up, ma non ho visto grandi miglioramenti estetici rispetto al metodo ambientale. Le torce / i punti luce devono essere gestiti separatamente - Penso che puoi vedere il modello di propagazione usato in MC se metti una torcia al centro di un muro e provi un po '. Nei miei test, li propongo in un campo luminoso separato, quindi aggiungo.
Bjorn Wesen,

3

Qualcuno ha detto di rispondere alla tua domanda se l'hai capito, quindi sì. Ho capito un metodo.

Quello che sto facendo è questo: in primo luogo, creare un array booleano 3d di "blocchi già modificati" sovrapposti al blocco. Quindi, riempi la luce del sole, la luce della torcia, ecc. Se hai cambiato qualcosa, premi i "blocchi modificati" in quella posizione su true. Inoltre, vai e cambia ogni blocco solido (e quindi non è necessario calcolare l'illuminazione per) in "già modificato".

Ora per le cose pesanti: vai attraverso l'intero pezzo con 16 passaggi (per ogni livello di luce), e se il suo "già cambiato" continua. Quindi ottenere il livello di luce per i blocchi attorno a se stesso. Ottieni il massimo livello di luce. Se quel livello di luce è uguale al livello di luce dei passaggi correnti, imposta il blocco su cui ti trovi sul livello corrente e imposta "già modificato" per quella posizione su vero. Continua.

So che è complicato, ho cercato di spiegare il mio meglio. Ma il fatto importante è che funziona ed è veloce.


2

Suggerirei un algoritmo che in qualche modo combina la tua soluzione multi-pass con il metodo ricorsivo originale ed è molto probabilmente un po 'più veloce di entrambi.

Avrai bisogno di 16 elenchi (o qualsiasi tipo di raccolta) di blocchi, uno per ogni livello di luce. (In realtà, ci sono modi per ottimizzare questo algoritmo per utilizzare un minor numero di elenchi, ma questo modo è più facile da descrivere.)

Innanzitutto, cancella gli elenchi e imposta il livello di luce di tutti i blocchi su zero, quindi inizializza le sorgenti luminose come fai nella tua soluzione attuale. Dopo (o durante), aggiungi tutti i blocchi con un livello di luce diverso da zero all'elenco corrispondente.

Ora, passa attraverso l'elenco dei blocchi con livello di luce 16. Se uno dei blocchi adiacenti ad essi ha un livello di luce inferiore a 15, imposta il loro livello di luce su 15 e aggiungili all'elenco appropriato. (Se fossero già in un altro elenco, puoi rimuoverli da esso, ma non fa alcun danno anche se non lo fai.)

Quindi ripetere lo stesso per tutti gli altri elenchi, in ordine decrescente di luminosità. Se scopri che un blocco nella lista ha già un livello di luce più alto di quanto dovrebbe per essere in quella lista, puoi presumere che sia già stato elaborato e nemmeno preoccuparti di controllare i suoi vicini. (Ancora una volta, potrebbe essere più veloce controllare solo i vicini - dipende da quanto spesso accade. Probabilmente dovresti provarlo in entrambi i modi e vedere quale modo è più veloce.)

Si può notare che non ho specificato come archiviare gli elenchi; praticamente qualsiasi implementazione ragionevole dovrebbe farlo, purché l'inserimento di un determinato blocco e l'estrazione di un blocco arbitrario siano entrambe operazioni veloci. Dovrebbe funzionare un elenco collegato, ma lo sarebbe anche qualsiasi implementazione decente a metà degli array a lunghezza variabile. Usa semplicemente ciò che funziona meglio per te.


Addendum: se la maggior parte delle tue luci non si muove molto spesso (e nemmeno le pareti), potrebbe essere ancora più veloce memorizzare, per ogni blocco illuminato, un puntatore alla sorgente luminosa che determina il suo livello di luce (o verso uno dei loro, se molti sono legati). In questo modo, puoi evitare quasi completamente gli aggiornamenti globali dell'illuminazione: se viene aggiunta una nuova sorgente luminosa (o una esistente già illuminata), devi solo fare un singolo passaggio di illuminazione ricorsiva per i blocchi circostanti, mentre se ne viene rimosso (o oscurato), devi solo aggiornare quei blocchi che puntano ad esso.

Puoi persino gestire le modifiche al muro in questo modo: quando un muro viene rimosso, avvia un nuovo passaggio di illuminazione ricorsiva in quel blocco; quando ne viene aggiunto uno, eseguire un ricalcolo dell'illuminazione per tutti i blocchi che puntano alla stessa sorgente luminosa del blocco appena murato.

(Se si verificano contemporaneamente diversi cambiamenti di illuminazione, ad esempio se si sposta una luce, che conta come una rimozione e un'aggiunta, è necessario combinare gli aggiornamenti in uno solo, utilizzando l'algoritmo sopra. In pratica, azzerare il livello di luce di tutti blocchi che puntano a sorgenti luminose rimosse, aggiungere eventuali blocchi illuminati che li circondano, nonché eventuali nuove fonti luminose (o fonti luminose esistenti nelle aree azzerate) agli elenchi appropriati ed eseguire gli aggiornamenti come sopra.)

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.