Beschleunigung der prozeduralen Texturgenerierung


14

Vor kurzem habe ich begonnen, an einem Spiel zu arbeiten, das in einem prozedural erzeugten Sonnensystem stattfindet. Nach einer kurzen Einarbeitungsphase (die noch nie mit Scala, OpenGL 2 ES oder Libgdx funktioniert hat) habe ich eine grundlegende Tech-Demo, in der Sie einen einzelnen prozedural strukturierten Planeten umrunden:

Bildbeschreibung hier eingeben

Das Problem, auf das ich stoße, ist die Leistung der Texturgenerierung. Ein kurzer Überblick darüber, was ich tue: Ein Planet ist ein Würfel, der zu einer Kugel deformiert wurde. Auf jede Seite wird eine angstbehaftete Textur (z. B. 256 x 256) angewendet, die zu einer 8n xn-Textur gebündelt wird, die an den Fragment-Shader gesendet wird. Die letzten beiden Leerzeichen werden nicht verwendet. Sie dienen nur dazu, sicherzustellen, dass die Breite eine Potenz von 2 ist. Die Textur wird derzeit auf der CPU mit der aktualisierten Version 2012 des Simplex-Rauschalgorithmus generiert, der im Artikel 'Simplex' verlinkt ist Lärm entmystifiziert '. Die Szene, mit der ich den Algorithmus teste, enthält zwei Kugeln: den Planeten und den Hintergrund. Beide verwenden eine Graustufentextur, die aus sechs Oktaven 3D-Simplex-Rauschen besteht. Wenn wir also 128 x 128 als Texturgröße auswählen, werden 128 x 128 x 6 x 2 x 6 = ungefähr 1,2 Millionen Aufrufe an die Rauschfunktion gesendet.

Da die Zielauflösung des Spiels 1280 x 720 beträgt, bevorzuge ich die Verwendung von 512 x 512-Texturen. Kombinieren Sie das mit der Tatsache, dass die tatsächlichen Texturen natürlich komplizierter sind als das Grundrauschen. (Es wird eine Tag- und Nacht-Textur geben, die in den auf Sonnenlicht basierenden Fragment-Shader eingemischt wird, und eine Spiegelmaske. Ich brauche Rauschen für Kontinente, Geländefarbabweichungen , Wolken, Lichter der Stadt usw.) und wir betrachten so etwas wie 512 x 512 x 6 x 3 x 15 = 70 Millionen Geräusche, die den Planeten allein fordern. Im letzten Spiel wird es Aktivitäten beim Reisen zwischen Planeten geben, daher wäre ein Warten von 5 oder 10 Sekunden, möglicherweise 20 Sekunden, akzeptabel, da ich die Textur im Hintergrund auf Reisen berechnen kann, obwohl dies offensichtlich umso schneller ist, je besser.

Zurück zu unserer Testszene, die Leistung auf meinem PC ist nicht allzu schlecht, obwohl sie immer noch zu langsam ist, wenn man bedenkt, dass das Endergebnis etwa 60-mal schlechter ausfallen wird:

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

Dies geschah, nachdem ich den gesamten leistungskritischen Code nach Java verschoben hatte, da der Versuch, dies in Scala zu tun, viel schlimmer war. Das Ausführen dieses Befehls auf meinem Telefon (einem Samsung Galaxy S3) führt jedoch zu einem problematischeren Ergebnis:

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

