Kami menulis mesin voxel kami sendiri

gambar

Catatan: kode sumber lengkap untuk proyek ini tersedia di sini: [ sumber ].

Ketika proyek yang saya kerjakan mulai kehabisan tenaga, saya menambahkan visualisasi baru yang memberi saya motivasi untuk melanjutkan.

Setelah merilis konsep Task-Bot asli [ terjemahan ke Habré], saya merasa bahwa saya dibatasi oleh ruang dua dimensi tempat saya bekerja. Tampaknya itu menahan kemungkinan untuk perilaku bot yang muncul.

Upaya sebelumnya yang gagal untuk mempelajari OpenGL modern telah menempatkan saya di hadapan saya penghalang mental, tetapi pada akhir Juli saya akhirnya berhasil menerobosnya. Hari ini, pada akhir Oktober, saya sudah memiliki pemahaman konsep yang cukup percaya diri, jadi saya merilis mesin voxel sederhana saya sendiri, yang akan menjadi lingkungan untuk kehidupan dan kemakmuran Bot-Tugas saya.

Saya memutuskan untuk membuat mesin sendiri, karena saya memerlukan kontrol penuh atas grafis; selain itu, saya ingin menguji diri saya sendiri. Di satu sisi, saya menciptakan sepeda, tetapi saya sangat menyukai proses ini!

Tujuan akhir dari seluruh proyek adalah simulasi lengkap ekosistem, di mana bot dalam peran agen memanipulasi lingkungan dan berinteraksi dengannya.

Karena mesin sudah bergerak maju sedikit dan saya akan pindah ke pemrograman bot lagi, saya memutuskan untuk menulis posting tentang mesin, fungsinya dan implementasinya untuk fokus pada tugas-tugas tingkat yang lebih tinggi di masa depan.

Konsep mesin


Mesin sepenuhnya ditulis dari awal dalam C ++ (dengan beberapa pengecualian, seperti menemukan jalan). Saya menggunakan SDL2 untuk membuat konteks dan memproses input, OpenGL untuk membuat adegan 3D, dan DearImgui untuk mengontrol simulasi.

Saya memutuskan untuk menggunakan voxel terutama karena saya ingin bekerja dengan kisi yang memiliki banyak keuntungan:

  • Membuat jaring untuk rendering dipahami dengan baik bagi saya.
  • Kemampuan penyimpanan data dunia lebih beragam dan mudah dipahami.
  • Saya telah membuat sistem untuk menghasilkan simulasi medan dan iklim berdasarkan jerat.
  • Tugas bot di grid lebih mudah untuk parameter.

Engine terdiri dari sistem data dunia, sistem render, dan beberapa kelas bantu (misalnya, untuk pemrosesan suara dan input).

Dalam artikel ini saya akan berbicara tentang daftar fitur saat ini, serta melihat lebih dekat pada subsistem yang lebih kompleks.

Kelas dunia


Kelas dunia berfungsi sebagai kelas dasar untuk menyimpan semua informasi dunia. Ini menangani pembuatan, pemuatan, dan penyimpanan data blok.

Data blok disimpan dalam potongan ukuran konstan (16 ^ 3), dan dunia menyimpan vektor fragmen yang dimuat ke dalam memori virtual. Dalam dunia besar, praktis perlu mengingat hanya bagian tertentu dari dunia, itulah sebabnya saya memilih pendekatan ini.

class World{ public: World(std::string _saveFile){ saveFile = _saveFile; loadWorld(); } //Data Storage std::vector<Chunk> chunks; //Loaded Chunks std::stack<int> updateModels; //Models to be re-meshed void bufferChunks(View view); //Generation void generate(); Blueprint blueprint; bool evaluateBlueprint(Blueprint &_blueprint); //File IO Management std::string saveFile; bool loadWorld(); bool saveWorld(); //other... int SEED = 100; int chunkSize = 16; int tickLength = 1; glm::vec3 dim = glm::vec3(20, 5, 20); //... 

Fragmen menyimpan data blok, serta beberapa metadata lainnya, dalam susunan datar. Awalnya, saya menerapkan pohon oktri saya sendiri untuk menyimpan fragmen, tetapi ternyata waktu akses acak terlalu tinggi untuk membuat jerat. Dan meskipun susunan flat tidak optimal dari sudut pandang memori, ia menyediakan kemampuan untuk membangun jerat dan manipulasi dengan blok dengan cepat, serta akses ke jalur pencarian.

