Bagaimana saya bisa meningkatkan kecepatan rendering game tipe Voxel / Minecraft?

35

Saya sedang menulis klon Minecraft saya sendiri (juga ditulis di Jawa). Ini bekerja sangat baik sekarang. Dengan jarak menonton 40 meter, saya dapat dengan mudah mencapai 60 FPS di MacBook Pro 8,1 saya. (Intel i5 + Intel HD Graphics 3000). Tetapi jika saya menempatkan jarak menonton di 70 meter, saya hanya mencapai 15-25 FPS. Di Minecraft sungguhan, saya bisa menempatkan jarak menonton jauh (= 256m) tanpa masalah. Jadi pertanyaan saya adalah apa yang harus saya lakukan untuk membuat game saya lebih baik?

Optimasi yang saya terapkan:

  • Hanya simpan potongan lokal di memori (tergantung pada jarak pandang pemain)
  • Frustum culling (Pertama pada bagian, kemudian pada blok)
  • Hanya menggambar wajah balok yang benar-benar terlihat
  • Menggunakan daftar per bongkahan yang berisi blok yang terlihat. Bongkahan yang menjadi terlihat akan menambahkan dirinya ke daftar ini. Jika mereka tidak terlihat, mereka secara otomatis dihapus dari daftar ini. Blok menjadi (dalam) terlihat dengan membangun atau menghancurkan blok tetangga.
  • Menggunakan daftar per chunk yang berisi blok pembaruan. Mekanisme yang sama dengan daftar blok yang terlihat.
  • Gunakan hampir tidak ada newpernyataan di dalam loop game. (Game saya berjalan sekitar 20 detik hingga Pengumpul Sampah diminta)
  • Saya menggunakan daftar panggilan OpenGL saat ini. ( glNewList(), glEndList(), glCallList()) Untuk setiap sisi dari jenis blok.

Saat ini saya bahkan tidak menggunakan sistem pencahayaan apa pun. Saya sudah mendengar tentang VBO. Tapi saya tidak tahu persis apa itu. Namun, saya akan melakukan riset tentang mereka. Akankah mereka meningkatkan kinerja? Sebelum menerapkan VBO, saya ingin mencoba menggunakan glCallLists()dan meneruskan daftar daftar panggilan. Alih-alih menggunakan ribuan kali glCallList(). (Saya ingin mencoba ini, karena saya pikir MineCraft yang asli tidak menggunakan VBO. Benar?)

Apakah ada trik lain untuk meningkatkan kinerja?

VisualVM profiling menunjukkan kepada saya ini (profil hanya 33 frame, dengan jarak pandang 70 meter):

masukkan deskripsi gambar di sini

Pembuatan profil dengan 40 meter (246 bingkai):

masukkan deskripsi gambar di sini

Catatan: Saya sedang menyinkronkan banyak metode dan blok kode, karena saya membuat potongan di utas lain. Saya berpikir bahwa mendapatkan kunci untuk suatu objek adalah masalah kinerja ketika melakukan ini dalam satu putaran game (tentu saja, saya berbicara tentang waktu ketika hanya ada loop game dan tidak ada potongan baru yang dihasilkan). Apakah ini benar?

Sunting: Setelah menghapus beberapa synchronisedblok dan beberapa perbaikan kecil lainnya. Performanya sudah jauh lebih baik. Berikut adalah hasil profil baru saya dengan 70 meter:

masukkan deskripsi gambar di sini

Saya pikir cukup jelas itulah selectVisibleBlocksmasalahnya di sini.

Terima kasih sebelumnya!
Martijn

Pembaruan : Setelah beberapa perbaikan tambahan (seperti menggunakan untuk loop menggantikan masing-masing, variabel buffering luar loop, dll ...), saya sekarang dapat menjalankan melihat jarak 60 cukup bagus.

Saya pikir saya akan mengimplementasikan VBO sesegera mungkin.

PS: Semua kode sumber tersedia di GitHub:
https://github.com/mcourteaux/CraftMania