Schon viel zu lange, und das berücksichtigt nicht einmal die Tatsache, dass es in der endgültigen Version Minuten statt Sekunden sind. Offensichtlich muss etwas getan werden. Persönlich sehe ich ein paar mögliche Wege, obwohl ich noch keinen davon besonders mag:

  • Berechnen Sie die Texturen nicht vorab, sondern lassen Sie den Fragment-Shader alles berechnen. Wahrscheinlich nicht machbar, weil ich irgendwann den Hintergrund als Vollbild-Quad mit einem Pixel-Shader hatte und ungefähr 1 fps auf meinem Handy hatte.
  • Verwenden Sie die GPU, um die Textur einmal zu rendern, zu speichern und die gespeicherte Textur von da an zu verwenden. Vorteil: Möglicherweise schneller als auf der CPU, da die GPU bei Gleitkommaberechnungen schneller sein soll. Nachteil: Effekte, die (leicht) als Funktionen von Simplex-Rauschen ausgedrückt werden können (z. B. Gasplanetenwirbel, Mondkrater usw.), sind in GLSL viel schwieriger zu codieren als in Scala / Java.
  • Berechnen Sie eine große Anzahl von Rauschstrukturen und versenden Sie diese mit der Anwendung. Ich würde das gerne vermeiden, wenn es überhaupt möglich ist.
  • Verringern Sie die Auflösung. Kauft mir einen 4-fachen Leistungszuwachs, der nicht wirklich ausreicht, und ich verliere viel an Qualität.
  • Finden Sie einen schnelleren Rauschalgorithmus. Wenn jemand eins hat, bin ich ganz Ohr, aber Simplex soll schon schneller sein als Perlin.
  • Verwenden Sie einen Pixel-Art-Stil, der Texturen mit geringerer Auflösung und weniger Rauschoktaven ermöglicht. Während ich mir das Spiel ursprünglich in diesem Stil vorgestellt hatte, bevorzuge ich den realistischen Ansatz.
  • Ich mache etwas falsch und die Leistung sollte schon ein oder zwei Größenordnungen besser sein. Wenn dies der Fall ist, lassen Sie es mich bitte wissen.

Wenn jemand Vorschläge, Tipps, Problemumgehungen oder andere Kommentare zu diesem Problem hat, würde ich sie gerne hören.

Als Antwort auf Layoric, hier ist der Code, den ich benutze:

//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; 
}

Könnten Sie angeben, was Sie derzeit in Java für Ihre Rauschfunktion haben? Das heißt nicht, dass es irgendwelche Leistungssteigerungen gibt, aber jemand könnte etwas entdecken, das Ihnen einen Schub gibt.
Darren Reid

Ich habe den Code, den ich verwende, zum ursprünglichen Beitrag hinzugefügt.
FalconNL

Nicht auf Ihr Q per se bezogen, aber Sie sollten dispose () auf Ihrer Pixmap aufrufen, nachdem Sie damit fertig sind.
Junkdog

Antworten:


10

Sie können die Ansätze (2) und (3) folgendermaßen kombinieren:

  • Verwenden Sie zunächst die GPU, um eine Reihe von Rauschtexturen zu generieren und zu speichern. Dies wird Ihr "Noise Cache" sein. Sie können es nur einmal im ersten Lauf tun.
  • Kombiniere ein paar Texturen aus dem Cache, um im Spiel eine Textur zu generieren - das sollte sehr schnell gehen. Fügen Sie dann bei Bedarf Spezialeffekte wie Wirbel hinzu.
  • Alternativ können Sie auch einige "Spezialeffekt" -Texturen vorgenerieren und diese einfach mischen, um das endgültige Ergebnis zu erhalten.

+1 Ich denke, eine Reihe von Texturen zu generieren und sie mit dem Spiel zu verpacken, um einfache Effekte zu kombinieren oder anzuwenden, wäre der beste Weg, dies zu tun.
TheNickmaster21

2

Die prozedurale Texturerzeugung ist in Bezug auf die Rechenzeit ein * * Mofo. Es ist was es ist.

Die beste Implementierung von Simplex Noise, die ich gefunden habe, ist die von Stefan Gustavson .

Abgesehen von der Verbesserung der tatsächlichen Rechenzeit (es ist tatsächlich ziemlich schwierig, die Tatsache zu überwinden , dass Sie beim Berechnen von prozeduralen Texturen im Format 1024 x 1024 einfach viel von Ihrem Computer verlangen), ist eine der besten Möglichkeiten, die wahrgenommene Wartezeit zu verringern Ihre App erledigt so viel Hintergrund-Thread-Arbeit wie möglich.

Beginnen Sie also mit dem Erzeugen von Texturen beim Spielstart im Hintergrund-Thread , während der Benutzer noch an Optionen und dem Menü herumfummelt, oder schauen Sie sich den Level-Start-Trailer an.

Die andere zu berücksichtigende Sache ist, einfach mehrere hundert erzeugte Texturen auf der Festplatte zwischenzuspeichern und beim Laden zufällig eine davon auszuwählen. Mehr Festplatte, aber weniger Ladezeit.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.