From 9bab6c04d1a6a54a60bd1506adcf7bc79f06621b Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Sat, 4 May 2019 00:02:10 +0100 Subject: [PATCH] Added VoxelLodTerrain, focused on LOD. No seam management, no editing yet. --- register_types.cpp | 2 + terrain/lod_octree.h | 215 +++++++++ terrain/voxel_block.cpp | 14 +- terrain/voxel_block.h | 11 +- terrain/voxel_lod_terrain.cpp | 734 ++++++++++++++++++++++++++++++ terrain/voxel_lod_terrain.h | 121 +++++ terrain/voxel_map.cpp | 27 +- terrain/voxel_map.h | 9 + terrain/voxel_mesh_updater.cpp | 50 +- terrain/voxel_mesh_updater.h | 6 +- terrain/voxel_provider_thread.cpp | 4 +- terrain/voxel_provider_thread.h | 3 +- util/utility.h | 12 + 13 files changed, 1190 insertions(+), 18 deletions(-) create mode 100644 terrain/lod_octree.h create mode 100644 terrain/voxel_lod_terrain.cpp create mode 100644 terrain/voxel_lod_terrain.h diff --git a/register_types.cpp b/register_types.cpp index eeb210e..5b91ffa 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -5,6 +5,7 @@ #include "providers/voxel_provider_image.h" #include "providers/voxel_provider_test.h" #include "terrain/voxel_box_mover.h" +#include "terrain/voxel_lod_terrain.h" #include "terrain/voxel_map.h" #include "terrain/voxel_terrain.h" #include "voxel_buffer.h" @@ -23,6 +24,7 @@ void register_voxel_types() { // Nodes ClassDB::register_class(); + ClassDB::register_class(); // Providers ClassDB::register_class(); diff --git a/terrain/lod_octree.h b/terrain/lod_octree.h new file mode 100644 index 0000000..aa66a31 --- /dev/null +++ b/terrain/lod_octree.h @@ -0,0 +1,215 @@ +#ifndef LOD_OCTREE_H +#define LOD_OCTREE_H + +#include "../math/vector3i.h" +#include "../octree_tables.h" +#include "../util/object_pool.h" + +template +class LodOctree { +public: + struct Node { + Node *children[8]; + // Whatever data to associate to the node, when it's a leaf. + // It needs to be booleanizable, where `true` means presence of data, and `false` means no data. + // Typically a pointer, but can be a straight boolean too. + T block; + // Position divided by chunk size at the current LOD, + // so it is sequential within each LOD, which makes it usable for grid storage + Vector3i position; + + Node() { + init(); + } + + inline bool has_children() const { + return children[0] != nullptr; + } + + inline void init() { + position = Vector3i(); + block = T(); + children[0] = nullptr; + // for (int i = 0; i < 8; ++i) { + // children[i] = nullptr; + // } + } + }; + + struct NoDestroyAction { + inline void operator()(Node *node, int lod) {} + }; + + template + void clear(A &destroy_action) { + join_all_recursively(&_root, _max_depth, destroy_action); + _max_depth = 0; + _base_size = 0; + } + + static void compute_lod_count(int base_size, int full_size) { + int po = 0; + while (full_size > base_size) { + full_size = full_size >> 1; + po += 1; + } + return po; + } + + template + void create_from_lod_count(int base_size, unsigned int lod_count, A &destroy_action) { + ERR_FAIL_COND(lod_count > 32); + clear(destroy_action); + _base_size = base_size; + _max_depth = lod_count - 1; + } + + int get_lod_count() const { + return _max_depth + 1; + } + + // The higher, the longer LODs will spread and higher the quality. + // The lower, the shorter LODs will spread and lower the quality. + void set_split_scale(float p_split_scale) { + + const float minv = 2.0; + const float maxv = 5.0; + + // Split scale must be greater than a threshold, + // otherwise lods will decimate too fast and it will look messy + if (p_split_scale < minv) { + p_split_scale = minv; + } else if (p_split_scale > maxv) { + p_split_scale = maxv; + } + + _split_scale = p_split_scale; + } + + float get_split_scale() const { + return _split_scale; + } + + static inline int get_lod_factor(int lod) { + return 1 << lod; + } + + template + void update(Vector3 view_pos, A &create_action, B &destroy_action) { + update(&_root, _max_depth, view_pos, create_action, destroy_action); + } + + template + void foreach_node(A &action) { + action(action, &_root, _max_depth); + } + +private: + template + void foreach_node(A action, Node *node, int lod) { + action(node, lod); + if (node->has_children()) { + for (int i = 0; i < 8; ++i) { + foreach_node(action, node->children[i], lod - 1); + } + } + } + + template + void update(Node *node, int lod, Vector3 view_pos, A &create_action, B &destroy_action) { + // This function should be called regularly over frames. + + int lod_factor = get_lod_factor(lod); + int chunk_size = _base_size * lod_factor; + Vector3 world_center = static_cast(chunk_size) * (node->position.to_vec3() + Vector3(0.5, 0.5, 0.5)); + float split_distance = chunk_size * _split_scale; + + if (!node->has_children()) { + + if (lod > 0 && world_center.distance_to(view_pos) < split_distance) { + // Split + for (int i = 0; i < 8; ++i) { + + Node *child = _pool.create(); + + child->position.x = node->position.x * 2 + OctreeTables::g_octant_position[i][0]; + child->position.y = node->position.y * 2 + OctreeTables::g_octant_position[i][1]; + child->position.z = node->position.z * 2 + OctreeTables::g_octant_position[i][2]; + + child->block = create_action(child, lod - 1); + + node->children[i] = child; + // If the node needs to split more, we'll ask more recycling at the next frame... + // That means the initialization of the game should do some warm up and fetch all leaves, + // otherwise it's gonna be rough + } + + if (node->block) { + destroy_action(node, lod); + node->block = T(); + } + } + + } else { + + bool no_split_child = true; + + for (int i = 0; i < 8; ++i) { + Node *child = node->children[i]; + update(child, lod - 1, view_pos, create_action, destroy_action); + no_split_child |= child->has_children(); + } + + if (no_split_child && world_center.distance_to(view_pos) > split_distance) { + // Join + if (node->has_children()) { + + for (int i = 0; i < 8; ++i) { + Node *child = node->children[i]; + destroy_action(child, lod - 1); + child->block = T(); + _pool.recycle(child); + } + + node->children[0] = nullptr; + + CRASH_COND(node->block); + node->block = create_action(node, lod); + } + } + } + } + + template + void join_all_recursively(Node *node, int lod, A &destroy_action) { + + if (node->has_children()) { + Node **children = node->children; + + for (int i = 0; i < 8; ++i) { + Node *child = children[i]; + join_all_recursively(child, lod - 1, destroy_action); + _pool.recycle(child); + children[0] = nullptr; + } + + } else { + if (node->block) { + destroy_action(node, lod); + node->block = T(); + } + } + } + + Node _root; + int _max_depth = 0; + float _base_size = 16; + float _split_scale = 2.0; + ObjectPool _pool; +}; + +// Notes: +// Population of an octree given its depth, thanks to Sage: +// ((1 << 3 * (depth + 1)) - 1 ) / 7 + +#endif // LOD_OCTREE_H diff --git a/terrain/voxel_block.cpp b/terrain/voxel_block.cpp index 6ee9181..fd6d20d 100644 --- a/terrain/voxel_block.cpp +++ b/terrain/voxel_block.cpp @@ -1,14 +1,15 @@ #include "voxel_block.h" // Helper -VoxelBlock *VoxelBlock::create(Vector3i bpos, Ref buffer, unsigned int size) { +VoxelBlock *VoxelBlock::create(Vector3i bpos, Ref buffer, unsigned int size, unsigned int p_lod_index) { const int bs = size; ERR_FAIL_COND_V(buffer.is_null(), NULL); ERR_FAIL_COND_V(buffer->get_size() != Vector3i(bs, bs, bs), NULL); VoxelBlock *block = memnew(VoxelBlock); block->pos = bpos; - block->_position_in_voxels = bpos * size; + block->lod_index = p_lod_index; + block->_position_in_voxels = bpos * (size << p_lod_index); block->voxels = buffer; //block->map = ↦ @@ -63,6 +64,10 @@ void VoxelBlock::set_mesh(Ref mesh, Ref world) { // } } +bool VoxelBlock::has_mesh() const { + return _mesh.is_valid(); +} + void VoxelBlock::enter_world(World *world) { if (_mesh_instance.is_valid()) { VisualServer &vs = *VisualServer::get_singleton(); @@ -82,4 +87,9 @@ void VoxelBlock::set_visible(bool visible) { VisualServer &vs = *VisualServer::get_singleton(); vs.instance_set_visible(_mesh_instance, visible); } + _visible = visible; +} + +bool VoxelBlock::is_visible() const { + return _visible; } diff --git a/terrain/voxel_block.h b/terrain/voxel_block.h index dcc1313..b7fca83 100644 --- a/terrain/voxel_block.h +++ b/terrain/voxel_block.h @@ -7,19 +7,21 @@ #include // Internal structure holding a reference to mesh visuals, physics and a block of voxel data. -// TODO Voxel data should be separated from this class VoxelBlock { public: - Ref voxels; // SIZE*SIZE*SIZE voxels - Vector3i pos; + Ref voxels; + Vector3i pos; // TODO Rename position + unsigned int lod_index = 0; - static VoxelBlock *create(Vector3i bpos, Ref buffer, unsigned int size); + static VoxelBlock *create(Vector3i bpos, Ref buffer, unsigned int size, unsigned int p_lod_index); void set_mesh(Ref mesh, Ref world); + bool has_mesh() const; void enter_world(World *world); void exit_world(); void set_visible(bool visible); + bool is_visible() const; private: VoxelBlock(); @@ -29,6 +31,7 @@ private: Ref _mesh; RID _mesh_instance; int _mesh_update_count; + bool _visible = true; }; #endif // VOXEL_BLOCK_H diff --git a/terrain/voxel_lod_terrain.cpp b/terrain/voxel_lod_terrain.cpp new file mode 100644 index 0000000..2ed1736 --- /dev/null +++ b/terrain/voxel_lod_terrain.cpp @@ -0,0 +1,734 @@ +#include "voxel_lod_terrain.h" +#include "../math/rect3i.h" +#include "voxel_map.h" +#include "voxel_mesh_updater.h" +#include "voxel_provider_thread.h" +#include +#include + +VoxelLodTerrain::VoxelLodTerrain() { + + print_line("Construct VoxelLodTerrain"); + + _lods[0].map.instance(); + + set_lod_count(4); + set_lod_split_scale(2); + + 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()); + + BlockState state = BLOCK_NONE; + Map::Element *E = lod.block_states.find(block_pos); + if (E) { + state = E->value(); + lod.block_states.erase(E); + } + + // 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]; + + lod.blocks_in_meshing_area.clear(); + + // 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; +} + +VoxelLodTerrain::BlockState VoxelLodTerrain::get_block_state(Vector3 bpos, unsigned int lod_index) const { + ERR_FAIL_COND_V(lod_index >= get_lod_count(), BLOCK_NONE); + const Lod &lod = _lods[lod_index]; + Vector3i ibpos(bpos); + const Map::Element *state = lod.block_states.find(ibpos); + if (state) { + return state->value(); + } else { + if (lod.map->has_block(ibpos)) { + return BLOCK_IDLE; + } else { + return BLOCK_NONE; + } + } +} + +bool VoxelLodTerrain::is_block_meshed(Vector3 bpos, unsigned int lod_index) const { + ERR_FAIL_COND_V(lod_index >= get_lod_count(), false); + const Lod &lod = _lods[lod_index]; + Vector3i ibpos(bpos); + const VoxelBlock *block = lod.map->get_block(ibpos); + if (block) { + return block->has_mesh(); + } else { + return false; + } +} + +bool VoxelLodTerrain::is_block_shown(Vector3 bpos, unsigned int lod_index) const { + ERR_FAIL_COND_V(lod_index >= get_lod_count(), false); + const Lod &lod = _lods[lod_index]; + Vector3i ibpos(bpos); + const VoxelBlock *block = lod.map->get_block(ibpos); + if (block) { + return block->is_visible(); + } else { + return false; + } +} + +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::make_block_dirty(Vector3i bpos, unsigned int lod_index) { + + CRASH_COND(lod_index >= get_lod_count()); + + // TODO Immediate update viewer distance? + + Lod &lod = _lods[lod_index]; + + Map::Element *state = lod.block_states.find(bpos); + + if (state == NULL) { + // The block is not dirty, so it will either be loaded or updated + + if (lod.map->has_block(bpos)) { + + lod.blocks_pending_update.push_back(bpos); + lod.block_states.insert(bpos, BLOCK_UPDATE_NOT_SENT); + + } else { + + lod.blocks_to_load.push_back(bpos); + lod.block_states.insert(bpos, BLOCK_LOAD); + } + + } else if (state->value() == BLOCK_UPDATE_SENT) { + // The updater is already processing the block, + // but the block was modified again so we schedule another update + state->value() = BLOCK_UPDATE_NOT_SENT; + lod.blocks_pending_update.push_back(bpos); + + } else if (state->value() == BLOCK_UPDATE_NOT_SENT) { + WARN_PRINT("Block already marked as BLOCK_UPDATE_NOT_SENT"); + } +} + +static void remove_positions_outside_box( + std::vector &positions, + Rect3i box, + Map &state_map) { + + 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); + state_map.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); + + // Here we go... + + // Find out which blocks _data_ need to be loaded + { + // This should be the same distance relatively to each LOD + int view_distance_blocks_within_lod = static_cast(_lod_octree.get_split_scale()) * 2 + 1; + + 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(view_distance_blocks_within_lod)); + + 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.block_states); + remove_positions_outside_box(lod.blocks_pending_update, new_box, lod.block_states); + + 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); + + } else if (!prev_contains && new_contains) { + // Load or update block + make_block_dirty(pos, lod_index); + } + } + } + } + } + + lod.last_viewer_block_pos = viewer_block_pos_within_lod; + lod.last_view_distance_blocks = view_distance_blocks_within_lod; + } + } + + // 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(); + } + + _provider_thread->push(input); + } + + // 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); + + 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]; + + Map::Element *state = lod.block_states.find(eo.block_position); + if (state == nullptr || state->value() != BLOCK_LOAD) { + // That block was not requested, or is no longer needed. drop it... + continue; + } + + 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); + lod.map->set_block_buffer(eo.block_position, eo.voxels); + + lod.block_states.erase(state); + + // 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; + + if (lod.map->is_block_surrounded(npos)) { + + Map::Element *state = lod.block_states.find(npos); + if (state && state->value() == BLOCK_UPDATE_NOT_SENT) { + // Assuming it is scheduled to be updated already. + // In case of BLOCK_UPDATE_SENT, we'll have to resend it. + continue; + } + + if (state) { + state->value() = BLOCK_UPDATE_NOT_SENT; + } else { + lod.block_states.insert(npos, BLOCK_UPDATE_NOT_SENT); + } + + lod.blocks_pending_update.push_back(npos); + } + } + } + } + + } else { + // Only update the block, neighbors will probably follow if needed + lod.block_states[eo.block_position] = BLOCK_UPDATE_NOT_SENT; + lod.blocks_pending_update.push_back(eo.block_position); + } + } + } + + // 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]; + + Map::Element *block_state = lod.block_states.find(block_pos); + CRASH_COND(block_state == NULL); + CRASH_COND(block_state->value() != BLOCK_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_state->value() = BLOCK_UPDATE_SENT; + } + + lod.blocks_pending_update.clear(); + } + + _block_updater->push(input); + } + + // Receive mesh updates + { + { + VoxelMeshUpdater::Output output; + _block_updater->pop(output); + + 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]; + + Map::Element *state = lod.block_states.find(ob.position); + if (state && state->value() == BLOCK_UPDATE_SENT) { + lod.block_states.erase(state); + } + + VoxelBlock *block = lod.map->get_block(ob.position); + if (block == NULL) { + // That block is no longer loaded, drop the result + continue; + } + + 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->set_visible(lod.blocks_in_meshing_area.has(ob.position)); + } + + shift_up(_blocks_pending_main_thread_update, queue_index); + } + + // Find out which blocks need to be shown + { + struct ShowAction { + VoxelLodTerrain *self; + bool operator()(LodOctree::Node *node, int lod_index) { + Lod &lod = self->_lods[lod_index]; + Vector3i bpos = node->position; + lod.blocks_in_meshing_area.insert(bpos); + VoxelBlock *block = lod.map->get_block(bpos); + if (block != nullptr) { + block->set_visible(true); + } + return true; + } + }; + + struct HideAction { + VoxelLodTerrain *self; + void operator()(LodOctree::Node *node, int lod_index) { + Lod &lod = self->_lods[lod_index]; + Vector3i bpos = node->position; + lod.blocks_in_meshing_area.erase(bpos); + VoxelBlock *block = lod.map->get_block(bpos); + if (block) { + block->set_visible(false); + } + } + }; + + ShowAction show_action; + show_action.self = this; + + HideAction hide_action; + hide_action.self = this; + + _lod_octree.update(viewer_pos, show_action, hide_action); + } +} + +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_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("get_block_state", "block_pos", "lod"), &VoxelLodTerrain::get_block_state); + ClassDB::bind_method(D_METHOD("is_block_meshed", "block_pos", "lod"), &VoxelLodTerrain::is_block_meshed); + ClassDB::bind_method(D_METHOD("is_block_shown", "block_pos", "lod"), &VoxelLodTerrain::is_block_shown); + + 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::NODE_PATH, "viewer_path"), "set_viewer_path", "get_viewer_path"); + + BIND_ENUM_CONSTANT(BLOCK_NONE); + BIND_ENUM_CONSTANT(BLOCK_LOAD); + BIND_ENUM_CONSTANT(BLOCK_UPDATE_NOT_SENT); + BIND_ENUM_CONSTANT(BLOCK_UPDATE_SENT); + BIND_ENUM_CONSTANT(BLOCK_IDLE); +} diff --git a/terrain/voxel_lod_terrain.h b/terrain/voxel_lod_terrain.h new file mode 100644 index 0000000..4801dee --- /dev/null +++ b/terrain/voxel_lod_terrain.h @@ -0,0 +1,121 @@ +#ifndef VOXEL_LOD_TERRAIN_HPP +#define VOXEL_LOD_TERRAIN_HPP + +#include "../providers/voxel_provider.h" +#include "lod_octree.h" +#include "voxel_map.h" +#include "voxel_mesh_updater.h" +#include +#include + +class VoxelMap; +class VoxelProviderThread; + +// Paged terrain made of voxel blocks of variable level of detail. +// Designed for highest view distances, preferably using smooth voxels. +// Voxels are polygonized around the viewer by distance in a very large sphere, usually extending beyond far clip. +// Data is streamed using a VoxelProvider, which must support LOD. +class VoxelLodTerrain : public Spatial { + GDCLASS(VoxelLodTerrain, Spatial) +public: + static const int MAX_LOD = 32; + + enum BlockState { + BLOCK_NONE, // There is no block + BLOCK_LOAD, // The block is loading + BLOCK_UPDATE_NOT_SENT, // The block needs an update but wasn't sent yet + BLOCK_UPDATE_SENT, // The block needs an update which was sent + BLOCK_IDLE // The block is up to date + }; + + VoxelLodTerrain(); + ~VoxelLodTerrain(); + + Ref get_material() const; + void set_material(Ref p_material); + + Ref get_provider() const; + void set_provider(Ref p_provider); + + int get_view_distance() const; + void set_view_distance(int p_distance_in_voxels); + + void set_lod_split_scale(float p_lod_split_scale); + float get_lod_split_scale() const; + + void set_lod_count(unsigned int p_lod_count); + int get_lod_count() const; + + void set_viewer_path(NodePath path); + NodePath get_viewer_path() const; + + BlockState get_block_state(Vector3 bpos, unsigned int lod_index) const; + bool is_block_meshed(Vector3 bpos, unsigned int lod_index) const; + bool is_block_shown(Vector3 bpos, unsigned int lod_index) const; + +protected: + static void _bind_methods(); + + void _notification(int p_what); + void _process(); + +private: + int get_block_size() const; + int get_block_size_pow2() const; + void make_all_view_dirty_deferred(); + Spatial *get_viewer() const; + void immerge_block(Vector3i block_pos, unsigned int lod_index); + void reset_updater(); + Vector3 get_viewer_pos() const; + void make_block_dirty(Vector3i bpos, unsigned int lod_index); + + void debug_print_lods(); + + template + void for_all_blocks(A &action) { + for (int lod_index = 0; lod_index < MAX_LOD; ++lod_index) { + if (_lods[lod_index].map.is_valid()) { + _lods[lod_index].map->for_all_blocks(action); + } + } + } + + // TODO Dare having a grid of octrees for infinite world? + // This octree doesn't hold any data... hence bool. + LodOctree _lod_octree; + + NodePath _viewer_path; + + Ref _provider; + VoxelProviderThread *_provider_thread = nullptr; + VoxelMeshUpdater *_block_updater = nullptr; + std::vector _blocks_pending_main_thread_update; + + Ref _material; + + // Each LOD works in a set of coordinates spanning 2x more voxels the higher their index is + struct Lod { + Ref map; + + Map block_states; + std::vector blocks_pending_update; + + // Reflects LodOctree but only in this LOD + // TODO Would be nice to use LodOctree directly! + Set blocks_in_meshing_area; + + // These are relative to this LOD, in block coordinates + Vector3i last_viewer_block_pos; + int last_view_distance_blocks = 0; + + // Members for memory caching + std::vector blocks_to_load; + //std::vector blocks_to_update; + }; + + Lod _lods[MAX_LOD]; +}; + +VARIANT_ENUM_CAST(VoxelLodTerrain::BlockState) + +#endif // VOXEL_LOD_TERRAIN_HPP diff --git a/terrain/voxel_map.cpp b/terrain/voxel_map.cpp index 7a664a9..245343a 100644 --- a/terrain/voxel_map.cpp +++ b/terrain/voxel_map.cpp @@ -8,7 +8,7 @@ VoxelMap::VoxelMap() : _last_accessed_block(NULL) { // TODO Make it configurable in editor (with all necessary notifications and updatings!) - set_block_size_pow2(4); + set_block_size_pow2(DEFAULT_BLOCK_SIZE_PO2); for (unsigned int i = 0; i < VoxelBuffer::MAX_CHANNELS; ++i) { _default_voxel[i] = 0; @@ -32,6 +32,14 @@ void VoxelMap::set_block_size_pow2(unsigned int p) { _block_size_mask = _block_size - 1; } +void VoxelMap::set_lod_index(int lod) { + _lod_index = lod; +} + +unsigned int VoxelMap::get_lod_index() const { + return _lod_index; +} + int VoxelMap::get_voxel(Vector3i pos, unsigned int c) { Vector3i bpos = voxel_to_block(pos); VoxelBlock *block = get_block(bpos); @@ -52,7 +60,7 @@ VoxelBlock *VoxelMap::get_or_create_block_at_voxel_pos(Vector3i pos) { buffer->create(_block_size, _block_size, _block_size); buffer->set_default_values(_default_voxel); - block = VoxelBlock::create(bpos, buffer, _block_size); + block = VoxelBlock::create(bpos, buffer, _block_size, _lod_index); set_block(bpos, block); } @@ -109,6 +117,19 @@ VoxelBlock *VoxelMap::get_block(Vector3i bpos) { return NULL; } +const VoxelBlock *VoxelMap::get_block(Vector3i bpos) const { + if (_last_accessed_block && _last_accessed_block->pos == bpos) { + return _last_accessed_block; + } + const VoxelBlock *const *p = _blocks.getptr(bpos); + if (p) { + const VoxelBlock *block = *p; + CRASH_COND(block == NULL); // The map should not contain null blocks + return block; + } + return NULL; +} + void VoxelMap::set_block(Vector3i bpos, VoxelBlock *block) { ERR_FAIL_COND(block == NULL); if (_last_accessed_block == NULL || _last_accessed_block->pos == bpos) { @@ -121,7 +142,7 @@ void VoxelMap::set_block_buffer(Vector3i bpos, Ref buffer) { ERR_FAIL_COND(buffer.is_null()); VoxelBlock *block = get_block(bpos); if (block == NULL) { - block = VoxelBlock::create(bpos, *buffer, _block_size); + block = VoxelBlock::create(bpos, *buffer, _block_size, _lod_index); set_block(bpos, block); } else { block->voxels = buffer; diff --git a/terrain/voxel_map.h b/terrain/voxel_map.h index ef601eb..b81957c 100644 --- a/terrain/voxel_map.h +++ b/terrain/voxel_map.h @@ -10,6 +10,8 @@ class VoxelMap : public Reference { GDCLASS(VoxelMap, Reference) public: + static const int DEFAULT_BLOCK_SIZE_PO2 = 4; + // Converts voxel coodinates into block coordinates. // Don't use division because it introduces an offset in negative coordinates. static _FORCE_INLINE_ Vector3i voxel_to_block_b(Vector3i pos, int block_size_pow2) { @@ -42,6 +44,9 @@ public: _FORCE_INLINE_ unsigned int get_block_size_pow2() const { return _block_size_pow2; } _FORCE_INLINE_ unsigned int get_block_size_mask() const { return _block_size_mask; } + void set_lod_index(int lod); + unsigned int get_lod_index() const; + int get_voxel(Vector3i pos, unsigned int c = 0); void set_voxel(int value, Vector3i pos, unsigned int c = 0); @@ -106,6 +111,7 @@ public: }*/ VoxelBlock *get_block(Vector3i bpos); + const VoxelBlock *get_block(Vector3i bpos) const; bool has_block(Vector3i pos) const; bool is_block_surrounded(Vector3i pos) const; @@ -146,6 +152,7 @@ private: // Voxel values that will be returned if access is out of map bounds uint8_t _default_voxel[VoxelBuffer::MAX_CHANNELS]; + // TODO Consider using OAHashMap // Blocks stored with a spatial hash in all 3D directions HashMap _blocks; @@ -156,6 +163,8 @@ private: unsigned int _block_size; unsigned int _block_size_pow2; unsigned int _block_size_mask; + + unsigned int _lod_index = 0; }; #endif // VOXEL_MAP_H diff --git a/terrain/voxel_mesh_updater.cpp b/terrain/voxel_mesh_updater.cpp index d783172..99e9427 100644 --- a/terrain/voxel_mesh_updater.cpp +++ b/terrain/voxel_mesh_updater.cpp @@ -1,5 +1,6 @@ #include "voxel_mesh_updater.h" #include "../util/utility.h" +#include "voxel_lod_terrain.h" #include VoxelMeshUpdater::VoxelMeshUpdater(Ref library, MeshingParams params) { @@ -48,7 +49,7 @@ void VoxelMeshUpdater::push(const Input &input) { for (int i = 0; i < input.blocks.size(); ++i) { - Vector3i pos = input.blocks[i].position; + const InputBlock &block = input.blocks[i]; // If a block is exactly on the priority position, update it instantly on the main thread // This is to eliminate latency for player's actions, assuming updating a block isn't slower than a frame @@ -65,18 +66,19 @@ void VoxelMeshUpdater::push(const Input &input) { continue; }*/ - int *index = _block_indexes.getptr(pos); + CRASH_COND(block.lod >= MAX_LOD) + int *index = _block_indexes[block.lod].getptr(block.position); if (index) { // The block is already in the update queue, replace it ++replaced_blocks; - _shared_input.blocks.write[*index] = input.blocks[i]; + _shared_input.blocks.write[*index] = block; } else { int j = _shared_input.blocks.size(); - _shared_input.blocks.push_back(input.blocks[i]); - _block_indexes[pos] = j; + _shared_input.blocks.push_back(block); + _block_indexes[block.lod][block.position] = j; } } @@ -191,6 +193,32 @@ void VoxelMeshUpdater::thread_func() { } } +static void scale_mesh_data(VoxelMesher::Output &data, float factor) { + + for (int i = 0; i < data.surfaces.size(); ++i) { + Array &surface = data.surfaces.write[i]; // There is COW here too but should not happen, hopefully + + if (surface.empty()) { + continue; + } + + PoolVector3Array positions = surface[Mesh::ARRAY_VERTEX]; // Array of Variants here, implicit cast going on + + // Now dear COW, let's make sure there is only ONE ref to that PoolVector, + // so you won't trash performance with pointless allocations + surface[Mesh::ARRAY_VERTEX] = Variant(); + + PoolVector3Array::Write w = positions.write(); + int len = positions.size(); + for (int j = 0; j < len; ++j) { + w[j] = w[j] * factor; + } + + // Thank you + surface[Mesh::ARRAY_VERTEX] = positions; + } +} + void VoxelMeshUpdater::process_block(const InputBlock &block, OutputBlock &output) { CRASH_COND(block.voxels.is_null()); @@ -206,6 +234,13 @@ void VoxelMeshUpdater::process_block(const InputBlock &block, OutputBlock &outpu } output.position = block.position; + output.lod = block.lod; + + if (block.lod > 0) { + float factor = 1 << block.lod; + scale_mesh_data(output.blocky_surfaces, factor); + scale_mesh_data(output.smooth_surfaces, factor); + } } // Sorts distance to viewer @@ -243,7 +278,10 @@ void VoxelMeshUpdater::thread_sync(int queue_index, Stats stats) { _input.priority_position = _shared_input.priority_position; _shared_input.blocks.clear(); - _block_indexes.clear(); + + for (unsigned int lod_index = 0; lod_index < MAX_LOD; ++lod_index) { + _block_indexes[lod_index].clear(); + } needs_sort = _needs_sort; _needs_sort = false; diff --git a/terrain/voxel_mesh_updater.h b/terrain/voxel_mesh_updater.h index d63240a..859b2c0 100644 --- a/terrain/voxel_mesh_updater.h +++ b/terrain/voxel_mesh_updater.h @@ -11,9 +11,12 @@ class VoxelMeshUpdater { public: + static const int MAX_LOD = 32; // Like VoxelLodTerrain + struct InputBlock { Ref voxels; Vector3i position; + unsigned int lod = 0; }; struct Input { @@ -29,6 +32,7 @@ public: VoxelMesher::Output blocky_surfaces; VoxelMesher::Output smooth_surfaces; Vector3i position; + unsigned int lod = 0; }; struct Stats { @@ -79,7 +83,7 @@ private: private: Input _shared_input; Mutex *_input_mutex; - HashMap _block_indexes; + HashMap _block_indexes[MAX_LOD]; bool _needs_sort; Output _shared_output; diff --git a/terrain/voxel_provider_thread.cpp b/terrain/voxel_provider_thread.cpp index 8b22411..3ed8a58 100644 --- a/terrain/voxel_provider_thread.cpp +++ b/terrain/voxel_provider_thread.cpp @@ -101,7 +101,7 @@ void VoxelProviderThread::thread_func() { buffer->create(bs, bs, bs); // Query voxel provider - Vector3i block_origin_in_voxels = block.block_position * bs; + Vector3i block_origin_in_voxels = block.block_position * (bs << block.lod); uint64_t time_before = OS::get_singleton()->get_ticks_usec(); _voxel_provider->emerge_block(buffer, block_origin_in_voxels, block.lod); uint64_t time_taken = OS::get_singleton()->get_ticks_usec() - time_before; @@ -120,7 +120,9 @@ void VoxelProviderThread::thread_func() { EmergeOutput eo; eo.origin_in_voxels = block_origin_in_voxels; + eo.block_position = block.block_position; eo.voxels = buffer; + eo.lod = block.lod; _output.push_back(eo); } diff --git a/terrain/voxel_provider_thread.h b/terrain/voxel_provider_thread.h index f376c93..c7dadd2 100644 --- a/terrain/voxel_provider_thread.h +++ b/terrain/voxel_provider_thread.h @@ -34,7 +34,8 @@ public: struct EmergeOutput { Ref voxels; - Vector3i origin_in_voxels; + Vector3i origin_in_voxels; // TODO Remove this, redundant now + Vector3i block_position; int lod = 0; }; diff --git a/util/utility.h b/util/utility.h index 62619c4..3736711 100644 --- a/util/utility.h +++ b/util/utility.h @@ -22,6 +22,18 @@ void shift_up(Vector &v, int pos) { v.resize(remaining); } +template +void shift_up(std::vector &v, int pos) { + + int j = 0; + for (int i = pos; i < v.size(); ++i, ++j) { + v[j] = v[i]; + } + + int remaining = v.size() - pos; + v.resize(remaining); +} + // Pops the last element of the vector and place it at the given position. // (The element that was at this position is the one removed). template