Martijn Courteaux
sumber
2
Bisakah Anda memberi kami suntikan profil pada 40m sehingga kami dapat melihat apa yang mungkin meningkatkan lebih cepat dari yang lain?
James
Mungkin terlalu spesifik, tetapi jika Anda pertimbangkan, hanya menanyakan teknik bagaimana mempercepat game 3D, terdengar menarik. Tapi judulnya bisa menakuti ppl.
Gustavo Maciel
@ Gtoknu: Apa yang Anda sarankan sebagai judul?
Martijn Courteaux
5
Bergantung pada siapa Anda bertanya, beberapa orang akan mengatakan bahwa Minecraft juga tidak secepat itu.
thedaian
Saya pikir sesuatu seperti "Teknik mana yang bisa mempercepat permainan 3D" harus jauh lebih baik. Pikirkan sesuatu, tetapi cobalah untuk tidak menggunakan kata "terbaik" atau mencoba untuk membandingkan dengan permainan lain. Kami tidak tahu persis apa yang mereka gunakan pada beberapa game.
Gustavo Maciel

Jawaban:

15

Anda menyebutkan melakukan pemusnahan frustrasi pada blok individu - cobalah membuangnya. Sebagian besar render potongan harus sepenuhnya terlihat atau seluruhnya tidak terlihat.

Minecraft hanya membangun kembali daftar tampilan / buffer vertex (saya tidak tahu mana yang digunakannya) ketika sebuah blok dimodifikasi dalam suatu potongan, dan begitu juga saya . Jika Anda mengubah daftar tampilan setiap kali tampilan berubah, Anda tidak mendapatkan manfaat dari daftar tampilan.

Selain itu, Anda tampaknya menggunakan bongkahan setinggi dunia. Perhatikan bahwa Minecraft menggunakan potongan kubik 16 × 16 × 16 untuk daftar tampilan, tidak seperti untuk memuat dan menyimpan. Jika Anda melakukan itu, bahkan ada lebih sedikit alasan untuk menyisihkan potongan individu.

(Catatan: Saya belum memeriksa kode Minecraft. Semua informasi ini adalah kabar angin atau kesimpulan saya sendiri dari mengamati rendering Minecraft saat saya bermain.)


Saran yang lebih umum:

Ingat bahwa rendering Anda dijalankan pada dua prosesor: CPU dan GPU. Ketika frame rate Anda tidak mencukupi, maka satu atau yang lain adalah sumber daya yang membatasi - program Anda terikat dengan CPU atau terikat GPU (dengan asumsi itu tidak bertukar atau memiliki masalah penjadwalan).

Jika program Anda berjalan pada 100% CPU (dan tidak memiliki tugas lain tanpa batas untuk diselesaikan), maka CPU Anda melakukan terlalu banyak pekerjaan. Anda harus mencoba menyederhanakan tugasnya (mis. Jangan mengurangi pemusnahan) dengan imbalan GPU lebih banyak. Saya sangat curiga ini adalah masalah Anda, mengingat deskripsi Anda.

Di sisi lain, jika GPU adalah batasnya (sayangnya, biasanya tidak ada monitor beban 0% -100% yang nyaman) maka Anda harus memikirkan cara mengirim data yang lebih sedikit, atau mengharuskannya untuk mengisi lebih sedikit piksel.

Kevin Reid
sumber
2
Referensi hebat, riset Anda tentang hal itu yang disebutkan di wiki Anda sangat membantu saya! +1
Gustavo Maciel
@OP: hanya render wajah yang terlihat (bukan blok ). Potongan 16x16x16 patologis tetapi monoton akan memiliki hampir 800 wajah yang terlihat, sedangkan blok yang terkandung akan memiliki 24.000 wajah yang terlihat. Setelah Anda selesai melakukannya, jawaban Kevin berisi peningkatan paling penting berikutnya.
AndrewS
@KevinReid Ada beberapa program untuk membantu debugging kinerja. AMD GPU PerfStudio misalnya memberi tahu Anda apakah CPU atau GPU-nya terikat dan pada GPU komponen apa yang terikat (tekstur vs fragmen vs vertex, dll.) Dan saya yakin Nvidia juga memiliki sesuatu yang serupa.
akaltar
3

Apa yang memanggil Vec3f.set begitu banyak? Jika Anda sedang membangun apa yang ingin Anda render dari awal setiap frame maka di situlah Anda ingin mulai mempercepatnya. Saya bukan pengguna OpenGL yang banyak dan saya tidak tahu banyak tentang bagaimana membuat Minecraft, tetapi tampaknya fungsi matematika yang Anda gunakan membunuh Anda sekarang (lihat saja berapa banyak waktu yang Anda habiskan di dalamnya dan berapa kali mereka dipanggil - mati dengan seribu luka memanggil mereka).