 class Chunk{ public: //Position information and size information glm::vec3 pos; int size; BiomeType biome; //Data Storage Member int data[16*16*16] = {0}; bool refreshModel = false; //Get the Flat-Array Index int getIndex(glm::vec3 _p); void setPosition(glm::vec3 _p, BlockType _type); BlockType getPosition(glm::vec3 _p); glm::vec4 getColorByID(BlockType _type); }; 

Jika saya pernah menerapkan fragmen penyimpanan dan pemuatan multi-utas, maka mengonversi susunan datar menjadi pohon octree yang jarang dan sebaliknya dapat menjadi opsi yang sepenuhnya memungkinkan untuk menghemat memori. Masih ada ruang untuk optimasi!

Implementasi saya dari pohon octree jarang disimpan dalam kode, sehingga Anda dapat menggunakannya dengan aman.

Penyimpanan fragmen dan penanganan memori


Fragmen hanya terlihat ketika mereka berada dalam jarak rendering dari posisi kamera saat ini. Ini berarti bahwa ketika kamera bergerak, Anda harus secara dinamis memuat dan menyusun fragmen di jerat.

Fragmen diserialisasi dengan menggunakan boost library, dan data dunia disimpan sebagai file teks sederhana, di mana setiap fragmen adalah satu baris file. Mereka dihasilkan dalam urutan tertentu sehingga mereka dapat "dipesan" dalam file dunia. Ini penting untuk optimasi lebih lanjut.

Dalam kasus dunia yang besar, hambatan utama adalah membaca file dunia dan memuat / menulis fragmen. Idealnya, kita hanya perlu mengunduh dan mentransfer file dunia.

Untuk melakukan ini, metode World::bufferChunks() menghapus fragmen yang ada di memori virtual tetapi tidak terlihat, dan secara cerdas memuat fragmen baru dari file dunia.

Dengan kecerdasan berarti bahwa ia hanya memutuskan fragmen baru mana yang akan dimuat, menyortirnya berdasarkan posisi mereka di file save, dan kemudian melakukan satu pass. Semuanya sangat sederhana.

