1468 lines
59 KiB
C++
1468 lines
59 KiB
C++
#include "voxel_lod_terrain_update_task.h"
|
|
#include "../../server/generate_block_task.h"
|
|
#include "../../server/load_block_data_task.h"
|
|
#include "../../server/mesh_block_task.h"
|
|
#include "../../server/save_block_data_task.h"
|
|
#include "../../server/voxel_server.h"
|
|
#include "../../util/container_funcs.h"
|
|
#include "../../util/math/conv.h"
|
|
#include "../../util/profiling.h"
|
|
#include "../../util/profiling_clock.h"
|
|
#include "../../util/string_funcs.h"
|
|
|
|
namespace zylann::voxel {
|
|
|
|
void VoxelLodTerrainUpdateTask::flush_pending_lod_edits(VoxelLodTerrainUpdateData::State &state, VoxelDataLodMap &data,
|
|
Ref<VoxelGenerator> generator, bool full_load_mode, const int mesh_block_size) {
|
|
ZN_PROFILE_SCOPE();
|
|
// Propagates edits performed so far to other LODs.
|
|
// These LODs must be currently in memory, otherwise terrain data will miss it.
|
|
// This is currently ensured by the fact we load blocks in a "pyramidal" way,
|
|
// i.e there is no way for a block to be loaded if its parent LOD isn't loaded already.
|
|
// In the future we may implement storing of edits to be applied later if blocks can't be found.
|
|
|
|
const int data_block_size = data.lods[0].map.get_block_size();
|
|
const int data_block_size_po2 = data.lods[0].map.get_block_size_pow2();
|
|
const int data_to_mesh_factor = mesh_block_size / data_block_size;
|
|
const unsigned int lod_count = data.lod_count;
|
|
|
|
static thread_local FixedArray<std::vector<Vector3i>, constants::MAX_LOD> tls_blocks_to_process_per_lod;
|
|
|
|
// Make sure LOD0 gets updates even if _lod_count is 1
|
|
VoxelLodTerrainUpdateData::Lod &lod0 = state.lods[0];
|
|
{
|
|
// Consume scheduled positions from LOD0
|
|
std::vector<Vector3i> &dst_lod0 = tls_blocks_to_process_per_lod[0];
|
|
|
|
MutexLock lock(state.blocks_pending_lodding_lod0_mutex);
|
|
|
|
// Not sure if could just use `=`? What would std::vector do with capacity?
|
|
dst_lod0.resize(state.blocks_pending_lodding_lod0.size());
|
|
memcpy(dst_lod0.data(), state.blocks_pending_lodding_lod0.data(), dst_lod0.size() * sizeof(Vector3i));
|
|
|
|
// Make sure LOD0 has its list cleared, because in case there is only 1 LOD,
|
|
// the chain of updates will not be entered
|
|
state.blocks_pending_lodding_lod0.clear();
|
|
}
|
|
{
|
|
VoxelDataLodMap::Lod &data_lod0 = data.lods[0];
|
|
RWLockRead rlock(data_lod0.map_lock);
|
|
|
|
std::vector<Vector3i> &blocks_pending_lodding_lod0 = tls_blocks_to_process_per_lod[0];
|
|
|
|
for (unsigned int i = 0; i < blocks_pending_lodding_lod0.size(); ++i) {
|
|
const Vector3i data_block_pos = blocks_pending_lodding_lod0[i];
|
|
VoxelDataBlock *data_block = data_lod0.map.get_block(data_block_pos);
|
|
ERR_CONTINUE(data_block == nullptr);
|
|
data_block->set_needs_lodding(false);
|
|
|
|
const Vector3i mesh_block_pos = math::floordiv(data_block_pos, data_to_mesh_factor);
|
|
auto mesh_block_it = lod0.mesh_map_state.map.find(mesh_block_pos);
|
|
if (mesh_block_it != lod0.mesh_map_state.map.end()) {
|
|
// If a mesh exists here, it will need an update.
|
|
// If there is no mesh, it will probably get created later when we come closer to it
|
|
schedule_mesh_update(mesh_block_it->second, mesh_block_pos, lod0.blocks_pending_update);
|
|
}
|
|
}
|
|
}
|
|
|
|
const int half_bs = data_block_size >> 1;
|
|
|
|
// Process downscales upwards in pairs of consecutive LODs.
|
|
// This ensures we don't process multiple times the same blocks.
|
|
// Only LOD0 is editable at the moment, so we'll downscale from there
|
|
for (uint8_t dst_lod_index = 1; dst_lod_index < lod_count; ++dst_lod_index) {
|
|
const uint8_t src_lod_index = dst_lod_index - 1;
|
|
std::vector<Vector3i> &src_lod_blocks_to_process = tls_blocks_to_process_per_lod[src_lod_index];
|
|
std::vector<Vector3i> &dst_lod_blocks_to_process = tls_blocks_to_process_per_lod[dst_lod_index];
|
|
|
|
VoxelLodTerrainUpdateData::Lod &dst_lod = state.lods[dst_lod_index];
|
|
|
|
VoxelDataLodMap::Lod &src_data_lod = data.lods[src_lod_index];
|
|
RWLockRead rlock(src_data_lod.map_lock);
|
|
|
|
VoxelDataLodMap::Lod &dst_data_lod = data.lods[dst_lod_index];
|
|
// TODO Could take long locking this, we may generate things first and assign to the map at the end.
|
|
// Besides, in per-block streaming mode, it is not needed because blocks are supposed to be present
|
|
RWLockRead wlock(dst_data_lod.map_lock);
|
|
|
|
for (unsigned int i = 0; i < src_lod_blocks_to_process.size(); ++i) {
|
|
const Vector3i src_bpos = src_lod_blocks_to_process[i];
|
|
const Vector3i dst_bpos = src_bpos >> 1;
|
|
|
|
VoxelDataBlock *src_block = src_data_lod.map.get_block(src_bpos);
|
|
VoxelDataBlock *dst_block = dst_data_lod.map.get_block(dst_bpos);
|
|
|
|
src_block->set_needs_lodding(false);
|
|
|
|
if (dst_block == nullptr) {
|
|
if (full_load_mode) {
|
|
// TODO Doing this on the main thread can be very demanding and cause a stall.
|
|
// We should find a way to make it asynchronous, not need mips, or not edit outside viewers area.
|
|
std::shared_ptr<VoxelBufferInternal> voxels = make_shared_instance<VoxelBufferInternal>();
|
|
voxels->create(Vector3iUtil::create(data_block_size));
|
|
if (generator.is_valid()) {
|
|
ZN_PROFILE_SCOPE_NAMED("Generate");
|
|
VoxelGenerator::VoxelQueryData q{ //
|
|
*voxels, //
|
|
dst_bpos << (dst_lod_index + data_block_size_po2), //
|
|
dst_lod_index
|
|
};
|
|
generator->generate_block(q);
|
|
}
|
|
dst_block = dst_data_lod.map.set_block_buffer(dst_bpos, voxels, true);
|
|
|
|
} else {
|
|
ERR_PRINT(String("Destination block {0} not found when cascading edits on LOD {1}")
|
|
.format(varray(dst_bpos, dst_lod_index)));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// The block and its lower LODs are expected to be available.
|
|
// Otherwise it means the function was called too late
|
|
CRASH_COND(src_block == nullptr);
|
|
//CRASH_COND(dst_block == nullptr);
|
|
|
|
{
|
|
const Vector3i mesh_block_pos = math::floordiv(dst_bpos, data_to_mesh_factor);
|
|
auto mesh_block_it = dst_lod.mesh_map_state.map.find(mesh_block_pos);
|
|
if (mesh_block_it != dst_lod.mesh_map_state.map.end()) {
|
|
schedule_mesh_update(mesh_block_it->second, mesh_block_pos, dst_lod.blocks_pending_update);
|
|
}
|
|
// If there is no mesh, it will probably get created later when we come closer to it
|
|
}
|
|
|
|
dst_block->set_modified(true);
|
|
|
|
if (dst_lod_index != lod_count - 1 && !dst_block->get_needs_lodding()) {
|
|
dst_block->set_needs_lodding(true);
|
|
dst_lod_blocks_to_process.push_back(dst_bpos);
|
|
}
|
|
|
|
const Vector3i rel = src_bpos - (dst_bpos << 1);
|
|
|
|
// Update lower LOD
|
|
// This must always be done after an edit before it gets saved, otherwise LODs won't match and it will look
|
|
// ugly.
|
|
// TODO Optimization: try to narrow to edited region instead of taking whole block
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Downscale");
|
|
RWLockWrite lock(src_block->get_voxels().get_lock());
|
|
src_block->get_voxels().downscale_to(
|
|
dst_block->get_voxels(), Vector3i(), src_block->get_voxels_const().get_size(), rel * half_bs);
|
|
}
|
|
}
|
|
|
|
src_lod_blocks_to_process.clear();
|
|
// No need to clear the last list because we never add blocks to it
|
|
}
|
|
|
|
// uint64_t time_spent = profiling_clock.restart();
|
|
// if (time_spent > 10) {
|
|
// print_line(String("Took {0} us to update lods").format(varray(time_spent)));
|
|
// }
|
|
}
|
|
|
|
struct BeforeUnloadDataAction {
|
|
std::vector<VoxelLodTerrainUpdateData::BlockToSave> &blocks_to_save;
|
|
bool save;
|
|
|
|
void operator()(VoxelDataBlock &block) {
|
|
// Save if modified
|
|
// TODO Don't ask for save if the stream doesn't support it!
|
|
if (save && block.is_modified()) {
|
|
//print_line(String("Scheduling save for block {0}").format(varray(block->position.to_vec3())));
|
|
VoxelLodTerrainUpdateData::BlockToSave b;
|
|
// We don't copy since the block will be unloaded anyways
|
|
b.voxels = block.get_voxels_shared();
|
|
b.position = block.position;
|
|
b.lod = block.lod_index;
|
|
blocks_to_save.push_back(b);
|
|
}
|
|
}
|
|
};
|
|
|
|
static void unload_data_block_no_lock(VoxelLodTerrainUpdateData::Lod &lod, VoxelDataLodMap::Lod &data_lod,
|
|
Vector3i block_pos, std::vector<VoxelLodTerrainUpdateData::BlockToSave> &blocks_to_save, bool can_save) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
data_lod.map.remove_block(block_pos, BeforeUnloadDataAction{ blocks_to_save, can_save });
|
|
|
|
//print_line(String("Unloading data block {0} lod {1}").format(varray(block_pos.to_vec3(), lod_index)));
|
|
MutexLock lock(lod.loading_blocks_mutex);
|
|
lod.loading_blocks.erase(block_pos);
|
|
|
|
// if (_instancer != nullptr) {
|
|
// _instancer->on_block_exit(block_pos, lod_index);
|
|
// }
|
|
|
|
// 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.
|
|
}
|
|
|
|
static void process_unload_data_blocks_sliding_box(VoxelLodTerrainUpdateData::State &state, VoxelDataLodMap &data,
|
|
Vector3 p_viewer_pos, std::vector<VoxelLodTerrainUpdateData::BlockToSave> &blocks_to_save, bool can_save,
|
|
const VoxelLodTerrainUpdateData::Settings &settings) {
|
|
ZN_PROFILE_SCOPE_NAMED("Sliding box data unload");
|
|
// TODO Could it actually be enough to have a rolling update on all blocks?
|
|
|
|
// This should be the same distance relatively to each LOD
|
|
const int data_block_size = data.lods[0].map.get_block_size();
|
|
const int data_block_size_po2 = data.lods[0].map.get_block_size_pow2();
|
|
const int data_block_region_extent =
|
|
VoxelServer::get_octree_lod_block_region_extent(settings.lod_distance, data_block_size);
|
|
|
|
const int mesh_block_size = 1 << settings.mesh_block_size_po2;
|
|
|
|
const int lod_count = data.lod_count;
|
|
|
|
// Ignore largest lod because it can extend a little beyond due to the view distance setting.
|
|
// Instead, those blocks are unloaded by the octree forest management.
|
|
// Iterating from big to small LOD so we can exit earlier if bounds don't intersect.
|
|
for (int lod_index = lod_count - 2; lod_index >= 0; --lod_index) {
|
|
ZN_PROFILE_SCOPE();
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.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.
|
|
|
|
const unsigned int block_size_po2 = data_block_size_po2 + lod_index;
|
|
const Vector3i viewer_block_pos_within_lod =
|
|
VoxelDataMap::voxel_to_block_b(math::floor_to_int(p_viewer_pos), block_size_po2);
|
|
|
|
const Box3i bounds_in_blocks = Box3i( //
|
|
settings.bounds_in_voxels.pos >> block_size_po2, //
|
|
settings.bounds_in_voxels.size >> block_size_po2);
|
|
|
|
const Box3i new_box =
|
|
Box3i::from_center_extents(viewer_block_pos_within_lod, Vector3iUtil::create(data_block_region_extent));
|
|
const Box3i prev_box = Box3i::from_center_extents(
|
|
lod.last_viewer_data_block_pos, Vector3iUtil::create(lod.last_view_distance_data_blocks));
|
|
|
|
if (!new_box.intersects(bounds_in_blocks) && !prev_box.intersects(bounds_in_blocks)) {
|
|
// If this box doesn't intersect either now or before, there is no chance a smaller one will
|
|
break;
|
|
}
|
|
|
|
// Eliminate pending blocks that aren't needed
|
|
|
|
if (prev_box != new_box) {
|
|
ZN_PROFILE_SCOPE_NAMED("Unload data");
|
|
VoxelDataLodMap::Lod &data_lod = data.lods[lod_index];
|
|
RWLockWrite wlock(data_lod.map_lock);
|
|
prev_box.difference(new_box, [&lod, &data_lod, &blocks_to_save, can_save](Box3i out_of_range_box) {
|
|
out_of_range_box.for_each_cell([&lod, &data_lod, &blocks_to_save, can_save](Vector3i pos) {
|
|
//print_line(String("Immerge {0}").format(varray(pos.to_vec3())));
|
|
unload_data_block_no_lock(lod, data_lod, pos, blocks_to_save, can_save);
|
|
});
|
|
});
|
|
}
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Cancel updates");
|
|
// Cancel block updates that are not within the padded region
|
|
// (since neighbors are always required to remesh)
|
|
|
|
const Box3i padded_new_box = new_box.padded(-1);
|
|
Box3i mesh_box;
|
|
if (mesh_block_size > data_block_size) {
|
|
const int factor = mesh_block_size / data_block_size;
|
|
mesh_box = padded_new_box.downscaled_inner(factor);
|
|
} else {
|
|
mesh_box = padded_new_box;
|
|
}
|
|
|
|
unordered_remove_if(lod.blocks_pending_update, [&lod, mesh_box](Vector3i bpos) {
|
|
if (mesh_box.contains(bpos)) {
|
|
return false;
|
|
} else {
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(bpos);
|
|
if (mesh_block_it != lod.mesh_map_state.map.end()) {
|
|
mesh_block_it->second.state = VoxelLodTerrainUpdateData::MESH_NEED_UPDATE;
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
lod.last_viewer_data_block_pos = viewer_block_pos_within_lod;
|
|
lod.last_view_distance_data_blocks = data_block_region_extent;
|
|
}
|
|
}
|
|
|
|
static void process_unload_mesh_blocks_sliding_box(VoxelLodTerrainUpdateData::State &state, Vector3 p_viewer_pos,
|
|
const VoxelLodTerrainUpdateData::Settings &settings) {
|
|
ZN_PROFILE_SCOPE_NAMED("Sliding box mesh unload");
|
|
// TODO Could it actually be enough to have a rolling update on all blocks?
|
|
|
|
// This should be the same distance relatively to each LOD
|
|
const int mesh_block_size_po2 = settings.mesh_block_size_po2;
|
|
const int mesh_block_size = 1 << mesh_block_size_po2;
|
|
const int mesh_block_region_extent =
|
|
VoxelServer::get_octree_lod_block_region_extent(settings.lod_distance, mesh_block_size);
|
|
|
|
// Ignore largest lod because it can extend a little beyond due to the view distance setting.
|
|
// Instead, those blocks are unloaded by the octree forest management.
|
|
// Iterating from big to small LOD so we can exit earlier if bounds don't intersect.
|
|
for (int lod_index = settings.lod_count - 2; lod_index >= 0; --lod_index) {
|
|
ZN_PROFILE_SCOPE();
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
unsigned int block_size_po2 = mesh_block_size_po2 + lod_index;
|
|
const Vector3i viewer_block_pos_within_lod = math::floor_to_int(p_viewer_pos) >> block_size_po2;
|
|
|
|
const Box3i bounds_in_blocks = Box3i( //
|
|
settings.bounds_in_voxels.pos >> block_size_po2, //
|
|
settings.bounds_in_voxels.size >> block_size_po2);
|
|
|
|
const Box3i new_box =
|
|
Box3i::from_center_extents(viewer_block_pos_within_lod, Vector3iUtil::create(mesh_block_region_extent));
|
|
const Box3i prev_box = Box3i::from_center_extents(
|
|
lod.last_viewer_mesh_block_pos, Vector3iUtil::create(lod.last_view_distance_mesh_blocks));
|
|
|
|
if (!new_box.intersects(bounds_in_blocks) && !prev_box.intersects(bounds_in_blocks)) {
|
|
// If this box doesn't intersect either now or before, there is no chance a smaller one will
|
|
break;
|
|
}
|
|
|
|
// Eliminate pending blocks that aren't needed
|
|
|
|
if (prev_box != new_box) {
|
|
ZN_PROFILE_SCOPE_NAMED("Unload meshes");
|
|
RWLockWrite wlock(lod.mesh_map_state.map_lock);
|
|
prev_box.difference(new_box, [&lod](Box3i out_of_range_box) {
|
|
out_of_range_box.for_each_cell([&lod](Vector3i pos) {
|
|
//print_line(String("Immerge {0}").format(varray(pos.to_vec3())));
|
|
//unload_mesh_block(pos, lod_index);
|
|
lod.mesh_map_state.map.erase(pos);
|
|
lod.mesh_blocks_to_unload.push_back(pos);
|
|
});
|
|
});
|
|
}
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Cancel updates");
|
|
// Cancel block updates that are not within the new region
|
|
unordered_remove_if(lod.blocks_pending_update, [new_box](Vector3i bpos) { //
|
|
return !new_box.contains(bpos);
|
|
});
|
|
}
|
|
|
|
lod.last_viewer_mesh_block_pos = viewer_block_pos_within_lod;
|
|
lod.last_view_distance_mesh_blocks = mesh_block_region_extent;
|
|
}
|
|
}
|
|
|
|
void process_octrees_sliding_box(VoxelLodTerrainUpdateData::State &state, Vector3 p_viewer_pos,
|
|
const VoxelLodTerrainUpdateData::Settings &settings) {
|
|
ZN_PROFILE_SCOPE_NAMED("Sliding box octrees");
|
|
// TODO Investigate if multi-octree can produce cracks in the terrain (so far I haven't noticed)
|
|
|
|
const unsigned int mesh_block_size_po2 = settings.mesh_block_size_po2;
|
|
const unsigned int octree_size_po2 = LodOctree::get_octree_size_po2(mesh_block_size_po2, settings.lod_count);
|
|
const unsigned int octree_size = 1 << octree_size_po2;
|
|
const unsigned int octree_region_extent = 1 + settings.view_distance_voxels / (1 << octree_size_po2);
|
|
|
|
const Vector3i viewer_octree_pos =
|
|
(math::floor_to_int(p_viewer_pos) + Vector3iUtil::create(octree_size / 2)) >> octree_size_po2;
|
|
|
|
const Box3i bounds_in_octrees = settings.bounds_in_voxels.downscaled(octree_size);
|
|
|
|
const Box3i new_box = Box3i::from_center_extents(viewer_octree_pos, Vector3iUtil::create(octree_region_extent))
|
|
.clipped(bounds_in_octrees);
|
|
const Box3i prev_box = state.last_octree_region_box;
|
|
|
|
if (new_box != prev_box) {
|
|
struct CleanOctreeAction {
|
|
VoxelLodTerrainUpdateData::State &state;
|
|
Vector3i block_offset_lod0;
|
|
|
|
void operator()(Vector3i node_pos, unsigned int lod_index) {
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
|
|
|
auto block_it = lod.mesh_map_state.map.find(bpos);
|
|
if (block_it != lod.mesh_map_state.map.end()) {
|
|
lod.mesh_blocks_to_deactivate.push_back(bpos);
|
|
block_it->second.active = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
struct ExitAction {
|
|
VoxelLodTerrainUpdateData::State &state;
|
|
unsigned int lod_count;
|
|
|
|
void operator()(const Vector3i &pos) {
|
|
Map<Vector3i, VoxelLodTerrainUpdateData::OctreeItem>::Element *E = state.lod_octrees.find(pos);
|
|
if (E == nullptr) {
|
|
return;
|
|
}
|
|
|
|
VoxelLodTerrainUpdateData::OctreeItem &item = E->value();
|
|
const Vector3i block_pos_maxlod = E->key();
|
|
|
|
const unsigned int last_lod_index = lod_count - 1;
|
|
|
|
// We just drop the octree and hide blocks it was considering as visible.
|
|
// Normally such octrees shouldn't bee too deep as they will likely be at the edge
|
|
// of the loaded area, unless the player teleported far away.
|
|
CleanOctreeAction a{ state, block_pos_maxlod << last_lod_index };
|
|
item.octree.clear(a);
|
|
|
|
state.lod_octrees.erase(E);
|
|
|
|
// Unload last lod from here, as it may extend a bit further than the others.
|
|
// Other LODs are unloaded earlier using a sliding region.
|
|
VoxelLodTerrainUpdateData::Lod &last_lod = state.lods[last_lod_index];
|
|
last_lod.mesh_map_state.map.erase(pos);
|
|
last_lod.mesh_blocks_to_unload.push_back(pos);
|
|
}
|
|
};
|
|
|
|
struct EnterAction {
|
|
VoxelLodTerrainUpdateData::State &state;
|
|
unsigned int lod_count;
|
|
|
|
void operator()(const Vector3i &pos) {
|
|
// That's a new cell we are entering, shouldn't be anything there
|
|
CRASH_COND(state.lod_octrees.has(pos));
|
|
|
|
// Create new octree
|
|
// TODO Use ObjectPool to store them, deletion won't be cheap
|
|
Map<Vector3i, VoxelLodTerrainUpdateData::OctreeItem>::Element *E =
|
|
state.lod_octrees.insert(pos, VoxelLodTerrainUpdateData::OctreeItem());
|
|
CRASH_COND(E == nullptr);
|
|
VoxelLodTerrainUpdateData::OctreeItem &item = E->value();
|
|
LodOctree::NoDestroyAction nda;
|
|
item.octree.create(lod_count, nda);
|
|
}
|
|
};
|
|
|
|
ExitAction exit_action{ state, settings.lod_count };
|
|
EnterAction enter_action{ state, settings.lod_count };
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Unload octrees");
|
|
|
|
const unsigned int last_lod_index = settings.lod_count - 1;
|
|
VoxelLodTerrainUpdateData::Lod &last_lod = state.lods[last_lod_index];
|
|
RWLockWrite wlock(last_lod.mesh_map_state.map_lock);
|
|
|
|
prev_box.difference(new_box, [exit_action](Box3i out_of_range_box) { //
|
|
out_of_range_box.for_each_cell(exit_action);
|
|
});
|
|
}
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Load octrees");
|
|
new_box.difference(prev_box, [enter_action](Box3i box_to_load) { //
|
|
box_to_load.for_each_cell(enter_action);
|
|
});
|
|
}
|
|
|
|
state.force_update_octrees_next_update = true;
|
|
}
|
|
|
|
state.last_octree_region_box = new_box;
|
|
}
|
|
|
|
static void add_transition_update(VoxelLodTerrainUpdateData::MeshBlockState &block, Vector3i bpos,
|
|
std::vector<Vector3i> &blocks_pending_transition_update) {
|
|
if (!block.pending_transition_update) {
|
|
blocks_pending_transition_update.push_back(bpos);
|
|
block.pending_transition_update = true;
|
|
}
|
|
}
|
|
|
|
static void add_transition_updates_around(VoxelLodTerrainUpdateData::Lod &lod, Vector3i block_pos,
|
|
std::vector<Vector3i> &blocks_pending_transition_update) {
|
|
//
|
|
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
|
|
const Vector3i npos = block_pos + Cube::g_side_normals[dir];
|
|
auto nblock_it = lod.mesh_map_state.map.find(npos);
|
|
|
|
if (nblock_it != lod.mesh_map_state.map.end()) {
|
|
add_transition_update(nblock_it->second, npos, blocks_pending_transition_update);
|
|
}
|
|
}
|
|
// TODO If a block appears at lod, neighbor blocks at lod-1 need to be updated.
|
|
// or maybe get_transition_mask needs a different approach that also looks at higher lods?
|
|
}
|
|
|
|
void try_schedule_loading_with_neighbors_no_lock(VoxelLodTerrainUpdateData::State &state, VoxelDataLodMap &data,
|
|
const Vector3i &p_data_block_pos, uint8_t lod_index,
|
|
std::vector<VoxelLodTerrainUpdateData::BlockLocation> &blocks_to_load, const Box3i &bounds_in_voxels) {
|
|
//
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
const VoxelDataLodMap::Lod &data_lod = data.lods[lod_index];
|
|
|
|
const int p = data_lod.map.get_block_size_pow2() + lod_index;
|
|
|
|
const int bound_min_x = bounds_in_voxels.pos.x >> p;
|
|
const int bound_min_y = bounds_in_voxels.pos.y >> p;
|
|
const int bound_min_z = bounds_in_voxels.pos.z >> p;
|
|
const int bound_max_x = (bounds_in_voxels.pos.x + bounds_in_voxels.size.x) >> p;
|
|
const int bound_max_y = (bounds_in_voxels.pos.y + bounds_in_voxels.size.y) >> p;
|
|
const int bound_max_z = (bounds_in_voxels.pos.z + bounds_in_voxels.size.z) >> p;
|
|
|
|
const int min_x = math::max(p_data_block_pos.x - 1, bound_min_x);
|
|
const int min_y = math::max(p_data_block_pos.y - 1, bound_min_y);
|
|
const int min_z = math::max(p_data_block_pos.z - 1, bound_min_z);
|
|
const int max_x = math::min(p_data_block_pos.x + 2, bound_max_x);
|
|
const int max_y = math::min(p_data_block_pos.y + 2, bound_max_y);
|
|
const int max_z = math::min(p_data_block_pos.z + 2, bound_max_z);
|
|
|
|
// Not locking here, we assume it's done by the caller
|
|
//RWLockRead rlock(data_lod.map_lock);
|
|
|
|
Vector3i bpos;
|
|
MutexLock lock(lod.loading_blocks_mutex);
|
|
for (bpos.y = min_y; bpos.y < max_y; ++bpos.y) {
|
|
for (bpos.z = min_z; bpos.z < max_z; ++bpos.z) {
|
|
for (bpos.x = min_x; bpos.x < max_x; ++bpos.x) {
|
|
const VoxelDataBlock *block = data_lod.map.get_block(bpos);
|
|
|
|
if (block == nullptr) {
|
|
if (!lod.has_loading_block(bpos)) {
|
|
blocks_to_load.push_back({ bpos, lod_index });
|
|
lod.loading_blocks.insert(bpos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
inline bool check_block_sizes(int data_block_size, int mesh_block_size) {
|
|
return (data_block_size == 16 || data_block_size == 32) && (mesh_block_size == 16 || mesh_block_size == 32) &&
|
|
mesh_block_size >= data_block_size;
|
|
}
|
|
|
|
bool check_block_mesh_updated(VoxelLodTerrainUpdateData::State &state, VoxelDataLodMap &data,
|
|
VoxelLodTerrainUpdateData::MeshBlockState &mesh_block, Vector3i mesh_block_pos, uint8_t lod_index,
|
|
std::vector<VoxelLodTerrainUpdateData::BlockLocation> &blocks_to_load,
|
|
const VoxelLodTerrainUpdateData::Settings &settings) {
|
|
//ZN_PROFILE_SCOPE();
|
|
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
const VoxelLodTerrainUpdateData::MeshState mesh_state = mesh_block.state;
|
|
|
|
switch (mesh_state) {
|
|
case VoxelLodTerrainUpdateData::MESH_NEVER_UPDATED:
|
|
case VoxelLodTerrainUpdateData::MESH_NEED_UPDATE: {
|
|
const int mesh_block_size = 1 << settings.mesh_block_size_po2;
|
|
const int data_block_size = data.lods[0].map.get_block_size();
|
|
#ifdef DEBUG_ENABLED
|
|
ERR_FAIL_COND_V(!check_block_sizes(data_block_size, mesh_block_size), false);
|
|
#endif
|
|
// Find data block neighbors positions
|
|
const int factor = mesh_block_size / data_block_size;
|
|
const Vector3i data_block_pos0 = factor * mesh_block_pos;
|
|
const Box3i data_box(data_block_pos0 - Vector3i(1, 1, 1), Vector3iUtil::create(factor) + Vector3i(2, 2, 2));
|
|
const Box3i bounds = settings.bounds_in_voxels.downscaled(data_block_size);
|
|
FixedArray<Vector3i, 56> neighbor_positions;
|
|
unsigned int neighbor_positions_count = 0;
|
|
data_box.for_inner_outline([bounds, &neighbor_positions, &neighbor_positions_count](Vector3i pos) {
|
|
if (bounds.contains(pos)) {
|
|
neighbor_positions[neighbor_positions_count] = pos;
|
|
++neighbor_positions_count;
|
|
}
|
|
});
|
|
|
|
bool surrounded = true;
|
|
if (settings.full_load_mode == false) {
|
|
const VoxelDataLodMap::Lod &data_lod = data.lods[lod_index];
|
|
// Check if neighbors are loaded
|
|
RWLockRead rlock(data_lod.map_lock);
|
|
// TODO Optimization: could put in a temp vector and insert in one go after the loop?
|
|
MutexLock lock(lod.loading_blocks_mutex);
|
|
for (unsigned int i = 0; i < neighbor_positions_count; ++i) {
|
|
const Vector3i npos = neighbor_positions[i];
|
|
if (!data_lod.map.has_block(npos)) {
|
|
// That neighbor is not loaded
|
|
surrounded = false;
|
|
if (!lod.has_loading_block(npos)) {
|
|
// Schedule loading for that neighbor
|
|
blocks_to_load.push_back({ npos, lod_index });
|
|
lod.loading_blocks.insert(npos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (surrounded) {
|
|
lod.blocks_pending_update.push_back(mesh_block_pos);
|
|
mesh_block.state = VoxelLodTerrainUpdateData::MESH_UPDATE_NOT_SENT;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
case VoxelLodTerrainUpdateData::MESH_UPDATE_NOT_SENT:
|
|
case VoxelLodTerrainUpdateData::MESH_UPDATE_SENT:
|
|
return false;
|
|
|
|
case VoxelLodTerrainUpdateData::MESH_UP_TO_DATE:
|
|
return true;
|
|
|
|
default:
|
|
CRASH_NOW();
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VoxelLodTerrainUpdateData::MeshBlockState &insert_new(
|
|
std::unordered_map<Vector3i, VoxelLodTerrainUpdateData::MeshBlockState> &mesh_map, Vector3i pos) {
|
|
#ifdef DEBUG_ENABLED
|
|
// We got here because the map didn't contain the element. If it did contain it already, that's a bug.
|
|
static VoxelLodTerrainUpdateData::MeshBlockState s_default;
|
|
ERR_FAIL_COND_V(mesh_map.find(pos) != mesh_map.end(), s_default);
|
|
#endif
|
|
// C++ standard says if the element is not present, it will be default-constructed.
|
|
// So here is how to insert a default, non-movable struct into an unordered_map.
|
|
// https://stackoverflow.com/questions/22229773/map-unordered-map-with-non-movable-default-constructible-value-type
|
|
VoxelLodTerrainUpdateData::MeshBlockState &block = mesh_map[pos];
|
|
|
|
// This approach doesn't compile, had to workaround with the writing [] operator.
|
|
/*
|
|
auto p = lod.mesh_map_state.map.emplace(pos, VoxelLodTerrainUpdateData::MeshBlockState());
|
|
// We got here because the map didn't contain the element. If it did contain it already, that's a bug.
|
|
CRASH_COND(p.second == false);
|
|
*/
|
|
|
|
return block;
|
|
}
|
|
|
|
static bool check_block_loaded_and_meshed(VoxelLodTerrainUpdateData::State &state,
|
|
const VoxelLodTerrainUpdateData::Settings &settings, VoxelDataLodMap &data, const Vector3i &p_mesh_block_pos,
|
|
uint8_t lod_index, std::vector<VoxelLodTerrainUpdateData::BlockLocation> &blocks_to_load) {
|
|
//
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
const int mesh_block_size = 1 << settings.mesh_block_size_po2;
|
|
const int data_block_size = data.lods[0].map.get_block_size();
|
|
|
|
#ifdef DEBUG_ENABLED
|
|
ERR_FAIL_COND_V(!check_block_sizes(data_block_size, mesh_block_size), false);
|
|
#endif
|
|
|
|
if (settings.full_load_mode == false) {
|
|
VoxelDataLodMap::Lod &data_lod = data.lods[lod_index];
|
|
|
|
if (mesh_block_size > data_block_size) {
|
|
const int factor = mesh_block_size / data_block_size;
|
|
const Vector3i data_block_pos0 = p_mesh_block_pos * factor;
|
|
|
|
bool loaded = true;
|
|
|
|
RWLockRead rlock(data_lod.map_lock);
|
|
for (int z = 0; z < factor; ++z) {
|
|
for (int x = 0; x < factor; ++x) {
|
|
for (int y = 0; y < factor; ++y) {
|
|
const Vector3i data_block_pos(data_block_pos0 + Vector3i(x, y, z));
|
|
VoxelDataBlock *data_block = data_lod.map.get_block(data_block_pos);
|
|
|
|
if (data_block == nullptr) {
|
|
loaded = false;
|
|
// TODO This is quite lossy in this case, if we ask for 8 blocks in an octant
|
|
try_schedule_loading_with_neighbors_no_lock(
|
|
state, data, data_block_pos, lod_index, blocks_to_load, settings.bounds_in_voxels);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!loaded) {
|
|
return false;
|
|
}
|
|
|
|
} else if (mesh_block_size == data_block_size) {
|
|
const Vector3i data_block_pos = p_mesh_block_pos;
|
|
RWLockRead rlock(data_lod.map_lock);
|
|
VoxelDataBlock *block = data_lod.map.get_block(data_block_pos);
|
|
if (block == nullptr) {
|
|
try_schedule_loading_with_neighbors_no_lock(
|
|
state, data, data_block_pos, lod_index, blocks_to_load, settings.bounds_in_voxels);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
VoxelLodTerrainUpdateData::MeshBlockState *mesh_block = nullptr;
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(p_mesh_block_pos);
|
|
if (mesh_block_it == lod.mesh_map_state.map.end()) {
|
|
// If this ever becomes a source of contention with the main thread's `apply_mesh_update`,
|
|
// we could defer additions to the end of octree fitting.
|
|
RWLockWrite wlock(lod.mesh_map_state.map_lock);
|
|
mesh_block = &insert_new(lod.mesh_map_state.map, p_mesh_block_pos);
|
|
} else {
|
|
mesh_block = &mesh_block_it->second;
|
|
}
|
|
|
|
return check_block_mesh_updated(state, data, *mesh_block, p_mesh_block_pos, lod_index, blocks_to_load, settings);
|
|
}
|
|
|
|
uint8_t VoxelLodTerrainUpdateTask::get_transition_mask(
|
|
const VoxelLodTerrainUpdateData::State &state, Vector3i block_pos, int lod_index, unsigned int lod_count) {
|
|
uint8_t transition_mask = 0;
|
|
|
|
if (lod_index + 1 >= static_cast<int>(lod_count)) {
|
|
return transition_mask;
|
|
}
|
|
|
|
const VoxelLodTerrainUpdateData::Lod &lower_lod = state.lods[lod_index + 1];
|
|
const VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
const Vector3i lower_pos = block_pos >> 1;
|
|
const Vector3i upper_pos = block_pos << 1;
|
|
|
|
// Based on octree rules, and the fact it must have run before, check neighbor blocks of same LOD:
|
|
// If one is missing or not visible, it means either of the following:
|
|
// - The neighbor at lod+1 is visible or not loaded (there must be a transition)
|
|
// - The neighbor at lod-1 is visible (no transition)
|
|
|
|
uint8_t visible_neighbors_of_same_lod = 0;
|
|
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
|
|
Vector3i npos = block_pos + Cube::g_side_normals[dir];
|
|
|
|
auto nblock_it = lod.mesh_map_state.map.find(npos);
|
|
|
|
if (nblock_it != lod.mesh_map_state.map.end() && nblock_it->second.active) {
|
|
visible_neighbors_of_same_lod |= (1 << dir);
|
|
}
|
|
}
|
|
|
|
if (visible_neighbors_of_same_lod != 0b111111) {
|
|
// At least one neighbor isn't visible.
|
|
// Check for neighbors at different LOD (there can be only one kind on a given side)
|
|
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
|
|
int dir_mask = (1 << dir);
|
|
|
|
if (visible_neighbors_of_same_lod & dir_mask) {
|
|
continue;
|
|
}
|
|
|
|
const Vector3i side_normal = Cube::g_side_normals[dir];
|
|
const Vector3i lower_neighbor_pos = (block_pos + side_normal) >> 1;
|
|
|
|
if (lower_neighbor_pos != lower_pos) {
|
|
auto lower_neighbor_block_it = lower_lod.mesh_map_state.map.find(lower_neighbor_pos);
|
|
|
|
if (lower_neighbor_block_it != lower_lod.mesh_map_state.map.end() &&
|
|
lower_neighbor_block_it->second.active) {
|
|
// The block has a visible neighbor of lower LOD
|
|
transition_mask |= dir_mask;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (lod_index > 0) {
|
|
// Check upper LOD neighbors.
|
|
// There are always 4 on each side, checking any is enough
|
|
|
|
Vector3i upper_neighbor_pos = upper_pos;
|
|
for (unsigned int i = 0; i < Vector3iUtil::AXIS_COUNT; ++i) {
|
|
if (side_normal[i] == -1) {
|
|
--upper_neighbor_pos[i];
|
|
} else if (side_normal[i] == 1) {
|
|
upper_neighbor_pos[i] += 2;
|
|
}
|
|
}
|
|
|
|
const VoxelLodTerrainUpdateData::Lod &upper_lod = state.lods[lod_index - 1];
|
|
auto upper_neighbor_block_it = upper_lod.mesh_map_state.map.find(upper_neighbor_pos);
|
|
|
|
if (upper_neighbor_block_it == upper_lod.mesh_map_state.map.end() ||
|
|
upper_neighbor_block_it->second.active == false) {
|
|
// The block has no visible neighbor yet. World border? Assume lower LOD.
|
|
transition_mask |= dir_mask;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return transition_mask;
|
|
}
|
|
|
|
static void process_octrees_fitting(VoxelLodTerrainUpdateData::State &state,
|
|
const VoxelLodTerrainUpdateData::Settings &settings, VoxelDataLodMap &data, Vector3 p_viewer_pos,
|
|
std::vector<VoxelLodTerrainUpdateData::BlockLocation> &data_blocks_to_load) {
|
|
//
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
const int mesh_block_size = 1 << settings.mesh_block_size_po2;
|
|
const int octree_leaf_node_size = mesh_block_size;
|
|
|
|
const bool force_update_octrees = state.force_update_octrees_next_update;
|
|
state.force_update_octrees_next_update = false;
|
|
|
|
// Octrees may not need to update every frame under certain conditions
|
|
if (!state.had_blocked_octree_nodes_previous_update && !force_update_octrees &&
|
|
p_viewer_pos.distance_squared_to(Vector3(state.local_viewer_pos_previous_octree_update)) <
|
|
math::squared(octree_leaf_node_size / 2)) {
|
|
return;
|
|
}
|
|
|
|
state.local_viewer_pos_previous_octree_update = p_viewer_pos;
|
|
|
|
static thread_local FixedArray<std::vector<Vector3i>, constants::MAX_LOD>
|
|
tls_blocks_pending_transition_update_per_lod;
|
|
//static thread_local FixedArray<std::vector<Vector3i>, constants::MAX_LOD> tls_mesh_blocks_to_add_per_lod;
|
|
|
|
for (unsigned int i = 0; i < tls_blocks_pending_transition_update_per_lod.size(); ++i) {
|
|
CRASH_COND(!tls_blocks_pending_transition_update_per_lod[i].empty());
|
|
//CRASH_COND(!tls_mesh_blocks_to_add_per_lod[i].empty());
|
|
}
|
|
|
|
const float lod_distance_octree_space = settings.lod_distance / octree_leaf_node_size;
|
|
|
|
unsigned int blocked_octree_nodes = 0;
|
|
|
|
// TODO Maintain a vector to make iteration faster?
|
|
for (Map<Vector3i, VoxelLodTerrainUpdateData::OctreeItem>::Element *E = state.lod_octrees.front(); E;
|
|
E = E->next()) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
struct OctreeActions {
|
|
VoxelLodTerrainUpdateData::State &state;
|
|
const VoxelLodTerrainUpdateData::Settings &settings;
|
|
VoxelDataLodMap &data;
|
|
std::vector<VoxelLodTerrainUpdateData::BlockLocation> &data_blocks_to_load;
|
|
Vector3i block_offset_lod0;
|
|
unsigned int blocked_count = 0;
|
|
float lod_distance_octree_space;
|
|
Vector3 viewer_pos_octree_space;
|
|
|
|
void create_child(Vector3i node_pos, int lod_index, LodOctree::NodeData &data) {
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
const Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(bpos);
|
|
|
|
// Never show a child that hasn't been meshed, if we got here that would be a bug
|
|
CRASH_COND(mesh_block_it == lod.mesh_map_state.map.end());
|
|
CRASH_COND(mesh_block_it->second.state != VoxelLodTerrainUpdateData::MESH_UP_TO_DATE);
|
|
|
|
//self->set_mesh_block_active(*block, true);
|
|
lod.mesh_blocks_to_activate.push_back(bpos);
|
|
mesh_block_it->second.active = true;
|
|
add_transition_update(
|
|
mesh_block_it->second, bpos, tls_blocks_pending_transition_update_per_lod[lod_index]);
|
|
add_transition_updates_around(lod, bpos, tls_blocks_pending_transition_update_per_lod[lod_index]);
|
|
}
|
|
|
|
void destroy_child(Vector3i node_pos, int lod_index) {
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
const Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(bpos);
|
|
|
|
if (mesh_block_it != lod.mesh_map_state.map.end()) {
|
|
//self->set_mesh_block_active(*block, false);
|
|
mesh_block_it->second.active = false;
|
|
lod.mesh_blocks_to_deactivate.push_back(bpos);
|
|
add_transition_updates_around(lod, bpos, tls_blocks_pending_transition_update_per_lod[lod_index]);
|
|
}
|
|
}
|
|
|
|
void show_parent(Vector3i node_pos, int lod_index) {
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
Vector3i bpos = node_pos + (block_offset_lod0 >> lod_index);
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(bpos);
|
|
|
|
// If we teleport far away, the area we were in is going to merge,
|
|
// and blocks may have been unloaded completely.
|
|
// So in that case it's normal to not find any block.
|
|
// Otherwise, there must always be a visible parent in the end, unless the octree vanished.
|
|
if (mesh_block_it != lod.mesh_map_state.map.end() &&
|
|
mesh_block_it->second.state == VoxelLodTerrainUpdateData::MESH_UP_TO_DATE) {
|
|
//self->set_mesh_block_active(*block, true);
|
|
mesh_block_it->second.active = true;
|
|
lod.mesh_blocks_to_activate.push_back(bpos);
|
|
add_transition_update(
|
|
mesh_block_it->second, bpos, tls_blocks_pending_transition_update_per_lod[lod_index]);
|
|
add_transition_updates_around(lod, bpos, tls_blocks_pending_transition_update_per_lod[lod_index]);
|
|
}
|
|
}
|
|
|
|
void hide_parent(Vector3i node_pos, int lod_index) {
|
|
destroy_child(node_pos, lod_index); // Same
|
|
}
|
|
|
|
bool can_create_root(int lod_index) {
|
|
const Vector3i offset = block_offset_lod0 >> lod_index;
|
|
const bool can =
|
|
check_block_loaded_and_meshed(state, settings, data, offset, lod_index, data_blocks_to_load);
|
|
if (!can) {
|
|
++blocked_count;
|
|
}
|
|
return can;
|
|
}
|
|
|
|
bool can_split(Vector3i node_pos, int lod_index, LodOctree::NodeData &node_data) {
|
|
ZN_PROFILE_SCOPE();
|
|
if (!LodOctree::is_below_split_distance(
|
|
node_pos, lod_index, viewer_pos_octree_space, lod_distance_octree_space)) {
|
|
return false;
|
|
}
|
|
const int child_lod_index = lod_index - 1;
|
|
const Vector3i offset = block_offset_lod0 >> child_lod_index;
|
|
bool can = true;
|
|
|
|
// Can only subdivide if higher detail meshes are ready to be shown, otherwise it will produce holes
|
|
for (int i = 0; i < 8; ++i) {
|
|
// Get block pos local-to-region + convert to local-to-terrain
|
|
const Vector3i child_pos = LodOctree::get_child_position(node_pos, i) + offset;
|
|
// We have to ping ALL children, because the reason we are here is we want them loaded
|
|
can &= check_block_loaded_and_meshed(
|
|
state, settings, data, child_pos, child_lod_index, data_blocks_to_load);
|
|
}
|
|
|
|
// Can only subdivide if blocks of a higher LOD index are present around,
|
|
// otherwise it will cause cracks.
|
|
// Need to check meshes, not voxels?
|
|
// const int lod_index = child_lod_index + 1;
|
|
// if (lod_index < self->get_lod_count()) {
|
|
// const Vector3i parent_offset = block_offset_lod0 >> lod_index;
|
|
// const Lod &lod = self->_lods[lod_index];
|
|
// can &= self->is_block_surrounded(node_pos + parent_offset, lod_index, lod.map);
|
|
// }
|
|
|
|
if (!can) {
|
|
++blocked_count;
|
|
}
|
|
|
|
return can;
|
|
}
|
|
|
|
bool can_join(Vector3i node_pos, int parent_lod_index) {
|
|
ZN_PROFILE_SCOPE();
|
|
if (LodOctree::is_below_split_distance(
|
|
node_pos, parent_lod_index, viewer_pos_octree_space, lod_distance_octree_space)) {
|
|
return false;
|
|
}
|
|
// Can only unsubdivide if the parent mesh is ready
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[parent_lod_index];
|
|
|
|
Vector3i bpos = node_pos + (block_offset_lod0 >> parent_lod_index);
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(bpos);
|
|
|
|
if (mesh_block_it == lod.mesh_map_state.map.end()) {
|
|
// The block got unloaded. Exceptionally, we can join.
|
|
// There will always be a grand-parent because we never destroy them when they split,
|
|
// and we never create a child without creating a parent first.
|
|
return true;
|
|
}
|
|
|
|
// The block is loaded (?) but the mesh isn't up to date, we need to ping and wait.
|
|
const bool can = check_block_mesh_updated(
|
|
state, data, mesh_block_it->second, bpos, parent_lod_index, data_blocks_to_load, settings);
|
|
|
|
if (!can) {
|
|
++blocked_count;
|
|
}
|
|
|
|
return can;
|
|
}
|
|
};
|
|
|
|
const Vector3i block_pos_maxlod = E->key();
|
|
const Vector3i block_offset_lod0 = block_pos_maxlod << (settings.lod_count - 1);
|
|
const Vector3 relative_viewer_pos = p_viewer_pos - Vector3(mesh_block_size * block_offset_lod0);
|
|
|
|
OctreeActions octree_actions{ //
|
|
state, //
|
|
settings, //
|
|
data, //
|
|
data_blocks_to_load, //
|
|
block_offset_lod0, //
|
|
0, //
|
|
lod_distance_octree_space, //
|
|
relative_viewer_pos / octree_leaf_node_size
|
|
};
|
|
VoxelLodTerrainUpdateData::OctreeItem &item = E->value();
|
|
item.octree.update(octree_actions);
|
|
|
|
blocked_octree_nodes += octree_actions.blocked_count;
|
|
}
|
|
|
|
// Ideally, this stat should stabilize to zero.
|
|
// If not, something in block management prevents LODs from properly show up and should be fixed.
|
|
state.stats.blocked_lods = blocked_octree_nodes;
|
|
state.had_blocked_octree_nodes_previous_update = blocked_octree_nodes > 0;
|
|
|
|
{
|
|
// In theory, blocks containing no surface have no reason to need a transition mask,
|
|
// but when we get a new mesh update into a block that previously had no surface, we still need it.
|
|
|
|
ZN_PROFILE_SCOPE_NAMED("Transition masks");
|
|
for (unsigned int lod_index = 0; lod_index < tls_blocks_pending_transition_update_per_lod.size(); ++lod_index) {
|
|
std::vector<Vector3i> &blocks_pending_transition_update =
|
|
tls_blocks_pending_transition_update_per_lod[lod_index];
|
|
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
for (unsigned int i = 0; i < blocks_pending_transition_update.size(); ++i) {
|
|
const Vector3i bpos = blocks_pending_transition_update[i];
|
|
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(bpos);
|
|
CRASH_COND(mesh_block_it == lod.mesh_map_state.map.end());
|
|
VoxelLodTerrainUpdateData::MeshBlockState &mesh_block = mesh_block_it->second;
|
|
|
|
if (mesh_block.active) {
|
|
const uint8_t mask =
|
|
VoxelLodTerrainUpdateTask::get_transition_mask(state, bpos, lod_index, settings.lod_count);
|
|
mesh_block.transition_mask = mask;
|
|
lod.mesh_blocks_to_update_transitions.push_back(
|
|
VoxelLodTerrainUpdateData::TransitionUpdate{ bpos, mask });
|
|
}
|
|
|
|
mesh_block.pending_transition_update = false;
|
|
}
|
|
|
|
blocks_pending_transition_update.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
inline Vector3i get_block_center(Vector3i pos, int bs, int lod) {
|
|
return (pos << lod) * bs + Vector3iUtil::create(bs / 2);
|
|
}
|
|
|
|
static void init_sparse_octree_priority_dependency(PriorityDependency &dep, Vector3i block_position, uint8_t lod,
|
|
int data_block_size, std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data,
|
|
const Transform3D &volume_transform, float octree_lod_distance) {
|
|
//
|
|
const Vector3i voxel_pos = get_block_center(block_position, data_block_size, lod);
|
|
const float block_radius = (data_block_size << lod) / 2;
|
|
dep.shared = shared_viewers_data;
|
|
dep.world_position = volume_transform.xform(voxel_pos);
|
|
const float transformed_block_radius =
|
|
volume_transform.basis.xform(Vector3(block_radius, block_radius, block_radius)).length();
|
|
|
|
// Distance beyond which it is safe to drop a block without risking to block LOD subdivision.
|
|
// This does not depend on viewer's view distance, but on LOD precision instead.
|
|
// TODO Should `data_block_size` be used here? Should it be mesh_block_size instead?
|
|
dep.drop_distance_squared = math::squared(2.f * transformed_block_radius *
|
|
VoxelServer::get_octree_lod_block_region_extent(octree_lod_distance, data_block_size));
|
|
}
|
|
|
|
static void request_block_generate(uint32_t volume_id, unsigned int data_block_size,
|
|
std::shared_ptr<StreamingDependency> &stream_dependency, Vector3i block_pos, int lod,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, const Transform3D &volume_transform,
|
|
float lod_distance, std::shared_ptr<AsyncDependencyTracker> tracker, bool allow_drop,
|
|
BufferedTaskScheduler &task_scheduler) {
|
|
//
|
|
CRASH_COND(data_block_size > 255);
|
|
CRASH_COND(stream_dependency == nullptr);
|
|
|
|
// We should not have done this request in the first place if both stream and generator are null
|
|
ERR_FAIL_COND(stream_dependency->generator.is_null());
|
|
|
|
GenerateBlockTask *task = memnew(GenerateBlockTask);
|
|
task->volume_id = volume_id;
|
|
task->position = block_pos;
|
|
task->lod = lod;
|
|
task->block_size = data_block_size;
|
|
task->stream_dependency = stream_dependency;
|
|
task->tracker = tracker;
|
|
task->drop_beyond_max_distance = allow_drop;
|
|
|
|
init_sparse_octree_priority_dependency(task->priority_dependency, block_pos, lod, data_block_size,
|
|
shared_viewers_data, volume_transform, lod_distance);
|
|
|
|
task_scheduler.push_main_task(task);
|
|
}
|
|
|
|
static void request_block_load(uint32_t volume_id, unsigned int data_block_size,
|
|
std::shared_ptr<StreamingDependency> &stream_dependency, Vector3i block_pos, int lod, bool request_instances,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, const Transform3D &volume_transform,
|
|
float lod_distance, BufferedTaskScheduler &task_scheduler) {
|
|
//
|
|
CRASH_COND(data_block_size > 255);
|
|
CRASH_COND(stream_dependency == nullptr);
|
|
|
|
if (stream_dependency->stream.is_valid()) {
|
|
PriorityDependency priority_dependency;
|
|
init_sparse_octree_priority_dependency(priority_dependency, block_pos, lod, data_block_size,
|
|
shared_viewers_data, volume_transform, lod_distance);
|
|
|
|
LoadBlockDataTask *task = memnew(LoadBlockDataTask(
|
|
volume_id, block_pos, lod, data_block_size, request_instances, stream_dependency, priority_dependency));
|
|
|
|
task_scheduler.push_io_task(task);
|
|
|
|
} else {
|
|
// Directly generate the block without checking the stream.
|
|
request_block_generate(volume_id, data_block_size, stream_dependency, block_pos, lod, shared_viewers_data,
|
|
volume_transform, lod_distance, nullptr, true, task_scheduler);
|
|
}
|
|
}
|
|
|
|
static void send_block_data_requests(uint32_t volume_id,
|
|
Span<const VoxelLodTerrainUpdateData::BlockLocation> blocks_to_load,
|
|
std::shared_ptr<StreamingDependency> &stream_dependency,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, unsigned int data_block_size,
|
|
bool request_instances, const Transform3D &volume_transform, float lod_distance,
|
|
BufferedTaskScheduler &task_scheduler) {
|
|
//
|
|
for (unsigned int i = 0; i < blocks_to_load.size(); ++i) {
|
|
const VoxelLodTerrainUpdateData::BlockLocation loc = blocks_to_load[i];
|
|
request_block_load(volume_id, data_block_size, stream_dependency, loc.position, loc.lod, request_instances,
|
|
shared_viewers_data, volume_transform, lod_distance, task_scheduler);
|
|
}
|
|
}
|
|
|
|
static void request_voxel_block_save(uint32_t volume_id, std::shared_ptr<VoxelBufferInternal> &voxels,
|
|
Vector3i block_pos, int lod, std::shared_ptr<StreamingDependency> &stream_dependency,
|
|
unsigned int data_block_size, BufferedTaskScheduler &task_scheduler) {
|
|
//
|
|
CRASH_COND(stream_dependency == nullptr);
|
|
ERR_FAIL_COND(stream_dependency->stream.is_null());
|
|
|
|
SaveBlockDataTask *task =
|
|
memnew(SaveBlockDataTask(volume_id, block_pos, lod, data_block_size, voxels, stream_dependency));
|
|
|
|
// No priority data, saving doesnt need sorting
|
|
|
|
task_scheduler.push_io_task(task);
|
|
}
|
|
|
|
void VoxelLodTerrainUpdateTask::send_block_save_requests(uint32_t volume_id,
|
|
Span<VoxelLodTerrainUpdateData::BlockToSave> blocks_to_save,
|
|
std::shared_ptr<StreamingDependency> &stream_dependency, unsigned int data_block_size,
|
|
BufferedTaskScheduler &task_scheduler) {
|
|
for (unsigned int i = 0; i < blocks_to_save.size(); ++i) {
|
|
VoxelLodTerrainUpdateData::BlockToSave &b = blocks_to_save[i];
|
|
ZN_PRINT_VERBOSE(format("Requesting save of block {} lod {}", b.position, b.lod));
|
|
request_voxel_block_save(
|
|
volume_id, b.voxels, b.position, b.lod, stream_dependency, data_block_size, task_scheduler);
|
|
}
|
|
}
|
|
|
|
static void request_block_mesh(uint32_t volume_id, const VoxelServer::BlockMeshInput &input,
|
|
std::shared_ptr<MeshingDependency> meshing_dependency,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, unsigned int data_block_size,
|
|
unsigned int mesh_block_size, const Transform3D &volume_transform, float lod_distance,
|
|
BufferedTaskScheduler &task_scheduler, const std::shared_ptr<VoxelDataLodMap> &data) {
|
|
//
|
|
ERR_FAIL_COND(meshing_dependency == nullptr);
|
|
ERR_FAIL_COND(meshing_dependency->mesher.is_null());
|
|
ERR_FAIL_COND(data_block_size > 255);
|
|
|
|
// We'll allocate this quite often. If it becomes a problem, it should be easy to pool.
|
|
MeshBlockTask *task = memnew(MeshBlockTask);
|
|
task->volume_id = volume_id;
|
|
task->blocks = input.data_blocks;
|
|
task->blocks_count = input.data_blocks_count;
|
|
task->position = input.render_block_position;
|
|
task->lod = input.lod;
|
|
task->meshing_dependency = meshing_dependency;
|
|
task->data_block_size = data_block_size;
|
|
task->data = data;
|
|
|
|
init_sparse_octree_priority_dependency(task->priority_dependency, input.render_block_position, input.lod,
|
|
mesh_block_size, shared_viewers_data, volume_transform, lod_distance);
|
|
|
|
task_scheduler.push_main_task(task);
|
|
}
|
|
|
|
static void send_mesh_requests(uint32_t volume_id, VoxelLodTerrainUpdateData::State &state,
|
|
const VoxelLodTerrainUpdateData::Settings &settings, const std::shared_ptr<VoxelDataLodMap> &data_ptr,
|
|
std::shared_ptr<MeshingDependency> meshing_dependency,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, const Transform3D &volume_transform,
|
|
BufferedTaskScheduler &task_scheduler) {
|
|
//
|
|
ZN_PROFILE_SCOPE_NAMED("Send mesh requests");
|
|
|
|
CRASH_COND(data_ptr == nullptr);
|
|
const VoxelDataLodMap &data = *data_ptr;
|
|
|
|
const int data_block_size = data.lods[0].map.get_block_size();
|
|
const int mesh_block_size = 1 << settings.mesh_block_size_po2;
|
|
const int render_to_data_factor = mesh_block_size / mesh_block_size;
|
|
|
|
for (unsigned int lod_index = 0; lod_index < settings.lod_count; ++lod_index) {
|
|
ZN_PROFILE_SCOPE();
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
|
|
for (unsigned int bi = 0; bi < lod.blocks_pending_update.size(); ++bi) {
|
|
ZN_PROFILE_SCOPE();
|
|
const Vector3i mesh_block_pos = lod.blocks_pending_update[bi];
|
|
|
|
auto mesh_block_it = lod.mesh_map_state.map.find(mesh_block_pos);
|
|
// A block must have been allocated before we ask for a mesh update
|
|
ERR_CONTINUE(mesh_block_it == lod.mesh_map_state.map.end());
|
|
VoxelLodTerrainUpdateData::MeshBlockState &mesh_block = mesh_block_it->second;
|
|
// All blocks we get here must be in the scheduled state
|
|
ERR_CONTINUE(mesh_block.state != VoxelLodTerrainUpdateData::MESH_UPDATE_NOT_SENT);
|
|
|
|
// Get block and its neighbors
|
|
VoxelServer::BlockMeshInput mesh_request;
|
|
mesh_request.render_block_position = mesh_block_pos;
|
|
mesh_request.lod = lod_index;
|
|
|
|
const Box3i data_box =
|
|
Box3i(render_to_data_factor * mesh_block_pos, Vector3iUtil::create(render_to_data_factor))
|
|
.padded(1);
|
|
|
|
const VoxelDataLodMap::Lod &data_lod = data.lods[lod_index];
|
|
RWLockRead rlock(data_lod.map_lock);
|
|
|
|
// Iteration order matters for thread access.
|
|
// The array also implicitely encodes block position due to the convention being used,
|
|
// so there is no need to also include positions in the request
|
|
data_box.for_each_cell_zxy([&mesh_request, &data_lod](Vector3i data_block_pos) {
|
|
const VoxelDataBlock *nblock = data_lod.map.get_block(data_block_pos);
|
|
// The block can actually be null on some occasions. Not sure yet if it's that bad
|
|
//CRASH_COND(nblock == nullptr);
|
|
if (nblock != nullptr) {
|
|
mesh_request.data_blocks[mesh_request.data_blocks_count] = nblock->get_voxels_shared();
|
|
}
|
|
++mesh_request.data_blocks_count;
|
|
});
|
|
|
|
request_block_mesh(volume_id, mesh_request, meshing_dependency, shared_viewers_data, data_block_size,
|
|
mesh_block_size, volume_transform, settings.lod_distance, task_scheduler, data_ptr);
|
|
|
|
mesh_block.state = VoxelLodTerrainUpdateData::MESH_UPDATE_SENT;
|
|
}
|
|
|
|
lod.blocks_pending_update.clear();
|
|
}
|
|
}
|
|
|
|
// Generates all non-present blocks in preparation for an edit.
|
|
// This function schedules one parallel task for every block.
|
|
// The returned tracker may be polled to detect when it is complete.
|
|
static std::shared_ptr<AsyncDependencyTracker> preload_boxes_async(VoxelLodTerrainUpdateData::State &state,
|
|
const VoxelLodTerrainUpdateData::Settings &settings, const VoxelDataLodMap &data, Span<const Box3i> voxel_boxes,
|
|
Span<IThreadedTask *> next_tasks, uint32_t volume_id, std::shared_ptr<StreamingDependency> &stream_dependency,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, const Transform3D &volume_transform,
|
|
BufferedTaskScheduler &task_scheduler) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
ERR_FAIL_COND_V_MSG(settings.full_load_mode == false, nullptr, "This function can only be used in full load mode");
|
|
|
|
struct TaskArguments {
|
|
Vector3i block_pos;
|
|
unsigned int lod_index;
|
|
};
|
|
|
|
std::vector<TaskArguments> todo;
|
|
|
|
const unsigned int data_block_size = data.lods[0].map.get_block_size();
|
|
|
|
for (unsigned int lod_index = 0; lod_index < settings.lod_count; ++lod_index) {
|
|
for (unsigned int box_index = 0; box_index < voxel_boxes.size(); ++box_index) {
|
|
ZN_PROFILE_SCOPE_NAMED("Box");
|
|
|
|
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
const Box3i voxel_box = voxel_boxes[box_index];
|
|
const Box3i block_box = voxel_box.downscaled(data_block_size << lod_index);
|
|
|
|
// ZN_PRINT_VERBOSE(String("Preloading box {0} at lod {1}")
|
|
// .format(varray(block_box.to_string(), lod_index)));
|
|
|
|
const VoxelDataLodMap::Lod &data_lod = data.lods[lod_index];
|
|
RWLockRead rlock(data_lod.map_lock);
|
|
MutexLock lock(lod.loading_blocks_mutex);
|
|
|
|
block_box.for_each_cell([&lod, &data_lod, lod_index, &todo](Vector3i block_pos) {
|
|
if (!data_lod.map.has_block(block_pos) && !lod.has_loading_block(block_pos)) {
|
|
todo.push_back({ block_pos, lod_index });
|
|
lod.loading_blocks.insert(block_pos);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
ZN_PRINT_VERBOSE(format("Preloading boxes with {} tasks", todo.size()));
|
|
|
|
std::shared_ptr<AsyncDependencyTracker> tracker = nullptr;
|
|
|
|
// TODO `next_tasks` is executed in parallel. But since they can be edits, may we do them in sequence?
|
|
|
|
if (todo.size() > 0) {
|
|
ZN_PROFILE_SCOPE_NAMED("Posting requests");
|
|
|
|
// Only create the tracker if we actually are creating tasks. If we still create it,
|
|
// no task will take ownership of it, so if it is not stored after this function returns,
|
|
// it would destroy `next_tasks`.
|
|
|
|
// This may first run the generation tasks, and then the edits
|
|
tracker = make_shared_instance<AsyncDependencyTracker>(
|
|
todo.size(), next_tasks, [](Span<IThreadedTask *> p_next_tasks) {
|
|
VoxelServer::get_singleton().push_async_tasks(p_next_tasks);
|
|
});
|
|
|
|
for (unsigned int i = 0; i < todo.size(); ++i) {
|
|
const TaskArguments args = todo[i];
|
|
request_block_generate(volume_id, data_block_size, stream_dependency, args.block_pos, args.lod_index,
|
|
shared_viewers_data, volume_transform, settings.lod_distance, tracker, false, task_scheduler);
|
|
}
|
|
|
|
} else if (next_tasks.size() > 0) {
|
|
// Nothing to preload, we may schedule `next_tasks` right now
|
|
VoxelServer::get_singleton().push_async_tasks(next_tasks);
|
|
}
|
|
|
|
return tracker;
|
|
}
|
|
|
|
static void process_async_edits(VoxelLodTerrainUpdateData::State &state,
|
|
const VoxelLodTerrainUpdateData::Settings &settings, const VoxelDataLodMap &data, uint32_t volume_id,
|
|
std::shared_ptr<StreamingDependency> &stream_dependency,
|
|
std::shared_ptr<PriorityDependency::ViewersData> &shared_viewers_data, const Transform3D &volume_transform,
|
|
BufferedTaskScheduler &task_scheduler) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
if (state.running_async_edits.size() == 0) {
|
|
// Schedule all next edits when the previous ones are done
|
|
|
|
std::vector<Box3i> boxes_to_preload;
|
|
std::vector<IThreadedTask *> tasks_to_schedule;
|
|
std::shared_ptr<AsyncDependencyTracker> last_tracker = nullptr;
|
|
|
|
for (unsigned int edit_index = 0; edit_index < state.pending_async_edits.size(); ++edit_index) {
|
|
VoxelLodTerrainUpdateData::AsyncEdit &edit = state.pending_async_edits[edit_index];
|
|
CRASH_COND(edit.task_tracker->has_next_tasks());
|
|
|
|
// Not sure if worth doing, I dont think tasks can be aborted before even being scheduled
|
|
if (edit.task_tracker->is_aborted()) {
|
|
ZN_PRINT_VERBOSE("Aborted async edit");
|
|
memdelete(edit.task);
|
|
continue;
|
|
}
|
|
|
|
boxes_to_preload.push_back(edit.box);
|
|
tasks_to_schedule.push_back(edit.task);
|
|
state.running_async_edits.push_back(
|
|
VoxelLodTerrainUpdateData::RunningAsyncEdit{ edit.task_tracker, edit.box });
|
|
}
|
|
|
|
if (boxes_to_preload.size() > 0) {
|
|
preload_boxes_async(state, settings, data, to_span_const(boxes_to_preload), to_span(tasks_to_schedule),
|
|
volume_id, stream_dependency, shared_viewers_data, volume_transform, task_scheduler);
|
|
}
|
|
|
|
state.pending_async_edits.clear();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void VoxelLodTerrainUpdateTask::run(ThreadedTaskContext ctx) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
struct SetCompleteOnScopeExit {
|
|
std::atomic_bool &_complete;
|
|
SetCompleteOnScopeExit(std::atomic_bool &b) : _complete(b) {}
|
|
~SetCompleteOnScopeExit() {
|
|
_complete = true;
|
|
}
|
|
};
|
|
|
|
CRASH_COND(_update_data == nullptr);
|
|
CRASH_COND(_data == nullptr);
|
|
CRASH_COND(_streaming_dependency == nullptr);
|
|
CRASH_COND(_shared_viewers_data == nullptr);
|
|
|
|
VoxelLodTerrainUpdateData &update_data = *_update_data;
|
|
VoxelLodTerrainUpdateData::State &state = update_data.state;
|
|
const VoxelLodTerrainUpdateData::Settings &settings = update_data.settings;
|
|
VoxelDataLodMap &data = *_data;
|
|
Ref<VoxelGenerator> generator = _streaming_dependency->generator;
|
|
Ref<VoxelStream> stream = _streaming_dependency->stream;
|
|
ProfilingClock profiling_clock;
|
|
ProfilingClock profiling_clock_total;
|
|
|
|
const bool stream_enabled = (stream.is_valid() || generator.is_valid()) &&
|
|
(Engine::get_singleton()->is_editor_hint() == false || settings.run_stream_in_editor);
|
|
|
|
CRASH_COND(data.lod_count != update_data.settings.lod_count);
|
|
|
|
for (unsigned int lod_index = 0; lod_index < state.lods.size(); ++lod_index) {
|
|
const VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
|
|
CRASH_COND(lod.mesh_blocks_to_unload.size() != 0);
|
|
CRASH_COND(lod.mesh_blocks_to_update_transitions.size() != 0);
|
|
CRASH_COND(lod.mesh_blocks_to_activate.size() != 0);
|
|
CRASH_COND(lod.mesh_blocks_to_deactivate.size() != 0);
|
|
}
|
|
|
|
SetCompleteOnScopeExit scoped_complete(update_data.task_is_complete);
|
|
|
|
CRASH_COND_MSG(update_data.task_is_complete, "Expected only one update task to run on a given volume");
|
|
MutexLock mutex_lock(update_data.completion_mutex);
|
|
|
|
// Update pending LOD data modifications due to edits.
|
|
// These are deferred from edits so we can batch them.
|
|
// It has to happen first because blocks can be unloaded afterwards.
|
|
flush_pending_lod_edits(state, data, generator, settings.full_load_mode, 1 << settings.mesh_block_size_po2);
|
|
|
|
static thread_local std::vector<VoxelLodTerrainUpdateData::BlockToSave> data_blocks_to_save;
|
|
static thread_local std::vector<VoxelLodTerrainUpdateData::BlockLocation> data_blocks_to_load;
|
|
data_blocks_to_load.clear();
|
|
|
|
profiling_clock.restart();
|
|
{
|
|
// Unload data blocks falling out of block region extent
|
|
if (update_data.settings.full_load_mode == false) {
|
|
process_unload_data_blocks_sliding_box(
|
|
state, data, _viewer_pos, data_blocks_to_save, stream.is_valid(), settings);
|
|
}
|
|
|
|
// Unload mesh blocks falling out of block region extent
|
|
process_unload_mesh_blocks_sliding_box(state, _viewer_pos, settings);
|
|
|
|
// Create and remove octrees in a grid around the viewer.
|
|
// Mesh blocks drive the loading of voxel data and visuals.
|
|
process_octrees_sliding_box(state, _viewer_pos, settings);
|
|
|
|
state.stats.blocked_lods = 0;
|
|
|
|
// Find which blocks we need to load and see, within each octree
|
|
if (stream_enabled) {
|
|
process_octrees_fitting(state, settings, data, _viewer_pos, data_blocks_to_load);
|
|
}
|
|
}
|
|
state.stats.time_detect_required_blocks = profiling_clock.restart();
|
|
|
|
BufferedTaskScheduler &task_scheduler = BufferedTaskScheduler::get_for_current_thread();
|
|
|
|
process_async_edits(state, settings, data, _volume_id, _streaming_dependency, _shared_viewers_data,
|
|
_volume_transform, task_scheduler);
|
|
|
|
profiling_clock.restart();
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("IO requests");
|
|
// It's possible the user didn't set a stream yet, or it is turned off
|
|
if (stream_enabled) {
|
|
const unsigned int data_block_size = data.lods[0].map.get_block_size();
|
|
send_block_data_requests(_volume_id, to_span_const(data_blocks_to_load), _streaming_dependency,
|
|
_shared_viewers_data, data_block_size, _request_instances, _volume_transform, settings.lod_distance,
|
|
task_scheduler);
|
|
send_block_save_requests(
|
|
_volume_id, to_span(data_blocks_to_save), _streaming_dependency, data_block_size, task_scheduler);
|
|
}
|
|
data_blocks_to_load.clear();
|
|
data_blocks_to_save.clear();
|
|
}
|
|
state.stats.time_io_requests = profiling_clock.restart();
|
|
|
|
// TODO Don't request meshes if there is no mesher
|
|
send_mesh_requests(_volume_id, state, settings, _data, _meshing_dependency, _shared_viewers_data, _volume_transform,
|
|
task_scheduler);
|
|
|
|
task_scheduler.flush();
|
|
|
|
state.stats.time_mesh_requests = profiling_clock.restart();
|
|
|
|
state.stats.time_total = profiling_clock.restart();
|
|
}
|
|
|
|
} // namespace zylann::voxel
|