Moved loading and meshing to 2 threads, will need a few bugfixes

This commit is contained in:
Marc Gilleron 2018-09-25 00:54:07 +01:00
parent 085f94579a
commit 8f349a1c9a
12 changed files with 1056 additions and 282 deletions

43
utility.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef HEADER_VOXEL_UTILITY_H
#define HEADER_VOXEL_UTILITY_H
#include <core/vector.h>
#include <core/dvector.h>
#include "vector3i.h"
// Takes elements starting from a given position and moves them at the beginning,
// then shrink the array to fit them. Other elements are discarded.
template <typename T>
void shift_up(Vector<T> &v, int pos) {
int j = 0;
for (int i = pos; i < v.size(); ++i, ++j) {
v.write[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>
void unordered_remove(Vector<T> &v, int pos) {
int last = v.size() - 1;
v.write[pos] = v[last];
v.resize(last);
}
template <typename T>
void copy_to(PoolVector<T> &to, const Vector<T> &from) {
to.resize(from.size());
typename PoolVector<T>::Write w = to.write();
for (unsigned int i = 0; i < from.size(); ++i) {
w[i] = from[i];
}
}
#endif // HEADER_VOXEL_UTILITY_H

View File

@ -2,21 +2,27 @@
#define VOXEL_MAP_H #define VOXEL_MAP_H
#include "voxel_buffer.h" #include "voxel_buffer.h"
#include "voxel_block.h"
#include <core/hash_map.h> #include <core/hash_map.h>
#include <scene/main/node.h> #include <scene/main/node.h>
class VoxelBlock;
// Infinite voxel storage by means of octants like Gridmap // Infinite voxel storage by means of octants like Gridmap
class VoxelMap : public Reference { class VoxelMap : public Reference {
GDCLASS(VoxelMap, Reference) GDCLASS(VoxelMap, Reference)
public: public:
// Converts voxel coodinates into block coordinates // Converts voxel coodinates into block coordinates.
_FORCE_INLINE_ Vector3i voxel_to_block(Vector3i pos) const { // 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) {
return Vector3i( return Vector3i(
pos.x >> _block_size_pow2, pos.x >> block_size_pow2,
pos.y >> _block_size_pow2, pos.y >> block_size_pow2,
pos.z >> _block_size_pow2); pos.z >> block_size_pow2);
}
_FORCE_INLINE_ Vector3i voxel_to_block(Vector3i pos) const {
return voxel_to_block_b(pos, _block_size_pow2);
} }
_FORCE_INLINE_ Vector3i to_local(Vector3i pos) const { _FORCE_INLINE_ Vector3i to_local(Vector3i pos) const {

237
voxel_mesh_updater.cpp Normal file
View File

@ -0,0 +1,237 @@
#include <core/os/os.h>
#include "voxel_mesh_updater.h"
#include "utility.h"
VoxelMeshUpdater::VoxelMeshUpdater(Ref<VoxelLibrary> library, MeshingParams params) {
CRASH_COND(library.is_null());
//CRASH_COND(params.materials.size() == 0);
_model_mesher.instance();
_model_mesher->set_library(library);
_model_mesher->set_occlusion_enabled(params.baked_ao);
_model_mesher->set_occlusion_darkness(params.baked_ao_darkness);
_smooth_mesher.instance();
_input_mutex = Mutex::create();
_output_mutex = Mutex::create();
_thread_exit = false;
_semaphore = Semaphore::create();
_thread = Thread::create(_thread_func, this);
}
VoxelMeshUpdater::~VoxelMeshUpdater() {
_thread_exit = true;
_semaphore->post();
Thread::wait_to_finish(_thread);
memdelete(_thread);
memdelete(_semaphore);
memdelete(_input_mutex);
memdelete(_output_mutex);
}
void VoxelMeshUpdater::push(const Input &input) {
bool should_run = false;
int replaced_blocks = 0;
{
MutexLock lock(_input_mutex);
for(int i = 0; i < input.blocks.size(); ++i) {
Vector3i pos = input.blocks[i].position;
// 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
/*if (pos == _shared_input.priority_position) {
OutputBlock ob;
process_block(_shared_input.blocks[i], ob);
{
MutexLock lock2(_output_mutex);
_shared_output.blocks.push_back(ob);
}
continue;
}*/
int *index = _block_indexes.getptr(pos);
if(index) {
// The block is already in the update queue, replace it
++replaced_blocks;
_shared_input.blocks.write[*index] = input.blocks[i];
} else {
int j = _shared_input.blocks.size();
_shared_input.blocks.push_back(input.blocks[i]);
_block_indexes[pos] = j;
}
}
_shared_input.priority_position = input.priority_position;
should_run = !_shared_input.is_empty();
}
if(replaced_blocks > 0)
print_line(String("VoxelMeshUpdater: {0} blocks already in queue were replaced").format(varray(replaced_blocks)));
if (should_run) {
_semaphore->post();
}
}
void VoxelMeshUpdater::pop(Output &output) {
MutexLock lock(_output_mutex);
output.blocks.append_array(_shared_output.blocks);
output.stats = _shared_output.stats;
_shared_output.blocks.clear();
}
void VoxelMeshUpdater::_thread_func(void *p_self) {
VoxelMeshUpdater *self = reinterpret_cast<VoxelMeshUpdater*>(p_self);
self->thread_func();
}
void VoxelMeshUpdater::thread_func() {
while (!_thread_exit) {
uint32_t sync_interval = 50.0; // milliseconds
uint32_t sync_time = OS::get_singleton()->get_ticks_msec() + sync_interval;
int queue_index = 0;
Stats stats;
thread_sync(queue_index, stats);
while (!_input.blocks.empty() && !_thread_exit) {
if (!_input.blocks.empty()) {
InputBlock block = _input.blocks[queue_index];
++queue_index;
if (queue_index >= _input.blocks.size()) {
_input.blocks.clear();
}
uint64_t time_before = OS::get_singleton()->get_ticks_usec();
OutputBlock ob;
process_block(block, ob);
uint64_t time_taken = OS::get_singleton()->get_ticks_usec() - time_before;
// Do some stats
if (stats.first) {
stats.first = false;
stats.min_time = time_taken;
stats.max_time = time_taken;
} else {
if(time_taken < stats.min_time)
stats.min_time = time_taken;
if(time_taken > stats.max_time)
stats.max_time = time_taken;
}
_output.blocks.push_back(ob);
}
uint32_t time = OS::get_singleton()->get_ticks_msec();
if (time >= sync_time) {
thread_sync(queue_index, stats);
sync_time = OS::get_singleton()->get_ticks_msec() + sync_interval;
queue_index = 0;
stats = Stats();
}
}
if (_thread_exit)
break;
// Wait for future wake-up
_semaphore->wait();
}
}
void VoxelMeshUpdater::process_block(const InputBlock &block, OutputBlock &output) {
CRASH_COND(block.voxels.is_null());
// Build cubic parts of the mesh
output.model_surfaces = _model_mesher->build(**block.voxels, Voxel::CHANNEL_TYPE, Vector3i(0, 0, 0), block.voxels->get_size() - Vector3(1, 1, 1));
// Build smooth parts of the mesh
output.smooth_surfaces = _smooth_mesher->build(**block.voxels, Voxel::CHANNEL_ISOLEVEL);
output.position = block.position;
}
// Sorts distance to viewer
// The closest block will be the first one in the array
struct BlockUpdateComparator {
Vector3i center;
inline bool operator()(const VoxelMeshUpdater::InputBlock &a, const VoxelMeshUpdater::InputBlock &b) const {
return a.position.distance_sq(center) < b.position.distance_sq(center);
}
};
void VoxelMeshUpdater::thread_sync(int queue_index, Stats stats) {
if (!_input.blocks.empty()) {
// Cleanup input vector
if (queue_index >= _input.blocks.size()) {
_input.blocks.clear();
} else if (queue_index > 0) {
// Shift up remaining items since we use a Vector
shift_up(_input.blocks, queue_index);
}
}
stats.remaining_blocks = _input.blocks.size();
{
// Get input
MutexLock lock(_input_mutex);
_input.blocks.append_array(_shared_input.blocks);
_input.priority_position = _shared_input.priority_position;
_shared_input.blocks.clear();
_block_indexes.clear();
}
if(!_output.blocks.empty()) {
// print_line(String("VoxelMeshUpdater: posting {0} blocks, {1} remaining ; cost [{2}..{3}] usec")
// .format(varray(_output.blocks.size(), _input.blocks.size(), stats.min_time, stats.max_time)));
// Post output
MutexLock lock(_output_mutex);
_shared_output.blocks.append_array(_output.blocks);
_shared_output.stats = stats;
_output.blocks.clear();
}
if (!_input.blocks.empty()) {
// Re-sort priority
SortArray<VoxelMeshUpdater::InputBlock, BlockUpdateComparator> sorter;
sorter.compare.center = _input.priority_position;
sorter.sort(_input.blocks.ptrw(), _input.blocks.size());
}
}

88
voxel_mesh_updater.h Normal file
View File

@ -0,0 +1,88 @@
#ifndef VOXEL_MESH_UPDATER_H
#define VOXEL_MESH_UPDATER_H
#include <core/vector.h>
#include <core/os/semaphore.h>
#include <core/os/thread.h>
#include "voxel_buffer.h"
#include "voxel_mesher.h"
#include "voxel_mesher_smooth.h"
class VoxelMeshUpdater {
public:
struct InputBlock {
Ref<VoxelBuffer> voxels;
Vector3i position;
};
struct Input {
Vector<InputBlock> blocks;
Vector3i priority_position;
bool is_empty() const {
return blocks.empty();
}
};
struct OutputBlock {
Array model_surfaces;
Array smooth_surfaces;
Vector3i position;
};
struct Stats {
bool first;
uint64_t min_time;
uint64_t max_time;
uint32_t remaining_blocks;
Stats() : first(true), min_time(0), max_time(0), remaining_blocks(0) {}
};
struct Output {
Vector<OutputBlock> blocks;
Stats stats;
};
struct MeshingParams {
bool baked_ao;
float baked_ao_darkness;
MeshingParams(): baked_ao(true), baked_ao_darkness(0.75)
{ }
};
VoxelMeshUpdater(Ref<VoxelLibrary> library, MeshingParams params);
~VoxelMeshUpdater();
void push(const Input &input);
void pop(Output &output);
private:
static void _thread_func(void *p_self);
void thread_func();
void thread_sync(int queue_index, Stats stats);
void process_block(const InputBlock &block, OutputBlock &output);
private:
Input _shared_input;
Mutex *_input_mutex;
Output _shared_output;
Mutex *_output_mutex;
Ref<VoxelMesher> _model_mesher;
Ref<VoxelMesherSmooth> _smooth_mesher;
Input _input;
HashMap<Vector3i, int, Vector3iHasher> _block_indexes;
Output _output;
Semaphore *_semaphore;
Thread *_thread;
bool _thread_exit;
};
#endif // VOXEL_MESH_UPDATER_H

View File

@ -1,20 +1,7 @@
#include "voxel_mesher.h" #include "voxel_mesher.h"
#include "voxel_library.h" #include "voxel_library.h"
#include "cube_tables.h" #include "cube_tables.h"
#include "utility.h"
template <typename T>
void copy_to(PoolVector<T> &to, const Vector<T> &from) {
to.resize(from.size());
typename PoolVector<T>::Write w = to.write();
for (unsigned int i = 0; i < from.size(); ++i) {
w[i] = from[i];
}
}
VoxelMesher::VoxelMesher() VoxelMesher::VoxelMesher()
: _baked_occlusion_darkness(0.75), : _baked_occlusion_darkness(0.75),
@ -24,16 +11,6 @@ void VoxelMesher::set_library(Ref<VoxelLibrary> library) {
_library = library; _library = library;
} }
void VoxelMesher::set_material(Ref<Material> material, unsigned int id) {
ERR_FAIL_COND(id >= MAX_MATERIALS);
_materials[id] = material;
}
Ref<Material> VoxelMesher::get_material(unsigned int id) const {
ERR_FAIL_COND_V(id >= MAX_MATERIALS, Ref<Material>());
return _materials[id];
}
void VoxelMesher::set_occlusion_darkness(float darkness) { void VoxelMesher::set_occlusion_darkness(float darkness) {
_baked_occlusion_darkness = darkness; _baked_occlusion_darkness = darkness;
if (_baked_occlusion_darkness < 0.0) if (_baked_occlusion_darkness < 0.0)
@ -66,16 +43,33 @@ inline bool is_transparent(const VoxelLibrary &lib, int voxel_id) {
return true; return true;
} }
Ref<ArrayMesh> VoxelMesher::build_ref(Ref<VoxelBuffer> buffer_ref, unsigned int channel, Ref<ArrayMesh> mesh) { Ref<ArrayMesh> VoxelMesher::build_mesh(Ref<VoxelBuffer> buffer_ref, unsigned int channel, Array materials, Ref<ArrayMesh> mesh) {
ERR_FAIL_COND_V(buffer_ref.is_null(), Ref<ArrayMesh>()); ERR_FAIL_COND_V(buffer_ref.is_null(), Ref<ArrayMesh>());
VoxelBuffer &buffer = **buffer_ref; VoxelBuffer &buffer = **buffer_ref;
mesh = build(buffer, channel, Vector3i(), buffer.get_size(), mesh); Array surfaces = build(buffer, channel, Vector3i(), buffer.get_size());
if(mesh.is_null())
mesh.instance();
int surface = mesh->get_surface_count();
for(int i = 0; i < surfaces.size(); ++i) {
Array arrays = surfaces[i];
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
Ref<Material> material = materials[i];
if(material.is_valid()) {
mesh->surface_set_material(surface, material);
}
}
return mesh; return mesh;
} }
Ref<ArrayMesh> VoxelMesher::build(const VoxelBuffer &buffer, unsigned int channel, Vector3i min, Vector3i max, Ref<ArrayMesh> mesh) { Array VoxelMesher::build(const VoxelBuffer &buffer, unsigned int channel, Vector3i min, Vector3i max) {
ERR_FAIL_COND_V(_library.is_null(), Ref<ArrayMesh>()); ERR_FAIL_COND_V(_library.is_null(), Array());
ERR_FAIL_COND_V(channel >= VoxelBuffer::MAX_CHANNELS, Ref<ArrayMesh>()); ERR_FAIL_COND_V(channel >= VoxelBuffer::MAX_CHANNELS, Array());
const VoxelLibrary &library = **_library; const VoxelLibrary &library = **_library;
@ -100,8 +94,6 @@ Ref<ArrayMesh> VoxelMesher::build(const VoxelBuffer &buffer, unsigned int channe
// - Slower // - Slower
// => Could be implemented in a separate class? // => Could be implemented in a separate class?
VOXEL_PROFILE_BEGIN("mesher_face_extraction")
// Data must be padded, hence the off-by-one // Data must be padded, hence the off-by-one
Vector3i::sort_min_max(min, max); Vector3i::sort_min_max(min, max);
const Vector3i pad(1, 1, 1); const Vector3i pad(1, 1, 1);
@ -268,24 +260,17 @@ Ref<ArrayMesh> VoxelMesher::build(const VoxelBuffer &buffer, unsigned int channe
} }
} }
VOXEL_PROFILE_END("mesher_face_extraction")
// Commit mesh // Commit mesh
Ref<ArrayMesh> mesh_ref = mesh;
if (mesh.is_null())
mesh_ref = Ref<ArrayMesh>(memnew(ArrayMesh));
VOXEL_PROFILE_BEGIN("mesher_add_surfaces")
// print_line(String("Made mesh v: ") + String::num(_arrays[0].positions.size()) // print_line(String("Made mesh v: ") + String::num(_arrays[0].positions.size())
// + String(", i: ") + String::num(_arrays[0].indices.size())); // + String(", i: ") + String::num(_arrays[0].indices.size()));
int surface = 0; Array surfaces;
for(int i = 0; i < MAX_MATERIALS; ++i) {
for (int i = 0; i < MAX_MATERIALS; ++i) {
const Arrays &arrays = _arrays[i]; const Arrays &arrays = _arrays[i];
if(arrays.positions.size() != 0) { if (arrays.positions.size() != 0) {
Array mesh_arrays; Array mesh_arrays;
mesh_arrays.resize(Mesh::ARRAY_MAX); mesh_arrays.resize(Mesh::ARRAY_MAX);
@ -310,23 +295,15 @@ Ref<ArrayMesh> VoxelMesher::build(const VoxelBuffer &buffer, unsigned int channe
mesh_arrays[Mesh::ARRAY_INDEX] = indices; mesh_arrays[Mesh::ARRAY_INDEX] = indices;
} }
mesh_ref->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, mesh_arrays); surfaces.append(mesh_arrays);
mesh_ref->surface_set_material(surface, _materials[i]);
++surface;
} }
} }
VOXEL_PROFILE_END("mesher_add_surfaces") return surfaces;
return mesh_ref;
} }
void VoxelMesher::_bind_methods() { void VoxelMesher::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_material", "material", "id"), &VoxelMesher::set_material);
ClassDB::bind_method(D_METHOD("get_material", "id"), &VoxelMesher::get_material);
ClassDB::bind_method(D_METHOD("set_library", "voxel_library"), &VoxelMesher::set_library); ClassDB::bind_method(D_METHOD("set_library", "voxel_library"), &VoxelMesher::set_library);
ClassDB::bind_method(D_METHOD("get_library"), &VoxelMesher::get_library); ClassDB::bind_method(D_METHOD("get_library"), &VoxelMesher::get_library);
@ -336,7 +313,7 @@ void VoxelMesher::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_occlusion_darkness", "value"), &VoxelMesher::set_occlusion_darkness); ClassDB::bind_method(D_METHOD("set_occlusion_darkness", "value"), &VoxelMesher::set_occlusion_darkness);
ClassDB::bind_method(D_METHOD("get_occlusion_darkness"), &VoxelMesher::get_occlusion_darkness); ClassDB::bind_method(D_METHOD("get_occlusion_darkness"), &VoxelMesher::get_occlusion_darkness);
ClassDB::bind_method(D_METHOD("build", "voxel_buffer", "channel", "existing_mesh"), &VoxelMesher::build_ref); ClassDB::bind_method(D_METHOD("build_mesh", "voxel_buffer", "channel", "materials", "existing_mesh"), &VoxelMesher::build_mesh);
#ifdef VOXEL_PROFILING #ifdef VOXEL_PROFILING
ClassDB::bind_method(D_METHOD("get_profiling_info"), &VoxelMesher::get_profiling_info); ClassDB::bind_method(D_METHOD("get_profiling_info"), &VoxelMesher::get_profiling_info);

View File

@ -8,7 +8,7 @@
#include <core/reference.h> #include <core/reference.h>
#include <scene/resources/mesh.h> #include <scene/resources/mesh.h>
// TODO Should be renamed VoxelMesherCubic or something like that // TODO Should be renamed VoxelMesherModel or something like that
class VoxelMesher : public Reference { class VoxelMesher : public Reference {
GDCLASS(VoxelMesher, Reference) GDCLASS(VoxelMesher, Reference)
@ -17,9 +17,6 @@ public:
VoxelMesher(); VoxelMesher();
void set_material(Ref<Material> material, unsigned int id);
Ref<Material> get_material(unsigned int id) const;
void set_library(Ref<VoxelLibrary> library); void set_library(Ref<VoxelLibrary> library);
Ref<VoxelLibrary> get_library() const { return _library; } Ref<VoxelLibrary> get_library() const { return _library; }
@ -29,8 +26,8 @@ public:
void set_occlusion_enabled(bool enable); void set_occlusion_enabled(bool enable);
bool get_occlusion_enabled() const { return _bake_occlusion; } bool get_occlusion_enabled() const { return _bake_occlusion; }
Ref<ArrayMesh> build(const VoxelBuffer &buffer_ref, unsigned int channel, Vector3i min, Vector3i max, Ref<ArrayMesh> mesh = Ref<Mesh>()); Array build(const VoxelBuffer &buffer_ref, unsigned int channel, Vector3i min, Vector3i max);
Ref<ArrayMesh> build_ref(Ref<VoxelBuffer> buffer_ref, unsigned int channel, Ref<ArrayMesh> mesh = Ref<ArrayMesh>()); Ref<ArrayMesh> build_mesh(Ref<VoxelBuffer> buffer_ref, unsigned int channel, Array materials, Ref<ArrayMesh> mesh = Ref<ArrayMesh>());
protected: protected:
static void _bind_methods(); static void _bind_methods();
@ -45,7 +42,6 @@ private:
}; };
Ref<VoxelLibrary> _library; Ref<VoxelLibrary> _library;
Ref<Material> _materials[MAX_MATERIALS];
Arrays _arrays[MAX_MATERIALS]; Arrays _arrays[MAX_MATERIALS];
float _baked_occlusion_darkness; float _baked_occlusion_darkness;
bool _bake_occlusion; bool _bake_occlusion;

View File

@ -68,18 +68,29 @@ VoxelMesherSmooth::ReuseCell::ReuseCell() {
VoxelMesherSmooth::VoxelMesherSmooth() { VoxelMesherSmooth::VoxelMesherSmooth() {
} }
Ref<ArrayMesh> VoxelMesherSmooth::build_ref(Ref<VoxelBuffer> voxels_ref, unsigned int channel, Ref<ArrayMesh> mesh) { Ref<ArrayMesh> VoxelMesherSmooth::build_mesh(Ref<VoxelBuffer> voxels_ref, unsigned int channel, Ref<ArrayMesh> mesh) {
ERR_FAIL_COND_V(voxels_ref.is_null(), Ref<ArrayMesh>()); ERR_FAIL_COND_V(voxels_ref.is_null(), Ref<ArrayMesh>());
VoxelBuffer &voxels = **voxels_ref; VoxelBuffer &buffer = **voxels_ref;
Array surfaces = build(buffer, channel);
return build(voxels, channel, mesh); if(mesh.is_null())
mesh.instance();
//int surface = mesh->get_surface_count();
for(int i = 0; i < surfaces.size(); ++i) {
Array arrays = surfaces[i];
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
//mesh->surface_set_material(surface, _materials[i]);
}
return mesh;
} }
Ref<ArrayMesh> VoxelMesherSmooth::build(const VoxelBuffer &voxels, unsigned int channel, Ref<ArrayMesh> mesh) { Array VoxelMesherSmooth::build(const VoxelBuffer &voxels, unsigned int channel) {
ERR_FAIL_COND_V(channel >= VoxelBuffer::MAX_CHANNELS, Ref<ArrayMesh>()); ERR_FAIL_COND_V(channel >= VoxelBuffer::MAX_CHANNELS, Array());
// Initialize dynamic memory: // Initialize dynamic memory:
// These vectors are re-used. // These vectors are re-used.
@ -90,7 +101,7 @@ Ref<ArrayMesh> VoxelMesherSmooth::build(const VoxelBuffer &voxels, unsigned int
m_output_normals.clear(); m_output_normals.clear();
m_output_indices.clear(); m_output_indices.clear();
build_mesh(voxels, channel); build_internal(voxels, channel);
// OS::get_singleton()->print("vertices: %i, normals: %i, indices: %i\n", // OS::get_singleton()->print("vertices: %i, normals: %i, indices: %i\n",
// m_output_vertices.size(), // m_output_vertices.size(),
// m_output_normals.size(), // m_output_normals.size(),
@ -98,7 +109,7 @@ Ref<ArrayMesh> VoxelMesherSmooth::build(const VoxelBuffer &voxels, unsigned int
if (m_output_vertices.size() == 0) { if (m_output_vertices.size() == 0) {
// The mesh can be empty // The mesh can be empty
return Ref<ArrayMesh>(); return Array();
} }
PoolVector<Vector3> vertices; PoolVector<Vector3> vertices;
@ -117,15 +128,13 @@ Ref<ArrayMesh> VoxelMesherSmooth::build(const VoxelBuffer &voxels, unsigned int
} }
arrays[Mesh::ARRAY_INDEX] = indices; arrays[Mesh::ARRAY_INDEX] = indices;
if (mesh.is_null()) Array surfaces;
mesh = Ref<ArrayMesh>(memnew(ArrayMesh)); surfaces.append(arrays);
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays); return surfaces;
return mesh;
} }
void VoxelMesherSmooth::build_mesh(const VoxelBuffer &voxels, unsigned int channel) { void VoxelMesherSmooth::build_internal(const VoxelBuffer &voxels, unsigned int channel) {
// Each 2x2 voxel group is a "cell" // Each 2x2 voxel group is a "cell"
@ -385,5 +394,5 @@ void VoxelMesherSmooth::emit_vertex(Vector3 primary, Vector3 normal) {
void VoxelMesherSmooth::_bind_methods() { void VoxelMesherSmooth::_bind_methods() {
ClassDB::bind_method(D_METHOD("build", "voxels", "channel", "existing_mesh"), &VoxelMesherSmooth::build_ref, DEFVAL(Variant())); ClassDB::bind_method(D_METHOD("build", "voxels", "channel", "existing_mesh"), &VoxelMesherSmooth::build_mesh, DEFVAL(Variant()));
} }

View File

@ -10,8 +10,8 @@ class VoxelMesherSmooth : public Reference {
public: public:
VoxelMesherSmooth(); VoxelMesherSmooth();
Ref<ArrayMesh> build_ref(Ref<VoxelBuffer> voxels_ref, unsigned int channel, Ref<ArrayMesh> mesh = Ref<ArrayMesh>()); Ref<ArrayMesh> build_mesh(Ref<VoxelBuffer> voxels_ref, unsigned int channel, Ref<ArrayMesh> mesh = Ref<ArrayMesh>());
Ref<ArrayMesh> build(const VoxelBuffer &voxels, unsigned int channel, Ref<ArrayMesh> mesh = Ref<ArrayMesh>()); Array build(const VoxelBuffer &voxels, unsigned int channel);
protected: protected:
static void _bind_methods(); static void _bind_methods();
@ -23,7 +23,7 @@ private:
ReuseCell(); ReuseCell();
}; };
void build_mesh(const VoxelBuffer &voxels, unsigned int channel); void build_internal(const VoxelBuffer &voxels, unsigned int channel);
ReuseCell &get_reuse_cell(Vector3i pos); ReuseCell &get_reuse_cell(Vector3i pos);
void emit_vertex(Vector3 primary, Vector3 normal); void emit_vertex(Vector3 primary, Vector3 normal);

206
voxel_provider_thread.cpp Normal file
View File

@ -0,0 +1,206 @@
#include "core/os/os.h"
#include "core/os/thread.h"
#include "core/os/semaphore.h"
#include "voxel_provider_thread.h"
#include "voxel_provider.h"
#include "voxel_map.h"
#include "utility.h"
VoxelProviderThread::VoxelProviderThread(Ref<VoxelProvider> provider, int block_size_pow2) {
CRASH_COND(provider.is_null());
CRASH_COND(block_size_pow2 <= 0);
_voxel_provider = provider;
_block_size_pow2 = block_size_pow2;
_input_mutex = Mutex::create();
_output_mutex = Mutex::create();
_semaphore = Semaphore::create();
_thread_exit = false;
_thread = Thread::create(_thread_func, this);
}
VoxelProviderThread::~VoxelProviderThread() {
_thread_exit = true;
_semaphore->post();
Thread::wait_to_finish(_thread);
memdelete(_thread);
memdelete(_semaphore);
memdelete(_input_mutex);
memdelete(_output_mutex);
}
void VoxelProviderThread::push(const InputData &input) {
bool should_run = false;
{
MutexLock lock(_input_mutex);
// TODO If the same update is sent twice, keep only the latest one
_shared_input.blocks_to_emerge.append_array(input.blocks_to_emerge);
_shared_input.blocks_to_immerge.append_array(input.blocks_to_immerge);
_shared_input.priority_block_position = input.priority_block_position;
should_run = !_shared_input.is_empty();
}
// Notify the thread it should run
if(should_run) {
_semaphore->post();
}
}
void VoxelProviderThread::pop(OutputData &out_data) {
MutexLock lock(_output_mutex);
out_data.emerged_blocks.append_array(_shared_output);
out_data.stats = _shared_stats;
_shared_output.clear();
}
void VoxelProviderThread::_thread_func(void *p_self) {
VoxelProviderThread *self = reinterpret_cast<VoxelProviderThread*>(p_self);
self->thread_func();
}
void VoxelProviderThread::thread_func() {
while(!_thread_exit) {
uint32_t sync_interval = 100.0; // milliseconds
uint32_t sync_time = OS::get_singleton()->get_ticks_msec() + sync_interval;
int emerge_index = 0;
Stats stats;
thread_sync(emerge_index, stats);
while(!_input.is_empty() && !_thread_exit) {
//print_line(String("Thread runs: {0}").format(varray(_input.blocks_to_emerge.size())));
// TODO Block saving
_input.blocks_to_immerge.clear();
if(!_input.blocks_to_emerge.empty()) {
Vector3i block_pos = _input.blocks_to_emerge[emerge_index];
++emerge_index;
if(emerge_index >= _input.blocks_to_emerge.size()) {
_input.blocks_to_emerge.clear();
}
int bs = 1 << _block_size_pow2;
Ref<VoxelBuffer> buffer = Ref<VoxelBuffer>(memnew(VoxelBuffer));
buffer->create(bs, bs, bs);
// Query voxel provider
Vector3i block_origin_in_voxels = block_pos * bs;
uint64_t time_before = OS::get_singleton()->get_ticks_usec();
_voxel_provider->emerge_block(buffer, block_origin_in_voxels);
uint64_t time_taken = OS::get_singleton()->get_ticks_usec() - time_before;
// Do some stats
if(stats.first) {
stats.first = false;
stats.min_time = time_taken;
stats.max_time = time_taken;
} else {
if(time_taken < stats.min_time)
stats.min_time = time_taken;
if(time_taken > stats.max_time)
stats.max_time = time_taken;
}
EmergeOutput eo;
eo.origin_in_voxels = block_origin_in_voxels;
eo.voxels = buffer;
_output.push_back(eo);
}
uint32_t time = OS::get_singleton()->get_ticks_msec();
if (time >= sync_time) {
thread_sync(emerge_index, stats);
sync_time = OS::get_singleton()->get_ticks_msec() + sync_interval;
emerge_index = 0;
stats = Stats();
}
}
if(_thread_exit)
break;
// Wait for future wake-up
_semaphore->wait();
}
print_line("Thread exits");
}
// Sorts distance to viewer
// The closest block will be the first one in the array
struct BlockPositionComparator {
Vector3i center;
inline bool operator()(const Vector3i &a, const Vector3i &b) const {
return a.distance_sq(center) < b.distance_sq(center);
}
};
void VoxelProviderThread::thread_sync(int emerge_index, Stats stats) {
if (!_input.blocks_to_emerge.empty()) {
// Cleanup emerge vector
if (emerge_index >= _input.blocks_to_emerge.size()) {
_input.blocks_to_emerge.clear();
} else if (emerge_index > 0) {
// Shift up remaining items since we use a Vector
shift_up(_input.blocks_to_emerge, emerge_index);
}
}
{
// Get input
MutexLock lock(_input_mutex);
_input.blocks_to_emerge.append_array(_shared_input.blocks_to_emerge);
_input.blocks_to_immerge.append_array(_shared_input.blocks_to_immerge);
_input.priority_block_position = _shared_input.priority_block_position;
_shared_input.blocks_to_emerge.clear();
_shared_input.blocks_to_immerge.clear();
}
stats.remaining_blocks = _input.blocks_to_emerge.size();
// print_line(String("VoxelProviderThread: posting {0} blocks, {1} remaining ; cost [{2}..{3}] usec")
// .format(varray(_output.size(), _input.blocks_to_emerge.size(), stats.min_time, stats.max_time)));
{
// Post output
MutexLock lock(_output_mutex);
_shared_output.append_array(_output);
_shared_stats = stats;
_output.clear();
}
if (!_input.blocks_to_emerge.empty()) {
// Re-sort priority
SortArray<Vector3i, BlockPositionComparator> sorter;
sorter.compare.center = _input.priority_block_position;
sorter.sort(_input.blocks_to_emerge.ptrw(), _input.blocks_to_emerge.size());
}
}

78
voxel_provider_thread.h Normal file
View File

@ -0,0 +1,78 @@
#ifndef VOXEL_PROVIDER_THREAD_H
#define VOXEL_PROVIDER_THREAD_H
#include "core/resource.h"
#include "vector3i.h"
class VoxelProvider;
class VoxelBuffer;
class Thread;
class Semaphore;
class VoxelProviderThread {
public:
struct ImmergeInput {
Vector3i origin;
Ref<VoxelBuffer> voxels;
};
struct InputData {
Vector<ImmergeInput> blocks_to_immerge;
Vector<Vector3i> blocks_to_emerge;
Vector3i priority_block_position;
inline bool is_empty() {
return blocks_to_emerge.empty() && blocks_to_immerge.empty();
}
};
struct EmergeOutput {
Ref<VoxelBuffer> voxels;
Vector3i origin_in_voxels;
};
struct Stats {
bool first;
uint64_t min_time;
uint64_t max_time;
int remaining_blocks;
Stats() : first(true), min_time(0), max_time(0), remaining_blocks(0) {}
};
struct OutputData {
Vector<EmergeOutput> emerged_blocks;
Stats stats;
};
VoxelProviderThread(Ref<VoxelProvider> provider, int block_size_pow2);
~VoxelProviderThread();
void push(const InputData &input);
void pop(OutputData &out_data);
private:
static void _thread_func(void *p_self);
void thread_func();
void thread_sync(int emerge_index, Stats stats);
private:
InputData _shared_input;
Mutex *_input_mutex;
Vector<EmergeOutput> _shared_output;
Stats _shared_stats;
Mutex *_output_mutex;
Semaphore *_semaphore;
bool _thread_exit;
Thread *_thread;
InputData _input;
Vector<EmergeOutput> _output;
int _block_size_pow2;
Ref<VoxelProvider> _voxel_provider;
};
#endif // VOXEL_PROVIDER_THREAD_H

View File

@ -1,6 +1,11 @@
#include "voxel_terrain.h" #include "voxel_terrain.h"
#include "voxel_map.h"
#include "voxel_block.h"
#include "voxel_provider_thread.h"
#include "voxel_raycast.h" #include "voxel_raycast.h"
#include "rect3i.h" #include "voxel_provider_test.h"
#include "utility.h"
#include <core/os/os.h> #include <core/os/os.h>
#include <scene/3d/mesh_instance.h> #include <scene/3d/mesh_instance.h>
#include <core/engine.h> #include <core/engine.h>
@ -10,22 +15,22 @@ VoxelTerrain::VoxelTerrain()
: Spatial(), _generate_collisions(true) { : Spatial(), _generate_collisions(true) {
_map = Ref<VoxelMap>(memnew(VoxelMap)); _map = Ref<VoxelMap>(memnew(VoxelMap));
_mesher = Ref<VoxelMesher>(memnew(VoxelMesher));
_mesher_smooth = Ref<VoxelMesherSmooth>(memnew(VoxelMesherSmooth));
_view_distance_blocks = 8; _view_distance_blocks = 8;
_last_view_distance_blocks = 0; _last_view_distance_blocks = 0;
_provider_thread = NULL;
_block_updater = NULL;
} }
// TODO UGLY! Lambdas or pointers needed... DO NOT use this outside of lambdas! VoxelTerrain::~VoxelTerrain() {
Vector3i g_viewer_block_pos; if(_provider_thread) {
memdelete(_provider_thread);
// Sorts distance to viewer
struct BlockUpdateComparator {
inline bool operator()(const Vector3i &a, const Vector3i &b) const {
return a.distance_sq(g_viewer_block_pos) > b.distance_sq(g_viewer_block_pos);
} }
}; if(_block_updater) {
memdelete(_block_updater);
}
}
// TODO See if there is a way to specify materials in voxels directly? // TODO See if there is a way to specify materials in voxels directly?
@ -33,8 +38,7 @@ bool VoxelTerrain::_set(const StringName &p_name, const Variant &p_value) {
if (p_name.operator String().begins_with("material/")) { if (p_name.operator String().begins_with("material/")) {
int idx = p_name.operator String().get_slicec('/', 1).to_int(); int idx = p_name.operator String().get_slicec('/', 1).to_int();
if (idx >= VoxelMesher::MAX_MATERIALS || idx < 0) ERR_FAIL_COND_V(idx >= VoxelMesher::MAX_MATERIALS || idx < 0, false);
return false;
set_material(idx, p_value); set_material(idx, p_value);
return true; return true;
} }
@ -46,8 +50,7 @@ bool VoxelTerrain::_get(const StringName &p_name, Variant &r_ret) const {
if (p_name.operator String().begins_with("material/")) { if (p_name.operator String().begins_with("material/")) {
int idx = p_name.operator String().get_slicec('/', 1).to_int(); int idx = p_name.operator String().get_slicec('/', 1).to_int();
if (idx >= VoxelMesher::MAX_MATERIALS || idx < 0) ERR_FAIL_COND_V(idx >= VoxelMesher::MAX_MATERIALS || idx < 0, false);
return false;
r_ret = get_material(idx); r_ret = get_material(idx);
return true; return true;
} }
@ -64,8 +67,20 @@ void VoxelTerrain::_get_property_list(List<PropertyInfo> *p_list) const {
void VoxelTerrain::set_provider(Ref<VoxelProvider> provider) { void VoxelTerrain::set_provider(Ref<VoxelProvider> provider) {
if(provider != _provider) { if(provider != _provider) {
if(_provider_thread) {
memdelete(_provider_thread);
_provider_thread = NULL;
}
_provider = provider; _provider = provider;
_provider_thread = memnew(VoxelProviderThread(_provider, _map->get_block_size_pow2()));
// Ref<VoxelProviderTest> test;
// test.instance();
// _provider_thread = memnew(VoxelProviderThread(test, _map->get_block_size_pow2()));
// The whole map might change, so make all area dirty // 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(); make_all_view_dirty_deferred();
} }
} }
@ -75,18 +90,29 @@ Ref<VoxelProvider> VoxelTerrain::get_provider() const {
} }
Ref<VoxelLibrary> VoxelTerrain::get_voxel_library() const { Ref<VoxelLibrary> VoxelTerrain::get_voxel_library() const {
return _mesher->get_library(); return _library;
} }
void VoxelTerrain::set_voxel_library(Ref<VoxelLibrary> library) { void VoxelTerrain::set_voxel_library(Ref<VoxelLibrary> library) {
if(library != _mesher->get_library()) {
if (library != _library) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
if(library->get_voxel_count() == 0) { if (library->get_voxel_count() == 0) {
library->load_default(); library->load_default();
} }
#endif #endif
_mesher->set_library(library); _library = library;
if(_block_updater) {
memdelete(_block_updater);
_block_updater = NULL;
}
// TODO Thread-safe way to change those parameters
VoxelMeshUpdater::MeshingParams params;
_block_updater = memnew(VoxelMeshUpdater(_library, params));
// Voxel appearance might completely change // Voxel appearance might completely change
make_all_view_dirty_deferred(); make_all_view_dirty_deferred();
@ -130,24 +156,34 @@ Spatial *VoxelTerrain::get_viewer(NodePath path) const {
void VoxelTerrain::set_material(int id, Ref<Material> material) { void VoxelTerrain::set_material(int id, Ref<Material> material) {
// TODO Update existing block surfaces // TODO Update existing block surfaces
_mesher->set_material(material, id); ERR_FAIL_COND(id < 0 || id >= VoxelMesher::MAX_MATERIALS);
_materials[id] = material;
} }
Ref<Material> VoxelTerrain::get_material(int id) const { Ref<Material> VoxelTerrain::get_material(int id) const {
return _mesher->get_material(id); ERR_FAIL_COND_V(id < 0 || id >= VoxelMesher::MAX_MATERIALS, Ref<Material>());
return _materials[id];
} }
//void VoxelTerrain::clear_update_queue() {
// _block_update_queue.clear();
// _dirty_blocks.clear();
//}
void VoxelTerrain::make_block_dirty(Vector3i bpos) { void VoxelTerrain::make_block_dirty(Vector3i bpos) {
// TODO Immediate update viewer distance // TODO Immediate update viewer distance?
if (is_block_dirty(bpos) == false) { if (is_block_dirty(bpos) == false) {
if(_map->has_block(bpos)) {
_blocks_pending_update.push_back(bpos);
_dirty_blocks[bpos] = BLOCK_UPDATE;
} else {
_blocks_pending_load.push_back(bpos);
_dirty_blocks[bpos] = BLOCK_LOAD;
}
//OS::get_singleton()->print("Dirty (%i, %i, %i)", bpos.x, bpos.y, bpos.z); //OS::get_singleton()->print("Dirty (%i, %i, %i)", bpos.x, bpos.y, bpos.z);
_block_update_queue.push_back(bpos);
_dirty_blocks[bpos] = true; // TODO What if a block is made dirty, goes through threaded update, then gets changed again before it gets updated?
// this will make the second change ignored, which is not correct!
} }
} }
@ -163,21 +199,42 @@ void VoxelTerrain::immerge_block(Vector3i bpos) {
// because it's too expensive to linear-search all blocks for each block // because it's too expensive to linear-search all blocks for each block
} }
bool VoxelTerrain::is_block_dirty(Vector3i bpos) { Dictionary VoxelTerrain::get_statistics() const {
Dictionary provider;
provider["min_time"] = _stats.provider.min_time;
provider["max_time"] = _stats.provider.max_time;
provider["remaining_blocks"] = _stats.provider.remaining_blocks;
Dictionary updater;
updater["min_time"] = _stats.updater.min_time;
updater["max_time"] = _stats.updater.max_time;
updater["remaining_blocks"] = _stats.updater.remaining_blocks;
updater["updated_blocks"] = _stats.updated_blocks;
updater["mesh_alloc_time"] = _stats.mesh_alloc_time;
Dictionary d;
d["provider"] = provider;
d["updater"] = updater;
return d;
}
bool VoxelTerrain::is_block_dirty(Vector3i bpos) const {
return _dirty_blocks.has(bpos); return _dirty_blocks.has(bpos);
} }
void VoxelTerrain::make_blocks_dirty(Vector3i min, Vector3i size) { //void VoxelTerrain::make_blocks_dirty(Vector3i min, Vector3i size) {
Vector3i max = min + size; // Vector3i max = min + size;
Vector3i pos; // Vector3i pos;
for (pos.z = min.z; pos.z < max.z; ++pos.z) { // for (pos.z = min.z; pos.z < max.z; ++pos.z) {
for (pos.y = min.y; pos.y < max.y; ++pos.y) { // for (pos.y = min.y; pos.y < max.y; ++pos.y) {
for (pos.x = min.x; pos.x < max.x; ++pos.x) { // for (pos.x = min.x; pos.x < max.x; ++pos.x) {
make_block_dirty(pos); // make_block_dirty(pos);
} // }
} // }
} // }
} //}
void VoxelTerrain::make_all_view_dirty_deferred() { void VoxelTerrain::make_all_view_dirty_deferred() {
// This trick will regenerate all chunks in view, according to the view distance found during block updates. // This trick will regenerate all chunks in view, according to the view distance found during block updates.
@ -204,7 +261,8 @@ void VoxelTerrain::make_voxel_dirty(Vector3i pos) {
Vector3i rpos = _map->to_local(pos); Vector3i rpos = _map->to_local(pos);
bool check_corners = _mesher->get_occlusion_enabled(); // TODO Thread-safe way of getting this parameter
bool check_corners = true;//_mesher->get_occlusion_enabled();
const int max = _map->get_block_size() - 1; const int max = _map->get_block_size() - 1;
@ -302,10 +360,6 @@ void VoxelTerrain::make_voxel_dirty(Vector3i pos) {
} }
} }
int VoxelTerrain::get_block_update_count() {
return _block_update_queue.size();
}
struct EnterWorldAction { struct EnterWorldAction {
World *world; World *world;
EnterWorldAction(World *w) : world(w) {} EnterWorldAction(World *w) : world(w) {}
@ -365,11 +419,31 @@ void VoxelTerrain::_notification(int p_what) {
} }
} }
void VoxelTerrain::_process() { void VoxelTerrain::remove_positions_outside_box(Vector<Vector3i> &positions, Rect3i box, HashMap<Vector3i, VoxelTerrain::BlockDirtyState, Vector3iHasher> &state_map) {
update_blocks(); for(int i = 0; i < positions.size(); ++i) {
const Vector3i bpos = positions[i];
if(!box.contains(bpos)) {
int last = positions.size() - 1;
positions.write[i] = positions[last];
positions.resize(last);
state_map.erase(bpos);
--i;
}
}
} }
void VoxelTerrain::update_blocks() { static inline bool is_mesh_empty(Ref<Mesh> mesh_ref) {
if (mesh_ref.is_null())
return true;
const Mesh &mesh = **mesh_ref;
if (mesh.get_surface_count() == 0)
return true;
if (mesh.surface_get_array_len(0) == 0)
return true;
return false;
}
void VoxelTerrain::_process() {
OS &os = *OS::get_singleton(); OS &os = *OS::get_singleton();
Engine &engine = *Engine::get_singleton(); Engine &engine = *Engine::get_singleton();
@ -390,9 +464,8 @@ void VoxelTerrain::update_blocks() {
viewer_block_pos = Vector3i(); viewer_block_pos = Vector3i();
} }
// Find out which blocks need to appear and which need to be unloaded
{ {
// Find out which blocks need to appear and which need to be unloaded
//Vector3i viewer_block_pos_delta = _last_viewer_block_pos - viewer_block_pos; //Vector3i viewer_block_pos_delta = _last_viewer_block_pos - viewer_block_pos;
Rect3i new_box = Rect3i::from_center_extents(viewer_block_pos, Vector3i(_view_distance_blocks)); Rect3i new_box = Rect3i::from_center_extents(viewer_block_pos, Vector3i(_view_distance_blocks));
Rect3i prev_box = Rect3i::from_center_extents(_last_viewer_block_pos, Vector3i(_last_view_distance_blocks)); Rect3i prev_box = Rect3i::from_center_extents(_last_viewer_block_pos, Vector3i(_last_view_distance_blocks));
@ -425,148 +498,197 @@ void VoxelTerrain::update_blocks() {
} }
} }
// Eliminate blocks in queue that aren't needed // Eliminate pending blocks that aren't needed
for(int i = 0; i < _block_update_queue.size(); ++i) { remove_positions_outside_box(_blocks_pending_load, new_box, _dirty_blocks);
const Vector3i bpos = _block_update_queue[i]; remove_positions_outside_box(_blocks_pending_update, new_box, _dirty_blocks);
if(!new_box.contains(bpos)) {
int last = _block_update_queue.size() - 1;
_block_update_queue.write[i] = _block_update_queue[last];
_block_update_queue.resize(last);
--i;
}
}
} }
_last_view_distance_blocks = _view_distance_blocks; _last_view_distance_blocks = _view_distance_blocks;
_last_viewer_block_pos = viewer_block_pos; _last_viewer_block_pos = viewer_block_pos;
// Sort updates so nearest blocks are done first // Send block loading requests
VOXEL_PROFILE_BEGIN("block_update_sorting") {
g_viewer_block_pos = viewer_block_pos; VoxelProviderThread::InputData input;
_block_update_queue.sort_custom<BlockUpdateComparator>();
VOXEL_PROFILE_END("block_update_sorting")
uint32_t time_before = os.get_ticks_msec(); input.priority_block_position = viewer_block_pos;
uint32_t max_time = 1000 / 120; input.blocks_to_emerge.append_array(_blocks_pending_load);
//input.blocks_to_immerge.append_array();
const unsigned int bs = _map->get_block_size(); //print_line(String("Sending {0} block requests").format(varray(input.blocks_to_emerge.size())));
const Vector3i block_size(bs, bs, bs); _blocks_pending_load.clear();
// Update a bunch of blocks until none are left or too much time elapsed _provider_thread->push(input);
while (!_block_update_queue.empty() && (os.get_ticks_msec() - time_before) < max_time) { }
//printf("Remaining: %i\n", _block_update_queue.size()); // Get block loading responses
{
const unsigned int bs = _map->get_block_size();
const Vector3i block_size(bs, bs, bs);
// TODO Move this to a thread VoxelProviderThread::OutputData output;
_provider_thread->pop(output);
//print_line(String("Receiving {0} blocks").format(varray(output.emerged_blocks.size())));
// Get request _stats.provider = output.stats;
Vector3i block_pos = _block_update_queue[_block_update_queue.size() - 1];
bool entire_block_changed = false; for(int i = 0; i < output.emerged_blocks.size(); ++i) {
if (!_map->has_block(block_pos)) { const VoxelProviderThread::EmergeOutput &o = output.emerged_blocks[i];
// The block's data isn't loaded yet
// Create buffer
if (!_provider.is_null()) {
VOXEL_PROFILE_BEGIN("voxel_buffer_creation_gen") // Check return
// TODO Shouldn't halt execution though, as it can bring the map in an invalid state!
ERR_FAIL_COND(o.voxels->get_size() != block_size);
Ref<VoxelBuffer> buffer_ref = Ref<VoxelBuffer>(memnew(VoxelBuffer)); // TODO Discard blocks out of range
buffer_ref->create(block_size.x, block_size.y, block_size.z);
VOXEL_PROFILE_END("voxel_buffer_creation_gen") // Store buffer
VOXEL_PROFILE_BEGIN("block_generation") Vector3i block_pos = _map->voxel_to_block(o.origin_in_voxels);
bool update_neighbors = !_map->has_block(block_pos);
_map->set_block_buffer(block_pos, o.voxels);
// Query voxel provider // Trigger mesh updates
_provider->emerge_block(buffer_ref, _map->block_to_voxel(block_pos)); if (update_neighbors) {
// All neighbors have to be checked
Vector3i ndir;
// TODO Cache blocks in a small local grid on the stack to reduce hashing
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 = block_pos + ndir;
// TODO What if the map is really composed of empty blocks?
if (_map->is_block_surrounded(npos)) {
// Check script return VoxelTerrain::BlockDirtyState *state = _dirty_blocks.getptr(npos);
// TODO Shouldn't halt execution though, as it can bring the map in an invalid state! if (state && *state == BLOCK_UPDATE) {
ERR_FAIL_COND(buffer_ref->get_size() != block_size); // Assuming it is scheduled to be updated already.
continue;
}
VOXEL_PROFILE_END("block_generation") _dirty_blocks[npos] = BLOCK_UPDATE;
_blocks_pending_update.push_back(npos);
// Store buffer }
_map->set_block_buffer(block_pos, buffer_ref);
entire_block_changed = true;
}
}
// Update views (mesh/collisions)
if (entire_block_changed) {
// All neighbors have to be checked
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 = block_pos + ndir;
// TODO What if the map is really composed of empty blocks?
if (_map->is_block_surrounded(npos)) {
update_block_mesh(npos);
} }
} }
} }
} else {
// Only update the block, neighbors will probably follow if needed
_dirty_blocks[block_pos] = BLOCK_UPDATE;
_blocks_pending_update.push_back(block_pos);
//OS::get_singleton()->print("Update (%i, %i, %i)\n", block_pos.x, block_pos.y, block_pos.z);
} }
} else { }
// Only update the block, neighbors will probably follow if needed }
update_block_mesh(block_pos);
//OS::get_singleton()->print("Update (%i, %i, %i)\n", block_pos.x, block_pos.y, block_pos.z); // Send mesh updates
{
VoxelMeshUpdater::Input input;
for(int i = 0; i < _blocks_pending_update.size(); ++i) {
Vector3i block_pos = _blocks_pending_update[i];
VoxelBlock *block = _map->get_block(block_pos);
if (block == NULL) {
continue;
}
// Create buffer padded with neighbor voxels
Ref<VoxelBuffer> nbuffer;
nbuffer.instance();
// TODO Make the buffer re-usable
// TODO Padding set to 3 at the moment because Transvoxel works on 2x2 cells.
// It should change for a smarter padding (if smooth isn't used for example).
unsigned int block_size = _map->get_block_size();
nbuffer->create(block_size + 3, block_size + 3, block_size + 3);
_map->get_buffer_copy(_map->block_to_voxel(block_pos) - Vector3i(1, 1, 1), **nbuffer, 0x3);
VoxelMeshUpdater::InputBlock iblock;
iblock.voxels = nbuffer;
iblock.position = block_pos;
input.blocks.push_back(iblock);
} }
// Pop request _block_updater->push(input);
_block_update_queue.resize(_block_update_queue.size() - 1); _blocks_pending_update.clear();
_dirty_blocks.erase(block_pos); }
// Get mesh updates
{
VoxelMeshUpdater::Output output;
_block_updater->pop(output);
_stats.updater = output.stats;
_stats.updated_blocks = output.blocks.size();
Ref<World> world = get_world();
uint32_t time_before = os.get_ticks_msec();
for (int i = 0; i < output.blocks.size(); ++i) {
const VoxelMeshUpdater::OutputBlock &ob = output.blocks[i];
VoxelBlock *block = _map->get_block(ob.position);
if (block == NULL) {
clear_block_update_state(ob.position);
continue;
}
// Note: I allocate the mesh here because Godot doesn't supports doing it in another thread without hanging the main one.
// Hopefully Vulkan will improve this?
Ref<ArrayMesh> mesh;
mesh.instance();
int surface_index = 0;
for (int i = 0; i < ob.model_surfaces.size(); ++i) {
Array surface = ob.model_surfaces[i];
if (surface.empty())
continue;
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, surface);
mesh->surface_set_material(surface_index, _materials[i]);
++surface_index;
}
for(int i = 0; i < ob.smooth_surfaces.size(); ++i) {
Array surface = ob.smooth_surfaces[i];
if (surface.empty())
continue;
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, surface);
// No material supported yet
++surface_index;
}
if (is_mesh_empty(mesh))
mesh = Ref<Mesh>();
block->set_mesh(mesh, world);
clear_block_update_state(ob.position);
}
uint32_t time_taken = os.get_ticks_msec() - time_before;
_stats.mesh_alloc_time = time_taken;
} }
//print_line(String("d:") + String::num(_dirty_blocks.size()) + String(", q:") + String::num(_block_update_queue.size())); //print_line(String("d:") + String::num(_dirty_blocks.size()) + String(", q:") + String::num(_block_update_queue.size()));
} }
static inline bool is_mesh_empty(Ref<Mesh> mesh_ref) { // To be called once a block is updated
if (mesh_ref.is_null()) void VoxelTerrain::clear_block_update_state(Vector3i block_pos) {
return true; VoxelTerrain::BlockDirtyState *state = _dirty_blocks.getptr(block_pos);
const Mesh &mesh = **mesh_ref; if (state) {
if (mesh.get_surface_count() == 0) if (*state == BLOCK_UPDATE)
return true; _dirty_blocks.erase(block_pos);
if (mesh.surface_get_array_len(0) == 0) else
return true; ;//print_line("Block update found non-update state");
return false; } else {
} ;//print_line("Block update found no update state");
void VoxelTerrain::update_block_mesh(Vector3i block_pos) {
VoxelBlock *block = _map->get_block(block_pos);
if (block == NULL) {
return;
} }
VOXEL_PROFILE_BEGIN("voxel_buffer_creation_extract")
// Create buffer padded with neighbor voxels
VoxelBuffer nbuffer;
// TODO Make the buffer re-usable
// TODO Padding set to 3 at the moment because Transvoxel works on 2x2 cells.
// It should change for a smarter padding (if smooth isn't used for example).
unsigned int block_size = _map->get_block_size();
nbuffer.create(block_size + 3, block_size + 3, block_size + 3);
VOXEL_PROFILE_END("voxel_buffer_creation_extract")
VOXEL_PROFILE_BEGIN("block_extraction")
_map->get_buffer_copy(_map->block_to_voxel(block_pos) - Vector3i(1, 1, 1), nbuffer, 0x3);
VOXEL_PROFILE_END("block_extraction")
// TODO Re-use existing meshes to optimize memory cost
// Build cubic parts of the mesh
Ref<ArrayMesh> mesh = _mesher->build(nbuffer, Voxel::CHANNEL_TYPE, Vector3i(0, 0, 0), nbuffer.get_size() - Vector3(1, 1, 1));
// Build smooth parts of the mesh
_mesher_smooth->build(nbuffer, Voxel::CHANNEL_ISOLEVEL, mesh);
if(is_mesh_empty(mesh))
mesh = Ref<Mesh>();
Ref<World> world = get_world();
block->set_mesh(mesh, world);
} }
//void VoxelTerrain::block_removed(VoxelBlock & block) { //void VoxelTerrain::block_removed(VoxelBlock & block) {
@ -647,9 +769,6 @@ void VoxelTerrain::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_view_distance", "distance_in_voxels"), &VoxelTerrain::set_view_distance); ClassDB::bind_method(D_METHOD("set_view_distance", "distance_in_voxels"), &VoxelTerrain::set_view_distance);
ClassDB::bind_method(D_METHOD("get_view_distance"), &VoxelTerrain::get_view_distance); ClassDB::bind_method(D_METHOD("get_view_distance"), &VoxelTerrain::get_view_distance);
ClassDB::bind_method(D_METHOD("get_block_update_count"), &VoxelTerrain::get_block_update_count);
ClassDB::bind_method(D_METHOD("get_mesher"), &VoxelTerrain::get_mesher);
ClassDB::bind_method(D_METHOD("get_generate_collisions"), &VoxelTerrain::get_generate_collisions); ClassDB::bind_method(D_METHOD("get_generate_collisions"), &VoxelTerrain::get_generate_collisions);
ClassDB::bind_method(D_METHOD("set_generate_collisions", "enabled"), &VoxelTerrain::set_generate_collisions); ClassDB::bind_method(D_METHOD("set_generate_collisions", "enabled"), &VoxelTerrain::set_generate_collisions);
@ -661,15 +780,13 @@ void VoxelTerrain::_bind_methods() {
ClassDB::bind_method(D_METHOD("voxel_to_block", "voxel_pos"), &VoxelTerrain::_voxel_to_block_binding); ClassDB::bind_method(D_METHOD("voxel_to_block", "voxel_pos"), &VoxelTerrain::_voxel_to_block_binding);
ClassDB::bind_method(D_METHOD("block_to_voxel", "block_pos"), &VoxelTerrain::_block_to_voxel_binding); ClassDB::bind_method(D_METHOD("block_to_voxel", "block_pos"), &VoxelTerrain::_block_to_voxel_binding);
ClassDB::bind_method(D_METHOD("make_block_dirty", "pos"), &VoxelTerrain::_make_block_dirty_binding); //ClassDB::bind_method(D_METHOD("make_block_dirty", "pos"), &VoxelTerrain::_make_block_dirty_binding);
ClassDB::bind_method(D_METHOD("make_blocks_dirty", "min", "size"), &VoxelTerrain::_make_blocks_dirty_binding); //ClassDB::bind_method(D_METHOD("make_blocks_dirty", "min", "size"), &VoxelTerrain::_make_blocks_dirty_binding);
ClassDB::bind_method(D_METHOD("make_voxel_dirty", "pos"), &VoxelTerrain::_make_voxel_dirty_binding); ClassDB::bind_method(D_METHOD("make_voxel_dirty", "pos"), &VoxelTerrain::_make_voxel_dirty_binding);
ClassDB::bind_method(D_METHOD("raycast", "origin", "direction", "max_distance"), &VoxelTerrain::_raycast_binding, DEFVAL(100)); ClassDB::bind_method(D_METHOD("raycast", "origin", "direction", "max_distance"), &VoxelTerrain::_raycast_binding, DEFVAL(100));
#ifdef VOXEL_PROFILING ClassDB::bind_method(D_METHOD("get_statistics"), &VoxelTerrain::get_statistics);
ClassDB::bind_method(D_METHOD("get_profiling_info"), &VoxelTerrain::get_profiling_info);
#endif
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "provider", PROPERTY_HINT_RESOURCE_TYPE, "VoxelProvider"), "set_provider", "get_provider"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "provider", PROPERTY_HINT_RESOURCE_TYPE, "VoxelProvider"), "set_provider", "get_provider");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "voxel_library", PROPERTY_HINT_RESOURCE_TYPE, "VoxelLibrary"), "set_voxel_library", "get_voxel_library"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "voxel_library", PROPERTY_HINT_RESOURCE_TYPE, "VoxelLibrary"), "set_voxel_library", "get_voxel_library");

