Przyspieszenie proceduralnego generowania tekstur


14

Ostatnio zacząłem pracować nad grą rozgrywaną w proceduralnie generowanym Układzie Słonecznym. Po trochę krzywej uczenia się (wcześniej nie pracowałem ze Scalą, OpenGL 2 ES ani Libgdx), mam podstawowe demo techniczne, w którym kręcisz się wokół jednej planety o teksturach proceduralnych:

wprowadź opis zdjęcia tutaj

Problem, na który wpadam, to wydajność generowania tekstur. Krótki przegląd tego, co robię: planeta to sześcian zdeformowany na kulę. Po każdej stronie nakładana jest tekstura anxn (np. 256 x 256), które są powiązane w jedną teksturę 8n xn, która jest wysyłana do modułu cieniującego fragmenty. Dwie ostatnie spacje nie są używane, są tam tylko po to, aby upewnić się, że szerokość wynosi potęgę 2. Tekstura jest obecnie generowana na procesorze, przy użyciu zaktualizowanej wersji 2012 algorytmu szumu simpleks, do którego odwołuje się artykuł „Simplex hałas odkamieniany ”. Scena, której używam do przetestowania algorytmu, zawiera dwie sfery: planetę i tło. Obie używają tekstury w skali szarości składającej się z sześciu oktaw szumu 3D simpleks, więc na przykład, jeśli wybieramy 128x128 jako rozmiar tekstury, 128 x 128 x 6 x 2 x 6 = około 1,2 miliona wywołań funkcji szumu.

Najbliżej planety jest to, co pokazano na zrzucie ekranu, a ponieważ docelowa rozdzielczość gry to 1280 x 720, co oznacza, że ​​wolałbym używać tekstur 512 x 512. Połącz to z faktem, że faktyczne tekstury będą oczywiście bardziej skomplikowane niż podstawowy hałas (pojawi się tekstura dnia i nocy, zmieszana z modułem cieniującym fragmenty opartym na świetle słonecznym i maską lustrzaną. Potrzebuję hałasu dla kontynentów, zmiany koloru terenu , chmury, światła miasta itp.), a my patrzymy na coś w rodzaju 512 x 512 x 6 x 3 x 15 = 70 milionów hałasu wymaga samej planety. W końcowej grze będą podróże między planetami, więc oczekiwanie 5 lub 10 sekund, być może 20, byłoby dopuszczalne, ponieważ mogę obliczyć teksturę w tle podczas podróży, chociaż oczywiście im szybciej, tym lepiej.

Wracając do naszej sceny testowej, wydajność na moim komputerze nie jest zbyt straszna, choć wciąż zbyt wolna, biorąc pod uwagę, że końcowy wynik będzie około 60 razy gorszy:

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

Dzieje się tak po tym, jak przeniosłem cały kluczowy dla wydajności kod na Javę, ponieważ próba zrobienia tego w Scali była znacznie gorsza. Uruchomienie tego na moim telefonie (Samsung Galaxy S3) daje jednak bardziej problematyczny wynik:

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

Jest już o wiele za długi, a to nawet nie uwzględnia faktu, że w ostatecznej wersji będą to minuty, a nie sekundy. Oczywiście należy coś zrobić. Osobiście widzę kilka potencjalnych ścieżek, choć nie jestem szczególnie zainteresowany żadną z nich:

  • Nie wstępnie obliczaj tekstur, ale pozwól, aby moduł cieniujący fragmentów obliczył wszystko. Prawdopodobnie nie jest to możliwe, ponieważ w pewnym momencie miałem tło jako pełnoekranowy quad z pikselowym shaderem i dostałem około 1 fps na telefonie.
  • Użyj procesora graficznego, aby raz wyrenderować teksturę, zapisz ją i użyj zapisanej tekstury od tego momentu. Zaleta: może być szybsza niż robienie tego na CPU, ponieważ GPU ma być szybsza w obliczeniach zmiennoprzecinkowych. Wada: efekty, których nie można (łatwo) wyrazić jako funkcje szumu simpleksowego (np. Wiry planety gazowej, kratery księżycowe itp.) Są znacznie trudniejsze do kodowania w GLSL niż w Scala / Java.
  • Oblicz dużą liczbę tekstur hałasu i dostarcz je wraz z aplikacją. Chciałbym tego uniknąć, jeśli to w ogóle możliwe.
  • Obniż rozdzielczość. Kupuje mi 4x wyższą wydajność, co tak naprawdę nie wystarcza, a ponadto tracę dużo jakości.
  • Znajdź szybszy algorytm hałasu. Jeśli ktoś ma taki, to jestem cały w uszach, ale simplex już ma być szybszy niż perlin.
  • Zastosuj styl grafiki pikselowej, pozwalając na tekstury o niższej rozdzielczości i mniej oktaw szumowych. Choć początkowo wyobrażałem sobie grę w tym stylu, wolę realistyczne podejście.
  • Robię coś złego i wydajność powinna być o jeden lub dwa rzędy wielkości lepsza. Jeśli tak jest, proszę dać mi znać.

Jeśli ktoś ma jakieś sugestie, wskazówki, obejścia lub inne komentarze dotyczące tego problemu, chciałbym je usłyszeć.

W odpowiedzi na Layoric, oto kod, którego używam:

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

Czy mógłbyś opublikować to, co aktualnie masz w Javie dla swojej funkcji szumu? Nie twierdząc, że można na tym zyskać, ale ktoś może dostrzec coś, co da ci impuls.
Darren Reid,

Dodałem kod, którego używam do oryginalnego postu.
FalconNL,

Nie jest związany z twoim Q-per se, ale powinieneś wywołać dispose () na swojej pixmapie po zakończeniu.
śmieciowy

Odpowiedzi:


10

Możesz łączyć podejścia (2) i (3) w następujący sposób:

  • Po pierwsze, użyj GPU, aby wygenerować wiele tekstur szumów i zapisać je. To będzie twoja „pamięć podręczna hałasu”; możesz to zrobić tylko raz przy pierwszym uruchomieniu.
  • Aby wygenerować teksturę w grze, połącz kilka tekstur z pamięci podręcznej - powinno to być naprawdę szybkie. Następnie w razie potrzeby dodaj efekty specjalne, takie jak wiry.
  • Możesz też wstępnie wygenerować tekstury z efektami specjalnymi i po prostu je wymieszać, aby uzyskać końcowy efekt.

+1 Myślę, że najlepszym sposobem na zrobienie tego jest zestaw tekstur i zapakowanie ich do gry w celu połączenia lub zastosowania prostych efektów.
TheNickmaster21,

2

Proceduralne generowanie tekstur jest ab * * mofo pod względem czasu obliczeniowego. Jest jak jest.

Najlepsze wdrożenie Simplex Noise, jakie znalazłem, to Stefan Gustavson .

Poza poprawą faktycznego czasu obliczeń (w rzeczywistości bardzo trudno jest ominąć fakt, że po prostu obliczasz dużo z komputera, gdy obliczasz 1024 x 1024 tekstury proceduralne), jednym z najlepszych sposobów na zmniejszenie postrzeganego czasu oczekiwania jest posiadanie Twoja aplikacja wykonuje jak najwięcej wątków w tle.

Zacznij więc generować tekstury podczas uruchamiania gry w wątku tła , podczas gdy użytkownik nadal bawi się opcjami i menu lub ogląda zwiastun rozpoczęcia poziomu.

Inną rzeczą do rozważenia jest po prostu buforowanie kilkuset wygenerowanych tekstur na dysk i losowe wybranie jednej z nich podczas ładowania. Więcej dysku, ale krótszy czas ładowania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.