Нещодавно я почав працювати над грою, яка проходить у генерованій процедурою сонячній системі. Після трохи кривої навчання (не працював ні з Scala, OpenGL 2 ES, ні з Libgdx), у мене є основна демонстрація технологій, де ви обертаєтесь навколо однієї процедурно текстурованої планети:
Проблема, з якою я стикаюся, - це продуктивність текстури. Короткий огляд того, що я роблю: планета - це куб, деформований у сферу. З кожної сторони застосовується текстура anxn (наприклад, 256 x 256), яка поєднується в одну текстуру 8n xn, що надсилається до фрагменту шейдера. Останні два пробіли не використовуються, вони знаходяться лише там, щоб переконатися, що ширина є потужністю 2. Текстура в даний час генерується в центральному процесорі, використовуючи оновлену версію алгоритму шуму симплекса 2012, пов'язану в папері "Simplex" шум демістифікований '. Сцена, яку я використовую для тестування алгоритму, містить дві сфери: планету та фон. Обидва використовують текстуру сірого масштабу, що складається з шести октав шуму симплекс-3D, тому, наприклад, якщо ми виберемо 128x128 як розмір текстури, є 128 x 128 x 6 x 2 x 6 = близько 1,2 мільйона викликів функції шуму.
Найближче до планети ви дізнаєтесь про те, що показано на скріншоті, і оскільки цільова роздільна здатність гри - 1280x720, це означає, що я вважаю за краще використовувати текстури 512x512. Поєднайте, що фактично фактури, звичайно, будуть складнішими, ніж основний шум (Буде денна та нічна текстура, змішана у фрагменті шейдера на основі сонячного світла, та дзеркальна маска. Мені потрібен шум для континентів, зміна кольору місцевості , хмари, вогні міста та ін.), і ми дивимось на щось на зразок 512 x 512 x 6 x 3 x 15 = 70 мільйонів шумових дзвінків лише для планети. У фінальній грі будуть подорожувати планети, тому зачекати 5 або 10 секунд, можливо, 20, було б прийнятним, оскільки я можу обчислити текстуру у фоновому режимі під час подорожі, хоча, очевидно, чим швидше, тим краще.
Повертаючись до нашої тестової сцени, продуктивність на моєму ПК не надто жахлива, хоча все ще занадто повільна, враховуючи, що кінцевий результат буде приблизно в 60 разів гіршим:
128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s
Це після того, як я перемістив весь критичний для продуктивності код на Java, оскільки спроба зробити це в Scala було набагато гірше. Однак, якщо це працює на моєму телефоні (Samsung Galaxy S3), проте, є більш проблемний результат:
128x128 : 2s
256x256 : 7s
512x512 : 29s
Вже занадто довго, і це навіть не враховує те, що в остаточній версії це буде хвилини замість секунд. Очевидно, що щось потрібно зробити. Особисто я бачу кілька потенційних проспектів, хоча я ще не особливо захоплююсь жодною з них:
- Не заздалегідь розраховуйте текстури, але нехай шейдер фрагмента все обчислює. Напевно, це неможливо, бо в один момент у мене був фон як повноекранний квадроцикл з піксельним шейдером, і я отримав близько 1 кадрів в секунду на своєму телефоні.
- Використовуйте графічний процесор, щоб повторно відобразити текстуру, зберегти її та використовувати з цього часу збережену текстуру. Вгору: може бути швидше, ніж робити це на процесорі, оскільки GPU повинен бути швидшим при розрахунках з плаваючою комою. Зворотна сторона: ефекти, які неможливо (легко) виразити як функції симплексного шуму (наприклад, газові вихори планети, місячні кратери тощо), в GLSL набагато складніше зафіксувати, ніж у Scala / Java.
- Обчисліть велику кількість шумових текстур і відправте їх із додатком. Я хотів би цього уникнути, якщо це можливо.
- Опустіть роздільну здатність. Мені купується 4-кратне підвищення продуктивності, що насправді недостатньо, я втрачаю багато якості.
- Знайдіть більш швидкий алгоритм шуму. Якщо у кого є такий, я все вуха, але симплекс вже повинен бути швидшим за перлін.
- Прийняти піксельний стиль мистецтва, що дозволяє текстури меншої роздільної здатності та меншу кількість октав шуму. Хоча я спочатку передбачав гру в цьому стилі, я віддав перевагу реалістичному підходу.
- Я роблю щось не так, і виступ повинен бути вже на один-два порядки кращим. Якщо це так, будь ласка, повідомте мене про це.
Якщо у когось є якісь пропозиції, поради, вирішення чи інші коментарі щодо цієї проблеми, я хотів би їх почути.
У відповідь на Layoric, ось код, який я використовую:
//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;
}