 void World::bufferChunks(View view){ //Load / Reload all Visible Chunks evaluateBlueprint(blueprint); //Chunks that should be loaded glm::vec3 a = glm::floor(view.viewPos/glm::vec3(chunkSize))-view.renderDistance; glm::vec3 b = glm::floor(view.viewPos/glm::vec3(chunkSize))+view.renderDistance; //Can't exceed a certain size a = glm::clamp(a, glm::vec3(0), dim-glm::vec3(1)); b = glm::clamp(b, glm::vec3(0), dim-glm::vec3(1)); //Chunks that need to be removed / loaded std::stack<int> remove; std::vector<glm::vec3> load; //Construct the Vector of chunks we should load for(int i = ax; i <= bx; i ++){ for(int j = ay; j <= by; j ++){ for(int k = az; k <= bz; k ++){ //Add the vector that we should be loading load.push_back(glm::vec3(i, j, k)); } } } //Loop over all existing chunks for(unsigned int i = 0; i < chunks.size(); i++){ //Check if any of these chunks are outside of the limits if(glm::any(glm::lessThan(chunks[i].pos, a)) || glm::any(glm::greaterThan(chunks[i].pos, b))){ //Add the chunk to the erase pile remove.push(i); } //Don't reload chunks that remain for(unsigned int j = 0; j < load.size(); j++){ if(glm::all(glm::equal(load[j], chunks[i].pos))){ //Remove the element from load load.erase(load.begin()+j); } } //Flags for the Viewclass to use later updateModels = remove; //Loop over the erase pile, delete the relevant chunks. while(!remove.empty()){ chunks.erase(chunks.begin()+remove.top()); remove.pop(); } //Check if we want to load any guys if(!load.empty()){ //Sort the loading vector, for single file-pass std::sort(load.begin(), load.end(), [](const glm::vec3& a, const glm::vec3& b) { if(ax > bx) return true; if(ax < bx) return false; if(ay > by) return true; if(ay < by) return false; if(az > bz) return true; if(az < bz) return false; return false; }); boost::filesystem::path data_dir( boost::filesystem::current_path() ); data_dir /= "save"; data_dir /= saveFile; std::ifstream in((data_dir/"world.region").string()); Chunk _chunk; int n = 0; while(!load.empty()){ //Skip Lines (this is dumb) while(n < load.back().x*dim.z*dim.y+load.back().y*dim.z+load.back().z){ in.ignore(1000000,'\n'); n++; } //Load the Chunk { boost::archive::text_iarchive ia(in); ia >> _chunk; chunks.push_back(_chunk); load.pop_back(); } } in.close(); } } 


Contoh memuat fragmen dengan jarak rendering kecil. Artefak distorsi layar disebabkan oleh perangkat lunak perekaman video. Terkadang terjadi lonjakan unduhan, terutama disebabkan oleh penyambungan

Selain itu, saya menetapkan bendera yang menunjukkan bahwa renderer harus membuat ulang jala dari fragmen yang dimuat.

Kelas Cetak Biru dan editBuffer


editBuffer adalah wadah bufferObjects yang dapat diurutkan yang berisi informasi tentang pengeditan di ruang dunia dan ruang fragmen.

 //EditBuffer Object Struct struct bufferObject { glm::vec3 pos; glm::vec3 cpos; BlockType type; }; //Edit Buffer! std::vector<bufferObject> editBuffer; 

Jika, ketika membuat perubahan ke dunia, menuliskannya ke file segera setelah melakukan perubahan, maka kita harus mentransfer seluruh file teks dan menulis SETIAP perubahan. Ini mengerikan dalam hal kinerja.

Jadi pertama-tama saya menulis semua perubahan yang perlu dilakukan untuk mengeditBuffer menggunakan metode addEditBuffer (yang juga menghitung posisi perubahan dalam ruang fragmen). Sebelum menulis mereka ke file, saya mengurutkan perubahan dalam urutan fragmen yang menjadi milik mereka sesuai dengan lokasi mereka di file.

Menulis perubahan pada file terdiri dari satu transfer file, memuat setiap baris (mis. Fragmen), yang untuknya ada perubahan di editBuffer, membuat semua perubahan dan menulisnya ke file sementara hingga editBuffer menjadi kosong. Ini dilakukan dalam fungsi evaluateBlueprint() , yang cukup cepat.

 bool World::evaluateBlueprint(Blueprint &_blueprint){ //Check if the editBuffer isn't empty! if(_blueprint.editBuffer.empty()){ return false; } //Sort the editBuffer std::sort(_blueprint.editBuffer.begin(), _blueprint.editBuffer.end(), std::greater<bufferObject>()); //Open the File boost::filesystem::path data_dir(boost::filesystem::current_path()); data_dir /= "save"; data_dir /= saveFile; //Load File and Write File std::ifstream in((data_dir/"world.region").string()); std::ofstream out((data_dir/"world.region.temp").string(), std::ofstream::app); //Chunk for Saving Data Chunk _chunk; int n_chunks = 0; //Loop over the Guy while(n_chunks < dim.x*dim.y*dim.z){ if(in.eof()){ return false; } //Archive Serializers boost::archive::text_oarchive oa(out); boost::archive::text_iarchive ia(in); //Load the Chunk ia >> _chunk; //Overwrite relevant portions while(!_blueprint.editBuffer.empty() && glm::all(glm::equal(_chunk.pos, _blueprint.editBuffer.back().cpos))){ //Change the Guy _chunk.setPosition(glm::mod(_blueprint.editBuffer.back().pos, glm::vec3(chunkSize)), _blueprint.editBuffer.back().type); _blueprint.editBuffer.pop_back(); } //Write the chunk back oa << _chunk; n_chunks++; } //Close the fstream and ifstream in.close(); out.close(); //Delete the first file, rename the temp file boost::filesystem::remove_all((data_dir/"world.region").string()); boost::filesystem::rename((data_dir/"world.region.temp").string(),(data_dir/"world.region").string()); //Success! return true; } 

Kelas cetak biru berisi editBuffer, serta beberapa metode yang memungkinkan Anda membuat editBuffers untuk objek tertentu (pohon, kaktus, gubuk, dll.). Kemudian cetak biru dapat dikonversi ke posisi di mana Anda ingin meletakkan objek, dan kemudian hanya menulisnya ke memori dunia.

Salah satu kesulitan terbesar ketika bekerja dengan fragmen adalah bahwa perubahan dalam beberapa blok antara batas fragmen dapat berubah menjadi proses yang monoton dengan banyak modulo aritmatika dan membagi perubahan menjadi beberapa bagian. Ini adalah masalah utama yang ditangani oleh kelas cetak biru dengan cemerlang.

Saya secara aktif menggunakannya pada tahap generasi dunia untuk memperluas "bottleneck" dari menulis perubahan ke file.

 void World::generate(){ //Create an editBuffer that contains a flat surface! blueprint.flatSurface(dim.x*chunkSize, dim.z*chunkSize); //Write the current blueprint to the world file. evaluateBlueprint(blueprint); //Add a tree Blueprint _tree; evaluateBlueprint(_tree.translate(glm::vec3(x, y, z))); } 

Kelas dunia menyimpan cetak biru perubahan yang dibuat untuk dunia, sehingga ketika bufferChunks () dipanggil, semua perubahan ditulis ke hard disk dalam satu pass dan kemudian dihapus dari memori virtual.

Rendering


Penyaji dalam strukturnya tidak terlalu rumit, tetapi membutuhkan pengetahuan tentang OpenGL untuk memahaminya. Tidak semua bagiannya menarik, terutama pembungkus fungsionalitas OpenGL. Saya bereksperimen dengan visualisasi selama beberapa waktu untuk mendapatkan apa yang saya sukai.

Karena simulasi bukan dari orang pertama, saya memilih proyeksi ortografi. Ini dapat diimplementasikan dalam format pseudo-3D (mis., Untuk memproyeksikan petak-petak dan menatanya dalam renderer perangkat lunak), tetapi tampaknya konyol bagi saya. Saya senang saya beralih menggunakan OpenGL.


Kelas dasar untuk rendering disebut View, berisi sebagian besar variabel penting yang mengontrol visualisasi simulasi:

  • Ukuran layar dan tekstur bayangan
  • Objek shader, kamera, matriks, dll. Faktor zoom
  • Nilai Boolean untuk hampir semua fungsi penyaji
    • Menu, kabut, kedalaman bidang, tekstur butir, dll.
  • Warna untuk penerangan, kabut, langit, pemilihan jendela, dll.

Selain itu, ada beberapa kelas pembantu yang melakukan rendering dan pembungkus OpenGL sendiri!

  • Shader kelas
    • Memuat, mengkompilasi, mengkompilasi dan menggunakan shader GLSL
  • Kelas Model
    • Berisi fragmen data VAO (Vertex Arrays Object) untuk rendering, fungsi membuat jerat dan metode render.
  • Papan iklan kelas
    • Berisi FBO (FrameBuffer Object) untuk di-render - berguna untuk membuat efek post-processing dan shadowing.
  • Kelas sprite
    • Menarik relatif berorientasi segi empat ke kamera, diambil dari file tekstur (untuk bot dan objek). Juga dapat menangani animasi!
  • Kelas antarmuka
    • Untuk bekerja dengan ImGUI
  • Kelas audio
    • Dukungan suara yang sangat sederhana (jika Anda mengkompilasi mesin, tekan "M")


Kedalaman Tinggi Bidang (DOF). Pada jarak rendering yang besar, ini bisa lambat, tapi saya melakukan semua ini di laptop saya. Mungkin di komputer yang bagus remnya tidak akan terlihat. Saya mengerti bahwa itu menegangkan mata saya dan melakukannya hanya untuk bersenang-senang.

Gambar di atas menunjukkan beberapa parameter yang dapat diubah selama manipulasi. Saya juga menerapkan peralihan ke mode layar penuh. Gambar menunjukkan contoh bot sprite yang diberikan sebagai segiempat bertekstur yang diarahkan ke kamera. Rumah dan kaktus dalam gambar dibangun menggunakan cetak biru.

Membuat Jejaring Fragmen


Awalnya, saya menggunakan versi naif untuk membuat jerat: Saya hanya membuat kubus dan membuang simpul yang tidak menyentuh ruang kosong. Namun, solusi ini lambat, dan ketika memuat fragmen baru, pembuatan jerat ternyata menjadi “bottleneck” yang lebih sempit daripada akses ke file.

Masalah utamanya adalah pembuatan fragmen VBO yang diberikan secara efisien, tetapi saya berhasil mengimplementasikan C ++ versi saya sendiri “greedy meshing” (greedy meshing), kompatibel dengan OpenGL (tanpa struktur aneh dengan loop). Anda dapat menggunakan kode saya dengan hati nurani yang bersih.

 void Model::fromChunkGreedy(Chunk chunk){ //... (this is part of the model class - find on github!) } 

Secara umum, transisi ke meshing serakah mengurangi jumlah segi empat yang ditarik rata-rata 60%. Kemudian, setelah optimasi minor lebih lanjut (pengindeksan VBO), jumlahnya dikurangi dengan 1/3 lainnya (dari 6 simpul ke ujung menjadi 4 simpul).

Saat merender adegan 5x1x5 dalam jendela yang tidak dimaksimalkan, saya mendapatkan rata-rata sekitar 140 FPS (dengan VSYNC dinonaktifkan).

Meskipun saya cukup senang dengan hasil ini, saya masih ingin datang dengan sistem untuk rendering model non-kubik dari data dunia. Tidak mudah untuk diintegrasikan dengan ketamakan rakus, jadi patut dipertimbangkan.

Shader dan penyorotan voxel


Implementasi GLSL shaders adalah salah satu yang paling menarik dan pada saat yang sama bagian yang paling mengganggu dari penulisan mesin karena kompleksitas debugging pada GPU. Saya bukan spesialis GLSL, jadi saya harus belajar banyak saat bepergian.

Efek yang saya terapkan secara aktif menggunakan FBO dan sampling tekstur (misalnya, kabur, membayangi, dan menggunakan informasi mendalam).

Saya masih tidak suka model pencahayaan saat ini, karena tidak menangani "gelap" dengan baik. Saya berharap ini akan diperbaiki di masa depan ketika saya bekerja pada siklus perubahan siang dan malam.

Saya juga menerapkan fungsi pemilihan voxel sederhana menggunakan algoritma Bresenham yang dimodifikasi (ini adalah keuntungan lain dari menggunakan voxels). Berguna untuk mendapatkan informasi spasial selama simulasi. Implementasi saya hanya berfungsi untuk proyeksi ortografis, tetapi Anda dapat menggunakannya.


Labu "disorot".

Kelas game


Beberapa kelas tambahan telah dibuat untuk memproses input, men-debug pesan, serta kelas Item terpisah dengan fungsionalitas dasar (yang akan diperluas lebih lanjut).

 class eventHandler{ /* This class handles user input, creates an appropriate stack of activated events and handles them so that user inputs have continuous effect. */ public: //Queued Inputs std::deque<SDL_Event*> inputs; //General Key Inputs std::deque<SDL_Event*> scroll; //General Key Inputs std::deque<SDL_Event*> rotate; //Rotate Key Inputs SDL_Event* mouse; //Whatever the mouse is doing at a moment SDL_Event* windowevent; //Whatever the mouse is doing at a moment bool _window; bool move = false; bool click = false; bool fullscreen = false; //Take inputs and add them to stack void input(SDL_Event *e, bool &quit, bool &paused); //Handle the existing stack every tick void update(World &world, Player &player, Population &population, View &view, Audio &audio); //Handle Individual Types of Events void handlePlayerMove(World &world, Player &player, View &view, int a); void handleCameraMove(World &world, View &view); }; 

Penangan event saya jelek, tapi fungsional. Saya dengan senang hati akan menerima rekomendasi untuk perbaikannya, terutama pada penggunaan Acara Poll SDL.

Catatan terbaru


Mesinnya sendiri hanyalah sebuah sistem di mana saya meletakkan bot-tugas saya (saya akan membicarakannya secara rinci di posting berikutnya). Tetapi jika Anda menemukan metode saya menarik dan Anda ingin tahu lebih banyak, maka menulis kepada saya.

Kemudian saya memindahkan sistem task-bot (jantung sebenarnya dari proyek ini) ke dunia 3D dan secara signifikan memperluas kemampuannya, tetapi lebih banyak tentang itu nanti (namun, kode telah diposting secara online)!

Source: https://habr.com/ru/post/id474414/


All Articles