Attempting to give priority to main mesh building over transition meshes.

This effectively creates them after ALL main meshes have been built,
resulting in a seemingly faster convergence of LOD, however when moving
around it causes some cracks to appear briefly. This can be bad if the
terrain is dark and the underlying skybox is bright. Might need to be
optional, or find a different approach.
This commit is contained in:
Marc Gilleron 2022-06-22 22:05:25 +01:00
parent 76de7a439b
commit c7d8c2557c
10 changed files with 258 additions and 80 deletions

View File

@ -48,6 +48,7 @@ struct MeshArrays {
int vi = vertices.size();
vertices.push_back(primary);
normals.push_back(normal);
// TODO Use an explicit struct for this, and use floatToBits in shader so we can pack more data safely
lod_data.push_back(Color(secondary.x, secondary.y, secondary.z, border_mask));
return vi;
}

View File

@ -203,8 +203,9 @@ bool VoxelServer::viewer_exists(uint32_t viewer_id) const {
return _world.viewers.is_valid(viewer_id);
}
void VoxelServer::push_main_thread_time_spread_task(zylann::ITimeSpreadTask *task) {
_time_spread_task_runner.push(task);
void VoxelServer::push_main_thread_time_spread_task(
zylann::ITimeSpreadTask *task, TimeSpreadTaskRunner::Priority priority) {
_time_spread_task_runner.push(task, priority);
}
void VoxelServer::push_main_thread_progressive_task(zylann::IProgressiveTask *task) {

View File

@ -50,7 +50,7 @@ public:
};
struct VolumeCallbacks {
void (*mesh_output_callback)(void *, const BlockMeshOutput &) = nullptr;
void (*mesh_output_callback)(void *, BlockMeshOutput &) = nullptr;
void (*data_output_callback)(void *, BlockDataOutput &) = nullptr;
void *data = nullptr;

View File

@ -58,12 +58,12 @@ VoxelTerrain::VoxelTerrain() {
// because this kind of task scheduling would otherwise delay the update by 1 frame
VoxelServer::VolumeCallbacks callbacks;
callbacks.data = this;
callbacks.mesh_output_callback = [](void *cb_data, const VoxelServer::BlockMeshOutput &ob) {
callbacks.mesh_output_callback = [](void *cb_data, VoxelServer::BlockMeshOutput &ob) {
VoxelTerrain *self = reinterpret_cast<VoxelTerrain *>(cb_data);
ApplyMeshUpdateTask *task = memnew(ApplyMeshUpdateTask);
task->volume_id = self->_volume_id;
task->self = self;
task->data = ob;
task->data = std::move(ob);
VoxelServer::get_singleton().push_main_thread_time_spread_task(task);
};
callbacks.data_output_callback = [](void *cb_data, VoxelServer::BlockDataOutput &ob) {

View File

@ -0,0 +1,33 @@
#include "build_transition_mesh_task.h"
#include "voxel_lod_terrain.h"
namespace zylann::voxel {
void BuildTransitionMeshTask::run(TimeSpreadTaskContext &ctx) {
ZN_PROFILE_SCOPE();
if (block == nullptr) {
// Got cancelled
return;
}
if (!VoxelServer::get_singleton().is_volume_valid(volume_id)) {
// Terrain got destroyed
return;
}
Ref<Material> material = volume->get_material();
const Transform3D transform = volume->get_global_transform();
const DirectMeshInstance::GIMode gi_mode = DirectMeshInstance::GIMode(volume->get_gi_mode());
Ref<ArrayMesh> transition_mesh =
build_mesh(to_span(mesh_data), Mesh::PrimitiveType(primitive_type), mesh_flags, material);
block->set_transition_mesh(transition_mesh, side, gi_mode);
if (transition_mesh.is_valid()) {
block->update_transition_mesh_transform(side, transform);
}
block->on_transition_mesh_task_completed(*this);
}
} // namespace zylann::voxel

View File

@ -0,0 +1,30 @@
#ifndef VOXEL_BUILD_TRANSITION_MESH_TASK_H
#define VOXEL_BUILD_TRANSITION_MESH_TASK_H
#include "../../meshers/voxel_mesher.h"
#include "../../util/tasks/time_spread_task_runner.h"
namespace zylann::voxel {
class VoxelLodTerrain;
class VoxelMeshBlockVLT;
// This mess is because Godot takes a non-neglibeable base amount of time to build meshe, regardless of their size
// (About 50us, so 300us for all 6 transitions of one block). Transition meshes are rarely needed immediately, so we
// defer them in some cases.
class BuildTransitionMeshTask : public ITimeSpreadTask {
public:
void run(TimeSpreadTaskContext &ctx) override;
VoxelLodTerrain *volume;
uint32_t volume_id;
uint32_t mesh_flags;
uint8_t primitive_type;
uint8_t side;
std::vector<VoxelMesher::Output::Surface> mesh_data;
VoxelMeshBlockVLT *block = nullptr; // To be set only when scheduled
};
} // namespace zylann::voxel
#endif // VOXEL_BUILD_TRANSITION_MESH_TASK_H

View File

@ -19,6 +19,7 @@
#include "../../util/thread/mutex.h"
#include "../../util/thread/rw_lock.h"
#include "../instancing/voxel_instancer.h"
#include "build_transition_mesh_task.h"
#include "voxel_lod_terrain_update_task.h"
#include <core/config/engine.h>
@ -30,58 +31,6 @@ namespace zylann::voxel {
namespace {
Ref<ArrayMesh> build_mesh(Span<const VoxelMesher::Output::Surface> surfaces, Mesh::PrimitiveType primitive, int flags,
Ref<Material> material) {
ZN_PROFILE_SCOPE();
Ref<ArrayMesh> mesh;
unsigned int surface_index = 0;
for (unsigned int i = 0; i < surfaces.size(); ++i) {
const VoxelMesher::Output::Surface &surface = surfaces[i];
Array arrays = surface.arrays;
if (arrays.is_empty()) {
continue;
}
CRASH_COND(arrays.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(arrays)) {
continue;
}
if (mesh.is_null()) {
mesh.instantiate();
}
// TODO Use `add_surface`, it's about 20% faster after measuring in Tracy (though we may see if Godot 4 expects
// the same)
mesh->add_surface_from_arrays(primitive, arrays, Array(), Dictionary(), flags);
mesh->surface_set_material(surface_index, material);
// No multi-material supported yet
++surface_index;
}
// Debug code to highlight vertex sharing
/*if (mesh->get_surface_count() > 0) {
Array wireframe_surface = generate_debug_seams_wireframe_surface(mesh, 0);
if (wireframe_surface.size() > 0) {
const int wireframe_surface_index = mesh->get_surface_count();
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_LINES, wireframe_surface);
Ref<SpatialMaterial> line_material;
line_material.instance();
line_material->set_flag(SpatialMaterial::FLAG_UNSHADED, true);
line_material->set_albedo(Color(1.0, 0.0, 1.0));
mesh->surface_set_material(wireframe_surface_index, line_material);
}
}*/
if (mesh.is_valid() && is_mesh_empty(**mesh)) {
mesh = Ref<Mesh>();
}
return mesh;
}
struct BeforeUnloadMeshAction {
std::vector<Ref<ShaderMaterial>> &shader_material_pool;
@ -93,6 +42,8 @@ struct BeforeUnloadMeshAction {
shader_material_pool.push_back(sm);
block.set_shader_material(Ref<ShaderMaterial>());
}
// Optimization: do it now because the actual destructor could be called much later
block.cancel_transition_mesh_tasks();
}
};
@ -179,12 +130,12 @@ VoxelLodTerrain::VoxelLodTerrain() {
// because this kind of task scheduling would otherwise delay the update by 1 frame
VoxelServer::VolumeCallbacks callbacks;
callbacks.data = this;
callbacks.mesh_output_callback = [](void *cb_data, const VoxelServer::BlockMeshOutput &ob) {
callbacks.mesh_output_callback = [](void *cb_data, VoxelServer::BlockMeshOutput &ob) {
VoxelLodTerrain *self = reinterpret_cast<VoxelLodTerrain *>(cb_data);
ApplyMeshUpdateTask *task = memnew(ApplyMeshUpdateTask);
task->volume_id = self->get_volume_id();
task->self = self;
task->data = ob;
task->data = std::move(ob);
VoxelServer::get_singleton().push_main_thread_time_spread_task(task);
// If two tasks are queued for the same mesh, cancel the old ones.
@ -1437,7 +1388,20 @@ void VoxelLodTerrain::apply_data_block_response(VoxelServer::BlockDataOutput &ob
}
}
void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
static bool is_surface_data_empty(Span<const VoxelMesher::Output::Surface> surfaces) {
for (unsigned int i = 0; i < surfaces.size(); ++i) {
const VoxelMesher::Output::Surface &surface = surfaces[i];
if (surface.arrays.size() != 0) {
const PackedVector3Array vertex_array = surface.arrays[Mesh::ARRAY_VERTEX];
if (vertex_array.size() > 0) {
return false;
}
}
}
return true;
}
void VoxelLodTerrain::apply_mesh_update(VoxelServer::BlockMeshOutput &ob) {
// The following is done on the main thread because Godot doesn't really support multithreaded Mesh allocation.
// This also proved to be very slow compared to the meshing process itself...
// hopefully Vulkan will allow us to upload graphical resources without stalling rendering as they upload?
@ -1488,7 +1452,7 @@ void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob)
VoxelMeshMap<VoxelMeshBlockVLT> &mesh_map = _mesh_maps_per_lod[ob.lod];
VoxelMeshBlockVLT *block = mesh_map.get_block(ob.position);
const VoxelMesher::Output &mesh_data = ob.surfaces;
VoxelMesher::Output &mesh_data = ob.surfaces;
Ref<ArrayMesh> mesh =
build_mesh(to_span_const(mesh_data.surfaces), mesh_data.primitive_type, mesh_data.mesh_flags, _material);
@ -1562,23 +1526,50 @@ void VoxelLodTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob)
}
block->set_mesh(mesh, DirectMeshInstance::GIMode(get_gi_mode()));
{
// TODO Optimize: don't build transition meshes until they need to be shown.
// Profiling has shown Godot takes as much time to build one as the main mesh of a block, so because there are 6
// transition meshes, we spend about 80% of the time on these. Which is counter-intuitive because transition
// meshes are tiny in comparison... (collision meshes still take 5x more time than building ALL rendering meshes
// but that's a different issue)
ZN_PROFILE_SCOPE_NAMED("Transition meshes");
for (unsigned int dir = 0; dir < mesh_data.transition_surfaces.size(); ++dir) {
Ref<ArrayMesh> transition_mesh = build_mesh(to_span(mesh_data.transition_surfaces[dir]),
mesh_data.primitive_type, mesh_data.mesh_flags, _material);
block->set_transition_mesh(transition_mesh, dir, DirectMeshInstance::GIMode(get_gi_mode()));
{
// Profiling has shown Godot takes as much time to build a transition mesh as the main mesh of a block, so
// because there are 6 transition meshes per block, we would spend about 80% of the time on these if we build
// them all. Which is counter-intuitive because transition meshes are tiny in comparison... (collision meshes
// still take 5x more time than building ALL rendering meshes but that's a different issue)
ZN_PROFILE_SCOPE_NAMED("Transition meshes");
for (unsigned int dir = 0; dir < mesh_data.transition_surfaces.size(); ++dir) {
std::vector<VoxelMesher::Output::Surface> &mesh_data_for_side = mesh_data.transition_surfaces[dir];
if ((transition_mask & (1 << dir)) != 0) {
// The mesh may currently be visible, build it now
Ref<ArrayMesh> transition_mesh = build_mesh(
to_span(mesh_data_for_side), mesh_data.primitive_type, mesh_data.mesh_flags, _material);
block->set_transition_mesh(transition_mesh, dir, DirectMeshInstance::GIMode(get_gi_mode()));
block->cancel_transition_mesh_task(dir);
} else if (is_surface_data_empty(to_span(mesh_data_for_side))) {
// No need to bother, just reset
block->set_transition_mesh(Ref<Mesh>(), dir, DirectMeshInstance::GIMode(get_gi_mode()));
block->cancel_transition_mesh_task(dir);
} else {
// We dont need this mesh immediately, defer it for later:
// One main use case is edits, which are often done in blocks that have no transitions visible.
BuildTransitionMeshTask *task = ZN_NEW(BuildTransitionMeshTask);
task->volume = this;
task->volume_id = _volume_id;
task->mesh_data = std::move(mesh_data_for_side);
task->side = dir;
task->mesh_flags = mesh_data.mesh_flags;
task->primitive_type = mesh_data.primitive_type;
task->block = block;
block->set_pending_transition_mesh_task(dir, *task);
VoxelServer::get_singleton().push_main_thread_time_spread_task(
task, TimeSpreadTaskRunner::PRIORITY_LOW);
}
}
}
if (has_collision) {
const uint32_t now = get_ticks_msec();
const uint64_t now = get_ticks_msec();
if (_collision_update_delay == 0 ||
static_cast<int>(now - block->last_collider_update_time) > _collision_update_delay) {

View File

@ -265,7 +265,7 @@ private:
void _process(float delta);
void apply_main_thread_update_tasks();
void apply_mesh_update(const VoxelServer::BlockMeshOutput &ob);
void apply_mesh_update(VoxelServer::BlockMeshOutput &ob);
void apply_data_block_response(VoxelServer::BlockDataOutput &ob);
void start_updater();
@ -305,6 +305,8 @@ private:
static void _bind_methods();
private:
friend class BuildTransitionMeshTask;
uint32_t _volume_id = 0;
ProcessCallback _process_callback = PROCESS_CALLBACK_IDLE;

View File

@ -1,7 +1,9 @@
#include "voxel_mesh_block_vlt.h"
#include "../../constants/voxel_string_names.h"
#include "../../util/godot/funcs.h"
#include "../../util/profiling.h"
#include "../free_mesh_task.h"
#include "build_transition_mesh_task.h"
namespace zylann::voxel {
@ -9,6 +11,7 @@ VoxelMeshBlockVLT::VoxelMeshBlockVLT(const Vector3i bpos, unsigned int size, uns
VoxelMeshBlock(bpos) {
_position_in_voxels = bpos * (size << p_lod_index);
lod_index = p_lod_index;
fill(_pending_transition_mesh_tasks, (BuildTransitionMeshTask *)nullptr);
#ifdef VOXEL_DEBUG_LOD_MATERIALS
Ref<SpatialMaterial> debug_material;
@ -31,6 +34,7 @@ VoxelMeshBlockVLT::~VoxelMeshBlockVLT() {
for (unsigned int i = 0; i < _transition_mesh_instances.size(); ++i) {
FreeMeshTask::try_add_and_destroy(_transition_mesh_instances[i]);
}
cancel_transition_mesh_tasks();
}
void VoxelMeshBlockVLT::set_mesh(Ref<Mesh> mesh, DirectMeshInstance::GIMode gi_mode) {
@ -74,7 +78,41 @@ void VoxelMeshBlockVLT::set_gi_mode(DirectMeshInstance::GIMode mode) {
}
}
void VoxelMeshBlockVLT::set_transition_mesh(Ref<Mesh> mesh, int side, DirectMeshInstance::GIMode gi_mode) {
void VoxelMeshBlockVLT::set_pending_transition_mesh_task(unsigned int side, BuildTransitionMeshTask &task) {
BuildTransitionMeshTask *prev_task = _pending_transition_mesh_tasks[side];
if (prev_task != nullptr) {
// This pointer should be valid because the task is assumed to unregister itself from the block when it
// completes.
prev_task->block = nullptr;
}
_pending_transition_mesh_tasks[side] = &task;
}
void VoxelMeshBlockVLT::on_transition_mesh_task_completed(const BuildTransitionMeshTask &p_task) {
const unsigned int side = p_task.side;
const BuildTransitionMeshTask *task = _pending_transition_mesh_tasks[side];
if (task == &p_task) {
_pending_transition_mesh_tasks[side] = nullptr;
}
}
void VoxelMeshBlockVLT::cancel_transition_mesh_tasks() {
for (unsigned int i = 0; i < _pending_transition_mesh_tasks.size(); ++i) {
cancel_transition_mesh_task(i);
}
}
void VoxelMeshBlockVLT::cancel_transition_mesh_task(unsigned int side) {
BuildTransitionMeshTask *task = _pending_transition_mesh_tasks[side];
if (task != nullptr) {
// This pointer should be valid because the task is assumed to unregister itself from the block when it
// completes.
task->block = nullptr;
_pending_transition_mesh_tasks[side] = nullptr;
}
}
void VoxelMeshBlockVLT::set_transition_mesh(Ref<Mesh> mesh, unsigned int side, DirectMeshInstance::GIMode gi_mode) {
DirectMeshInstance &mesh_instance = _transition_mesh_instances[side];
if (mesh.is_valid()) {
@ -82,8 +120,8 @@ void VoxelMeshBlockVLT::set_transition_mesh(Ref<Mesh> mesh, int side, DirectMesh
// Create instance if it doesn't exist
mesh_instance.create();
mesh_instance.set_gi_mode(gi_mode);
set_mesh_instance_visible(mesh_instance, _visible && _parent_visible && _is_transition_visible(side));
}
set_mesh_instance_visible(mesh_instance, _visible && _parent_visible && _is_transition_visible(side));
mesh_instance.set_mesh(mesh);
@ -190,7 +228,7 @@ void VoxelMeshBlockVLT::set_transition_mask(uint8_t m) {
}
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
DirectMeshInstance &mi = _transition_mesh_instances[dir];
if ((diff & (1 << dir)) && mi.is_valid()) {
if (mi.is_valid() && (diff & (1 << dir))) {
set_mesh_instance_visible(mi, _visible && _parent_visible && _is_transition_visible(dir));
}
}
@ -208,6 +246,7 @@ void VoxelMeshBlockVLT::set_parent_transform(const Transform3D &parent_transform
ZN_PROFILE_SCOPE();
if (_mesh_instance.is_valid() || _static_body.is_valid()) {
// TODO Optimize: could be optimized due to the basis being identity
const Transform3D local_transform(Basis(), _position_in_voxels);
const Transform3D world_transform = parent_transform * local_transform;
@ -228,6 +267,16 @@ void VoxelMeshBlockVLT::set_parent_transform(const Transform3D &parent_transform
}
}
void VoxelMeshBlockVLT::update_transition_mesh_transform(unsigned int side, const Transform3D &parent_transform) {
DirectMeshInstance &mi = _transition_mesh_instances[side];
if (mi.is_valid()) {
// TODO Optimize: could be optimized due to the basis being identity
const Transform3D local_transform(Basis(), _position_in_voxels);
const Transform3D world_transform = parent_transform * local_transform;
mi.set_transform(world_transform);
}
}
// Returns `true` when finished
bool VoxelMeshBlockVLT::update_fading(float speed) {
// TODO Should probably not be on the block directly?
@ -280,4 +329,58 @@ bool VoxelMeshBlockVLT::update_fading(float speed) {
return finished;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Ref<ArrayMesh> build_mesh(Span<const VoxelMesher::Output::Surface> surfaces, Mesh::PrimitiveType primitive, int flags,
Ref<Material> material) {
ZN_PROFILE_SCOPE();
Ref<ArrayMesh> mesh;
unsigned int surface_index = 0;
for (unsigned int i = 0; i < surfaces.size(); ++i) {
const VoxelMesher::Output::Surface &surface = surfaces[i];
Array arrays = surface.arrays;
if (arrays.is_empty()) {
continue;
}
CRASH_COND(arrays.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(arrays)) {
continue;
}
if (mesh.is_null()) {
mesh.instantiate();
}
// TODO Use `add_surface`, it's about 20% faster after measuring in Tracy (though we may see if Godot 4 expects
// the same)
mesh->add_surface_from_arrays(primitive, arrays, Array(), Dictionary(), flags);
mesh->surface_set_material(surface_index, material);
// No multi-material supported yet
++surface_index;
}
// Debug code to highlight vertex sharing
/*if (mesh->get_surface_count() > 0) {
Array wireframe_surface = generate_debug_seams_wireframe_surface(mesh, 0);
if (wireframe_surface.size() > 0) {
const int wireframe_surface_index = mesh->get_surface_count();
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_LINES, wireframe_surface);
Ref<SpatialMaterial> line_material;
line_material.instance();
line_material->set_flag(SpatialMaterial::FLAG_UNSHADED, true);
line_material->set_albedo(Color(1.0, 0.0, 1.0));
mesh->surface_set_material(wireframe_surface_index, line_material);
}
}*/
if (mesh.is_valid() && is_mesh_empty(**mesh)) {
mesh = Ref<Mesh>();
}
return mesh;
}
} // namespace zylann::voxel

View File

@ -1,10 +1,13 @@
#ifndef VOXEL_MESH_BLOCK_VLT_H
#define VOXEL_MESH_BLOCK_VLT_H
#include "../../util/tasks/time_spread_task_runner.h"
#include "../voxel_mesh_block.h"
namespace zylann::voxel {
class BuildTransitionMeshTask;
// Stores mesh and collider for one chunk of `VoxelTerrain`.
// It doesn't store voxel data, because it may be using different block size, or different data structure.
class VoxelMeshBlockVLT : public VoxelMeshBlock {
@ -29,7 +32,7 @@ public:
bool got_first_mesh_update = false;
uint32_t last_collider_update_time = 0;
uint64_t last_collider_update_time = 0;
bool has_deferred_collider_update = false;
std::vector<Array> deferred_collider_data;
@ -50,13 +53,21 @@ public:
}
void set_gi_mode(DirectMeshInstance::GIMode mode);
void set_transition_mesh(Ref<Mesh> mesh, int side, DirectMeshInstance::GIMode gi_mode);
void set_transition_mesh(Ref<Mesh> mesh, unsigned int side, DirectMeshInstance::GIMode gi_mode);
void set_pending_transition_mesh_task(unsigned int side, BuildTransitionMeshTask &task);
void on_transition_mesh_task_completed(const BuildTransitionMeshTask &task);
void cancel_transition_mesh_tasks();
void cancel_transition_mesh_task(unsigned int side);
void set_shader_material(Ref<ShaderMaterial> material);
inline Ref<ShaderMaterial> get_shader_material() const {
return _shader_material;
}
void set_parent_transform(const Transform3D &parent_transform);
void update_transition_mesh_transform(unsigned int side, const Transform3D &parent_transform);
template <typename F>
void for_each_mesh_instance_with_transform(F f) const {
@ -74,12 +85,15 @@ public:
private:
void _set_visible(bool visible);
inline bool _is_transition_visible(int side) const {
inline bool _is_transition_visible(unsigned int side) const {
return _transition_mask & (1 << side);
}
Ref<ShaderMaterial> _shader_material;
// Not owned. Keeping a reference to cancel it if the block gets destroyed.
FixedArray<BuildTransitionMeshTask *, Cube::SIDE_COUNT> _pending_transition_mesh_tasks;
FixedArray<DirectMeshInstance, Cube::SIDE_COUNT> _transition_mesh_instances;
uint8_t _transition_mask = 0;
@ -90,6 +104,9 @@ private:
#endif
};
Ref<ArrayMesh> build_mesh(Span<const VoxelMesher::Output::Surface> surfaces, Mesh::PrimitiveType primitive, int flags,
Ref<Material> material);
} // namespace zylann::voxel
#endif // VOXEL_MESH_BLOCK_VLT_H