Idealnya dunia Anda akan disegmentasi sedemikian rupa sehingga Anda dapat mengelompokkan berbagai hal untuk dirender bersama, membangun Objek Penyangga Vertex dan menggunakannya kembali di beberapa bingkai. Anda hanya perlu memodifikasi VBO jika dunia menunjukkan perubahan (seperti yang diedit pengguna). Anda kemudian dapat membuat / memusnahkan VBO untuk apa yang Anda wakili karena hadir dalam rentang yang terlihat untuk menjaga konsumsi memori tetap rendah, Anda hanya akan menerima pukulan karena VBO dibuat daripada setiap frame.

Jika jumlah "doa" benar di profil Anda, Anda sering menelepon banyak hal. (10 juta panggilan ke Vec3f.set ... aduh!)

Roger Perkins
sumber
Saya menggunakan metode ini untuk banyak hal. Ini hanya menetapkan tiga nilai untuk vektor. Ini jauh lebih baik daripada mengalokasikan setiap kali objek baru.
Martijn Courteaux
2

Deskripsi saya (dari eksperimen saya sendiri) di sini berlaku:

Untuk rendering voxel, apa yang lebih efisien: VBO pra-dibuat atau shader geometri?

Minecraft dan kode Anda kemungkinan menggunakan pipeline fungsi tetap; upaya saya sendiri telah dengan GLSL tetapi intinya berlaku umum, saya merasa:

(Dari memori) saya membuat frustum yang setengah blok lebih besar dari layar frustum. Saya kemudian menguji titik pusat setiap potongan ( minecraft memiliki 16 * 16 * 128 blok ).

Wajah-wajah di masing-masing memiliki rentang dalam elemen-array VBO (banyak wajah dari potongan berbagi VBO yang sama sampai 'penuh'; berpikir seperti malloc; mereka yang memiliki tekstur yang sama di VBO yang sama jika mungkin) dan indeks titik untuk utara wajah, wajah selatan dan sebagainya berbatasan bukan campuran. Ketika saya menggambar, saya melakukan a glDrawRangeElementsuntuk wajah utara, dengan yang normal sudah diproyeksikan dan dinormalisasi, dalam seragam. Lalu saya melakukan wajah selatan dan seterusnya, jadi normalnya tidak dalam VBO. Untuk setiap potongan, saya hanya perlu memancarkan wajah-wajah yang akan terlihat - hanya mereka yang berada di tengah layar yang perlu menggambar sisi kiri dan kanan, misalnya; ini sederhana GL_CULL_FACEpada tingkat aplikasi.

Speedup terbesar, iirc, adalah pemusnahan permukaan interior saat memolimerisasi setiap potongan.

Juga penting adalah manajemen tekstur-atlas dan menyortir wajah dengan tekstur dan menempatkan wajah dengan tekstur yang sama dalam vbo yang sama dengan yang dari potongan lain. Anda ingin menghindari perubahan tekstur terlalu banyak dan menyortir wajah berdasarkan tekstur dan sebagainya meminimalkan jumlah bentang dalam glDrawRangeElements. Menggabungkan wajah ubin yang berdekatan menjadi persegi panjang yang lebih besar juga merupakan masalah besar. Saya berbicara tentang menggabungkan jawaban lain yang dikutip di atas.

Jelas Anda hanya mempolimerisasi bongkahan yang pernah terlihat, Anda dapat membuang bongkahan yang sudah lama tidak terlihat, dan Anda menyambung kembali bongkahan yang diedit (karena ini adalah kejadian yang jarang terjadi dibandingkan dengan membuatnya).

Akan
sumber
Saya suka ide optimasi frustum Anda. Tetapi bukankah Anda mencampuradukkan istilah "blok" dan "potongan" dalam penjelasan Anda?
Martijn Courteaux
mungkin iya. Bongkahan balok adalah balok balok dalam bahasa Inggris.
Will
1

Di mana semua perbandingan Anda ( BlockDistanceComparator) berasal? Jika itu dari fungsi sortir, dapatkah itu diganti dengan radix sort (yang secara asimptotik lebih cepat, dan bukan berbasis perbandingan)?

Melihat timing Anda, bahkan jika penyortiran itu sendiri tidak begitu buruk, relativeToOriginfungsi Anda dipanggil dua kali untuk setiap comparefungsi; semua data itu harus dihitung sekali. Seharusnya lebih cepat untuk menyortir struktur bantu misalnya

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

dan kemudian di pseudoCode

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

