Accélérer la génération de textures procédurales

14

Récemment, j'ai commencé à travailler sur un jeu qui se déroule dans un système solaire généré de manière procédurale. Après un peu de courbe d'apprentissage (n'ayant jamais travaillé avec Scala, OpenGL 2 ES ou Libgdx auparavant), j'ai une démonstration technologique de base où vous tournez autour d'une seule planète texturée procédurale:

entrez la description de l'image ici

Le problème que je rencontre est la performance de la génération de texture. Un bref aperçu de ce que je fais: une planète est un cube qui a été déformé en sphère. De chaque côté, une texture anxn (par exemple 256 x 256) est appliquée, qui sont regroupées dans une texture 8n xn qui est envoyée au shader de fragment. Les deux derniers espaces ne sont pas utilisés, ils ne sont là que pour s'assurer que la largeur est une puissance de 2. La texture est actuellement générée sur le CPU, en utilisant la version mise à jour 2012 de l'algorithme de bruit simplex lié à dans le papier 'Simplex le bruit démystifié ». La scène que j'utilise pour tester l'algorithme contient deux sphères: la planète et l'arrière-plan. Les deux utilisent une texture en niveaux de gris composée de six octaves de bruit simplex 3D.Par exemple, si nous choisissons 128x128 comme taille de texture, il y a 128 x 128 x 6 x 2 x 6 = environ 1,2 million d'appels à la fonction de bruit.

Le plus proche de la planète concerne ce qui est montré dans la capture d'écran et puisque la résolution cible du jeu est 1280x720, cela signifie que je préférerais utiliser des textures 512x512. Combinez cela avec le fait que les textures réelles seront bien sûr plus compliquées que le bruit de base (il y aura une texture de jour et de nuit, mélangée dans le shader de fragments basé sur la lumière du soleil et un masque spéculaire. J'ai besoin de bruit pour les continents, de variation de couleur du terrain , nuages, lumières de la ville, etc.) et nous examinons quelque chose comme 512 x 512 x 6 x 3 x 15 = 70 millions d'appels de bruit pour la planète seule. Dans le jeu final, il y aura des activités lors des voyages entre les planètes, donc une attente de 5 ou 10 secondes, peut-être 20, serait acceptable car je peux calculer la texture en arrière-plan pendant le voyage, bien que plus vite, mieux c'est.

Pour en revenir à notre scène de test, les performances sur mon PC ne sont pas trop terribles, mais encore trop lentes étant donné que le résultat final sera environ 60 fois pire:

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

C'est après que j'ai déplacé tout le code essentiel aux performances vers Java, car essayer de le faire dans Scala était bien pire. L'exécuter sur mon téléphone (un Samsung Galaxy S3), cependant, produit un résultat plus problématique:

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

Déjà beaucoup trop long, et cela ne tient même pas compte du fait que ce sera des minutes au lieu des secondes dans la version finale. Il est clair que quelque chose doit être fait. Personnellement, je vois quelques pistes potentielles, mais je ne suis pas encore particulièrement intéressé par l'une d'entre elles:

  • Ne précalculez pas les textures, mais laissez le fragment shader tout calculer. Probablement pas faisable, car à un moment donné, j'ai eu l'arrière-plan en quad plein écran avec un pixel shader et j'ai obtenu environ 1 fps sur mon téléphone.
  • Utilisez le GPU pour rendre la texture une fois, stockez-la et utilisez ensuite la texture stockée. Upside: peut être plus rapide que de le faire sur le CPU car le GPU est censé être plus rapide lors des calculs en virgule flottante. Inconvénient: les effets qui ne peuvent pas (facilement) être exprimés en fonction du bruit simplex (par exemple les tourbillons de planètes gazeuses, les cratères lunaires, etc.) sont beaucoup plus difficiles à coder en GLSL qu'en Scala / Java.
  • Calculez une grande quantité de textures de bruit et envoyez-les avec l'application. J'aimerais éviter cela si possible.
  • Réduisez la résolution. M'achète un gain de performances 4x, ce qui n'est pas vraiment suffisant et je perds beaucoup de qualité.
  • Trouvez un algorithme de bruit plus rapide. Si quelqu'un en a un, je suis tout ouïe, mais le simplexe est déjà censé être plus rapide que le perlin.
  • Adoptez un style pixel art, permettant des textures de résolution inférieure et moins d'octaves de bruit. Alors que j'avais initialement envisagé le jeu dans ce style, j'en suis venu à préférer l'approche réaliste.
  • Je fais quelque chose de mal et les performances devraient déjà être d'un ou deux ordres de grandeur meilleures. Si tel est le cas, faites-le moi savoir.

Si quelqu'un a des suggestions, des conseils, des solutions de contournement ou d'autres commentaires concernant ce problème, j'aimerais les entendre.

En réponse à Layoric, voici le code que j'utilise:

//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; 
}
FalconNL
la source
Pourriez-vous publier ce que vous avez actuellement en Java pour votre fonction de bruit? Cela ne veut pas dire qu'il existe des gains de performances à en tirer, mais quelqu'un pourrait repérer quelque chose pour vous donner un coup de pouce.
Darren Reid
J'ai ajouté le code que j'utilise au message d'origine.
FalconNL
Pas lié à votre Q en soi, mais vous devez appeler dispose () sur votre pixmap une fois que vous en avez terminé.
junkdog

Réponses:

10

Vous pouvez combiner les approches (2) et (3) comme ceci:

  • Tout d'abord, utilisez le GPU pour générer un certain nombre de textures de bruit et enregistrez-les. Ce sera votre "cache de bruit"; vous ne pouvez le faire qu'une seule fois lors de la première manche.
  • Pour générer une texture dans le jeu, combinez quelques textures à partir du cache - cela devrait être très rapide. Ensuite, si nécessaire, ajoutez des effets spéciaux comme des tourbillons en plus.
  • Alternativement, vous pouvez aussi pré-générer des textures "d'effets spéciaux" et simplement les mélanger pour obtenir le résultat final.
Ça ne fait rien
la source
+1 Je pense que la meilleure façon de le faire serait de générer un tas de textures et de les empaqueter avec le jeu pour combiner ou appliquer des effets simples.
TheNickmaster21
2

La génération de texture procédurale est ab * * d'un mofo en termes de temps de calcul. C'est ce que c'est.

La meilleure implémentation de Simplex Noise que j'ai trouvée est celle de Stefan Gustavson .

Au-delà de l'amélioration du temps de calcul réel (il est en fait assez difficile de passer outre le fait que vous demandez simplement beaucoup de choses à votre ordinateur lorsque vous calculez des textures procédurales 1024x1024), l'un des meilleurs moyens de réduire la perception temps d'attente est d'avoir votre application effectue le plus de travail possible en arrière-plan.

Commencez donc à générer des textures au lancement du jeu sur le fil d'arrière-plan , pendant que l'utilisateur manipule toujours les options et le menu ou regarde la bande-annonce de démarrage du niveau.

L'autre chose à considérer est de simplement mettre en cache plusieurs centaines de textures générées sur le disque et de sélectionner au hasard l'une d'entre elles au moment du chargement. Plus de disque, mais moins de temps de chargement.

bobobobo
la source