#include "voxel_lod_terrain.h" #include "../math/rect3i.h" #include "../util/profiling_clock.h" #include "voxel_map.h" #include "voxel_mesh_updater.h" #include "voxel_provider_thread.h" #include VoxelLodTerrain::VoxelLodTerrain() { print_line("Construct VoxelLodTerrain"); _lods[0].map.instance(); set_lod_count(8); set_lod_split_scale(3); reset_updater(); } VoxelLodTerrain::~VoxelLodTerrain() { print_line("Destroy VoxelLodTerrain"); if (_provider_thread) { memdelete(_provider_thread); } if (_block_updater) { memdelete(_block_updater); } } Ref VoxelLodTerrain::get_material() const { return _material; } void VoxelLodTerrain::set_material(Ref p_material) { _material = p_material; } Ref VoxelLodTerrain::get_provider() const { return _provider; } int VoxelLodTerrain::get_block_size() const { return _lods[0].map->get_block_size(); } int VoxelLodTerrain::get_block_size_pow2() const { return _lods[0].map->get_block_size_pow2(); } void VoxelLodTerrain::set_provider(Ref p_provider) { if (p_provider != _provider) { if (_provider_thread) { memdelete(_provider_thread); _provider_thread = nullptr; } _provider = p_provider; _provider_thread = memnew(VoxelProviderThread(_provider, get_block_size_pow2())); // The whole map might change, so make all area dirty // TODO Actually, we should regenerate the whole map, not just update all its blocks make_all_view_dirty_deferred(); } } void VoxelLodTerrain::make_all_view_dirty_deferred() { for (int i = 0; i < get_lod_count(); ++i) { Lod &lod = _lods[i]; lod.last_view_distance_blocks = 0; } } int VoxelLodTerrain::get_view_distance() const { return 0; } void VoxelLodTerrain::set_view_distance(int p_distance_in_voxels) { // TODO this will be used to cap mesh visibility // ERR_FAIL_COND(p_distance_in_voxels < 0) // int d = p_distance_in_voxels / get_block_size(); // if (d != _view_distance_blocks) { // print_line(String("View distance changed from ") + String::num(_view_distance_blocks) + String(" blocks to ") + String::num(d)); // _view_distance_blocks = d; // // Blocks too far away will be removed in _process, same for blocks to load // } } Spatial *VoxelLodTerrain::get_viewer() const { if (_viewer_path.is_empty()) { return nullptr; } Node *node = get_node(_viewer_path); if (node == nullptr) { return nullptr; } return Object::cast_to(node); } void VoxelLodTerrain::immerge_block(Vector3i block_pos, unsigned int lod_index) { ERR_FAIL_COND(lod_index >= get_lod_count()); ERR_FAIL_COND(_lods[lod_index].map.is_null()); Lod &lod = _lods[lod_index]; // TODO Schedule block saving when supported lod.map->remove_block(block_pos, VoxelMap::NoAction()); lod.loading_blocks.erase(block_pos); // Blocks in the update queue will be cancelled in _process, // because it's too expensive to linear-search all blocks for each block // No need to remove things from blocks_pending_load, // This vector is filled and cleared immediately in the main process. // It is a member only to re-use its capacity memory over frames. } void VoxelLodTerrain::reset_updater() { if (_block_updater) { memdelete(_block_updater); _block_updater = NULL; } // TODO Thread-safe way to change those parameters VoxelMeshUpdater::MeshingParams params; params.smooth_surface = true; _block_updater = memnew(VoxelMeshUpdater(Ref(), params)); // TODO Revert any pending update states! } void VoxelLodTerrain::set_lod_split_scale(float p_lod_split_scale) { _lod_octree.set_split_scale(p_lod_split_scale); } float VoxelLodTerrain::get_lod_split_scale() const { return _lod_octree.get_split_scale(); } void VoxelLodTerrain::set_lod_count(unsigned int p_lod_count) { ERR_FAIL_COND(p_lod_count >= MAX_LOD); if (get_lod_count() != p_lod_count) { int bs = get_block_size(); _lod_octree.create_from_lod_count(bs, p_lod_count, LodOctree::NoDestroyAction()); for (int lod_index = 0; lod_index < MAX_LOD; ++lod_index) { Lod &lod = _lods[lod_index]; // Instance new maps if we have more lods, or clear them otherwise if (lod_index < get_lod_count()) { if (lod.map.is_null()) { lod.map.instance(); lod.map->set_lod_index(lod_index); } else { lod.map->clear(); } } else { if (lod.map.is_valid()) { lod.map.unref(); } } } } } int VoxelLodTerrain::get_lod_count() const { return _lod_octree.get_lod_count(); } void VoxelLodTerrain::set_viewer_path(NodePath path) { _viewer_path = path; } NodePath VoxelLodTerrain::get_viewer_path() const { return _viewer_path; } int VoxelLodTerrain::get_block_region_extent() const { // This is the radius of blocks around the viewer in which we may load them. // It depends on the LOD split scale, which tells how close to a block we need to be for it to subdivide. // Each LOD is fractal so that value is the same for each of them. return static_cast(_lod_octree.get_split_scale()) * 2 + 2; } Dictionary VoxelLodTerrain::get_block_info(Vector3 fbpos, unsigned int lod_index) const { // Gets some info useful for debugging Dictionary d; ERR_FAIL_COND_V(lod_index >= get_lod_count(), d); const Lod &lod = _lods[lod_index]; Vector3i bpos(fbpos); bool meshed = false; bool visible = false; int loading_state = 0; const VoxelBlock *block = lod.map->get_block(bpos); if (block) { meshed = block->has_been_meshed(); visible = block->is_visible(); loading_state = 2; } else if (lod.loading_blocks.has(bpos)) { loading_state = 1; } d["loading"] = loading_state; d["meshed"] = meshed; d["visible"] = visible; return d; } Vector3 VoxelLodTerrain::voxel_to_block_position(Vector3 vpos, unsigned int lod_index) const { ERR_FAIL_COND_V(lod_index >= get_lod_count(), Vector3()); const Lod &lod = _lods[lod_index]; Vector3i bpos = lod.map->voxel_to_block(Vector3i(vpos)) >> lod_index; return bpos.to_vec3(); } void VoxelLodTerrain::_notification(int p_what) { struct EnterWorldAction { World *world; EnterWorldAction(World *w) : world(w) {} void operator()(VoxelBlock *block) { block->enter_world(world); } }; struct ExitWorldAction { void operator()(VoxelBlock *block) { block->exit_world(); } }; struct SetVisibilityAction { bool visible; SetVisibilityAction(bool v) : visible(v) {} void operator()(VoxelBlock *block) { block->set_visible(visible); } }; switch (p_what) { case NOTIFICATION_ENTER_TREE: set_process(true); break; case NOTIFICATION_PROCESS: if (!Engine::get_singleton()->is_editor_hint()) { _process(); } break; case NOTIFICATION_EXIT_TREE: break; case NOTIFICATION_ENTER_WORLD: for_all_blocks(EnterWorldAction(*get_world())); break; case NOTIFICATION_EXIT_WORLD: for_all_blocks(ExitWorldAction()); break; case NOTIFICATION_VISIBILITY_CHANGED: for_all_blocks(SetVisibilityAction(is_visible())); break; // TODO Listen for transform changes default: break; } } Vector3 VoxelLodTerrain::get_viewer_pos() const { if (Engine::get_singleton()->is_editor_hint()) { // TODO Use editor's camera here return Vector3(); } else { // TODO Use viewport camera, much easier Spatial *viewer = get_viewer(); if (viewer) { return viewer->get_global_transform().origin; } } return Vector3(); } void VoxelLodTerrain::load_block_and_neighbors(const Vector3i &p_bpos, unsigned int lod_index) { CRASH_COND(lod_index >= get_lod_count()); Lod &lod = _lods[lod_index]; Vector3i bpos; for (int y = -1; y < 2; ++y) { for (int z = -1; z < 2; ++z) { for (int x = -1; x < 2; ++x) { bpos.x = p_bpos.x + x; bpos.y = p_bpos.y + y; bpos.z = p_bpos.z + z; VoxelBlock *block = lod.map->get_block(bpos); if (block == nullptr) { if (!lod.loading_blocks.has(bpos)) { lod.blocks_to_load.push_back(bpos); lod.loading_blocks.insert(bpos); } } } } } } static void remove_positions_outside_box( std::vector &positions, Rect3i box, Set &position_set) { for (int i = 0; i < positions.size(); ++i) { const Vector3i bpos = positions[i]; if (!box.contains(bpos)) { int last = positions.size() - 1; positions[i] = positions[last]; positions.resize(last); position_set.erase(bpos); --i; } } } void VoxelLodTerrain::_process() { if (get_lod_count() == 0) { // If there isn't a LOD 0, there is nothing to load return; } OS &os = *OS::get_singleton(); Vector3 viewer_pos = get_viewer_pos(); Vector3i viewer_block_pos = _lods[0].map->voxel_to_block(viewer_pos); ProfilingClock profiling_clock; // Here we go... // Remove blocks falling out of block region extent { // This should be the same distance relatively to each LOD int block_region_extent = get_block_region_extent(); for (unsigned int lod_index = 0; lod_index < get_lod_count(); ++lod_index) { Lod &lod = _lods[lod_index]; // Each LOD keeps a box of loaded blocks, and only some of the blocks will get polygonized. // The player can edit them so changes can be propagated to lower lods. int block_size_po2 = _lods[0].map->get_block_size_pow2() + lod_index; Vector3i viewer_block_pos_within_lod = VoxelMap::voxel_to_block_b(viewer_pos, block_size_po2); Rect3i new_box = Rect3i::from_center_extents(viewer_block_pos_within_lod, Vector3i(block_region_extent)); Rect3i prev_box = Rect3i::from_center_extents(lod.last_viewer_block_pos, Vector3i(lod.last_view_distance_blocks)); // Eliminate pending blocks that aren't needed remove_positions_outside_box(lod.blocks_to_load, new_box, lod.loading_blocks); remove_positions_outside_box(lod.blocks_pending_update, new_box, lod.loading_blocks); if (prev_box != new_box) { Rect3i bounds = Rect3i::get_bounding_box(prev_box, new_box); Vector3i max = bounds.pos + bounds.size; // TODO This will explode if the player teleports! // There is a smarter way to only iterate relevant blocks Vector3i pos; for (pos.z = bounds.pos.z; pos.z < max.z; ++pos.z) { for (pos.y = bounds.pos.y; pos.y < max.y; ++pos.y) { for (pos.x = bounds.pos.x; pos.x < max.x; ++pos.x) { bool prev_contains = prev_box.contains(pos); bool new_contains = new_box.contains(pos); if (prev_contains && !new_contains) { // Unload block immerge_block(pos, lod_index); } } } } } lod.last_viewer_block_pos = viewer_block_pos_within_lod; lod.last_view_distance_blocks = block_region_extent; } } // Find which blocks we need to load and see { struct SubdivideAction { VoxelLodTerrain *self; int blocked_count = 0; bool can_do(LodOctree::Node *node, unsigned int lod_index) { CRASH_COND(lod_index == 0); unsigned int child_lod_index = lod_index - 1; Lod &lod = self->_lods[child_lod_index]; bool can = true; // Can only subdivide if higher detail meshes are ready to be shown, otherwise it will produce holes for (int i = 0; i < 8; ++i) { Vector3i child_pos = LodOctree::get_child_position(node->position, i); VoxelBlock *block = lod.map->get_block(child_pos); if (block == nullptr) { can = false; self->load_block_and_neighbors(child_pos, child_lod_index); } else if (!block->has_been_meshed()) { // TODO There is a case where a whole region of map cannot load, // because we end here everytime instead of the above. can = false; } } if (!can) { ++blocked_count; } return can; } bool operator()(LodOctree::Node *node, unsigned int lod_index) { Lod &lod = self->_lods[lod_index]; Vector3i bpos = node->position; VoxelBlock *block = lod.map->get_block(bpos); CRASH_COND(block == nullptr); block->set_visible(true); return true; } }; struct UnsubdivideAction { VoxelLodTerrain *self; int blocked_count = 0; bool can_do(LodOctree::Node *node, unsigned int lod_index) { // Can only unsubdivide if the parent mesh is ready Lod &lod = self->_lods[lod_index]; const Vector3i &bpos = node->position; VoxelBlock *block = lod.map->get_block(bpos); bool can = true; if (block == nullptr) { can = false; self->load_block_and_neighbors(bpos, lod_index); } else if (!block->has_been_meshed()) { can = false; } if (!can) { ++blocked_count; } return can; } void operator()(LodOctree::Node *node, unsigned int lod_index) { Lod &lod = self->_lods[lod_index]; const Vector3i &bpos = node->position; VoxelBlock *block = lod.map->get_block(bpos); if (block) { block->set_visible(false); } } }; SubdivideAction subdivide_action; subdivide_action.self = this; UnsubdivideAction unsubdivide_action; unsubdivide_action.self = this; _lod_octree.update(viewer_pos, subdivide_action, unsubdivide_action); // Ideally, this stat should stabilize to zero. // If not, something in the meshing process prevents LODs to properly show up and should be fixed. _stats.blocked_lods = subdivide_action.blocked_count + unsubdivide_action.blocked_count; } // Send block loading requests { VoxelProviderThread::InputData input; input.priority_block_position = viewer_block_pos; for (unsigned int lod_index = 0; lod_index < get_lod_count(); ++lod_index) { Lod &lod = _lods[lod_index]; for (int i = 0; i < lod.blocks_to_load.size(); ++i) { VoxelProviderThread::EmergeInput input_block; input_block.block_position = lod.blocks_to_load[i]; input_block.lod = lod_index; input.blocks_to_emerge.push_back(input_block); } lod.blocks_to_load.clear(); } //print_line(String("Sending {0}").format(varray(input.blocks_to_emerge.size()))); _provider_thread->push(input); } _stats.time_request_blocks_to_load = profiling_clock.restart(); // Get block loading responses // Note: if block loading is too fast, this can cause stutters. // It should only happen on first load, though. { VoxelProviderThread::OutputData output; _provider_thread->pop(output); _stats.provider = output.stats; //print_line(String("Loaded {0} blocks").format(varray(output.emerged_blocks.size()))); for (int i = 0; i < output.emerged_blocks.size(); ++i) { const VoxelProviderThread::EmergeOutput &eo = output.emerged_blocks[i]; if (eo.lod >= get_lod_count()) { // That block was requested at a time where LOD was higher... drop it continue; } Lod &lod = _lods[eo.lod]; Set::Element *E = lod.loading_blocks.find(eo.block_position); if (E == nullptr) { // That block was not requested, or is no longer needed. drop it... continue; } lod.loading_blocks.erase(E); if (eo.voxels->get_size() != lod.map->get_block_size()) { // Voxel block size is incorrect, drop it ERR_PRINT("Block size obtained from provider is different from expected size"); continue; } // Store buffer bool check_neighbors = !lod.map->has_block(eo.block_position); VoxelBlock *block = lod.map->set_block_buffer(eo.block_position, eo.voxels); //print_line(String("Adding block {0} at lod {1}").format(varray(eo.block_position.to_vec3(), eo.lod))); // The block will be made visible only by LodOctree block->set_visible(false); // if the block is surrounded or any of its neighbors becomes surrounded, and are marked to mesh, // it should be added to meshing requests if (check_neighbors) { // The block was not there before, so its Moore neighbors may be surrounded now. Vector3i ndir; for (ndir.z = -1; ndir.z < 2; ++ndir.z) { for (ndir.x = -1; ndir.x < 2; ++ndir.x) { for (ndir.y = -1; ndir.y < 2; ++ndir.y) { Vector3i npos = eo.block_position + ndir; VoxelBlock *nblock = lod.map->get_block(npos); if (nblock == nullptr) { continue; } if (lod.map->is_block_surrounded(npos)) { if (nblock->get_mesh_state() == VoxelBlock::MESH_UPDATE_NOT_SENT) { // Assuming it is scheduled to be updated already. // In case of BLOCK_UPDATE_SENT, we'll have to resend it. continue; } nblock->set_mesh_state(VoxelBlock::MESH_UPDATE_NOT_SENT); lod.blocks_pending_update.push_back(npos); } } } } } else { // Only update the block, neighbors will probably follow if needed block->set_mesh_state(VoxelBlock::MESH_UPDATE_NOT_SENT); lod.blocks_pending_update.push_back(eo.block_position); } } } _stats.time_process_load_responses = profiling_clock.restart(); // Send mesh updates { VoxelMeshUpdater::Input input; for (unsigned int lod_index = 0; lod_index < get_lod_count(); ++lod_index) { Lod &lod = _lods[lod_index]; for (int i = 0; i < lod.blocks_pending_update.size(); ++i) { Vector3i block_pos = lod.blocks_pending_update[i]; VoxelBlock *block = lod.map->get_block(block_pos); CRASH_COND(block == nullptr); CRASH_COND(block->get_mesh_state() != VoxelBlock::MESH_UPDATE_NOT_SENT); // TODO Perhaps we could do a bit of early-rejection before spending time in buffer copy? // Create buffer padded with neighbor voxels Ref nbuffer; nbuffer.instance(); // TODO Make the buffer re-usable unsigned int block_size = lod.map->get_block_size(); unsigned int padding = _block_updater->get_required_padding(); nbuffer->create( block_size + 2 * padding, block_size + 2 * padding, block_size + 2 * padding); unsigned int channels_mask = (1 << VoxelBuffer::CHANNEL_ISOLEVEL); lod.map->get_buffer_copy(lod.map->block_to_voxel(block_pos) - Vector3i(padding), **nbuffer, channels_mask); VoxelMeshUpdater::InputBlock iblock; iblock.voxels = nbuffer; iblock.position = block_pos; iblock.lod = lod_index; input.blocks.push_back(iblock); block->set_mesh_state(VoxelBlock::MESH_UPDATE_SENT); } lod.blocks_pending_update.clear(); } //print_line(String("Sending {0} updates").format(varray(input.blocks.size()))); _block_updater->push(input); } _stats.time_request_blocks_to_update = profiling_clock.restart(); // Receive mesh updates { { VoxelMeshUpdater::Output output; _block_updater->pop(output); _stats.updater = output.stats; for (unsigned int i = 0; i < output.blocks.size(); ++i) { const VoxelMeshUpdater::OutputBlock &ob = output.blocks[i]; if (ob.lod >= get_lod_count()) { // Sorry, LOD configuration changed, drop that mesh continue; } _blocks_pending_main_thread_update.push_back(ob); } } Ref world = get_world(); uint32_t timeout = os.get_ticks_msec() + 10; // Allocate milliseconds max to upload meshes int queue_index = 0; // The following is done on the main thread because Godot doesn't really support multithreaded Mesh allocation. // This also proved to be very slow compared to the meshing process itself... // hopefully Vulkan will allow us to upload graphical resources without stalling rendering as they upload? for (; queue_index < _blocks_pending_main_thread_update.size() && os.get_ticks_msec() < timeout; ++queue_index) { const VoxelMeshUpdater::OutputBlock &ob = _blocks_pending_main_thread_update[queue_index]; if (ob.lod >= get_lod_count()) { // Sorry, LOD configuration changed, drop that mesh continue; } Lod &lod = _lods[ob.lod]; VoxelBlock *block = lod.map->get_block(ob.position); if (block == NULL) { // That block is no longer loaded, drop the result continue; } if (block->get_mesh_state() == VoxelBlock::MESH_UPDATE_SENT) { block->set_mesh_state(VoxelBlock::MESH_UP_TO_DATE); } Ref mesh; mesh.instance(); int surface_index = 0; for (int i = 0; i < ob.smooth_surfaces.surfaces.size(); ++i) { Array surface = ob.smooth_surfaces.surfaces[i]; if (surface.empty()) { continue; } CRASH_COND(surface.size() != Mesh::ARRAY_MAX); mesh->add_surface_from_arrays(ob.smooth_surfaces.primitive_type, surface); mesh->surface_set_material(surface_index, _material); // No multi-material supported yet ++surface_index; } if (is_mesh_empty(mesh)) { mesh = Ref(); } block->set_mesh(mesh, world); block->mark_been_meshed(); } shift_up(_blocks_pending_main_thread_update, queue_index); } _stats.time_process_update_responses = profiling_clock.restart(); _stats.time_process_lod = profiling_clock.restart(); } Dictionary VoxelLodTerrain::get_stats() const { Dictionary process; process["time_request_blocks_to_load"] = _stats.time_request_blocks_to_load; process["time_process_load_responses"] = _stats.time_process_load_responses; process["time_request_blocks_to_update"] = _stats.time_request_blocks_to_update; process["time_process_update_responses"] = _stats.time_process_update_responses; process["time_process_lod"] = _stats.time_process_lod; Dictionary d; d["provider"] = VoxelProviderThread::to_dictionary(_stats.provider); d["updater"] = VoxelMeshUpdater::to_dictionary(_stats.updater); d["process"] = process; d["blocked_lods"] = _stats.blocked_lods; return d; } void VoxelLodTerrain::_bind_methods() { ClassDB::bind_method(D_METHOD("set_provider", "provider"), &VoxelLodTerrain::set_provider); ClassDB::bind_method(D_METHOD("get_provider"), &VoxelLodTerrain::get_provider); ClassDB::bind_method(D_METHOD("set_material", "material"), &VoxelLodTerrain::set_material); ClassDB::bind_method(D_METHOD("get_material"), &VoxelLodTerrain::get_material); ClassDB::bind_method(D_METHOD("set_view_distance", "distance_in_voxels"), &VoxelLodTerrain::set_view_distance); ClassDB::bind_method(D_METHOD("get_view_distance"), &VoxelLodTerrain::get_view_distance); ClassDB::bind_method(D_METHOD("get_viewer_path"), &VoxelLodTerrain::get_viewer_path); ClassDB::bind_method(D_METHOD("set_viewer_path", "path"), &VoxelLodTerrain::set_viewer_path); ClassDB::bind_method(D_METHOD("set_lod_count", "lod_count"), &VoxelLodTerrain::set_lod_count); ClassDB::bind_method(D_METHOD("get_lod_count"), &VoxelLodTerrain::get_lod_count); ClassDB::bind_method(D_METHOD("set_lod_split_scale", "lod_split_scale"), &VoxelLodTerrain::set_lod_split_scale); ClassDB::bind_method(D_METHOD("get_lod_split_scale"), &VoxelLodTerrain::get_lod_split_scale); ClassDB::bind_method(D_METHOD("get_block_region_extent"), &VoxelLodTerrain::get_block_region_extent); ClassDB::bind_method(D_METHOD("get_block_info", "block_pos", "lod"), &VoxelLodTerrain::get_block_info); ClassDB::bind_method(D_METHOD("get_stats"), &VoxelLodTerrain::get_stats); ClassDB::bind_method(D_METHOD("voxel_to_block_position", "lod_index"), &VoxelLodTerrain::voxel_to_block_position); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "provider", PROPERTY_HINT_RESOURCE_TYPE, "VoxelProvider"), "set_provider", "get_provider"); ADD_PROPERTY(PropertyInfo(Variant::INT, "view_distance"), "set_view_distance", "get_view_distance"); ADD_PROPERTY(PropertyInfo(Variant::INT, "lod_count"), "set_lod_count", "get_lod_count"); ADD_PROPERTY(PropertyInfo(Variant::REAL, "lod_split_scale"), "set_lod_split_scale", "get_lod_split_scale"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "viewer_path"), "set_viewer_path", "get_viewer_path"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "material", PROPERTY_HINT_RESOURCE_TYPE, "Material"), "set_material", "get_material"); }