เมื่อไม่นานมานี้ฉันเริ่มทำงานกับเกมที่เกิดขึ้นในระบบสุริยจักรวาล หลังจากเส้นโค้งการเรียนรู้เล็กน้อย (โดยที่ไม่เคยทำงานกับ Scala, OpenGL 2 ES หรือ Libgdx มาก่อน) ฉันมีการสาธิตเทคโนโลยีพื้นฐานที่คุณหมุนรอบดาวเคราะห์ที่มีพื้นผิวเดียว:
ปัญหาที่ฉันพบคือประสิทธิภาพของการสร้างพื้นผิว ภาพรวมอย่างรวดเร็วของสิ่งที่ฉันทำ: ดาวเคราะห์เป็นลูกบาศก์ที่ได้รับการเปลี่ยนรูปทรงกลม ในแต่ละด้านจะมีการใช้พื้นผิว anxn (เช่น 256 x 256) ซึ่งรวมอยู่ในพื้นผิว 8n xn หนึ่งอันที่ถูกส่งไปยังส่วนย่อย ไม่ได้ใช้ช่องว่างสองอันสุดท้ายพวกเขาอยู่ที่นั่นเพื่อให้แน่ใจว่าความกว้างเท่ากับกำลังงาน 2 พื้นผิวถูกสร้างขึ้นบน CPU ในปัจจุบันโดยใช้อัลกอริธึมเสียงรบกวน simplex เวอร์ชั่น 2012 ที่เชื่อมโยงกับกระดาษ'Simplex เสียง demystified '. ฉากที่ฉันใช้ทดสอบอัลกอริธึมมีสองทรงกลม: ดาวเคราะห์และพื้นหลัง ทั้งสองใช้พื้นผิวแบบ greyscale ซึ่งประกอบด้วยเสียงหกมิติแบบหกเสี้ยวง่ายเช่นถ้าเราเลือก 128x128 เป็นขนาดพื้นผิวมีขนาด 128 x 128 x 6 x 2 x 6 = ประมาณ 1.2 ล้านการโทรไปยังฟังก์ชั่นเสียงรบกวน
สิ่งที่ใกล้เคียงที่สุดที่คุณจะไปถึงดาวเคราะห์คือเกี่ยวกับสิ่งที่แสดงในภาพหน้าจอและเนื่องจากความละเอียดเป้าหมายของเกมคือ 1280x720 ซึ่งหมายความว่าฉันต้องการใช้พื้นผิว 512x512 รวมกับความจริงที่ว่าพื้นผิวจริงจะมีความซับซ้อนมากกว่าเสียงรบกวนขั้นพื้นฐาน (จะมีทั้งกลางวันและกลางคืนพื้นผิวผสมใน shader ส่วนตามแสงแดดและหน้ากาก specular ฉันต้องการเสียงสำหรับทวีปการแปรผันของสีภูมิประเทศ เมฆแสงไฟของเมือง ฯลฯ ) และเรากำลังมองหาบางสิ่งบางอย่างเช่น 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
ยาวเกินไปแล้วและนั่นก็ไม่ได้คำนึงถึงความจริงที่ว่ามันจะเป็นนาทีแทนที่จะเป็นวินาทีในรุ่นสุดท้าย เห็นได้ชัดว่าสิ่งที่ต้องทำ โดยส่วนตัวแล้วฉันเห็นหนทางที่เป็นไปได้สองสามอย่างถึงแม้ว่าฉันจะไม่กระตือรือร้นกับพวกเขามากนัก:
- อย่าคำนวณพื้นผิวล่วงหน้า แต่ให้ shader ส่วนคำนวณทุกอย่าง อาจเป็นไปไม่ได้เพราะ ณ จุดหนึ่งฉันมีพื้นหลังเป็นรูปสี่เหลี่ยมเต็มหน้าจอที่มีตัวปรับพิกเซลและฉันได้รับประมาณ 1 เฟรมต่อวินาทีในโทรศัพท์ของฉัน
- ใช้ GPU เพื่อแสดงพื้นผิวหนึ่งครั้งเก็บและใช้พื้นผิวที่เก็บไว้จากนั้นเป็นต้นไป Upside: อาจเร็วกว่าทำบน CPU เนื่องจาก GPU ควรจะเร็วกว่าในการคำนวณจุดลอย ข้อเสีย: เอฟเฟกต์ที่ไม่สามารถแสดงได้อย่างง่ายดาย (เช่นง่าย ๆ ) ของเสียงรบกวนแบบง่าย ๆ (เช่น vortices ดาวเคราะห์ก๊าซหลุมอุกกาบาตดวงจันทร์ ฯลฯ ) นั้นยากที่จะเขียนโค้ดใน GLSL ได้มากกว่าใน Scala / Java
- คำนวณพื้นผิวของเสียงรบกวนจำนวนมากและส่งไปพร้อมกับแอปพลิเคชัน ฉันต้องการหลีกเลี่ยงสิ่งนี้หากเป็นไปได้
- ลดความละเอียดลง ซื้อการเพิ่มประสิทธิภาพ 4x ซึ่งไม่เพียงพอจริง ๆ บวกกับการสูญเสียคุณภาพจำนวนมาก
- ค้นหาอัลกอริธึมด้านเสียงที่เร็วขึ้น ถ้าใครมีผมคนเดียวฉันก็หู แต่ simplex น่าจะเร็วกว่า perlin แล้ว
- ใช้รูปแบบพิกเซลเพื่อลดความละเอียดของพื้นผิวและลดระดับเสียงรบกวน ในขณะที่ฉันจินตนาการถึงเกมในรูปแบบนี้ในตอนแรกฉันก็ชอบวิธีการที่สมจริง
- ฉันกำลังทำอะไรผิดพลาดและการแสดงควรมีขนาดหนึ่งหรือสองคำสั่งที่ดีกว่า หากเป็นกรณีนี้โปรดแจ้งให้เราทราบ
หากใครมีข้อเสนอแนะเคล็ดลับการแก้ปัญหาหรือความคิดเห็นอื่น ๆ เกี่ยวกับปัญหานี้ฉันชอบที่จะได้ยินพวกเขา
ในการตอบสนองต่อ 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;
}