Gần đây tôi đã bắt đầu làm việc với một trò chơi diễn ra trong một hệ mặt trời được tạo theo thủ tục. Sau một chút thời gian học tập (chưa từng làm việc với Scala, OpenGL 2 ES hoặc Libgdx trước đó), tôi có một bản demo công nghệ cơ bản nơi bạn quay quanh một hành tinh có kết cấu thủ tục duy nhất:
Vấn đề tôi gặp phải là hiệu suất của việc tạo kết cấu. Tổng quan nhanh về những gì tôi đang làm: một hành tinh là một khối lập phương đã bị biến dạng thành hình cầu. Ở mỗi bên, kết cấu lo lắng (ví dụ 256 x 256) được áp dụng, được gói trong một kết cấu 8n xn được gửi đến trình đổ bóng mảnh. Hai không gian cuối cùng không được sử dụng, chúng chỉ ở đó để đảm bảo chiều rộng là sức mạnh của 2. Kết cấu hiện được tạo trên CPU, sử dụng phiên bản cập nhật 2012 của thuật toán nhiễu đơn giản được liên kết trong bài báo 'Simplex tiếng ồn làm sáng tỏ '. Cảnh tôi đang sử dụng để kiểm tra thuật toán chứa hai hình cầu: hành tinh và hậu cảnh. Cả hai đều sử dụng kết cấu thang độ xám bao gồm sáu quãng tám nhiễu 3D đơn giản, vì vậy, nếu chúng ta chọn 128x128 làm kích thước kết cấu thì có 128 x 128 x 6 x 2 x 6 = khoảng 1,2 triệu cuộc gọi đến chức năng nhiễu.
Gần nhất bạn sẽ đến hành tinh này là về những gì được hiển thị trong ảnh chụp màn hình và vì độ phân giải mục tiêu của trò chơi là 1280x720, điều đó có nghĩa là tôi thích sử dụng họa tiết 512x512. Kết hợp với thực tế, kết cấu thực tế tất nhiên sẽ phức tạp hơn tiếng ồn cơ bản (Sẽ có kết cấu ngày và đêm, được pha trộn trong shader mảnh dựa trên ánh sáng mặt trời và mặt nạ đặc biệt. Tôi cần tiếng ồn cho các lục địa, biến đổi màu địa hình , mây, ánh đèn thành phố, v.v.) và chúng ta đang xem xét một cái gì đó như 512 x 512 x 6 x 3 x 15 = 70 triệu tiếng ồn gọi riêng cho hành tinh này. Trong trò chơi cuối cùng, sẽ có các hoạt động khi di chuyển giữa các hành tinh, do đó, chờ 5 hoặc 10 giây, có thể là 20, sẽ được chấp nhận vì tôi có thể tính toán kết cấu trong nền khi đi du lịch, mặc dù rõ ràng càng nhanh càng tốt.
Quay trở lại cảnh thử nghiệm của chúng tôi, hiệu suất trên PC của tôi không quá khủng khiếp, mặc dù vẫn quá chậm khi xem xét kết quả cuối cùng sẽ tồi tệ hơn khoảng 60 lần:
128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s
Đây là sau khi tôi chuyển tất cả mã quan trọng về hiệu năng sang Java, vì cố gắng làm như vậy trong Scala còn tệ hơn rất nhiều. Tuy nhiên, việc chạy này trên điện thoại của tôi (Samsung Galaxy S3) tạo ra một kết quả có vấn đề hơn:
128x128 : 2s
256x256 : 7s
512x512 : 29s
Đã quá lâu và điều đó thậm chí còn không bao gồm trong thực tế rằng nó sẽ là vài phút thay vì vài giây trong phiên bản cuối cùng. Rõ ràng một cái gì đó cần phải được thực hiện. Cá nhân, tôi thấy một vài con đường tiềm năng, mặc dù tôi chưa đặc biệt quan tâm đến bất kỳ ai trong số họ:
- Đừng tính toán trước kết cấu, nhưng hãy để shader mảnh tính toán mọi thứ. Có lẽ là không khả thi, bởi vì tại một thời điểm, tôi có nền là một quad toàn màn hình với một pixel shader và tôi nhận được khoảng 1 khung hình / giây trên điện thoại của mình.
- Sử dụng GPU để kết xuất kết cấu một lần, lưu trữ và sử dụng kết cấu được lưu trữ từ đó về sau. Ưu điểm: có thể nhanh hơn so với thực hiện trên CPU vì GPU được cho là nhanh hơn khi tính toán dấu phẩy động. Nhược điểm: các hiệu ứng không thể (dễ dàng) được thể hiện dưới dạng các hàm của nhiễu đơn giản (ví dụ như các xoáy hành tinh khí, miệng hố mặt trăng, v.v.) khó mã hóa trong GLSL hơn rất nhiều so với Scala / Java.
- Tính toán một lượng lớn kết cấu nhiễu và gửi chúng cùng với ứng dụng. Tôi muốn tránh điều này nếu có thể.
- Hạ nghị quyết. Mua cho tôi mức tăng hiệu suất gấp 4 lần, điều đó không thực sự đủ cộng với tôi mất rất nhiều chất lượng.
- Tìm một thuật toán tiếng ồn nhanh hơn. Nếu bất cứ ai có một tôi đều là tai, nhưng Simplex được cho là nhanh hơn perlin.
- Áp dụng phong cách nghệ thuật pixel, cho phép kết cấu có độ phân giải thấp hơn và quãng tám nhiễu ít hơn. Trong khi ban đầu tôi hình dung trò chơi theo phong cách này, tôi đã thích cách tiếp cận thực tế hơn.
- Tôi đang làm gì đó sai và hiệu suất sẽ tốt hơn một hoặc hai bậc. Nếu đây là trường hợp, xin vui lòng cho tôi biết.
Nếu bất cứ ai có bất kỳ đề xuất, lời khuyên, cách giải quyết hoặc ý kiến khác liên quan đến vấn đề này, tôi rất muốn nghe họ.
Đáp lại Layoric, đây là mã tôi đang sử dụng:
//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;
}