diff --git a/register_types.cpp b/register_types.cpp index 34ba825d..b6091b86 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -23,6 +23,7 @@ #include "terrain/voxel_lod_terrain.h" #include "terrain/voxel_map.h" #include "terrain/voxel_terrain.h" +#include "terrain/voxel_viewer.h" #include "util/macros.h" #include "voxel_buffer.h" #include "voxel_memory_pool.h" @@ -51,6 +52,7 @@ void register_voxel_types() { // Nodes ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); // Streams ClassDB::register_class(); diff --git a/server/struct_db.h b/server/struct_db.h index eedd0b2f..d478ce85 100644 --- a/server/struct_db.h +++ b/server/struct_db.h @@ -138,6 +138,16 @@ public: } } + template + inline void for_each(F f) const { + for (size_t i = 0; i < _slots.size(); ++i) { + const Slot &s = _slots[i]; + if (s.valid) { + f(s.data); + } + } + } + template inline void for_each_with_id(F f) { for (size_t i = 0; i < _slots.size(); ++i) { @@ -148,6 +158,16 @@ public: } } + template + inline void for_each_with_id(F f) const { + for (size_t i = 0; i < _slots.size(); ++i) { + const Slot &s = _slots[i]; + if (s.valid) { + f(s.data, make_id(i, s.version)); + } + } + } + inline void clear() { _slots.clear(); } diff --git a/server/voxel_server.cpp b/server/voxel_server.cpp index fb7dce74..d85a99e0 100644 --- a/server/voxel_server.cpp +++ b/server/voxel_server.cpp @@ -53,7 +53,7 @@ VoxelServer::VoxelServer() { // TODO Try more threads, it should be possible // This pool works on visuals so it must have low latency _meshing_thread_pool.set_thread_count(2); - _meshing_thread_pool.set_priority_update_period(32); + _meshing_thread_pool.set_priority_update_period(64); _meshing_thread_pool.set_batch_count(1); for (size_t i = 0; i < _meshing_thread_pool.get_thread_count(); ++i) { @@ -70,8 +70,14 @@ VoxelServer::VoxelServer() { _smooth_meshers[i] = mesher; } + if (Engine::get_singleton()->is_editor_hint()) { + // Default viewer + const uint32_t default_viewer_id = add_viewer(); + Viewer &default_viewer = _world.viewers.get(default_viewer_id); + default_viewer.is_default = true; + } + // Init world - // TODO How to make this use memnew and memdelete? _world.viewers_for_priority = gd_make_shared(); PRINT_VERBOSE(String("Size of BlockDataRequest: {0}").format(varray((int)sizeof(BlockDataRequest)))); @@ -280,21 +286,69 @@ void VoxelServer::remove_volume(uint32_t volume_id) { } uint32_t VoxelServer::add_viewer() { + if (Engine::get_singleton()->is_editor_hint()) { + // Remove default viewer if any + _world.viewers.for_each_with_id([this](Viewer &viewer, uint32_t id) { + if (viewer.is_default) { + // Safe because StructDB does not shift items + _world.viewers.destroy(id); + } + }); + } + return _world.viewers.create(Viewer()); } +void VoxelServer::remove_viewer(uint32_t viewer_id) { + _world.viewers.destroy(viewer_id); + + if (Engine::get_singleton()->is_editor_hint()) { + // Re-add default viewer + if (_world.viewers.count() == 0) { + const uint32_t default_viewer_id = add_viewer(); + Viewer &default_viewer = _world.viewers.get(default_viewer_id); + default_viewer.is_default = true; + } + } +} + void VoxelServer::set_viewer_position(uint32_t viewer_id, Vector3 position) { Viewer &viewer = _world.viewers.get(viewer_id); viewer.world_position = position; } -void VoxelServer::set_viewer_region_extents(uint32_t viewer_id, Vector3 extents) { +void VoxelServer::set_viewer_distance(uint32_t viewer_id, unsigned int distance) { Viewer &viewer = _world.viewers.get(viewer_id); - // TODO + viewer.view_distance = distance; } -void VoxelServer::remove_viewer(uint32_t viewer_id) { - _world.viewers.destroy(viewer_id); +unsigned int VoxelServer::get_viewer_distance(uint32_t viewer_id) const { + const Viewer &viewer = _world.viewers.get(viewer_id); + return viewer.view_distance; +} + +void VoxelServer::set_viewer_requires_visuals(uint32_t viewer_id, bool enabled) { + Viewer &viewer = _world.viewers.get(viewer_id); + viewer.require_visuals = enabled; +} + +bool VoxelServer::is_viewer_requiring_visuals(uint32_t viewer_id) const { + const Viewer &viewer = _world.viewers.get(viewer_id); + return viewer.require_visuals; +} + +void VoxelServer::set_viewer_requires_collisions(uint32_t viewer_id, bool enabled) { + Viewer &viewer = _world.viewers.get(viewer_id); + viewer.require_collisions = enabled; +} + +bool VoxelServer::is_viewer_requiring_collisions(uint32_t viewer_id) const { + const Viewer &viewer = _world.viewers.get(viewer_id); + return viewer.require_collisions; +} + +bool VoxelServer::viewer_exists(uint32_t viewer_id) const { + return _world.viewers.try_get(viewer_id) != nullptr; } void VoxelServer::process() { @@ -387,7 +441,7 @@ void VoxelServer::process() { _world.viewers_for_priority->positions.resize(viewer_count); } size_t i = 0; - _world.viewers.for_each([&i, this](const Viewer &viewer) { + _world.viewers.for_each([&i, this](Viewer &viewer) { _world.viewers_for_priority->positions[i] = viewer.world_position; ++i; }); @@ -565,7 +619,7 @@ void VoxelServer::BlockMeshRequest::run(VoxelTaskContext ctx) { } if (smooth_enabled) { - VOXEL_PROFILE_SCOPE("Smooth meshing"); + VOXEL_PROFILE_SCOPE_NAMED("Smooth meshing"); Ref smooth_mesher = VoxelServer::get_singleton()->_smooth_meshers[ctx.thread_index]; CRASH_COND(smooth_mesher.is_null()); smooth_mesher->build(smooth_surfaces_output, input); diff --git a/server/voxel_server.h b/server/voxel_server.h index 2d5b9d8b..985f3f2c 100644 --- a/server/voxel_server.h +++ b/server/voxel_server.h @@ -52,6 +52,20 @@ public: std::vector data_output; }; + struct Viewer { + // enum Flags { + // FLAG_DATA = 1, + // FLAG_VISUAL = 2, + // FLAG_COLLISION = 4, + // FLAGS_COUNT = 3 + // }; + Vector3 world_position; + unsigned int view_distance = 128; + bool require_collisions = false; + bool require_visuals = true; + bool is_default = false; + }; + static VoxelServer *get_singleton(); static void create_singleton(); static void destroy_singleton(); @@ -71,9 +85,20 @@ public: void remove_volume(uint32_t volume_id); uint32_t add_viewer(); - void set_viewer_position(uint32_t viewer_id, Vector3 position); - void set_viewer_region_extents(uint32_t viewer_id, Vector3 extents); void remove_viewer(uint32_t viewer_id); + void set_viewer_position(uint32_t viewer_id, Vector3 position); + void set_viewer_distance(uint32_t viewer_id, unsigned int distance); + unsigned int get_viewer_distance(uint32_t viewer_id) const; + void set_viewer_requires_visuals(uint32_t viewer_id, bool enabled); + bool is_viewer_requiring_visuals(uint32_t viewer_id) const; + void set_viewer_requires_collisions(uint32_t viewer_id, bool enabled); + bool is_viewer_requiring_collisions(uint32_t viewer_id) const; + bool viewer_exists(uint32_t viewer_id) const; + + template + inline void for_each_viewer(F f) const { + _world.viewers.for_each_with_id(f); + } // Gets by how much voxels must be padded with neighbors in order to be polygonized properly void get_min_max_block_padding( @@ -122,10 +147,6 @@ private: std::shared_ptr meshing_dependency; }; - struct Viewer { - Vector3 world_position; - }; - struct VoxelViewersArray { // These positions are written by the main thread and read by block processing threads. // It's only used to adjust task priority so using a lock isn't worth it. In worst case scenario, diff --git a/terrain/voxel_block.cpp b/terrain/voxel_block.cpp index 8769fa12..909f557d 100644 --- a/terrain/voxel_block.cpp +++ b/terrain/voxel_block.cpp @@ -1,4 +1,5 @@ #include "voxel_block.h" +#include "../util/macros.h" #include "../util/profiling.h" #include "../voxel_string_names.h" #include @@ -103,16 +104,13 @@ void VoxelBlock::set_world(Ref p_world) { } } -void VoxelBlock::set_mesh(Ref mesh, Spatial *node, bool generate_collision, Vector surface_arrays, bool debug_collision) { +void VoxelBlock::set_mesh(Ref mesh) { // TODO Don't add mesh instance to the world if it's not visible. // I suspect Godot is trying to include invisible mesh instances into the culling process, // which is killing performance when LOD is used (i.e many meshes are in pool but hidden) // This needs investigation. if (mesh.is_valid()) { - ERR_FAIL_COND(node == nullptr); - ERR_FAIL_COND(node->get_world() != _world); - Transform transform(Basis(), _position_in_voxels.to_vec3()); if (!_mesh_instance.is_valid()) { @@ -132,46 +130,18 @@ void VoxelBlock::set_mesh(Ref mesh, Spatial *node, bool generate_collision _mesh_instance.set_material_override(_debug_material); #endif - if (generate_collision) { - Ref shape = create_concave_polygon_shape(surface_arrays); - - if (!_static_body.is_valid()) { - _static_body.create(); - _static_body.set_world(*_world); - _static_body.set_attached_object(node); - _static_body.set_transform(transform); - } else { - _static_body.remove_shape(0); - } - _static_body.add_shape(shape); - _static_body.set_debug(debug_collision, *_world); - _static_body.set_shape_enabled(0, _visible); - } - } else { - if (_mesh_instance.is_valid()) { // Delete instance if it exists _mesh_instance.destroy(); } - - if (_static_body.is_valid()) { - _static_body.destroy(); - } } - - ++_mesh_update_count; - - // if(_mesh_update_count > 1) { - // print_line(String("Block {0} was updated {1} times").format(varray(pos.to_vec3(), _mesh_update_count))); - // } } void VoxelBlock::set_transition_mesh(Ref mesh, int side) { DirectMeshInstance &mesh_instance = _transition_mesh_instances[side]; if (mesh.is_valid()) { - if (!mesh_instance.is_valid()) { // Create instance if it doesn't exist mesh_instance.create(); @@ -191,7 +161,6 @@ void VoxelBlock::set_transition_mesh(Ref mesh, int side) { #endif } else { - if (mesh_instance.is_valid()) { // Delete instance if it exists mesh_instance.destroy(); @@ -203,6 +172,12 @@ bool VoxelBlock::has_mesh() const { return _mesh_instance.get_mesh().is_valid(); } +void VoxelBlock::drop_mesh() { + if (_mesh_instance.is_valid()) { + _mesh_instance.destroy(); + } +} + void VoxelBlock::set_mesh_state(MeshState ms) { _mesh_state = ms; } @@ -302,8 +277,44 @@ bool VoxelBlock::is_modified() const { } void VoxelBlock::set_modified(bool modified) { - // if (_modified != modified) { - // print_line(String("Marking block {0}[lod{1}] as modified").format(varray(bpos.to_vec3(), lod_index))); - // } +#ifdef TOOLS_ENABLED + if (_modified == false && modified) { + PRINT_VERBOSE(String("Marking block {0} as modified").format(varray(position.to_vec3()))); + } +#endif _modified = modified; } + +void VoxelBlock::set_collision_mesh(Vector surface_arrays, bool debug_collision, Spatial *node) { + if (surface_arrays.size() == 0) { + drop_collision(); + return; + } + + ERR_FAIL_COND(node == nullptr); + ERR_FAIL_COND_MSG(node->get_world() != _world, "Physics body and attached node must be from the same world"); + + if (!_static_body.is_valid()) { + const Transform transform(Basis(), _position_in_voxels.to_vec3()); + _static_body.create(); + _static_body.set_world(*_world); + // This allows collision signals to provide the terrain node in the `collider` field + _static_body.set_attached_object(node); + _static_body.set_transform(transform); + + } else { + _static_body.remove_shape(0); + } + + Ref shape = create_concave_polygon_shape(surface_arrays); + + _static_body.add_shape(shape); + _static_body.set_debug(debug_collision, *_world); + _static_body.set_shape_enabled(0, _visible); +} + +void VoxelBlock::drop_collision() { + if (_static_body.is_valid()) { + _static_body.destroy(); + } +} diff --git a/terrain/voxel_block.h b/terrain/voxel_block.h index 8798fcf7..e927d1ed 100644 --- a/terrain/voxel_block.h +++ b/terrain/voxel_block.h @@ -6,6 +6,7 @@ #include "../util/direct_static_body.h" #include "../util/fixed_array.h" #include "../voxel_buffer.h" +#include "voxel_viewer_ref_count.h" //#define VOXEL_DEBUG_LOD_MATERIALS @@ -26,21 +27,32 @@ public: Vector3i position; unsigned int lod_index = 0; bool pending_transition_update = false; + VoxelViewerRefCount viewers; static VoxelBlock *create(Vector3i bpos, Ref buffer, unsigned int size, unsigned int p_lod_index); ~VoxelBlock(); - // Visuals and physics - void set_world(Ref p_world); - void set_mesh(Ref mesh, Spatial *node, bool generate_collision, Vector surface_arrays, bool debug_collision); + + // Visuals + + void set_mesh(Ref mesh); void set_transition_mesh(Ref mesh, int side); bool has_mesh() const; + void drop_mesh(); void set_shader_material(Ref material); inline Ref get_shader_material() const { return _shader_material; } + // Collisions + + void set_collision_mesh(Vector surface_arrays, bool debug_collision, Spatial *node); + void drop_collision(); + // TODO Collision layer and mask + + // State + void set_mesh_state(MeshState ms); MeshState get_mesh_state() const; @@ -53,8 +65,6 @@ public: //void set_transition_bit(uint8_t side, bool value); inline uint8_t get_transition_mask() const { return _transition_mask; } - // Voxel data - void set_needs_lodding(bool need_lodding); inline bool get_needs_lodding() const { return _needs_lodding; } @@ -89,7 +99,6 @@ private: Ref _debug_transition_material; #endif - int _mesh_update_count = 0; // TODO Never really used? bool _visible = true; bool _parent_visible = true; MeshState _mesh_state = MESH_NEVER_UPDATED; diff --git a/terrain/voxel_lod_terrain.cpp b/terrain/voxel_lod_terrain.cpp index 12bc29ed..1bf40528 100644 --- a/terrain/voxel_lod_terrain.cpp +++ b/terrain/voxel_lod_terrain.cpp @@ -1110,7 +1110,10 @@ void VoxelLodTerrain::_process() { has_collision = ob.lod < _collision_lod_count; } - block->set_mesh(mesh, this, has_collision, mesh_data.surfaces, get_tree()->is_debugging_collisions_hint()); + block->set_mesh(mesh); + if (has_collision) { + block->set_collision_mesh(mesh_data.surfaces, get_tree()->is_debugging_collisions_hint(), this); + } { VOXEL_PROFILE_SCOPE(); diff --git a/terrain/voxel_terrain.cpp b/terrain/voxel_terrain.cpp index 7067811f..d80edede 100644 --- a/terrain/voxel_terrain.cpp +++ b/terrain/voxel_terrain.cpp @@ -25,9 +25,6 @@ VoxelTerrain::VoxelTerrain() { _map.instance(); - _view_distance_blocks = 8; - _last_view_distance_blocks = 0; - Ref library; library.instance(); set_voxel_library(library); @@ -193,49 +190,28 @@ void VoxelTerrain::set_voxel_library(Ref library) { start_updater(); // Voxel appearance might completely change - make_all_view_dirty_deferred(); + make_all_view_dirty(); } void VoxelTerrain::set_generate_collisions(bool enabled) { _generate_collisions = enabled; } -int VoxelTerrain::get_view_distance() const { - return _view_distance_blocks * _map->get_block_size(); +unsigned int VoxelTerrain::get_max_view_distance() const { + return _max_view_distance_blocks * _map->get_block_size(); } -void VoxelTerrain::set_view_distance(int distance_in_voxels) { +void VoxelTerrain::set_max_view_distance(unsigned int distance_in_voxels) { ERR_FAIL_COND(distance_in_voxels < 0); - const int d = distance_in_voxels / _map->get_block_size(); - if (d != _view_distance_blocks) { - PRINT_VERBOSE(String("View distance changed from ") + String::num(_view_distance_blocks) + String(" blocks to ") + String::num(d)); - _view_distance_blocks = d; + const unsigned int d = distance_in_voxels / _map->get_block_size(); + if (d != _max_view_distance_blocks) { + PRINT_VERBOSE(String("View distance changed from ") + + String::num(_max_view_distance_blocks) + String(" blocks to ") + String::num(d)); + _max_view_distance_blocks = d; // Blocks too far away will be removed in _process, same for blocks to load } } -void VoxelTerrain::set_viewer_path(NodePath path) { - _viewer_path = path; -} - -NodePath VoxelTerrain::get_viewer_path() const { - return _viewer_path; -} - -Spatial *VoxelTerrain::get_viewer() const { - if (!is_inside_tree()) { - return nullptr; - } - if (_viewer_path.is_empty()) { - return nullptr; - } - Node *node = get_node(_viewer_path); - if (node == nullptr) { - return nullptr; - } - return Object::cast_to(node); -} - void VoxelTerrain::set_material(unsigned int id, Ref material) { // TODO Update existing block surfaces ERR_FAIL_COND(id < 0 || id >= VoxelMesherBlocky::MAX_MATERIALS); @@ -248,28 +224,16 @@ Ref VoxelTerrain::get_material(unsigned int id) const { } void VoxelTerrain::make_block_dirty(Vector3i bpos) { - // TODO Immediate update viewer distance? - VoxelBlock *block = _map->get_block(bpos); + ERR_FAIL_COND_MSG(block == nullptr, "Requested update to a block that isn't loaded"); + make_block_dirty(block); +} - if (block == nullptr) { - // The block isn't available, we may need to load it - if (!_loading_blocks.has(bpos)) { - _blocks_pending_load.push_back(bpos); - _loading_blocks.insert(bpos); - } - - } else if (block->get_mesh_state() != VoxelBlock::MESH_UPDATE_NOT_SENT) { - // Regardless of if the updater is updating the block already, - // the block was modified again so we schedule another update - block->set_mesh_state(VoxelBlock::MESH_UPDATE_NOT_SENT); - _blocks_pending_update.push_back(bpos); - - if (!block->is_modified()) { - PRINT_VERBOSE(String("Marking block {0} as modified").format(varray(bpos.to_vec3()))); - block->set_modified(true); - } - } +void VoxelTerrain::make_block_dirty(VoxelBlock *block) { + // TODO Immediate update viewer distance? + CRASH_COND(block == nullptr); + block->set_modified(true); + try_schedule_block_update(block); //OS::get_singleton()->print("Dirty (%i, %i, %i)", bpos.x, bpos.y, bpos.z); @@ -277,6 +241,121 @@ void VoxelTerrain::make_block_dirty(Vector3i bpos) { // this will make the second change ignored, which is not correct! } +void VoxelTerrain::try_schedule_block_update(VoxelBlock *block) { + CRASH_COND(block == nullptr); + + if (block->get_mesh_state() != VoxelBlock::MESH_UPDATE_NOT_SENT) { + // Regardless of if the updater is updating the block already, + // the block was modified again so we schedule another update + block->set_mesh_state(VoxelBlock::MESH_UPDATE_NOT_SENT); + _blocks_pending_update.push_back(block->position); + } +} + +void VoxelTerrain::view_block(Vector3i bpos, bool data_flag, bool mesh_flag, bool collision_flag) { + VoxelBlock *block = _map->get_block(bpos); + + if (block == nullptr) { + // The block isn't loaded + LoadingBlock *loading_block = _loading_blocks.getptr(bpos); + + if (loading_block == nullptr) { + // First viewer to request it + LoadingBlock loading_block; + loading_block.viewers.add(data_flag, mesh_flag, collision_flag); + + // Schedule a loading request + _loading_blocks.set(bpos, loading_block); + _blocks_pending_load.push_back(bpos); + + } else { + // More viewers + loading_block->viewers.add(data_flag, mesh_flag, collision_flag); + } + + } else { + // The block is loaded + VoxelViewerRefCount &viewers = block->viewers; + + if (data_flag) { + viewers.add(VoxelViewerRefCount::TYPE_DATA); + } + + if (mesh_flag) { + viewers.add(VoxelViewerRefCount::TYPE_MESH); + if (viewers.get(VoxelViewerRefCount::TYPE_MESH) == 1) { + // First to request a mesh (means it was not requested when the block was loaded earlier) + // Trigger mesh update + try_schedule_block_update(block); + } + } + + if (collision_flag) { + viewers.add(VoxelViewerRefCount::TYPE_COLLISION); + if (viewers.get(VoxelViewerRefCount::TYPE_COLLISION) == 1) { + try_schedule_block_update(block); + } + } + + // TODO viewers with varying flags during the game is not supported at the moment. + // They have to be re-created, which may cause world re-load... + } +} + +void VoxelTerrain::unview_block(Vector3i bpos, bool data_flag, bool mesh_flag, bool collision_flag) { + VoxelBlock *block = _map->get_block(bpos); + + if (block == nullptr) { + // The block isn't loaded + LoadingBlock *loading_block = _loading_blocks.getptr(bpos); + CRASH_COND_MSG(loading_block == nullptr, "Request to unview a loading block that was never requested"); + + loading_block->viewers.remove(data_flag, mesh_flag, collision_flag); + + if (loading_block->viewers.get(VoxelViewerRefCount::TYPE_DATA) == 0) { + // No longer want to load it + _loading_blocks.erase(bpos); + + // TODO Do we really need that vector after all? + for (size_t i = 0; i < _blocks_pending_load.size(); ++i) { + if (_blocks_pending_load[i] == bpos) { + _blocks_pending_load[i] = _blocks_pending_load.back(); + _blocks_pending_load.pop_back(); + break; + } + } + } + + } else { + // The block is loaded + VoxelViewerRefCount &viewers = block->viewers; + + if (mesh_flag) { + viewers.remove(VoxelViewerRefCount::TYPE_MESH); + if (viewers.get(VoxelViewerRefCount::TYPE_MESH) == 0) { + // Mesh no longer required + block->drop_mesh(); + } + } + + if (collision_flag) { + viewers.remove(VoxelViewerRefCount::TYPE_COLLISION); + if (viewers.get(VoxelViewerRefCount::TYPE_COLLISION) == 0) { + // Collision no longer required + block->drop_collision(); + } + } + + if (data_flag) { + viewers.remove(VoxelViewerRefCount::TYPE_DATA); + } + if (viewers.get(VoxelViewerRefCount::TYPE_DATA) == 0) { + // The block itself is no longer wanted + immerge_block(bpos); + } + } +} + namespace { struct ScheduleSaveAction { std::vector &blocks_to_save; @@ -314,6 +393,14 @@ void VoxelTerrain::immerge_block(Vector3i bpos) { // Blocks in the update queue will be cancelled in _process, // because it's too expensive to linear-search all blocks for each block + + // for (size_t i = 0; i < _blocks_pending_update.size(); ++i) { + // if (_blocks_pending_update[i] == bpos) { + // _blocks_pending_update[i] = _blocks_pending_update.back(); + // _blocks_pending_update.pop_back(); + // break; + // } + // } } void VoxelTerrain::save_all_modified_blocks(bool with_copy) { @@ -353,12 +440,13 @@ Dictionary VoxelTerrain::get_statistics() const { // } //} -void VoxelTerrain::make_all_view_dirty_deferred() { - // We do this because we query blocks only when the viewer area changes. - // This trick will regenerate all chunks in view, according to the view distance found during block updates. - // The point of doing this instead of immediately scheduling updates is that it will - // always use an up-to-date view distance, which is not necessarily loaded yet on initialization. - _last_view_distance_blocks = 0; +void VoxelTerrain::make_all_view_dirty() { + // Mark all loaded blocks dirty within range of viewers that require meshes + _map->for_all_blocks([this](VoxelBlock *b) { + if (b->viewers.get(VoxelViewerRefCount::TYPE_MESH) > 0) { + make_block_dirty(b); + } + }); // Vector3i radius(_view_distance_blocks, _view_distance_blocks, _view_distance_blocks); // make_blocks_dirty(-radius, 2*radius); @@ -405,13 +493,20 @@ void VoxelTerrain::stop_streamer() { } void VoxelTerrain::reset_map() { + // Discard everything, to reload it all + _map->for_all_blocks([this](VoxelBlock *block) { emit_block_unloaded(block); }); _map->create(get_block_size_pow2(), 0); - // To force queries to happen again, because we only listen for viewer position changes - make_all_view_dirty_deferred(); + _loading_blocks.clear(); + _blocks_pending_load.clear(); + _blocks_pending_update.clear(); + _blocks_to_save.clear(); + + // No need to care about refcounts, we drop everything anyways. Will pair it back on next process. + _paired_viewers.clear(); } inline int get_border_index(int x, int max) { @@ -655,29 +750,6 @@ static void remove_positions_outside_box( } } -void VoxelTerrain::get_viewer_pos_and_direction(Vector3 &out_pos, Vector3 &out_direction) const { - if (Engine::get_singleton()->is_editor_hint()) { - // TODO Use editor's camera here - out_pos = Vector3(); - out_direction = Vector3(0, -1, 0); - - } else { - // TODO Have option to use viewport camera - const Spatial *viewer = get_viewer(); - - if (viewer) { - Transform gt = viewer->get_global_transform(); - out_pos = gt.origin; - out_direction = -gt.basis.get_axis(Vector3::AXIS_Z); - - } else { - // TODO Just remember last viewer pos - out_pos = (_last_viewer_block_pos << _map->get_block_size_pow2()).to_vec3(); - out_direction = Vector3(0, -1, 0); - } - } -} - void VoxelTerrain::send_block_data_requests() { VOXEL_PROFILE_SCOPE(); @@ -715,6 +787,17 @@ void VoxelTerrain::emit_block_unloaded(const VoxelBlock *block) { emit_signal(VoxelStringNames::get_singleton()->block_unloaded, args, 2); } +bool VoxelTerrain::try_get_paired_viewer_index(uint32_t id, size_t &out_i) const { + for (size_t i = 0; i < _paired_viewers.size(); ++i) { + const PairedViewer &p = _paired_viewers[i]; + if (p.id == id) { + out_i = i; + return true; + } + } + return false; +} + void VoxelTerrain::_process() { VOXEL_PROFILE_SCOPE(); @@ -734,47 +817,121 @@ void VoxelTerrain::_process() { _stats.dropped_block_loads = 0; _stats.dropped_block_meshs = 0; - // Get viewer location - // TODO Transform to local (Spatial Transform) - Vector3 viewer_pos; - Vector3 viewer_direction; - get_viewer_pos_and_direction(viewer_pos, viewer_direction); - Vector3i viewer_block_pos = _map->voxel_to_block(Vector3i(viewer_pos)); + // Update viewers + std::vector unpaired_viewer_indexes; + { + // Our node doesn't have bounds yet, so for now viewers are always paired. + + // Destroyed viewers + for (size_t i = 0; i < _paired_viewers.size(); ++i) { + PairedViewer &p = _paired_viewers[i]; + if (!VoxelServer::get_singleton()->viewer_exists(p.id)) { + p.state.view_distance_blocks = 0; + unpaired_viewer_indexes.push_back(i); + } + } + + // New viewers and updates + VoxelServer::get_singleton()->for_each_viewer([this](const VoxelServer::Viewer &viewer, uint32_t viewer_id) { + size_t i; + if (!try_get_paired_viewer_index(viewer_id, i)) { + PairedViewer p; + p.id = viewer_id; + p.state.view_distance_blocks = 0; + i = _paired_viewers.size(); + _paired_viewers.push_back(p); + } + + PairedViewer &p = _paired_viewers[i]; + + p.prev_state = p.state; + + p.state.view_distance_blocks = + min(viewer.view_distance >> get_block_size_pow2(), _max_view_distance_blocks); + p.state.block_position = _map->voxel_to_block(Vector3i(viewer.world_position)); + p.state.requires_collisions = VoxelServer::get_singleton()->is_viewer_requiring_collisions(viewer_id); + p.state.requires_meshes = VoxelServer::get_singleton()->is_viewer_requiring_visuals(viewer_id); + }); + } // Find out which blocks need to appear and which need to be unloaded { VOXEL_PROFILE_SCOPE(); - Rect3i new_box = Rect3i::from_center_extents(viewer_block_pos, Vector3i(_view_distance_blocks)); - Rect3i prev_box = Rect3i::from_center_extents(_last_viewer_block_pos, Vector3i(_last_view_distance_blocks)); + for (size_t i = 0; i < _paired_viewers.size(); ++i) { + const PairedViewer &viewer = _paired_viewers[i]; - if (prev_box != new_box) { - //print_line(String("Loaded area changed: from ") + prev_box.to_string() + String(" to ") + new_box.to_string()); + const Rect3i new_box = Rect3i::from_center_extents( + viewer.state.block_position, Vector3i(viewer.state.view_distance_blocks)); + const Rect3i prev_box = Rect3i::from_center_extents( + viewer.prev_state.block_position, Vector3i(viewer.prev_state.view_distance_blocks)); - prev_box.difference(new_box, [this](Rect3i out_of_range_box) { - out_of_range_box.for_each_cell([=](Vector3i bpos) { - // Unload block - immerge_block(bpos); + if (prev_box != new_box) { + // Unview blocks that just fell out of range + prev_box.difference(new_box, [this, &viewer](Rect3i out_of_range_box) { + out_of_range_box.for_each_cell([this, &viewer](Vector3i bpos) { + unview_block(bpos, true, + viewer.prev_state.requires_meshes, + viewer.prev_state.requires_collisions); + }); }); - }); - new_box.difference(prev_box, [this](Rect3i box_to_load) { - box_to_load.for_each_cell([=](Vector3i bpos) { - // Load or update block - make_block_dirty(bpos); + // View blocks that just entered the range + new_box.difference(prev_box, [this, &viewer](Rect3i box_to_load) { + box_to_load.for_each_cell([this, &viewer](Vector3i bpos) { + // Load or update block + view_block(bpos, true, + viewer.state.requires_meshes, + viewer.state.requires_collisions); + }); }); - }); + } + + // Blocks that remained within range of the viewer may need some changes too if viewer flags were modified. + // This operates on a DISTINCT set of blocks than the one above. + + if (viewer.state.requires_collisions != viewer.prev_state.requires_collisions) { + const Rect3i box = new_box.clipped(prev_box); + if (viewer.state.requires_collisions) { + box.for_each_cell([this](Vector3i bpos) { + view_block(bpos, false, false, true); + }); + + } else { + box.for_each_cell([this](Vector3i bpos) { + unview_block(bpos, false, false, true); + }); + } + } + + if (viewer.state.requires_meshes != viewer.prev_state.requires_meshes) { + const Rect3i box = new_box.clipped(prev_box); + if (viewer.state.requires_meshes) { + box.for_each_cell([this](Vector3i bpos) { + view_block(bpos, false, true, false); + }); + + } else { + box.for_each_cell([this](Vector3i bpos) { + unview_block(bpos, false, true, false); + }); + } + } + + // Eliminate pending blocks that aren't needed + //remove_positions_outside_box(_blocks_pending_load, new_box, _loading_blocks); + //remove_positions_outside_box(_blocks_pending_update, new_box, _loading_blocks); } - - // Eliminate pending blocks that aren't needed - remove_positions_outside_box(_blocks_pending_load, new_box, _loading_blocks); - remove_positions_outside_box(_blocks_pending_update, new_box, _loading_blocks); } _stats.time_detect_required_blocks = profiling_clock.restart(); - _last_view_distance_blocks = _view_distance_blocks; - _last_viewer_block_pos = viewer_block_pos; + // We no longer need unpaired viewers + for (size_t i = 0; i < unpaired_viewer_indexes.size(); ++i) { + size_t vi = unpaired_viewer_indexes[i]; + _paired_viewers[vi] = _paired_viewers.back(); + _paired_viewers.pop_back(); + } // It's possible the user didn't set a stream yet, or it is turned off if (_stream.is_valid() && (Engine::get_singleton()->is_editor_hint() == false || _run_stream_in_editor)) { @@ -803,16 +960,18 @@ void VoxelTerrain::_process() { const Vector3i block_pos = ob.position; + LoadingBlock loading_block; { - Set::Element *E = _loading_blocks.find(block_pos); + LoadingBlock *loading_block_ptr = _loading_blocks.getptr(block_pos); - if (E == nullptr) { + if (loading_block_ptr == nullptr) { // That block was not requested, drop it ++_stats.dropped_block_loads; continue; } - _loading_blocks.erase(E); + _loading_blocks.erase(block_pos); + loading_block = *loading_block_ptr; } if (ob.dropped) { @@ -839,11 +998,17 @@ void VoxelTerrain::_process() { // TODO Discard blocks out of range - // Store buffer + // Create or update block data VoxelBlock *block = _map->get_block(block_pos); - bool update_neighbors = block == nullptr; + const bool was_not_loaded = block == nullptr; block = _map->set_block_buffer(block_pos, ob.voxels); block->set_world(get_world()); + + if (was_not_loaded) { + // Set viewers count that are currently expecting the block + block->viewers = loading_block.viewers; + } + emit_block_loaded(block); // TODO The following code appears to have order-dependency with block loading. @@ -852,7 +1017,7 @@ void VoxelTerrain::_process() { // but it needs to be made more robust // Trigger mesh updates - if (update_neighbors) { + if (was_not_loaded) { // All neighbors have to be checked. If they are now surrounded, they can be updated Vector3i ndir; for (ndir.z = -1; ndir.z < 2; ++ndir.z) { @@ -920,7 +1085,9 @@ void VoxelTerrain::_process() { CRASH_COND(block->get_mesh_state() != VoxelBlock::MESH_UPDATE_NOT_SENT); // The block contains empty voxels - block->set_mesh(Ref(), this, _generate_collisions, Vector(), get_tree()->is_debugging_collisions_hint()); + + block->drop_mesh(); + block->drop_collision(); block->set_mesh_state(VoxelBlock::MESH_UP_TO_DATE); // Optional, but I guess it might spare some memory. @@ -963,7 +1130,7 @@ void VoxelTerrain::_process() { _stats.time_request_blocks_to_update = profiling_clock.restart(); - // Get mesh updates + // Receive mesh updates { VOXEL_PROFILE_SCOPE_NAMED("Receive mesh updates"); @@ -1040,9 +1207,16 @@ void VoxelTerrain::_process() { if (is_mesh_empty(mesh)) { mesh = Ref(); + collidable_surfaces.clear(); } - block->set_mesh(mesh, this, _generate_collisions, collidable_surfaces, get_tree()->is_debugging_collisions_hint()); + const bool gen_collisions = + _generate_collisions && block->viewers.get(VoxelViewerRefCount::TYPE_COLLISION) > 0; + + block->set_mesh(mesh); + if (gen_collisions) { + block->set_collision_mesh(collidable_surfaces, get_tree()->is_debugging_collisions_hint(), this); + } block->set_parent_visible(is_visible()); } @@ -1126,15 +1300,12 @@ void VoxelTerrain::_bind_methods() { ClassDB::bind_method(D_METHOD("set_material", "id", "material"), &VoxelTerrain::set_material); ClassDB::bind_method(D_METHOD("get_material", "id"), &VoxelTerrain::get_material); - ClassDB::bind_method(D_METHOD("set_view_distance", "distance_in_voxels"), &VoxelTerrain::set_view_distance); - ClassDB::bind_method(D_METHOD("get_view_distance"), &VoxelTerrain::get_view_distance); + ClassDB::bind_method(D_METHOD("set_max_view_distance", "distance_in_voxels"), &VoxelTerrain::set_max_view_distance); + ClassDB::bind_method(D_METHOD("get_max_view_distance"), &VoxelTerrain::get_max_view_distance); ClassDB::bind_method(D_METHOD("get_generate_collisions"), &VoxelTerrain::get_generate_collisions); ClassDB::bind_method(D_METHOD("set_generate_collisions", "enabled"), &VoxelTerrain::set_generate_collisions); - ClassDB::bind_method(D_METHOD("get_viewer_path"), &VoxelTerrain::get_viewer_path); - ClassDB::bind_method(D_METHOD("set_viewer_path", "path"), &VoxelTerrain::set_viewer_path); - ClassDB::bind_method(D_METHOD("voxel_to_block", "voxel_pos"), &VoxelTerrain::_b_voxel_to_block); ClassDB::bind_method(D_METHOD("block_to_voxel", "block_pos"), &VoxelTerrain::_b_block_to_voxel); @@ -1153,8 +1324,7 @@ void VoxelTerrain::_bind_methods() { "set_stream", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "voxel_library", PROPERTY_HINT_RESOURCE_TYPE, "VoxelLibrary"), "set_voxel_library", "get_voxel_library"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "view_distance"), "set_view_distance", "get_view_distance"); - ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "viewer_path"), "set_viewer_path", "get_viewer_path"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_view_distance"), "set_max_view_distance", "get_max_view_distance"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "generate_collisions"), "set_generate_collisions", "get_generate_collisions"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "run_stream_in_editor"), diff --git a/terrain/voxel_terrain.h b/terrain/voxel_terrain.h index 7f1b41ec..9c7c3e67 100644 --- a/terrain/voxel_terrain.h +++ b/terrain/voxel_terrain.h @@ -28,16 +28,14 @@ public: void set_voxel_library(Ref library); Ref get_voxel_library() const; - void make_block_dirty(Vector3i bpos); - //void make_blocks_dirty(Vector3i min, Vector3i size); void make_voxel_dirty(Vector3i pos); void make_area_dirty(Rect3i box); void set_generate_collisions(bool enabled); bool get_generate_collisions() const { return _generate_collisions; } - int get_view_distance() const; - void set_view_distance(int distance_in_voxels); + unsigned int get_max_view_distance() const; + void set_max_view_distance(unsigned int distance_in_voxels); // TODO Make this obsolete with multi-viewers void set_viewer_path(NodePath path); @@ -82,16 +80,20 @@ private: void _on_stream_params_changed(); void _set_block_size_po2(int p_block_size_po2); - void make_all_view_dirty_deferred(); + void make_all_view_dirty(); void start_updater(); void stop_updater(); void start_streamer(); void stop_streamer(); void reset_map(); - Spatial *get_viewer() const; - + void view_block(Vector3i bpos, bool data_flag, bool mesh_flag, bool collision_flag); + void unview_block(Vector3i bpos, bool data_flag, bool mesh_flag, bool collision_flag); void immerge_block(Vector3i bpos); + void make_block_dirty(Vector3i bpos); + void make_block_dirty(VoxelBlock *block); + void try_schedule_block_update(VoxelBlock *block); + void save_all_modified_blocks(bool with_copy); void get_viewer_pos_and_direction(Vector3 &out_pos, Vector3 &out_direction) const; void send_block_data_requests(); @@ -99,6 +101,8 @@ private: void emit_block_loaded(const VoxelBlock *block); void emit_block_unloaded(const VoxelBlock *block); + bool try_get_paired_viewer_index(uint32_t id, size_t &out_i) const; + Dictionary get_statistics() const; static void _bind_methods(); @@ -113,29 +117,42 @@ private: uint32_t _volume_id = 0; VoxelServer::ReceptionBuffers _reception_buffers; + struct PairedViewer { + struct State { + Vector3i block_position; + int view_distance_blocks; + bool requires_collisions; + bool requires_meshes; + }; + uint32_t id; + State state; + State prev_state; + }; + + std::vector _paired_viewers; + // Voxel storage Ref _map; // How many blocks to load around the viewer - int _view_distance_blocks; + unsigned int _max_view_distance_blocks = 8; // TODO Terrains only need to handle the visible portion of voxels, which reduces the bounds blocks to handle. // Therefore, could a simple grid be better to use than a hashmap? - Set _loading_blocks; - Vector _blocks_pending_load; - Vector _blocks_pending_update; + struct LoadingBlock { + VoxelViewerRefCount viewers; + }; + HashMap _loading_blocks; + std::vector _blocks_pending_load; + std::vector _blocks_pending_update; std::vector _blocks_to_save; Ref _stream; Ref _library; - NodePath _viewer_path; - Vector3i _last_viewer_block_pos; - int _last_view_distance_blocks; - bool _generate_collisions = true; bool _run_stream_in_editor = true; diff --git a/terrain/voxel_viewer.cpp b/terrain/voxel_viewer.cpp new file mode 100644 index 00000000..b4e17bf4 --- /dev/null +++ b/terrain/voxel_viewer.cpp @@ -0,0 +1,95 @@ +#include "voxel_viewer.h" +#include "../server/voxel_server.h" +#include + +VoxelViewer::VoxelViewer() { + if (!Engine::get_singleton()->is_editor_hint()) { + set_process(true); + } +} + +void VoxelViewer::set_view_distance(unsigned int distance) { + _view_distance = distance; + if (is_active()) { + VoxelServer::get_singleton()->set_viewer_distance(_viewer_id, distance); + } +} + +unsigned int VoxelViewer::get_view_distance() const { + return _view_distance; +} + +void VoxelViewer::set_requires_visuals(bool enabled) { + _requires_visuals = enabled; + if (is_active()) { + VoxelServer::get_singleton()->set_viewer_requires_visuals(_viewer_id, enabled); + } +} + +bool VoxelViewer::is_requiring_visuals() const { + return _requires_visuals; +} + +void VoxelViewer::set_requires_collisions(bool enabled) { + _requires_collisions = enabled; + if (is_active()) { + VoxelServer::get_singleton()->set_viewer_requires_collisions(_viewer_id, enabled); + } +} + +bool VoxelViewer::is_requiring_collisions() const { + return _requires_collisions; +} + +void VoxelViewer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + if (!Engine::get_singleton()->is_editor_hint()) { + _viewer_id = VoxelServer::get_singleton()->add_viewer(); + VoxelServer::get_singleton()->set_viewer_distance(_viewer_id, _view_distance); + VoxelServer::get_singleton()->set_viewer_requires_visuals(_viewer_id, _requires_visuals); + VoxelServer::get_singleton()->set_viewer_requires_collisions(_viewer_id, _requires_collisions); + const Vector3 pos = get_global_transform().origin; + VoxelServer::get_singleton()->set_viewer_position(_viewer_id, pos); + } + } break; + + case NOTIFICATION_EXIT_TREE: + if (!Engine::get_singleton()->is_editor_hint()) { + VoxelServer::get_singleton()->remove_viewer(_viewer_id); + } + break; + + case NOTIFICATION_PROCESS: + _process(); + break; + + default: + break; + } +} + +void VoxelViewer::_process() { + const Vector3 pos = get_global_transform().origin; + VoxelServer::get_singleton()->set_viewer_position(_viewer_id, pos); +} + +bool VoxelViewer::is_active() const { + return is_inside_tree() && !Engine::get_singleton()->is_editor_hint(); +} + +void VoxelViewer::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_view_distance", "distance"), &VoxelViewer::set_view_distance); + ClassDB::bind_method(D_METHOD("get_view_distance"), &VoxelViewer::get_view_distance); + + ClassDB::bind_method(D_METHOD("set_requires_visuals", "enabled"), &VoxelViewer::set_requires_visuals); + ClassDB::bind_method(D_METHOD("is_requiring_visuals"), &VoxelViewer::is_requiring_visuals); + + ClassDB::bind_method(D_METHOD("set_requires_collisions", "enabled"), &VoxelViewer::set_requires_collisions); + ClassDB::bind_method(D_METHOD("is_requiring_collisions"), &VoxelViewer::is_requiring_collisions); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "view_distance"), "set_view_distance", "get_view_distance"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "requires_visuals"), "set_requires_visuals", "is_requiring_visuals"); + ADD_PROPERTY( + PropertyInfo(Variant::BOOL, "requires_collisions"), "set_requires_collisions", "is_requiring_collisions"); +} diff --git a/terrain/voxel_viewer.h b/terrain/voxel_viewer.h new file mode 100644 index 00000000..de193273 --- /dev/null +++ b/terrain/voxel_viewer.h @@ -0,0 +1,38 @@ +#ifndef VOXEL_VIEWER_H +#define VOXEL_VIEWER_H + +#include + +// Triggers loading of voxel nodes around its position. Voxels will update in priority closer to viewers. +// Usually added as child of the player's camera. +class VoxelViewer : public Spatial { + GDCLASS(VoxelViewer, Spatial) +public: + VoxelViewer(); + + // Distance in world space units + void set_view_distance(unsigned int distance); + unsigned int get_view_distance() const; + + void set_requires_visuals(bool enabled); + bool is_requiring_visuals() const; + + void set_requires_collisions(bool enabled); + bool is_requiring_collisions() const; + +protected: + void _notification(int p_what); + +private: + static void _bind_methods(); + + void _process(); + bool is_active() const; + + uint32_t _viewer_id = 0; + unsigned int _view_distance = 128; + bool _requires_visuals = true; + bool _requires_collisions = false; +}; + +#endif // VOXEL_VIEWER_H diff --git a/terrain/voxel_viewer_ref_count.h b/terrain/voxel_viewer_ref_count.h new file mode 100644 index 00000000..67cee174 --- /dev/null +++ b/terrain/voxel_viewer_ref_count.h @@ -0,0 +1,60 @@ +#ifndef VOXEL_VIEWER_REF_COUNT_H +#define VOXEL_VIEWER_REF_COUNT_H + +#include "../util/fixed_array.h" + +class VoxelViewerRefCount { +public: + enum ViewerType { + TYPE_DATA = 0, // This one is always counted, others are optional + TYPE_MESH, + TYPE_COLLISION, + TYPE_COUNT + }; + + VoxelViewerRefCount() { + _counts.fill(0); + } + + inline void add(bool data, bool mesh, bool collision) { + if (data) { + add(TYPE_DATA); + } + if (mesh) { + add(TYPE_MESH); + } + if (collision) { + add(TYPE_COLLISION); + } + } + + inline void remove(bool data, bool mesh, bool collision) { + if (data) { + remove(TYPE_DATA); + } + if (mesh) { + remove(TYPE_MESH); + } + if (collision) { + remove(TYPE_COLLISION); + } + } + + inline void add(ViewerType t) { + ++_counts[t]; + } + + inline void remove(ViewerType t) { + CRASH_COND_MSG(_counts[t] == 0, "Trying to remove a viewer to a block that had none"); + --_counts[t]; + } + + inline uint32_t get(ViewerType t) const { + return _counts[t]; + } + +private: + FixedArray _counts; +}; + +#endif // VOXEL_VIEWER_REF_COUNT_H