diff --git a/.dir-locals.el b/.dir-locals.el index 50d1a911..967cb411 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,3 +1,3 @@ -((c++-mode . (mode . clang-format-on-save))) +((c++-mode . ((mode . clang-format-on-save)))) ((c-mode . ((mode c++-mode)))) diff --git a/modules/terrain_editor/terrain_chunk.cpp b/modules/terrain_editor/terrain_chunk.cpp index dbca0bbd..83d48ad9 100644 --- a/modules/terrain_editor/terrain_chunk.cpp +++ b/modules/terrain_editor/terrain_chunk.cpp @@ -1,7 +1,14 @@ #include "terrain_chunk.h" +#include "core/config/engine.h" +#include "macros.h" +#include "scene/3d/camera_3d.h" +#include "scene/main/viewport.h" #include "terrain_editor/terrain_mesh_generator.h" -void TerrainChunk::_bind_methods() {} +void TerrainChunk::_bind_methods() { + BIND_PROPERTY(Variant::FLOAT, size); + BIND_PROPERTY(Variant::INT, lod0_detail); +} void TerrainChunk::ready() { if ((this->generator = cast_to(get_parent()))) { @@ -10,41 +17,85 @@ void TerrainChunk::ready() { print_error(vformat("Chunk %s ready without generator.", get_path())); return; } - this->set_mesh(this->lod1); + process_lod(); on_terrain_changed(); } void TerrainChunk::on_terrain_changed() { if (this->generator) { Vector3 const position{ get_global_position() }; - this->generator->generate_grid({ { position.x, position.z }, { this->size, this->size } }, this->mesh, this->lod1_detail); + this->meshes.resize_zeroed(5); + size_t lod{ 0 }; + for (MeshStatus &status : this->meshes) { + if (!status.mesh.is_valid()) { + status.mesh.instantiate(); + } + size_t base_detail{ lod == 0 ? this->lod0_detail : this->lod0_detail / (2 * lod * lod) }; + status.dirty = true; + this->generator->push_task({ { position.x, position.z }, { this->size, this->size } }, status.mesh, base_detail > 1 ? base_detail : 1, callable_mp(this, &self_type::lod_generated).bind(lod)); + lod++; + } + } +} + +void TerrainChunk::lod_generated(size_t lod) { + this->meshes.set(lod, MeshStatus{ this->meshes[lod].mesh, false }); +} + +void TerrainChunk::process_lod() { + size_t result{ (size_t)this->meshes.size() }; + if (is_ready() && this->meshes.size() > 0) { + Vector3 position{ get_global_position() }; + Vector3 camera{ get_viewport()->get_camera_3d()->get_global_position() }; + position.y = camera.y = 0; + float distance{ camera.distance_to(position) }; + size_t lod{ size_t(Math::floor(distance / (this->lod4_distance / 5.f))) }; + result = lod < 5 ? lod : 4; + while (this->meshes[result].dirty && result < (this->meshes.size() - 1)) { + result++; + } + if (this->meshes[result].mesh != this->get_mesh()) { + this->set_mesh(this->meshes[result].mesh); + } } } void TerrainChunk::_notification(int what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } switch (what) { default: return; case NOTIFICATION_READY: + set_process_thread_group(ProcessThreadGroup::PROCESS_THREAD_GROUP_SUB_THREAD); + set_process(true); ready(); return; + case NOTIFICATION_PROCESS: + process_lod(); + return; } } void TerrainChunk::set_size(float size) { this->size = size; - on_terrain_changed(); + if (is_ready()) { + on_terrain_changed(); + } } float TerrainChunk::get_size() const { return this->size; } -void TerrainChunk::set_lod1_detail(int detail) { - this->lod1_detail = detail; - on_terrain_changed(); +void TerrainChunk::set_lod0_detail(int detail) { + this->lod0_detail = detail; + if (is_ready()) { + on_terrain_changed(); + } } -int TerrainChunk::get_lod1_detail() const { - return this->lod1_detail; +int TerrainChunk::get_lod0_detail() const { + return this->lod0_detail; } diff --git a/modules/terrain_editor/terrain_chunk.h b/modules/terrain_editor/terrain_chunk.h index 8598a03a..3359d385 100644 --- a/modules/terrain_editor/terrain_chunk.h +++ b/modules/terrain_editor/terrain_chunk.h @@ -1,13 +1,21 @@ #pragma once +#include "core/templates/pair.h" +#include "core/templates/vector.h" #include "scene/3d/mesh_instance_3d.h" class TerrainMeshGenerator; class TerrainChunk : public MeshInstance3D { GDCLASS(TerrainChunk, MeshInstance3D); + struct MeshStatus { + Ref mesh; + bool dirty; + }; static void _bind_methods(); void ready(); void on_terrain_changed(); + void lod_generated(size_t lod); + void process_lod(); protected: void _notification(int what); @@ -15,12 +23,13 @@ protected: public: void set_size(float size); float get_size() const; - void set_lod1_detail(int detail); - int get_lod1_detail() const; + void set_lod0_detail(int detail); + int get_lod0_detail() const; private: - Ref lod1{ memnew(ArrayMesh) }; + Vector meshes{}; TerrainMeshGenerator *generator{ nullptr }; - int lod1_detail{ 100 }; - float size{ 50 }; + int lod0_detail{ 200 }; + float lod4_distance{ 1500 }; + float size{ 200 }; }; diff --git a/modules/terrain_editor/terrain_mesh_generator.cpp b/modules/terrain_editor/terrain_mesh_generator.cpp index aeeed8b7..569f3fd2 100644 --- a/modules/terrain_editor/terrain_mesh_generator.cpp +++ b/modules/terrain_editor/terrain_mesh_generator.cpp @@ -23,31 +23,79 @@ void TerrainMeshGenerator::_bind_methods() { BIND_HPROPERTY(Variant::OBJECT, chunk_scene, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); ADD_SIGNAL(MethodInfo(sig_primitives_changed)); ADD_SIGNAL(MethodInfo(sig_primitive_list_changed, PropertyInfo(Variant::ARRAY, "array", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainPrimitive", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE)))); - ClassDB::bind_method(D_METHOD("generate_grid", "area", "out_mesh", "side_points"), &self_type::generate_grid); + ClassDB::bind_method(D_METHOD("push_task", "area", "out_mesh", "side_points"), &self_type::push_task); } -void TerrainMeshGenerator::ready() { - this->thread.instantiate(); - if (this->chunks.is_empty()) { - rebuild_chunks(); +void TerrainMeshGenerator::enter_tree() { + this->thread.start(&TerrainMeshGenerator::background_generation_thread, this); +} + +void TerrainMeshGenerator::process() { + if (this->output_lock.try_lock()) { + for (TerrainMeshTask &task : this->output_queue) { + task.mesh->clear_surfaces(); + task.out_surface->commit(task.mesh); + task.callback.call(); + } + this->output_queue.clear(); + this->output_lock.unlock(); + } + if (this->input_lock.try_lock()) { + set_process(!this->input_queue.is_empty()); + this->input_lock.unlock(); } } void TerrainMeshGenerator::on_configuration_changed() { - emit_signal(sig_primitives_changed); + this->settings_lock.lock(); + // copy main thread primitives buffer data to worker thread buffer + this->primitives.clear(); + for (Variant var : this->primitives_buffer) { + Ref primitive{ var }; + if (primitive.is_valid()) { + this->primitives.push_back(primitive->duplicate(true)); + } + } + this->settings_lock.unlock(); + if (is_ready()) { + emit_signal(sig_primitive_list_changed, get_primitives()); + emit_signal(sig_primitives_changed); + } } void TerrainMeshGenerator::_notification(int what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } switch (what) { default: return; + case NOTIFICATION_ENTER_TREE: + set_process(true); + enter_tree(); + return; case NOTIFICATION_READY: - ready(); + rebuild_chunks(); + break; + case NOTIFICATION_PROCESS: + process(); + return; + case NOTIFICATION_EXIT_TREE: + if (this->thread.is_started()) { + this->input_lock.lock(); + this->end_thread = true; + this->input_lock.unlock(); + this->work_signal.post(); + this->thread.wait_to_finish(); + } return; } } void TerrainMeshGenerator::rebuild_chunks() { + if (!is_ready()) { + return; + } for (TerrainChunk *chunk : this->chunks) { chunk->queue_free(); } @@ -71,6 +119,43 @@ void TerrainMeshGenerator::rebuild_chunks() { } } +void TerrainMeshGenerator::background_generation_thread(void *user) { + TerrainMeshGenerator *self{ static_cast(user) }; + if (self == nullptr) { + return; + } + TerrainMeshTask task{}; + for (;;) { + // await more work signal + self->work_signal.wait(); + // pull task from input queue + self->input_lock.lock(); + if (self->end_thread) { + self->input_lock.unlock(); + break; + } else { + task = self->input_queue[0]; + self->input_queue.remove_at(0); + self->input_lock.unlock(); + // create surface + task.out_surface.instantiate(); + // lock settings and generate grid + self->settings_lock.lock(); + if (self->primitives.is_empty()) { + print_error("Terrain mesh generator thread has no primitive list"); + } else { + self->generate_grid(task.area, task.out_surface, task.side_points); + } + self->settings_lock.unlock(); + // push output onto output queue + self->output_lock.lock(); + self->output_queue.push_back(task); + self->call_deferred("set_process", true); + self->output_lock.unlock(); + } + } +} + Color TerrainMeshGenerator::color_at_height(float height) const { float const mapped{ Math::remap(height, this->color_gradient_start_height, this->color_gradient_end_height, 0.f, 1.f) }; return this->vertex_color_gradient.is_valid() @@ -111,19 +196,17 @@ void face_from(Ref surface, size_t tl, size_t side_points) { } } -void TerrainMeshGenerator::generate_grid(Rect2 area, Ref mesh, size_t side_points) { - mesh->clear_surfaces(); - surface->clear(); +void TerrainMeshGenerator::generate_grid(Rect2 area, Ref mesh, size_t side_points) { Vector2 point_distance{ area.size / (float)side_points }; ++side_points; Vector2 at{ area.position }; - surface->begin(Mesh::PRIMITIVE_TRIANGLES); + mesh->begin(Mesh::PRIMITIVE_TRIANGLES); for (size_t xpoint{ 0 }; xpoint < side_points; ++xpoint) { for (size_t ypoint{ 0 }; ypoint < side_points; ++ypoint) { float const height{ evaluate_point(at) }; - surface->set_color(color_at_height(height)); - surface->set_uv({ at / area.size }); - surface->add_vertex({ at.x - area.position.x, height, at.y - area.position.y }); + mesh->set_color(color_at_height(height)); + mesh->set_uv({ at / area.size }); + mesh->add_vertex({ at.x - area.position.x, height, at.y - area.position.y }); at.y += point_distance.y; } at.y = area.position.y; @@ -131,41 +214,68 @@ void TerrainMeshGenerator::generate_grid(Rect2 area, Ref mesh, size_t } for (size_t xface{ 0 }; xface < side_points - 1; ++xface) { for (size_t yface{ 0 }; yface < side_points - 1; ++yface) { - face_from(surface, xface + yface * (size_t)side_points, side_points); + face_from(mesh, xface + yface * (size_t)side_points, side_points); } } - surface->generate_normals(); - surface->generate_tangents(); - surface->commit(mesh); + mesh->generate_normals(); + mesh->generate_tangents(); +} + +void TerrainMeshGenerator::push_task(Rect2 area, Ref mesh, size_t points, Callable callback) { + this->input_lock.lock(); + for (TerrainMeshTask &task : this->input_queue) { + if (task.mesh == mesh) { + task.area = area; + task.side_points = points; + task.callback = callback; + this->input_lock.unlock(); + return; + } + } + TerrainMeshTask task{ + .mesh = mesh, + .area = area, + .side_points = points, + .out_surface = nullptr, + .callback = callback + }; + for (size_t i{ 0 }; i < this->input_queue.size(); ++i) { + if (this->input_queue[i].side_points >= points) { + this->input_queue.insert(i, task); + this->work_signal.post(); + this->input_lock.unlock(); + return; + } + } + this->input_queue.push_back(task); + this->work_signal.post(); + this->input_lock.unlock(); } void TerrainMeshGenerator::set_primitives(Array primitives) { - for (Ref primitive : this->primitives) { + // synchronise primitives + for (Variant var : this->primitives_buffer) { + Ref primitive{ var }; if (primitive.is_valid()) { primitive->disconnect_changed(this->generation_changed); } } - this->primitives.clear(); - for (Variant var : primitives) { + this->primitives_buffer = primitives; + for (Variant var : this->primitives_buffer) { Ref primitive{ var }; - this->primitives.push_back(primitive); if (primitive.is_valid()) { primitive->connect_changed(this->generation_changed); } } - emit_signal(sig_primitive_list_changed, get_primitives()); on_configuration_changed(); } Array TerrainMeshGenerator::get_primitives() const { - Array a; - for (Ref primitive : this->primitives) { - a.push_back(primitive); - } - return a; + return this->primitives_buffer; } void TerrainMeshGenerator::set_vertex_color_gradient(Ref gradient) { + this->settings_lock.lock(); if (this->vertex_color_gradient.is_valid()) { this->vertex_color_gradient->disconnect_changed(this->generation_changed); } @@ -173,6 +283,7 @@ void TerrainMeshGenerator::set_vertex_color_gradient(Ref gradient) { if (gradient.is_valid()) { this->vertex_color_gradient->connect_changed(this->generation_changed); } + this->settings_lock.unlock(); on_configuration_changed(); } @@ -181,7 +292,9 @@ Ref TerrainMeshGenerator::get_vertex_color_gradient() const { } void TerrainMeshGenerator::set_color_gradient_start_height(float value) { + this->settings_lock.lock(); this->color_gradient_start_height = value; + this->settings_lock.unlock(); on_configuration_changed(); } @@ -190,7 +303,9 @@ float TerrainMeshGenerator::get_color_gradient_start_height() const { } void TerrainMeshGenerator::set_color_gradient_end_height(float value) { + this->settings_lock.lock(); this->color_gradient_end_height = value; + this->settings_lock.unlock(); on_configuration_changed(); } @@ -199,10 +314,12 @@ float TerrainMeshGenerator::get_color_gradient_end_height() const { } void TerrainMeshGenerator::set_chunk_count(int num) { + this->settings_lock.lock(); if (num != this->chunks_per_side) { this->chunks_per_side = num; rebuild_chunks(); } + this->settings_lock.unlock(); } int TerrainMeshGenerator::get_chunk_count() const { @@ -210,9 +327,11 @@ int TerrainMeshGenerator::get_chunk_count() const { } void TerrainMeshGenerator::set_chunk_scene(Ref scene) { + this->settings_lock.lock(); if (!scene.is_valid() || scene->get_state()->get_node_type(0) == TerrainChunk::get_class_static()) { this->chunk_scene = scene; } + this->settings_lock.unlock(); } Ref TerrainMeshGenerator::get_chunk_scene() const { diff --git a/modules/terrain_editor/terrain_mesh_generator.h b/modules/terrain_editor/terrain_mesh_generator.h index 08c6f709..283396f6 100644 --- a/modules/terrain_editor/terrain_mesh_generator.h +++ b/modules/terrain_editor/terrain_mesh_generator.h @@ -4,11 +4,9 @@ #include "core/object/object.h" #include "core/os/semaphore.h" #include "core/os/thread.h" -#include "core/templates/hash_map.h" #include "core/variant/callable.h" #include "scene/main/node.h" #include "scene/resources/gradient.h" -#include "scene/resources/material.h" #include "scene/resources/mesh.h" #include "scene/resources/surface_tool.h" #include "terrain_editor/terrain_chunk.h" @@ -16,18 +14,29 @@ class TerrainMeshGenerator : public Node { GDCLASS(TerrainMeshGenerator, Node); + struct TerrainMeshTask { + Ref mesh; + Rect2 area; + size_t side_points; + Ref out_surface; + Callable callback; + }; static void _bind_methods(); - void ready(); + void enter_tree(); + void process(); void on_configuration_changed(); protected: void _notification(int what); void rebuild_chunks(); + static void background_generation_thread(void *user); -public: Color color_at_height(float height) const; float evaluate_point(Vector2 at) const; - void generate_grid(Rect2 area, Ref mesh, size_t side_points); + void generate_grid(Rect2 area, Ref mesh, size_t side_points); + +public: + void push_task(Rect2 area, Ref mesh, size_t side_points, Callable callback = Callable()); void set_primitives(Array array); Array get_primitives() const; void set_vertex_color_gradient(Ref gradient); @@ -42,14 +51,27 @@ public: Ref get_chunk_scene() const; private: - Vector> primitives{}; - Ref surface{ memnew(SurfaceTool) }; + // main thread exclusive data + Ref chunk_scene{}; + Vector chunks{}; + + // worker thread owned data + Mutex settings_lock{}; // needs to be locked to modify any of the below, worker thread locks while working + Array primitives_buffer{}; // main thread buffer for primitives + Vector> primitives{}; // work thread buffer for primitives Ref vertex_color_gradient{}; float color_gradient_start_height{ 0.f }; float color_gradient_end_height{ 10.f }; int chunks_per_side{ 10 }; - Ref chunk_scene{}; - Vector chunks{}; + + Thread thread{}; + Mutex input_lock{}; + bool end_thread{ false }; + Vector input_queue{}; + Semaphore work_signal{}; + + Mutex output_lock{}; + Vector output_queue{}; private: Callable generation_changed{ callable_mp(this, &self_type::on_configuration_changed) }; diff --git a/project/scenes/editor.tscn b/project/scenes/editor.tscn index 00a1dfb4..1fef7d23 100644 --- a/project/scenes/editor.tscn +++ b/project/scenes/editor.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=12 format=3 uid="uid://xm383pc5pcnn"] +[gd_scene load_steps=13 format=3 uid="uid://xm383pc5pcnn"] [ext_resource type="PackedScene" uid="uid://cnux2fqne284i" path="res://objects/primitive_nodes/point_primitive_node.tscn" id="1_b1cmn"] +[ext_resource type="PackedScene" uid="uid://wkqhvjnxs2mx" path="res://objects/terrain_chunk.tscn" id="1_pxqd5"] [sub_resource type="PointPrimitive" id="PointPrimitive_pxqd5"] slope = -0.585 @@ -107,6 +108,7 @@ primitives = [SubResource("PointPrimitive_pxqd5"), SubResource("NoisePrimitive_b vertex_color_gradient = SubResource("Gradient_b1cmn") color_gradient_end_height = 200.0 chunk_count = 5 +chunk_scene = ExtResource("1_pxqd5") point_primitive_object = ExtResource("1_b1cmn") [node name="WorldEnvironment" type="WorldEnvironment" parent="."]