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:
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;
}
Odpowiedzi:
Możesz łączyć podejścia (2) i (3) w następujący sposób:
źródło
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.
źródło