Maaf jika itu bukan struct Java yang valid (saya belum menyentuh Java sejak undergrad) tapi semoga Anda mendapatkan idenya.

selion
sumber
Saya menemukan ini lucu. Java tidak memiliki struct. Nah, ada sesuatu yang disebut seperti itu di dunia Jawa tetapi itu ada hubungannya dengan database, bukan hal yang sama sekali. Mereka dapat membuat kelas akhir dengan anggota publik, saya kira itu berhasil.
Theraot
1

Ya gunakan VBO dan menghadap CULL, tapi itu berlaku untuk hampir setiap permainan. Yang ingin Anda lakukan hanyalah merender kubus jika terlihat oleh pemain, DAN jika blok bersentuhan dengan cara tertentu (misalkan bongkahan yang tidak dapat Anda lihat karena berada di bawah tanah), Anda menambahkan simpul dari blok dan membuat hampir seperti "blok yang lebih besar", atau dalam kasus Anda - sepotong. Ini disebut serakah serakah dan secara drastis meningkatkan kinerja. Saya mengembangkan game (berbasis voxel) dan menggunakan algoritma serakah yang serakah.

Alih-alih merender semuanya seperti ini:

memberikan

Ini membuatnya seperti ini:

render2

Kelemahan dari hal ini adalah Anda harus melakukan lebih banyak perhitungan per potongan pada build dunia awal, atau jika pemain menghapus / menambah blok.

hampir semua jenis mesin voxel membutuhkan ini untuk kinerja yang baik.

Apa yang dilakukan adalah memeriksa untuk melihat apakah wajah blok menyentuh wajah blok lain, dan jika demikian: hanya render sebagai satu (atau nol) wajah blok. Ini adalah sentuhan yang mahal ketika Anda membuat potongan sangat cepat.

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}
Liam Larsen
sumber
1
Dan apakah itu sepadan? Sepertinya sistem LOD akan lebih tepat.
MichaelHouse
0

Tampaknya kode Anda tenggelam dalam objek dan panggilan fungsi. Mengukur angka, sepertinya tidak ada yang terjadi.

Anda dapat mencoba menemukan lingkungan Java yang berbeda, atau hanya mengacaukan pengaturan yang Anda miliki, tetapi cara sederhana dan sederhana untuk membuat kode Anda, tidak cepat, tetapi banyak yang kurang lambat setidaknya secara internal di Vec3f untuk berhenti coding OOO *. Jadikan setiap metode berisi sendiri, jangan panggil metode lain mana pun hanya untuk melakukan beberapa tugas kasar.

Sunting: Meskipun ada overhead di semua tempat, sepertinya memesan blok sebelum rendering adalah pemakan kinerja terburuk. Apakah itu benar-benar perlu? Jika demikian, Anda mungkin harus memulai dengan melalui loop dan menghitung setiap blok jarak ke asal, dan kemudian urutkan berdasarkan itu.

* Terlalu Berorientasi Objek

aaaaaaaaaaaa
sumber
Yap, Anda akan menghemat memori, tetapi kehilangan CPU! Jadi OOO tidak terlalu bagus dalam permainan waktu nyata.
Gustavo Maciel
Segera setelah Anda mulai membuat profil (dan bukan hanya pengambilan sampel), sebaris apa pun yang biasanya JVM lenyap. Ini seperti teori kuantum, tidak dapat mengukur sesuatu tanpa mengubah hasilnya: p
Michael
@ Gtoknu Itu tidak benar secara universal, pada beberapa tingkat OOO panggilan fungsi mulai mengambil lebih banyak memori daripada kode inline. Saya akan mengatakan ada bagian yang baik dari kode yang dimaksud yaitu sekitar titik impas untuk memori.
aaaaaaaaaaaa
0

Anda juga dapat mencoba memecah operasi Matematika ke operator bitwise. Jika Anda memiliki 128 / 16, mencoba untuk membuat operator bitwise: 128 << 4. Ini akan banyak membantu masalah Anda. Jangan mencoba membuat segalanya berjalan dengan kecepatan penuh. Buat pembaruan game Anda pada tingkat 60 atau sesuatu, dan bahkan memecahnya untuk hal-hal lain, tetapi Anda harus melakukan penghancuran dan atau menempatkan voxel atau Anda harus membuat daftar todo, yang akan menurunkan fps Anda. Anda dapat melakukan tingkat pembaruan sekitar 20 untuk entitas. Dan sekitar 10 untuk pembaruan dan atau generasi dunia.

JBakker
sumber