Added VoxelLodTerrain, focused on LOD. No seam management, no editing yet.

This commit is contained in:
Marc Gilleron 2019-05-04 00:02:10 +01:00
parent 9363f54e8d
commit 9bab6c04d1
13 changed files with 1190 additions and 18 deletions

View File

@ -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<VoxelTerrain>();
ClassDB::register_class<VoxelLodTerrain>();
// Providers
ClassDB::register_class<VoxelProvider>();

215
terrain/lod_octree.h Normal file
View File

@ -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 T>
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 <typename A>
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 <typename A>
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 <typename A, typename B>
void update(Vector3 view_pos, A &create_action, B &destroy_action) {
update(&_root, _max_depth, view_pos, create_action, destroy_action);
}
template <typename A>
void foreach_node(A &action) {
action(action, &_root, _max_depth);
}
private:
template <typename A>
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 <typename A, typename B>
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<real_t>(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 <typename A>
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<Node> _pool;
};
// Notes:
// Population of an octree given its depth, thanks to Sage:
// ((1 << 3 * (depth + 1)) - 1 ) / 7
#endif // LOD_OCTREE_H

View File

@ -1,14 +1,15 @@
#include "voxel_block.h"
// Helper
VoxelBlock *VoxelBlock::create(Vector3i bpos, Ref<VoxelBuffer> buffer, unsigned int size) {
VoxelBlock *VoxelBlock::create(Vector3i bpos, Ref<VoxelBuffer> 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 = &map;
@ -63,6 +64,10 @@ void VoxelBlock::set_mesh(Ref<Mesh> mesh, Ref<World> 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;
}

View File

@ -7,19 +7,21 @@
#include <scene/3d/physics_body.h>
// 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<VoxelBuffer> voxels; // SIZE*SIZE*SIZE voxels
Vector3i pos;
Ref<VoxelBuffer> voxels;
Vector3i pos; // TODO Rename position
unsigned int lod_index = 0;
static VoxelBlock *create(Vector3i bpos, Ref<VoxelBuffer> buffer, unsigned int size);
static VoxelBlock *create(Vector3i bpos, Ref<VoxelBuffer> buffer, unsigned int size, unsigned int p_lod_index);
void set_mesh(Ref<Mesh> mesh, Ref<World> 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> _mesh;
RID _mesh_instance;
int _mesh_update_count;
bool _visible = true;
};
#endif // VOXEL_BLOCK_H

View File

@ -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 <core/engine.h>
#include <core/os/os.h>
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<Material> VoxelLodTerrain::get_material() const {
return _material;
}
void VoxelLodTerrain::set_material(Ref<Material> p_material) {
_material = p_material;
}
Ref<VoxelProvider> 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<VoxelProvider> 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<Spatial>(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<Vector3i, BlockState>::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<VoxelLibrary>(), 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<bool>::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<Vector3i, BlockState>::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<Vector3i, BlockState>::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<Vector3i> &positions,
Rect3i box,
Map<Vector3i, VoxelLodTerrain::BlockState> &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<int>(_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<Vector3i, BlockState>::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<Vector3i, BlockState>::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<Vector3i, BlockState>::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<VoxelBuffer> 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> 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<Vector3i, BlockState>::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<ArrayMesh> 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<Mesh>();
}
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<bool>::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<bool>::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);
}

121
terrain/voxel_lod_terrain.h Normal file
View File

@ -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 <core/set.h>
#include <scene/3d/spatial.h>
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<Material> get_material() const;
void set_material(Ref<Material> p_material);
Ref<VoxelProvider> get_provider() const;
void set_provider(Ref<VoxelProvider> 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 <typename A>
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<bool> _lod_octree;
NodePath _viewer_path;
Ref<VoxelProvider> _provider;
VoxelProviderThread *_provider_thread = nullptr;
VoxelMeshUpdater *_block_updater = nullptr;
std::vector<VoxelMeshUpdater::OutputBlock> _blocks_pending_main_thread_update;
Ref<Material> _material;
// Each LOD works in a set of coordinates spanning 2x more voxels the higher their index is
struct Lod {
Ref<VoxelMap> map;
Map<Vector3i, BlockState> block_states;
std::vector<Vector3i> blocks_pending_update;
// Reflects LodOctree but only in this LOD
// TODO Would be nice to use LodOctree directly!
Set<Vector3i> 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<Vector3i> blocks_to_load;
//std::vector<Vector3i> blocks_to_update;
};
Lod _lods[MAX_LOD];
};
VARIANT_ENUM_CAST(VoxelLodTerrain::BlockState)
#endif // VOXEL_LOD_TERRAIN_HPP

View File

@ -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<VoxelBuffer> 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;

View File

@ -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<Vector3i, VoxelBlock *, Vector3iHasher> _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

View File

@ -1,5 +1,6 @@
#include "voxel_mesh_updater.h"
#include "../util/utility.h"
#include "voxel_lod_terrain.h"
#include <core/os/os.h>
VoxelMeshUpdater::VoxelMeshUpdater(Ref<VoxelLibrary> 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;

View File

@ -11,9 +11,12 @@
class VoxelMeshUpdater {
public:
static const int MAX_LOD = 32; // Like VoxelLodTerrain
struct InputBlock {
Ref<VoxelBuffer> 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<Vector3i, int, Vector3iHasher> _block_indexes;
HashMap<Vector3i, int, Vector3iHasher> _block_indexes[MAX_LOD];
bool _needs_sort;
Output _shared_output;

View File

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

View File

@ -34,7 +34,8 @@ public:
struct EmergeOutput {
Ref<VoxelBuffer> voxels;
Vector3i origin_in_voxels;
Vector3i origin_in_voxels; // TODO Remove this, redundant now
Vector3i block_position;
int lod = 0;
};

View File

@ -22,6 +22,18 @@ void shift_up(Vector<T> &v, int pos) {
v.resize(remaining);
}
template <typename T>
void shift_up(std::vector<T> &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 <typename T>