Saya menemukan dunia besar Minecraft yang luar biasa sangat lambat untuk dinavigasi, bahkan dengan quad core dan kartu grafis gemuk.
Saya menganggap kelambatan Minecraft berasal dari:
- Java, karena partisi spasial dan manajemen memori lebih cepat dalam bahasa asli C ++.
- Partisi dunia yang lemah.
Saya bisa salah pada kedua asumsi tersebut. Namun, ini membuat saya berpikir tentang cara terbaik untuk mengelola dunia voxel besar. Karena merupakan dunia 3D, di mana blok bisa eksis dalam setiap bagian dari dunia, itu pada dasarnya adalah array besar 3D [x][y][z]
, di mana setiap blok di dunia memiliki tipe (yaitu BlockType.Empty = 0
, BlockType.Dirt = 1
, dll)
Saya berasumsi bahwa untuk membuat dunia semacam ini berkinerja baik, Anda perlu:
- Gunakan pohon dari beberapa varietas ( okt / kd / bsp ) untuk membagi semua kubus; sepertinya oct / kd akan menjadi pilihan yang lebih baik, karena Anda hanya dapat mempartisi pada tingkat per kubus bukan tingkat per segitiga.
- Gunakan beberapa algoritme untuk mengetahui blok mana yang saat ini dapat dilihat, karena blok yang lebih dekat dengan pengguna dapat mengaburkan blok di belakang, sehingga tidak ada gunanya untuk merendernya.
- Jaga agar objek blok tetap ringan, sehingga cepat untuk menambah dan menghapusnya dari pohon.
Saya kira tidak ada jawaban yang tepat untuk ini, tetapi saya akan tertarik untuk melihat pendapat orang tentang masalah ini. Bagaimana Anda meningkatkan kinerja di dunia berbasis voxel yang besar?
sumber
Jawaban:
Sehubungan dengan Java vs C ++, saya telah menulis mesin voxel di keduanya (versi C ++ ditunjukkan di atas). Saya juga sudah menulis mesin voxel sejak 2004 (ketika mereka tidak mode). :) Saya dapat mengatakan dengan sedikit keraguan bahwa kinerja C ++ jauh lebih unggul (tetapi juga lebih sulit untuk dikodekan). Ini kurang tentang kecepatan komputasi, dan lebih banyak tentang manajemen memori. Hands down, ketika Anda mengalokasikan / mendeallocating data sebanyak yang ada di dunia voxel, C (++) adalah bahasa yang harus dikalahkan. Namun, Anda harus memikirkan tujuan Anda. Jika kinerja adalah prioritas tertinggi Anda, lanjutkan dengan C ++. Jika Anda hanya ingin menulis permainan tanpa kinerja yang luar biasa, Java pasti dapat diterima (seperti yang dibuktikan oleh Minecraft). Ada banyak kasus sepele / tepi, tetapi secara umum Anda dapat mengharapkan Java berjalan sekitar 1,75-2,0 kali lebih lambat dari (ditulis dengan baik) C ++. Anda dapat melihat versi mesin saya yang kurang optimal dan dioptimalkan sedang berjalan di sini (EDIT: versi yang lebih baru di sini ). Sementara generasi chunk mungkin tampak lambat, perlu diingat itu menghasilkan diagram voronoi 3D secara volumetrik, menghitung permukaan normal, pencahayaan, AO, dan bayangan pada CPU dengan metode brute-force. Saya telah mencoba berbagai teknik dan saya bisa mendapatkan sekitar 100x generasi chunk lebih cepat menggunakan berbagai teknik caching dan instancing.
Untuk menjawab sisa pertanyaan Anda, ada banyak hal yang dapat Anda lakukan untuk meningkatkan kinerja.
Berikan data sesedikit mungkin ke kartu video. Satu hal yang cenderung dilupakan orang adalah semakin banyak data yang Anda berikan ke GPU, semakin banyak waktu yang diperlukan. Saya lulus dalam satu warna dan posisi simpul. Jika saya ingin melakukan siklus siang / malam, saya bisa melakukan gradasi warna, atau saya bisa menghitung ulang pemandangan saat matahari berangsur-angsur berubah.
Karena mengirimkan data ke GPU sangat mahal, dimungkinkan untuk menulis mesin dalam perangkat lunak yang lebih cepat dalam beberapa hal. Keuntungan dari perangkat lunak adalah dapat melakukan semua jenis manipulasi data / akses memori yang tidak mungkin dilakukan pada GPU.
Bermain dengan ukuran bets. Jika Anda menggunakan GPU, kinerja dapat bervariasi secara dramatis berdasarkan seberapa besar setiap larik simpul yang Anda lewati. Dengan demikian, bermain-main dengan ukuran potongan (jika Anda menggunakan potongan). Saya telah menemukan bahwa potongan 64x64x64 bekerja cukup baik. Apa pun yang terjadi, pertahankan potongan kubik Anda (tidak ada prisma persegi panjang). Ini akan membuat pengkodean dan berbagai operasi (seperti transformasi) lebih mudah, dan dalam beberapa kasus, lebih banyak performan. Jika Anda hanya menyimpan satu nilai untuk panjang setiap dimensi, ingatlah bahwa ada dua register yang lebih sedikit yang dipertukarkan saat menghitung.
Pertimbangkan daftar tampilan (untuk OpenGL). Meskipun mereka adalah cara "lama", mereka bisa lebih cepat. Anda harus memanggang daftar tampilan menjadi variabel ... jika Anda memanggil operasi pembuatan daftar tampilan secara realtime, itu akan lambat sekali. Bagaimana daftar tampilan lebih cepat? Itu hanya memperbarui status, vs atribut per-simpul. Ini berarti saya bisa melewatkan hingga enam wajah, lalu satu warna (vs warna untuk setiap simpul voxel). Jika Anda menggunakan GL_QUADS dan voxels kubik, ini bisa menghemat hingga 20 byte (160 bit) per voxel! (15 byte tanpa alfa, meskipun biasanya Anda ingin menjaga hal-hal 4-byte selaras.)
Saya menggunakan metode brute-force rendering "chunk", atau halaman data, yang merupakan teknik umum. Tidak seperti octrees, jauh lebih mudah / lebih cepat untuk membaca / memproses data, meskipun jauh lebih sedikit memori-friendly (namun, hari ini Anda bisa mendapatkan 64 gigabytes memori untuk $ 200- $ 300) ... bukan berarti bahwa rata-rata pengguna memiliki itu. Jelas, Anda tidak dapat mengalokasikan satu array besar untuk seluruh dunia (1024x1024x1024 set voxels adalah 4 gigabytes memori, dengan asumsi 32-bit int digunakan per voxel). Jadi, Anda mengalokasikan / membatalkan banyak array kecil, berdasarkan kedekatannya dengan pemirsa. Anda juga dapat mengalokasikan data, mendapatkan daftar tampilan yang diperlukan, lalu membuang data untuk menghemat memori. Saya pikir kombo yang ideal mungkin menggunakan pendekatan hibrida dari octrees dan array - menyimpan data dalam array ketika melakukan generasi prosedural dunia, pencahayaan, dll.
Jadikan dekat ke jauh ... piksel yang terpotong menghemat waktu. GPU akan melempar piksel jika tidak lulus uji buffer kedalaman.
Berikan hanya potongan / halaman di viewport (cukup jelas). Bahkan jika gpu tahu cara klip polgyons di luar viewport, melewati data ini masih membutuhkan waktu. Saya tidak tahu seperti apa struktur yang paling efisien untuk ini ("memalukan," Saya tidak pernah menulis pohon BSP), tetapi bahkan raycast sederhana pada basis per potong dapat meningkatkan kinerja, dan jelas menguji terhadap frustum penglihatan akan menghemat waktu.
Info yang jelas, tetapi untuk pemula: singkirkan setiap poligon tunggal yang tidak ada di permukaan - yaitu jika voxel terdiri dari enam wajah, lepaskan wajah yang tidak pernah di-render (menyentuh voxel lain).
Sebagai aturan umum semua yang Anda lakukan dalam pemrograman: CACHE LOCALITY! Jika Anda dapat menyimpan hal-hal cache-lokal (bahkan untuk sejumlah kecil waktu, itu akan membuat perbedaan besar. Ini berarti menjaga data Anda kongruen (di wilayah memori yang sama), dan tidak mengganti area memori untuk diproses terlalu sering. Jadi , idealnya, bekerja pada satu chunk per thread, dan jaga agar memori itu eksklusif untuk thread.Ini tidak hanya berlaku untuk cache CPU. Pikirkan hirarki cache seperti ini (paling lambat hingga tercepat): jaringan (cloud / database / dll) -> hard drive (dapatkan SSD jika Anda belum memilikinya), ram (dapatkan saluran tripple atau RAM lebih besar jika Anda belum memilikinya), CPU Cache (s), register. Coba simpan data Anda di akhir yang terakhir, dan tidak menukar lebih dari yang Anda harus.
Threading. Lakukan. Dunia Voxel sangat cocok untuk threading, karena setiap bagian dapat dihitung (sebagian besar) secara independen dari yang lain ... Saya melihat peningkatan hampir-4x (pada inti 4, inti 8 i7) dalam generasi dunia prosedural ketika saya menulis rutinitas untuk threading.
Jangan gunakan tipe data char / byte. Atau celana pendek. Rata-rata konsumen Anda akan memiliki prosesor AMD atau Intel modern (seperti halnya Anda, mungkin). Prosesor ini tidak memiliki register 8 bit. Mereka menghitung byte dengan menempatkannya ke dalam slot 32 bit, kemudian mengubahnya kembali (mungkin) dalam memori. Kompiler Anda dapat melakukan semua jenis voodoo, tetapi menggunakan nomor 32 atau 64 bit akan memberi Anda hasil yang paling dapat diprediksi (dan tercepat). Demikian juga, nilai "bool" tidak membutuhkan 1 bit; kompiler akan sering menggunakan 32 bit penuh untuk bool. Mungkin tergoda untuk melakukan jenis kompresi tertentu pada data Anda. Misalnya, Anda dapat menyimpan 8 voxel sebagai satu angka (2 ^ 8 = 256 kombinasi) jika semuanya jenis / warna yang sama. Namun, Anda harus berpikir tentang konsekuensi dari ini - mungkin menghemat banyak memori, tetapi itu juga dapat menghambat kinerja, bahkan dengan waktu dekompresi yang kecil, karena bahkan jumlah waktu ekstra yang kecil itu secara kubik dengan ukuran dunia Anda. Bayangkan menghitung raycast; untuk setiap langkah raycast, Anda harus menjalankan algoritma dekompresi (kecuali jika Anda menemukan cara cerdas untuk menggeneralisasi perhitungan untuk 8 voxels dalam satu langkah ray).
Seperti yang dikatakan Jose Chavez, pola desain kelas terbang dapat bermanfaat. Sama seperti Anda akan menggunakan bitmap untuk mewakili ubin dalam game 2D, Anda dapat membangun dunia Anda dari beberapa jenis ubin (atau blok) 3D. Kelemahan dari ini adalah pengulangan tekstur, tetapi Anda dapat memperbaiki ini dengan menggunakan tekstur varians yang cocok bersama. Sebagai aturan praktis, Anda ingin memanfaatkan instancing di mana pun Anda bisa.
Hindari pemrosesan simpul dan piksel dalam shader saat mengeluarkan geometri. Dalam mesin voxel Anda pasti akan memiliki banyak segitiga, sehingga bahkan shader piksel sederhana dapat sangat mengurangi waktu render Anda. Lebih baik me-render ke buffer, lalu membuat Anda pixel shader sebagai proses pasca. Jika Anda tidak bisa melakukan itu, coba lakukan perhitungan di vertex shader Anda. Kalkulasi lain harus dimasukkan ke dalam data titik jika memungkinkan. Akses tambahan menjadi sangat mahal jika Anda harus merender ulang semua geometri (seperti pemetaan bayangan atau pemetaan lingkungan). Terkadang lebih baik untuk melepaskan adegan dinamis demi detail yang lebih kaya. Jika gim Anda memiliki adegan yang dapat dimodifikasi (yaitu medan yang dapat dirusak), Anda selalu dapat menghitung ulang adegan saat semuanya dihancurkan. Rekompilasi tidak mahal dan harus di bawah satu detik.
Lepaskan loop Anda dan jaga agar array tetap rata! Jangan lakukan ini:
EDIT: Melalui pengujian yang lebih luas, saya telah menemukan ini bisa salah. Gunakan kasing yang paling sesuai untuk skenario Anda. Secara umum, array harus rata, tetapi menggunakan multi-index loop seringkali bisa lebih cepat tergantung pada case
EDIT 2: saat menggunakan loop multi-indeks, yang terbaik untuk loop ke dalam urutan z, y, x daripada sebaliknya. Kompiler Anda mungkin mengoptimalkan ini, tetapi saya akan terkejut jika melakukannya. Ini memaksimalkan efisiensi dalam akses memori dan lokalitas.
Anda dapat membaca lebih lanjut tentang implementasi saya di situs saya
sumber
Ada banyak hal yang bisa dilakukan Minecraft dengan lebih efisien. Misalnya, Minecraft memuat seluruh pilar vertikal sekitar 16x16 ubin dan menjadikannya. Saya merasa sangat tidak efisien untuk mengirim dan membuat banyak ubin tidak perlu. Tetapi saya tidak merasa bahwa pilihan bahasa adalah yang penting.
Java bisa sangat cepat tetapi untuk sesuatu yang berorientasi data ini, C ++ memang memiliki keuntungan besar dengan overhead yang jauh lebih sedikit untuk mengakses array dan bekerja dalam byte. Di sisi lain, jauh lebih mudah untuk melakukan threading di semua platform di Jawa. Kecuali Anda berencana untuk menggunakan OpenMP atau OpenCL, Anda tidak akan menemukan kenyamanan itu di C ++.
Sistem ideal saya akan menjadi hierarki yang sedikit lebih kompleks.
Tile adalah unit tunggal, kemungkinan sekitar 4 byte untuk menyimpan informasi seperti jenis material dan pencahayaan.
Segmen akan menjadi blok ubin berukuran 32x32x32.
Sektor akan menjadi blok segmen 16x16x8.
Dunia akan menjadi peta sektor yang tak terbatas.
sumber
Minecraft cukup cepat, bahkan pada 2-core saya. Java tampaknya tidak menjadi faktor pembatas, di sini, meskipun ada sedikit kelambatan server. Game lokal tampaknya lebih baik, jadi saya akan mengasumsikan beberapa inefisiensi, di sana.
Mengenai pertanyaan Anda, Notch (penulis Minecraft) telah membuat blog panjang lebar tentang teknologi. Secara khusus, dunia disimpan dalam "chunk" (Anda kadang-kadang melihat ini, terutama ketika seseorang hilang karena dunia belum terisi.), Jadi optimisasi pertama adalah memutuskan apakah suatu bongkahan dapat dilihat atau tidak .
Di dalam bongkahan, seperti yang sudah Anda duga, aplikasi harus memutuskan apakah suatu blok dapat dilihat atau tidak, berdasarkan apakah dikaburkan oleh blok lain atau tidak.
Perhatikan juga, bahwa ada FAKTA blok, yang dapat dianggap tidak terlihat, berdasarkan entah dikaburkan (yaitu, blok lain menutupi wajah) atau dengan arah mana kamera menunjuk (jika kamera menghadap Utara, Anda dapat dapat melihat wajah Utara blok APA PUN!)
Teknik umum juga termasuk tidak menyimpan objek blok yang terpisah tetapi, "sepotong" tipe blok, dengan satu blok prototipe untuk masing-masing, bersama dengan beberapa set data minimal untuk menggambarkan bagaimana blok ini dapat dikustomisasi. Misalnya, tidak ada blok granit khusus (yang saya tahu), tetapi air memiliki data untuk mengetahui seberapa dalam di sepanjang sisi-sisi, dari mana orang dapat menghitung arah alirannya.
Pertanyaan Anda tidak jelas jika Anda ingin mengoptimalkan kecepatan render, ukuran data, atau apa. Klarifikasi akan sangat membantu.
sumber
Berikut ini hanya beberapa kata dari info dan saran umum, yang dapat saya berikan sebagai, um, modder Minecraft yang terlalu berpengalaman (yang mungkin setidaknya sebagian memberi Anda beberapa panduan.)
Alasan mengapa Minecraft lambat terkait dengan keputusan desain tingkat rendah yang dipertanyakan - misalnya, setiap kali sebuah blok direferensikan oleh pemosisian, permainan memvalidasi koordinat dengan sekitar 7 jika pernyataan untuk memastikan itu tidak di luar batas. . Selain itu, tidak ada cara untuk mengambil 'bongkahan' (unit blok 16x16x256 yang berfungsi dengan game) kemudian merujuk blok di dalamnya secara langsung, untuk mem-bypass pencarian cache dan, erm, masalah validasi konyol (iow, setiap referensi blok juga melibatkan pencarian bongkahan, antara lain.) Dalam mod saya, saya menciptakan cara untuk mengambil dan mengubah susunan blok secara langsung, yang mendorong generasi dungeon besar-besaran dari laggy yang tidak bisa dimainkan menjadi sangat cepat.
EDIT: Penghapusan klaim bahwa mendeklarasikan variabel pada ruang lingkup yang berbeda menghasilkan keuntungan kinerja, ini sebenarnya tidak tampak seperti masalahnya. Saya percaya pada saat itu saya menggabungkan hasil ini dengan sesuatu yang saya eksperimen (khususnya, menghilangkan gips antara ganda dan mengapung dalam kode terkait ledakan dengan menggabungkan ke ganda ... dapat dimengerti, ini memiliki dampak besar!)
Selain itu, meskipun ini bukan bidang tempat saya menghabiskan banyak waktu, sebagian besar gangguan kinerja di Minecraft adalah masalah rendering (sekitar 75% dari waktu permainan didedikasikan untuk itu di sistem saya). Jelas Anda tidak terlalu peduli jika kekhawatiran itu mendukung lebih banyak pemain dalam multipemain (server tidak membuat apa pun), tetapi itu penting sejauh mesin semua orang bisa bermain.
Jadi, apa pun bahasa yang Anda pilih, cobalah untuk menjadi sangat akrab dengan implementasi / detail level rendah, karena bahkan satu detail kecil dalam proyek seperti ini dapat membuat semua perbedaan (satu contoh bagi saya dalam C ++ adalah "Dapatkah kompiler menjalankan fungsi inline secara statis pointer? "Ya itu bisa! Membuat perbedaan yang luar biasa dalam salah satu proyek yang saya kerjakan, karena saya memiliki lebih sedikit kode dan keuntungan dari inlining.)
Saya benar-benar tidak menyukai jawaban itu karena itu membuat desain tingkat tinggi sulit, tetapi itu adalah kebenaran yang menyakitkan jika kinerja menjadi perhatian. Semoga Anda menemukan ini bermanfaat!
Juga, jawaban Gavin mencakup beberapa perincian yang saya tidak ingin ulangi (dan lebih banyak lagi! Dia jelas lebih banyak tahu tentang masalah ini daripada saya), dan saya setuju dengan dia untuk sebagian besar. Saya harus bereksperimen dengan komentarnya mengenai prosesor dan ukuran variabel yang lebih pendek, saya belum pernah mendengarnya - saya ingin membuktikan kepada diri sendiri bahwa itu benar!
sumber
Masalahnya adalah memikirkan bagaimana Anda pertama-tama memuat data. Jika Anda mengalirkan data peta ke memori saat diperlukan, ada batas wajar untuk apa yang dapat Anda render, ini sudah merupakan peningkatan kinerja rendering.
Apa yang Anda lakukan dengan data ini terserah Anda. Untuk kinerja GFX, Anda kemudian dapat menggunakan Kliping untuk klip objek yang tersembunyi, objek yang terlalu kecil untuk terlihat, dll.
Jika Anda hanya mencari teknik Kinerja Grafik, saya yakin Anda dapat menemukan banyak hal di internet.
sumber
Yang perlu dilihat adalah pola desain Flyweight . Saya percaya sebagian besar jawaban di sini merujuk pola desain ini dengan satu atau lain cara.
Meskipun saya tidak tahu metode pasti yang digunakan Minecraft untuk meminimalkan memori untuk setiap jenis blok, ini adalah cara yang mungkin untuk digunakan dalam game Anda. Idenya adalah memiliki hanya satu objek, seperti objek prototipe, yang menyimpan informasi tentang semua blok. Satu-satunya perbedaan adalah lokasi setiap blok.
Tetapi bahkan lokasi dapat diminimalkan: jika Anda tahu satu blok tanah adalah satu jenis, mengapa tidak menyimpan dimensi tanah itu sebagai satu blok raksasa, dengan satu set data lokasi?
Jelas satu-satunya cara untuk mengetahui adalah mulai menerapkan sendiri, dan melakukan beberapa tes memori untuk kinerja. Marilah kita tahu bagaimana kelanjutannya!
sumber