Accelerare la generazione di texture procedurali


14

Di recente ho iniziato a lavorare su un gioco che si svolge in un sistema solare generato proceduralmente. Dopo un po 'di una curva di apprendimento (non avevo mai lavorato con Scala, OpenGL 2 ES o Libgdx prima), ho una demo tecnica di base che gira intorno a un singolo pianeta strutturato in modo procedurale:

inserisci qui la descrizione dell'immagine

Il problema in cui mi imbatto è la prestazione della generazione di texture. Una rapida panoramica di quello che sto facendo: un pianeta è un cubo che è stato deformato in una sfera. Su ciascun lato, viene applicata una trama anxn (ad es. 256 x 256), che sono raggruppate in una trama 8n xn che viene inviata allo shader di frammento. Gli ultimi due spazi non vengono utilizzati, sono solo lì per assicurarsi che la larghezza sia una potenza di 2. La trama è attualmente generata sulla CPU, utilizzando la versione aggiornata 2012 dell'algoritmo di rumore simplex collegato al documento 'Simplex rumore demistificato ". La scena che sto usando per testare l'algoritmo contiene due sfere: il pianeta e lo sfondo. Entrambi usano una trama in scala di grigi composta da sei ottave di rumore simplex 3D, quindi per esempio se scegliamo 128x128 come dimensione della trama ci sono 128 x 128 x 6 x 2 x 6 = circa 1,2 milioni di chiamate alla funzione rumore.

Il più vicino che raggiungerai al pianeta riguarda ciò che viene mostrato nello screenshot e poiché la risoluzione target del gioco è 1280x720, ciò significa che preferirei usare trame 512x512. Combina che con il fatto che le trame reali saranno ovviamente più complicate del rumore di base (ci sarà una trama diurna e notturna, miscelata nello shader di frammenti basato sulla luce solare e una maschera speculare. Ho bisogno di rumore per i continenti, variazione di colore del terreno , nuvole, luci della città, ecc.) e stiamo osservando qualcosa come 512 x 512 x 6 x 3 x 15 = 70 milioni di chiamate al rumore per il solo pianeta. Nel gioco finale, ci saranno attività quando si viaggia tra i pianeti, quindi un'attesa di 5 o 10 secondi, possibilmente 20, sarebbe accettabile poiché posso calcolare la trama in background mentre viaggio, anche se ovviamente più veloce è, meglio è.

Tornando alla nostra scena di test, le prestazioni sul mio PC non sono troppo terribili, anche se ancora troppo lente considerando che il risultato finale sarà circa 60 volte peggiore:

128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s

Questo dopo che ho spostato tutto il codice critico per le prestazioni su Java, dal momento che provare a farlo in Scala era molto peggio. L'esecuzione sul mio telefono (un Samsung Galaxy S3), tuttavia, produce un risultato più problematico:

128x128 :  2s
256x256 :  7s
512x512 : 29s

Già troppo a lungo, e questo non è nemmeno un fattore di fatto nel fatto che ci saranno minuti anziché secondi nella versione finale. Chiaramente qualcosa deve essere fatto. Personalmente, vedo alcune potenziali strade, anche se non sono ancora particolarmente appassionato di nessuno di essi:

  • Non ricalcolare le trame, ma lascia che lo shader di frammenti calcoli tutto. Probabilmente non è possibile, perché a un certo punto avevo lo sfondo come quad a schermo intero con un pixel shader e ho ottenuto circa 1 fps sul mio telefono.
  • Utilizzare la GPU per eseguire il rendering della trama una volta, archiviarla e utilizzare la trama memorizzata da quel momento in poi. Upside: potrebbe essere più veloce di farlo sulla CPU poiché la GPU dovrebbe essere più veloce nei calcoli in virgola mobile. Unico inconveniente: gli effetti che non possono (facilmente) essere espressi come funzioni del rumore simplex (ad es. Vortici di pianeti gassosi, crateri lunari, ecc.) Sono molto più difficili da codificare in GLSL che in Scala / Java.
  • Calcola una grande quantità di trame di rumore e spediscile con l'applicazione. Vorrei evitarlo, se possibile.
  • Abbassa la risoluzione. Mi offre un guadagno in termini di prestazioni 4x, il che non è abbastanza, inoltre perdo molta qualità.
  • Trova un algoritmo di rumore più veloce. Se qualcuno ne ha uno, sono tutto orecchi, ma il simplex dovrebbe già essere più veloce del perlin.
  • Adotta uno stile pixel art, che consente trame a bassa risoluzione e meno ottave di rumore. Mentre inizialmente immaginavo il gioco in questo stile, sono arrivato a preferire un approccio realistico.
  • Sto facendo qualcosa di sbagliato e le prestazioni dovrebbero già essere di uno o due ordini di grandezza migliori. In questo caso, per favore fatemi sapere.

Se qualcuno ha suggerimenti, suggerimenti, soluzioni alternative o altri commenti su questo problema, mi piacerebbe ascoltarli.

