Implemented viewers for VoxelTerrain

master
Marc Gilleron 2020-09-06 19:01:12 +01:00
parent b8c97ebc55
commit 81e1872f8b
12 changed files with 698 additions and 198 deletions

View File

@ -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<VoxelTerrain>();
ClassDB::register_class<VoxelLodTerrain>();
ClassDB::register_class<VoxelViewer>();
// Streams
ClassDB::register_class<VoxelStream>();

View File

@ -138,6 +138,16 @@ public:
}
}
template <typename F>
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 <typename F>
inline void for_each_with_id(F f) {
for (size_t i = 0; i < _slots.size(); ++i) {
@ -148,6 +158,16 @@ public:
}
}
template <typename F>
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();
}

View File

@ -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<VoxelViewersArray>();
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<VoxelMesher> smooth_mesher = VoxelServer::get_singleton()->_smooth_meshers[ctx.thread_index];
CRASH_COND(smooth_mesher.is_null());
smooth_mesher->build(smooth_surfaces_output, input);

View File

@ -52,6 +52,20 @@ public:
std::vector<BlockDataOutput> 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 <typename F>
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<MeshingDependency> 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,

View File

@ -1,4 +1,5 @@
#include "voxel_block.h"
#include "../util/macros.h"
#include "../util/profiling.h"
#include "../voxel_string_names.h"
#include <scene/3d/spatial.h>
@ -103,16 +104,13 @@ void VoxelBlock::set_world(Ref<World> p_world) {
}
}
void VoxelBlock::set_mesh(Ref<Mesh> mesh, Spatial *node, bool generate_collision, Vector<Array> surface_arrays, bool debug_collision) {
void VoxelBlock::set_mesh(Ref<Mesh> 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> mesh, Spatial *node, bool generate_collision
_mesh_instance.set_material_override(_debug_material);
#endif
if (generate_collision) {
Ref<Shape> 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> 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> 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<Array> 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> 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();
}
}

View File

@ -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<VoxelBuffer> buffer, unsigned int size, unsigned int p_lod_index);
~VoxelBlock();
// Visuals and physics
void set_world(Ref<World> p_world);
void set_mesh(Ref<Mesh> mesh, Spatial *node, bool generate_collision, Vector<Array> surface_arrays, bool debug_collision);
// Visuals
void set_mesh(Ref<Mesh> mesh);
void set_transition_mesh(Ref<Mesh> mesh, int side);
bool has_mesh() const;
void drop_mesh();
void set_shader_material(Ref<ShaderMaterial> material);
inline Ref<ShaderMaterial> get_shader_material() const { return _shader_material; }
// Collisions
void set_collision_mesh(Vector<Array> 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<Material> _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;

View File

@ -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();

View File

@ -25,9 +25,6 @@ VoxelTerrain::VoxelTerrain() {
_map.instance();
_view_distance_blocks = 8;
_last_view_distance_blocks = 0;
Ref<VoxelLibrary> library;
library.instance();
set_voxel_library(library);
@ -193,49 +190,28 @@ void VoxelTerrain::set_voxel_library(Ref<VoxelLibrary> 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<Spatial>(node);
}
void VoxelTerrain::set_material(unsigned int id, Ref<Material> material) {
// TODO Update existing block surfaces
ERR_FAIL_COND(id < 0 || id >= VoxelMesherBlocky::MAX_MATERIALS);
@ -248,28 +224,16 @@ Ref<Material> 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<VoxelTerrain::BlockToSave> &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<size_t> 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<Vector3i>::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<Mesh>(), this, _generate_collisions, Vector<Array>(), 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<Mesh>();
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"),

View File

@ -28,16 +28,14 @@ public:
void set_voxel_library(Ref<VoxelLibrary> library);
Ref<VoxelLibrary> 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<PairedViewer> _paired_viewers;
// Voxel storage
Ref<VoxelMap> _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<Vector3i> _loading_blocks;
Vector<Vector3i> _blocks_pending_load;
Vector<Vector3i> _blocks_pending_update;
struct LoadingBlock {
VoxelViewerRefCount viewers;
};
HashMap<Vector3i, LoadingBlock, Vector3iHasher> _loading_blocks;
std::vector<Vector3i> _blocks_pending_load;
std::vector<Vector3i> _blocks_pending_update;
std::vector<BlockToSave> _blocks_to_save;
Ref<VoxelStream> _stream;
Ref<VoxelLibrary> _library;
NodePath _viewer_path;
Vector3i _last_viewer_block_pos;
int _last_view_distance_blocks;
bool _generate_collisions = true;
bool _run_stream_in_editor = true;

95
terrain/voxel_viewer.cpp Normal file
View File

@ -0,0 +1,95 @@
#include "voxel_viewer.h"
#include "../server/voxel_server.h"
#include <core/engine.h>
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");
}

38
terrain/voxel_viewer.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef VOXEL_VIEWER_H
#define VOXEL_VIEWER_H
#include <scene/3d/spatial.h>
// 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

View File

@ -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<uint32_t, TYPE_COUNT> _counts;
};
#endif // VOXEL_VIEWER_REF_COUNT_H