View File

@ -1,12 +1,17 @@
#ifndef VOXEL_TERRAIN_H #ifndef VOXEL_TERRAIN_H
#define VOXEL_TERRAIN_H #define VOXEL_TERRAIN_H
#include "voxel_map.h" #include "vector3i.h"
#include "voxel_mesher.h"
#include "voxel_mesher_smooth.h"
#include "voxel_provider.h"
#include "zprofiling.h" #include "zprofiling.h"
#include <scene/main/node.h> #include "voxel_provider.h"
#include "voxel_provider_thread.h"
#include "voxel_mesh_updater.h"
#include "rect3i.h"
#include <scene/3d/spatial.h>
class VoxelMap;
class VoxelLibrary;
// Infinite static terrain made of voxels. // Infinite static terrain made of voxels.
// It is loaded around VoxelTerrainStreamers. // It is loaded around VoxelTerrainStreamers.
@ -14,6 +19,7 @@ class VoxelTerrain : public Spatial /*, public IVoxelMapObserver*/ {
GDCLASS(VoxelTerrain, Spatial) GDCLASS(VoxelTerrain, Spatial)
public: public:
VoxelTerrain(); VoxelTerrain();
~VoxelTerrain();
void set_provider(Ref<VoxelProvider> provider); void set_provider(Ref<VoxelProvider> provider);
Ref<VoxelProvider> get_provider() const; Ref<VoxelProvider> get_provider() const;
@ -21,13 +27,10 @@ public:
void set_voxel_library(Ref<VoxelLibrary> library); void set_voxel_library(Ref<VoxelLibrary> library);
Ref<VoxelLibrary> get_voxel_library() const; Ref<VoxelLibrary> get_voxel_library() const;
void force_load_blocks(Vector3i center, Vector3i extents);
int get_block_update_count();
void make_block_dirty(Vector3i bpos); void make_block_dirty(Vector3i bpos);
void make_blocks_dirty(Vector3i min, Vector3i size); //void make_blocks_dirty(Vector3i min, Vector3i size);
void make_voxel_dirty(Vector3i pos); void make_voxel_dirty(Vector3i pos);
bool is_block_dirty(Vector3i bpos); bool is_block_dirty(Vector3i bpos) const;
void set_generate_collisions(bool enabled); void set_generate_collisions(bool enabled);
bool get_generate_collisions() const { return _generate_collisions; } bool get_generate_collisions() const { return _generate_collisions; }
@ -41,9 +44,18 @@ public:
void set_material(int id, Ref<Material> material); void set_material(int id, Ref<Material> material);
Ref<Material> get_material(int id) const; Ref<Material> get_material(int id) const;
Ref<VoxelMesher> get_mesher() { return _mesher; }
Ref<VoxelMap> get_map() { return _map; } Ref<VoxelMap> get_map() { return _map; }
struct Stats {
VoxelMeshUpdater::Stats updater;
VoxelProviderThread::Stats provider;
uint32_t mesh_alloc_time;
uint32_t updated_blocks;
Stats(): mesh_alloc_time(0), updated_blocks(0)
{ }
};
protected: protected:
void _notification(int p_what); void _notification(int p_what);
@ -54,26 +66,27 @@ private:
void _process(); void _process();
void update_blocks();
void update_block_mesh(Vector3i block_pos);
void make_all_view_dirty_deferred(); void make_all_view_dirty_deferred();
enum BlockDirtyState {
BLOCK_LOAD,
BLOCK_UPDATE
};
Spatial *get_viewer(NodePath path) const; Spatial *get_viewer(NodePath path) const;
void immerge_block(Vector3i bpos); void immerge_block(Vector3i bpos);
// Observer events Dictionary get_statistics() const;
//void block_removed(VoxelBlock & block);
static void _bind_methods(); static void _bind_methods();
// Convenience // Convenience
Vector3 _voxel_to_block_binding(Vector3 pos); Vector3 _voxel_to_block_binding(Vector3 pos);
Vector3 _block_to_voxel_binding(Vector3 pos); Vector3 _block_to_voxel_binding(Vector3 pos);
void _force_load_blocks_binding(Vector3 center, Vector3 extents) { force_load_blocks(center, extents); } //void _force_load_blocks_binding(Vector3 center, Vector3 extents) { force_load_blocks(center, extents); }
void _make_block_dirty_binding(Vector3 bpos) { make_block_dirty(bpos); } //void _make_block_dirty_binding(Vector3 bpos) { make_block_dirty(bpos); }
void _make_blocks_dirty_binding(Vector3 min, Vector3 size) { make_blocks_dirty(min, size); } //void _make_blocks_dirty_binding(Vector3 min, Vector3 size) { make_blocks_dirty(min, size); }
void _make_voxel_dirty_binding(Vector3 pos) { make_voxel_dirty(pos); } void _make_voxel_dirty_binding(Vector3 pos) { make_voxel_dirty(pos); }
Variant _raycast_binding(Vector3 origin, Vector3 direction, real_t max_distance); Variant _raycast_binding(Vector3 origin, Vector3 direction, real_t max_distance);
@ -81,6 +94,10 @@ private:
void set_voxel(Vector3 pos, int value, int c); void set_voxel(Vector3 pos, int value, int c);
int get_voxel(Vector3 pos, int c); int get_voxel(Vector3 pos, int c);
void clear_block_update_state(Vector3i block_pos);
static void remove_positions_outside_box(Vector<Vector3i> &positions, Rect3i box, HashMap<Vector3i, BlockDirtyState, Vector3iHasher> &state_map);
private: private:
// Voxel storage // Voxel storage
Ref<VoxelMap> _map; Ref<VoxelMap> _map;
@ -91,14 +108,15 @@ private:
// TODO Terrains only need to handle the visible portion of voxels, which reduces the bounds blocks to handle. // TODO Terrains only need to handle the visible portion of voxels, which reduces the bounds blocks to handle.
// Therefore, could a simple grid be better to use than a hashmap? // Therefore, could a simple grid be better to use than a hashmap?
Vector<Vector3i> _block_update_queue; Vector<Vector3i> _blocks_pending_load;
HashMap<Vector3i, bool, Vector3iHasher> _dirty_blocks; // only the key is relevant Vector<Vector3i> _blocks_pending_update;
HashMap<Vector3i, BlockDirtyState, Vector3iHasher> _dirty_blocks; // only the key is relevant
Ref<VoxelMesher> _mesher;
// TODO I'm not sure it will stay here... refactoring ahead
Ref<VoxelMesherSmooth> _mesher_smooth;
Ref<VoxelProvider> _provider; Ref<VoxelProvider> _provider;
VoxelProviderThread *_provider_thread;
Ref<VoxelLibrary> _library;
VoxelMeshUpdater *_block_updater;
NodePath _viewer_path; NodePath _viewer_path;
Vector3i _last_viewer_block_pos; Vector3i _last_viewer_block_pos;
@ -106,10 +124,9 @@ private:
bool _generate_collisions; bool _generate_collisions;
#ifdef VOXEL_PROFILING Ref<Material> _materials[VoxelMesher::MAX_MATERIALS];
ZProfiler _zprofiler;
Dictionary get_profiling_info() { return _zprofiler.get_all_serialized_info(); } Stats _stats;
#endif
}; };
#endif // VOXEL_TERRAIN_H #endif // VOXEL_TERRAIN_H