In risposta a Layoric, ecco il codice che sto usando:

//The function that generates the simplex noise texture
public static Texture simplex(int size) {
    byte[] data = new byte[size * size * columns * 4];
    int offset = 0;
    for (int y = 0; y < size; y++) {
        for (int s = 0; s < columns; s++) {
            for (int x = 0; x < size; x++) {
                //Scale x and y to [-1,1] range
                double tx = ((double)x / (size - 1)) * 2 - 1;
                double ty = 1 - ((double)y / (size - 1)) * 2;

                //Determine point on cube in worldspace
                double cx = 0, cy = 0, cz = 0;
                if      (s == 0) { cx =   1; cy =  tx; cz =  ty; }
                else if (s == 1) { cx = -tx; cy =   1; cz =  ty; }
                else if (s == 2) { cx = - 1; cy = -tx; cz =  ty; }
                else if (s == 3) { cx =  tx; cy = - 1; cz =  ty; }
                else if (s == 4) { cx = -ty; cy =  tx; cz =   1; }
                else if (s == 5) { cx =  ty; cy =  tx; cz = - 1; }

                //Determine point on sphere in worldspace
                double sx = cx * Math.sqrt(1 - cy*cy/2 - cz*cz/2 + cy*cy*cz*cz/3);
                double sy = cy * Math.sqrt(1 - cz*cz/2 - cx*cx/2 + cz*cz*cx*cx/3);
                double sz = cz * Math.sqrt(1 - cx*cx/2 - cy*cy/2 + cx*cx*cy*cy/3);

                //Generate 6 octaves of noise
                float gray = (float)(SimplexNoise.fbm(6, sx, sy, sz, 8) / 2 + 0.5);

                //Set components of the current pixel
                data[offset    ] = (byte)(gray * 255);
                data[offset + 1] = (byte)(gray * 255);
                data[offset + 2] = (byte)(gray * 255);
                data[offset + 3] = (byte)(255);

                //Move to the next pixel
                offset += 4;
            }
        }
    }

    Pixmap pixmap = new Pixmap(columns * size, size, Pixmap.Format.RGBA8888);
    pixmap.getPixels().put(data).position(0);

    Texture texture = new Texture(pixmap, true);
    texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
    return texture;
}

//SimplexNoise.fbm
//The noise function is the same one found in http://webstaff.itn.liu.se/~stegu/simplexnoise/SimplexNoise.java
//the only modification being that I replaced the 32 in the last line with 16 in order to end up with
//noise in the range [-0.5, 0.5] instead of [-1,1]
public static double fbm(int octaves, double x, double y, double z, double frequency) {
    double value = 0;
    double f = frequency;
    double amp = 1;
    for (int i = 0; i < octaves; i++) {
        value += noise(x*f, y*f, z*f) * amp;
        f *= 2;
        amp /= 2;
    }
    return value; 
}

Potresti pubblicare ciò che hai attualmente in Java per la tua funzione noise? Non dire che esiste qualche guadagno in termini di prestazioni, ma qualcuno potrebbe individuare qualcosa per darti una spinta.
Darren Reid,

Ho aggiunto il codice che sto usando al post originale.
FalconNL

Non correlato alla tua Q in sé, ma dovresti chiamare dispose () sulla tua pixmap dopo aver finito.
junkdog,

Risposte:


10

È possibile combinare gli approcci (2) e (3) in questo modo:

  • Innanzitutto, usa GPU per generare una serie di trame di rumore e salvarle. Questa sarà la tua "noise cache"; puoi farlo solo una volta al primo avvio.
  • Per generare una trama nel gioco, combina alcune trame dalla cache: questo dovrebbe essere molto veloce. Quindi, se necessario, aggiungi effetti speciali come i vortici.
  • In alternativa, puoi anche pre-generare alcune trame "ad effetti speciali" e fonderle per ottenere il risultato finale.

+1 Penso che generare un mucchio di trame e confezionarle con il gioco per combinare o applicare semplici effetti sarebbe il modo migliore per farlo.
TheNickmaster21

2

La generazione di texture procedurali è un * * mofo in termini di tempo di calcolo. È quello che è.

La migliore implementazione di Simplex Noise che ho trovato è quella di Stefan Gustavson .

Oltre al miglioramento del tempo di calcolo effettivo (in realtà è piuttosto difficile superare il fatto che stai semplicemente chiedendo molto dal tuo computer quando calcoli trame procedurali 1024x1024), uno dei modi migliori per ridurre la percezione tempi di attesa è avere la tua app esegue il maggior numero di thread in background possibile.

Quindi inizia a generare trame all'avvio del gioco sul thread in background , mentre l'utente sta ancora armeggiando con le opzioni e il menu o sta guardando il trailer di avvio di livello.

L'altra cosa da considerare è, basta memorizzare nella cache diverse centinaia di trame generate su disco e selezionarne una a caso al momento del caricamento. Più disco, ma meno tempo di caricamento.

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.