commit f410d037e772c53f72aaa57599d516c659cd2bd6 Author: Sara Date: Mon Apr 13 12:30:06 2026 +0200 feat: extracted code from project diff --git a/SCsub b/SCsub new file mode 100644 index 0000000..2760ab7 --- /dev/null +++ b/SCsub @@ -0,0 +1,3 @@ +Import('env') + +env.add_source_files(env.modules_sources, "*.cpp") diff --git a/TODO.org b/TODO.org new file mode 100644 index 0000000..c7a7ff5 --- /dev/null +++ b/TODO.org @@ -0,0 +1,14 @@ +- [x] Cache chunk meshes +- [ ] Noise modifier +- [ ] Min/Max modifier +- [ ] Only load defined chunks +- [ ] More accurate modifier bounds +- [ ] Separate terrain module into it's own repository +- [ ] Texturing step +- [ ] Use Semaphores in mesh gen multi-threading +- [ ] Separate source files for terrain modifiers +- [x] Stop threads when generation finishes +- [ ] Regenerate chunks when deleting/reordering modifiers +- [x] Stop processing terrain after generation is complete +- [x] Don't start generation threads if all meshes are cached +- [ ] Scaling modifiers diff --git a/config.py b/config.py new file mode 100644 index 0000000..58c88bf --- /dev/null +++ b/config.py @@ -0,0 +1,5 @@ +def can_build(env, platform): + return True; + +def configure(env): + pass; diff --git a/register_types.cpp b/register_types.cpp new file mode 100644 index 0000000..150ee0c --- /dev/null +++ b/register_types.cpp @@ -0,0 +1,25 @@ +#include "register_types.h" + +#include "core/object/class_db.h" +#include "terrain/terrain.h" +#include "terrain/terrain_chunk.h" +#include "terrain/terrain_modifier.h" + +void initialize_terrain_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + ClassDB::register_class(); + ClassDB::register_abstract_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); +} + +void uninitialize_terrain_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } +} diff --git a/register_types.h b/register_types.h new file mode 100644 index 0000000..8500356 --- /dev/null +++ b/register_types.h @@ -0,0 +1,9 @@ +#ifndef TERRAIN_REGISTER_TYPES_H +#define TERRAIN_REGISTER_TYPES_H + +#include "modules/register_module_types.h" + +void initialize_terrain_module(ModuleInitializationLevel p_level); +void uninitialize_terrain_module(ModuleInitializationLevel p_level); + +#endif // !TERRAIN_REGISTER_TYPES_H diff --git a/terrain.cpp b/terrain.cpp new file mode 100644 index 0000000..bcbee7f --- /dev/null +++ b/terrain.cpp @@ -0,0 +1,329 @@ +#include "terrain.h" +#include "terrain/terrain_chunk.h" +#include "terrain/terrain_modifier.h" + +void Terrain::_bind_methods() { + BIND_HPROPERTY(Variant::OBJECT, mesh_material, PROPERTY_HINT_RESOURCE_TYPE, "Material"); + BIND_PROPERTY(Variant::INT, side_length); + BIND_PROPERTY(Variant::INT, chunk_size); + BIND_PROPERTY(Variant::INT, thread_count); + BIND_HPROPERTY(Variant::ARRAY, terrain_meshes, PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainMeshChunk", Variant::OBJECT, PROPERTY_HINT_NODE_TYPE), PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY); +} + +void Terrain::child_order_changed() { + this->modifiers.clear(); + for (Variant var : get_children()) { + if (TerrainModifier * mod{ cast_to(var) }) { + mod->set_terrain(this); + this->modifiers.push_back(mod); + } + } +} + +void Terrain::update_meshes() { + size_t num{ 1 }; + this->dirty_meshes_lock.lock(); + num = num > this->dirty_meshes.size() ? this->dirty_meshes.size() : num; + this->dirty_meshes_lock.unlock(); + for (size_t i{ 0 }; i < num; i++) { + this->dirty_meshes_lock.lock(); + TerrainChunkMesh *mesh{ this->dirty_meshes[0] }; + this->dirty_meshes.remove_at(0); + this->dirty_meshes_lock.unlock(); + mesh->apply_new_mesh(); + } +} + +void Terrain::update_threads() { + this->workload_lock.lock(); + if (this->workload.is_empty()) { + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + if (thread.is_started()) { + thread.wait_to_finish(); + } + } + } else { + print_line(vformat("Starting threads; workload: %d", this->workload.size())); + this->threads_stop = false; + for (Thread &thread : this->threads) { + if (!thread.is_started()) { + thread.start(Terrain::generate_meshes_thread, this); + } + } + this->workload_lock.unlock(); + } +} + +void Terrain::update_process() { + for (Thread &thread : this->threads) { + if (thread.is_started()) { + return; + } + } + this->dirty_meshes_lock.lock(); + bool workload_empty{ this->dirty_meshes.is_empty() }; + this->dirty_meshes_lock.unlock(); + if (!workload_empty) { + return; + } + this->workload_lock.lock(); + workload_empty = this->workload.is_empty(); + this->workload_lock.unlock(); + if (!workload_empty) { + return; + } + print_line("Terrain processing stopped"); + set_process(false); +} + +void Terrain::synchronous_generate_terrain() { + print_line("Force-regenerating entire terrain in one go."); + this->workload_lock.lock(); + this->threads_stop = false; + this->workload.clear(); + this->workload.append_array(this->meshes); + for (Thread &thread : this->threads) { + if (!thread.is_started()) { + thread.start(&Terrain::generate_meshes_thread, this); + } + } + do { + this->workload_lock.unlock(); + Thread::yield(); + this->workload_lock.lock(); + } while (!this->workload.is_empty()); + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + thread.wait_to_finish(); + } + for (TerrainChunkMesh *mesh : this->dirty_meshes) { + mesh->apply_new_mesh(); + } + this->dirty_meshes.clear(); +} + +void Terrain::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_CHILD_ORDER_CHANGED: + if (is_ready()) { + this->child_order_changed(); + } + return; + case NOTIFICATION_READY: { + this->meshes.clear(); + this->modifiers.clear(); + for (Variant var : get_children()) { + if (TerrainChunkMesh * mesh{ cast_to(var) }) { + this->meshes.push_back(mesh); + mesh->set_terrain(this); + } + if (TerrainModifier * mod{ cast_to(var) }) { + this->modifiers.push_back(mod); + mod->set_terrain(this); + } + } + size_t expected_size{ this->side_length / this->chunk_size }; + if (this->meshes.size() != expected_size * expected_size) { + construct_chunk_grid(); + synchronous_generate_terrain(); + } + return; + } + case NOTIFICATION_PROCESS: + update_meshes(); + update_threads(); + update_process(); + return; + case NOTIFICATION_EXIT_TREE: + this->workload_lock.lock(); + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + if (thread.is_started()) { + thread.wait_to_finish(); + } + } + return; + } +} + +void Terrain::generate_meshes_thread(void *terrain) { + Terrain *self{ static_cast(terrain) }; + print_line("thread", Thread::get_caller_id(), "start"); + for (;;) { + self->workload_lock.lock(); + if (self->threads_stop) { + self->workload_lock.unlock(); + print_line(Thread::get_caller_id(), "exiting"); + break; + } + if (self->workload.is_empty()) { + self->workload_lock.unlock(); + Thread::yield(); + continue; + } + TerrainChunkMesh *mesh{ self->workload[0] }; + self->workload.remove_at(0); + self->workload_lock.unlock(); + if (!mesh->is_inside_tree()) { + print_line(Thread::get_caller_id(), "mesh is outside tree, exiting"); + break; + } + mesh->update_mesh(); + Thread::yield(); + } + print_line(Thread::get_caller_id(), "done"); + return; +} + +void Terrain::construct_chunk_grid() { + print_line("Constructing chunk grid"); + this->workload_lock.lock(); + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + if (thread.is_started()) { + thread.wait_to_finish(); + } + } + this->workload_lock.lock(); + for (TerrainChunkMesh *mesh : this->meshes) { + remove_child(mesh); + mesh->queue_free(); + } + this->meshes.clear(); + this->workload.clear(); + size_t const chunks_per_side{ this->side_length / this->chunk_size }; + Vector3 const origin{ (float)this->chunk_size / 2.f, 0.f, (float)this->chunk_size / 2.f }; + for (size_t y{ 0 }; y < chunks_per_side; ++y) { + for (size_t x{ 0 }; x < chunks_per_side; ++x) { + TerrainChunkMesh *chunk{ memnew(TerrainChunkMesh) }; + chunk->set_size(this->chunk_size); + chunk->set_detail(this->detail); + chunk->set_terrain(this); + chunk->set_material_override(this->mesh_material); + chunk->set_position(origin + Vector3{ (float)this->chunk_size * (float)x, 0.f, (float)this->chunk_size * (float)y }); + chunk->set_name(vformat("Chunk%dx%d", x, y)); + add_child(chunk); + chunk->set_owner(get_owner()); + this->meshes.push_back(chunk); + this->workload.push_back(chunk); + } + } + this->threads_stop = false; + this->dirty_meshes.clear(); + this->workload_lock.unlock(); + set_process(true); +} + +float Terrain::height_at(Vector2 world_coordinate) { + float height{ 0 }; + for (TerrainModifier *mod : this->modifiers) { + if (!mod->is_inside_tree()) { + return height; + } + height = mod->evaluate_at(world_coordinate, height); + } + return height; +} + +void Terrain::push_changed(Rect2 area) { + for (TerrainChunkMesh *mesh : this->meshes) { + this->workload_lock.lock(); + if (area.intersects(mesh->get_bounds()) && !this->workload.has(mesh)) { + workload.push_back(mesh); + } + this->workload_lock.unlock(); + } + set_process(true); +} + +void Terrain::mesh_dirty(TerrainChunkMesh *mesh) { + this->dirty_meshes_lock.lock(); + this->dirty_meshes.push_back(mesh); + callable_mp(cast_to(this), &self_type::set_process).call_deferred(true); + this->dirty_meshes_lock.unlock(); +} + +void Terrain::set_mesh_material(Ref material) { + this->mesh_material = material; + for (TerrainChunkMesh *mesh : this->meshes) { + mesh->set_material_override(material); + } +} + +Ref Terrain::get_mesh_material() const { + return this->mesh_material; +} + +void Terrain::set_side_length(size_t length) { + this->side_length = length; + if (is_inside_tree()) { + construct_chunk_grid(); + } +} + +size_t Terrain::get_side_length() const { + return this->side_length; +} + +void Terrain::set_chunk_size(size_t size) { + this->chunk_size = size; + if (is_inside_tree()) { + construct_chunk_grid(); + } +} + +size_t Terrain::get_chunk_size() const { + return this->chunk_size; +} + +void Terrain::set_detail(size_t detail) { + this->detail = detail; + if (is_inside_tree()) { + construct_chunk_grid(); + } +} + +size_t Terrain::get_detail() const { + return this->detail; +} + +void Terrain::set_thread_count(size_t num) { + this->workload_lock.lock(); + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + thread.wait_to_finish(); + } + this->threads_stop = false; + this->threads.resize_initialized(num); +} + +size_t Terrain::get_thread_count() const { + return this->threads.size(); +} + +void Terrain::set_terrain_meshes(Array array) { + return; + this->meshes.clear(); + for (Variant var : array) { + if (TerrainChunkMesh * mesh{ cast_to(var) }) { + mesh->set_terrain(this); + this->meshes.push_back(mesh); + } + } +} + +Array Terrain::get_terrain_meshes() const { + Array a{}; + for (TerrainChunkMesh *mesh : this->meshes) { + a.push_back(mesh); + } + return a; +} diff --git a/terrain.h b/terrain.h new file mode 100644 index 0000000..b8dae5e --- /dev/null +++ b/terrain.h @@ -0,0 +1,61 @@ +#pragma once + +#include "core/math/rect2.h" +#include "core/os/mutex.h" +#include "core/os/thread.h" +#include "core/templates/vector.h" +#include "scene/main/node.h" +#include "scene/resources/material.h" +class TerrainChunkMesh; +class TerrainModifier; + +class Terrain : public Node { + GDCLASS(Terrain, Node); + static void _bind_methods(); + void child_order_changed(); + void update_meshes(); + void update_threads(); + void update_process(); + void synchronous_generate_terrain(); + +protected: + void _notification(int what); + static void generate_meshes_thread(void *terrain); + +public: + void construct_chunk_grid(); + float height_at(Vector2 world_coordinate); + void push_changed(Rect2 area); + void mesh_dirty(TerrainChunkMesh *mesh); + +private: + Ref mesh_material{}; + Vector workload{}; + bool threads_stop{ false }; + Mutex workload_lock; + + Vector dirty_meshes{}; + Mutex dirty_meshes_lock{}; + + Vector meshes{}; + Vector modifiers{}; + LocalVector threads{}; + + size_t side_length{ 200 }; + size_t chunk_size{ 50 }; + size_t detail{ 1 }; + +public: + void set_mesh_material(Ref material); + Ref get_mesh_material() const; + void set_side_length(size_t length); + size_t get_side_length() const; + void set_chunk_size(size_t size); + size_t get_chunk_size() const; + void set_detail(size_t detail); + size_t get_detail() const; + void set_thread_count(size_t num); + size_t get_thread_count() const; + void set_terrain_meshes(Array array); + Array get_terrain_meshes() const; +}; diff --git a/terrain_chunk.cpp b/terrain_chunk.cpp new file mode 100644 index 0000000..ac81ac3 --- /dev/null +++ b/terrain_chunk.cpp @@ -0,0 +1,137 @@ +#include "terrain_chunk.h" +#include "core/math/math_defs.h" +#include "core/math/math_funcs.h" +#include "core/object/object.h" +#include "core/variant/variant.h" +#include "scene/3d/physics/collision_shape_3d.h" +#include "scene/3d/physics/static_body_3d.h" +#include "scene/resources/3d/height_map_shape_3d.h" +#include "scene/resources/surface_tool.h" +#include "terrain/terrain.h" + +void TerrainChunkMesh::_bind_methods() { + BIND_HPROPERTY(Variant::OBJECT, shape, PROPERTY_HINT_RESOURCE_TYPE, "HeightMapShape3D", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE); + BIND_HPROPERTY(Variant::INT, size, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE); +} + +void TerrainChunkMesh::ready() { + this->position_buffer = get_global_position(); + float const sizef{ (float)get_size() }; + this->bounds.position = { this->position_buffer.x - sizef / 2.f, this->position_buffer.z - sizef / 2.f }; + this->bounds.size = { sizef, sizef }; + + add_child(this->body = memnew(StaticBody3D)); + ERR_FAIL_COND_EDMSG(this->body == nullptr, "Failed to instantiate StaticBody3D"); + this->body->add_child(this->collider = memnew(CollisionShape3D)); + this->body->set_owner(this); + ERR_FAIL_COND_EDMSG(this->collider == nullptr, "Failed to instantiate CollisionShape3D"); + this->collider->set_owner(this); + if (this->shape.is_null()) { + this->shape = memnew(HeightMapShape3D); + this->shape->set_map_depth(heightmap_side_length()); + this->shape->set_map_width(heightmap_side_length()); + } else { + this->heightmap.append_array(this->shape->get_map_data()); + } + ERR_FAIL_COND_EDMSG(!this->shape.is_valid(), "Failed to instantiate HeightMapShape3D"); + this->collider->set_shape(this->shape); +} + +void TerrainChunkMesh::generate_vertices() { + ERR_FAIL_COND_EDMSG(this->terrain == nullptr, "TerrainChunkMesh::generate_vertices: no terrain assigned"); + ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate_vertices: size <= 0"); + ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate_vertices: points per side <= 0"); + float const half_extent{ (float)this->size / 2.f }; + float const point_distance{ (float)this->size / ((float)points_per_side() - 1) }; + Vector3 origin{ this->position_buffer - Vector3{ half_extent, 0, half_extent } }; + for (size_t x{ 0 }; x < points_per_side(); ++x) { + for (size_t y{ 0 }; y < points_per_side(); ++y) { + Vector2 const coordinate{ origin.x + point_distance * x, origin.z + point_distance * y }; + this->surface->set_uv({ (float)x / (float)points_per_side(), (float)y / (float)points_per_side() }); + float height{ this->terrain->height_at(coordinate) }; + this->surface->add_vertex({ coordinate.x - this->position_buffer.x, height, coordinate.y - this->position_buffer.z }); + } + } + this->heightmap.resize_initialized(heightmap_side_length() * heightmap_side_length()); + for (size_t x{ 0 }; x < heightmap_side_length(); x++) { + for (size_t y{ 0 }; y < heightmap_side_length(); y++) { + Vector2 coordinate{ origin.x + x, origin.z + y }; + this->heightmap.set(x + y * heightmap_side_length(), this->terrain->height_at(coordinate)); + } + } +} + +void TerrainChunkMesh::generate_faces() { + LocalVector &verts{ this->surface->get_vertex_array() }; + ERR_FAIL_COND_EDMSG(verts.size() == 0, "TerrainChunkMesh::generate_faces: no vertices in surface, call generate_vertices first"); + size_t const faces_per_side{ points_per_side() - 1 }; + for (size_t x{ 0 }; x < faces_per_side; ++x) { + for (size_t y{ 0 }; y < faces_per_side; ++y) { + size_t const tl{ x + y * points_per_side() }; + float tl_br{ verts[tl].vertex.distance_to(verts[tl + points_per_side() + 1].vertex) }; + float tr_bl{ verts[tl + 1].vertex.distance_to(verts[tl + points_per_side()].vertex) }; + if (tl_br < tr_bl) { + surface->add_index(tl); + surface->add_index(tl + points_per_side() + 1); + surface->add_index(tl + 1); + surface->add_index(tl); + surface->add_index(tl + points_per_side()); + surface->add_index(tl + points_per_side() + 1); + } else { + surface->add_index(tl + points_per_side()); + surface->add_index(tl + points_per_side() + 1); + surface->add_index(tl + 1); + surface->add_index(tl + 1); + surface->add_index(tl); + surface->add_index(tl + points_per_side()); + } + } + } +} + +void TerrainChunkMesh::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_ENTER_TREE: + if (!is_ready()) { + ready(); + } + return; + } +} + +void TerrainChunkMesh::apply_new_mesh() { + this->lock.lock(); + set_mesh(this->new_mesh); + if (this->shape->get_map_depth() != heightmap_side_length() || this->shape->get_map_width() != heightmap_side_length()) { + this->shape->set_map_depth(heightmap_side_length()); + this->shape->set_map_width(heightmap_side_length()); + } + this->shape->set_map_data(heightmap); + this->lock.unlock(); +} + +void TerrainChunkMesh::update_mesh() { + ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate: size <= 0"); + ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate: points per side <= 0"); + this->lock.lock(); + this->surface = memnew(SurfaceTool); + this->surface->begin(Mesh::PRIMITIVE_TRIANGLES); + generate_vertices(); + generate_faces(); + this->surface->generate_normals(); + this->surface->generate_tangents(); + this->new_mesh = memnew(ArrayMesh); + this->surface->commit(this->new_mesh); + this->lock.unlock(); + this->terrain->mesh_dirty(this); +} + +size_t TerrainChunkMesh::points_per_side() const { + return this->size * this->detail; +} + +int TerrainChunkMesh::heightmap_side_length() const { + return get_size() + 1; +} diff --git a/terrain_chunk.h b/terrain_chunk.h new file mode 100644 index 0000000..fb427e6 --- /dev/null +++ b/terrain_chunk.h @@ -0,0 +1,50 @@ +#pragma once + +#include "core/math/rect2.h" +#include "macros.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/physics/collision_shape_3d.h" +#include "scene/3d/physics/static_body_3d.h" +#include "scene/resources/3d/height_map_shape_3d.h" +#include "scene/resources/mesh.h" +#include "scene/resources/surface_tool.h" +class Terrain; + +class TerrainChunkMesh : public MeshInstance3D { + GDCLASS(TerrainChunkMesh, MeshInstance3D); + static void _bind_methods(); + void ready(); + void generate_vertices(); + void generate_faces(); + +protected: + void _notification(int what); + +public: + void apply_new_mesh(); + void update_mesh(); + size_t points_per_side() const; + int heightmap_side_length() const; + +private: + Mutex lock{}; + Vector3 position_buffer{}; + Ref surface{}; + Ref new_mesh{}; + Terrain *terrain{ nullptr }; + size_t detail{ 1 }; + size_t size{ 1 }; + Rect2 bounds{}; + + Ref shape{}; + StaticBody3D *body{ nullptr }; + CollisionShape3D *collider{ nullptr }; + Vector heightmap{}; + +public: + GET_SET_FNS(Rect2, bounds); + GET_SET_FNS(Terrain *, terrain); + GET_SET_FNS(Ref, shape); + GET_SET_FNS(size_t, detail); + GET_SET_FNS(size_t, size); +}; diff --git a/terrain_modifier.cpp b/terrain_modifier.cpp new file mode 100644 index 0000000..f8ada28 --- /dev/null +++ b/terrain_modifier.cpp @@ -0,0 +1,475 @@ +#include "terrain_modifier.h" +#include "core/config/engine.h" +#include "core/math/math_funcs.h" +#include "core/variant/variant.h" +#include "macros.h" +#include "scene/main/node.h" +#include "terrain/terrain.h" +#include + +String TerrainModifier::sig_terrain_changed{ "terrain_changed" }; + +void TerrainModifier::_bind_methods() { + ADD_SIGNAL(MethodInfo(sig_terrain_changed, PropertyInfo(Variant::OBJECT, "terrain", PROPERTY_HINT_NODE_TYPE, "Terrain"))); +} + +void TerrainModifier::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_ENTER_TREE: + if (Engine::get_singleton()->is_editor_hint()) { + set_notify_transform(true); + } + this->thread_safe_global_position = get_global_position(); + case NOTIFICATION_TRANSFORM_CHANGED: + this->thread_safe_global_position = get_global_position(); + return; + } +} + +void TerrainModifier::push_changed(Rect2 area) { + if (this->terrain) { + this->terrain->push_changed(area); + } +} + +float TerrainModifier::evaluate_at(Vector2 world_coordinate, float before) { + Vector3 const global_position{ get_thread_safe_global_position() }; + return global_position.y; +} + +void TerrainModifier::set_bounds(Rect2 bounds) { + if (this->bounds != bounds) { + push_changed(bounds); + push_changed(this->bounds); + this->bounds = bounds; + } +} + +Rect2 TerrainModifier::get_bounds() const { + return this->bounds; +} + +Vector3 TerrainModifier::get_thread_safe_global_position() const { + return this->thread_safe_global_position; +} + +void SharedMutex::lock_shared() { + this->lock.lock(); + this->shared_count++; + this->lock.unlock(); +} + +void SharedMutex::unlock_shared() { + this->lock.lock(); + this->shared_count--; + this->lock.unlock(); +} + +void SharedMutex::lock_exclusive() { + while (true) { + this->lock.lock(); + if (this->shared_count == 0) { + return; + } + this->lock.unlock(); + } +} + +void SharedMutex::unlock_exclusive() { + this->lock.unlock(); +} + +void TerrainModifierDistance::_bind_methods() { + BIND_HPROPERTY(Variant::OBJECT, distance_weight_curve, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); +} + +void TerrainModifierDistance::curves_changed() { + if (!update_bounds()) { + push_changed(get_bounds()); + } + this->lock.lock_exclusive(); + this->distance_weight_curve_buffer = this->distance_weight_curve.is_valid() ? this->distance_weight_curve->duplicate(true) : nullptr; + this->lock.unlock_exclusive(); +} + +bool TerrainModifierDistance::update_bounds() { + Rect2 const before{ get_bounds() }; + Rect2 bounds{}; + Vector3 position{ get_thread_safe_global_position() }; + bounds.position = { position.x, position.z }; + bounds.size = { 0, 0 }; + this->lock.lock_shared(); + if (this->distance_weight_curve.is_valid()) { + float const max_radius{ this->distance_weight_curve->get_max_domain() }; + float const max_diameter{ 2.f * max_radius }; + bounds.size = { max_diameter, max_diameter }; + bounds.position -= { max_radius, max_radius }; + } + bool const changed{ before != bounds }; + this->lock.unlock_shared(); + this->lock.lock_exclusive(); + set_bounds(bounds); + this->lock.unlock_exclusive(); + return changed; +} + +void TerrainModifierDistance::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_READY: + update_bounds(); + set_notify_transform(true); + return; + case NOTIFICATION_TRANSFORM_CHANGED: + if (is_inside_tree()) { + if (!update_bounds()) { + push_changed(get_bounds()); + } + } + return; + } +} + +float TerrainModifierDistance::distance_at(Vector2 const &world_coordinate) { + Vector3 const global_position{ get_thread_safe_global_position() }; + return world_coordinate.distance_to({ global_position.x, global_position.z }); +} + +float TerrainModifierDistance::evaluate_at(Vector2 world_coordinate, float before) { + this->lock.lock_shared(); + if (this->distance_weight_curve_buffer.is_null()) { + this->lock.unlock_shared(); + return before; + } + float const distance{ distance_at(world_coordinate) }; + if (distance >= this->distance_weight_curve_buffer->get_max_domain()) { + this->lock.unlock_shared(); + return before; + } + float const weight_offset{ std::clamp(distance, this->distance_weight_curve_buffer->get_min_domain(), this->distance_weight_curve_buffer->get_max_domain()) }; + float const weight{ this->distance_weight_curve_buffer->sample(weight_offset) }; + float out{ weight <= 0.f ? before : Math::lerp(before, get_thread_safe_global_position().y, weight) }; + this->lock.unlock_shared(); + + return out; +} + +PackedStringArray TerrainModifierDistance::get_configuration_warnings() const { + PackedStringArray warnings{ super_type::get_configuration_warnings() }; + if (this->distance_weight_curve.is_null()) { + warnings.push_back("distance_weight_curve is invalid, add a valid distance_weight_curve"); + } + return warnings; +} + +void TerrainModifierDistance::set_distance_weight_curve(Ref curve) { + this->lock.lock_exclusive(); + if (Engine::get_singleton()->is_editor_hint()) { + if (this->distance_weight_curve.is_valid()) { + this->distance_weight_curve->disconnect_changed(callable_mp(this, &self_type::curves_changed)); + } + if (curve.is_valid()) { + curve->connect_changed(callable_mp(this, &self_type::curves_changed)); + } + } + this->distance_weight_curve = curve; + this->lock.unlock_exclusive(); + curves_changed(); + update_configuration_warnings(); +} + +Ref TerrainModifierDistance::get_distance_weight_curve() const { + return this->distance_weight_curve; +} + +void TerrainModifierPathPoint::_bind_methods() {} + +void TerrainModifierPathPoint::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_ENTER_TREE: + this->path = cast_to(get_parent()); + return; + case NOTIFICATION_READY: + set_notify_transform(true); + if (this->path) { + this->path->path_changed(); + } + return; + case NOTIFICATION_TRANSFORM_CHANGED: + if (this->path && is_inside_tree()) { + this->path->path_changed(); + } + return; + case NOTIFICATION_EXIT_TREE: + this->path = nullptr; + return; + } +} + +void TerrainModifierPath::_bind_methods() { + BIND_HPROPERTY(Variant::OBJECT, curve_left, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); + BIND_HPROPERTY(Variant::OBJECT, curve_right, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); +} + +void TerrainModifierPath::curves_changed() { + if (!update_bounds()) { + push_changed(get_bounds()); + } + this->lock.lock_exclusive(); + this->curve_left_buffer = this->curve_left.is_valid() ? this->curve_left->duplicate(true) : nullptr; + this->curve_right_buffer = this->curve_right.is_valid() ? this->curve_right->duplicate(true) : nullptr; + this->lock.unlock_exclusive(); +} + +bool TerrainModifierPath::update_bounds() { + Vector2 min{}, max{}; + this->lock.lock_shared(); + if (this->points.is_empty() || this->curve_left.is_null()) { + Vector3 point{ this->get_thread_safe_global_position() }; + min.x = point.x; + min.y = point.z; + max = min; + } else { + max = min = { this->points[0].x, this->points[0].y }; + for (Vector3 const &point : this->points) { + max.x = max.x > point.x ? max.x : point.x; + max.y = max.y > point.z ? max.y : point.z; + min.x = min.x < point.x ? min.x : point.x; + min.y = min.y < point.z ? min.y : point.z; + } + float max_distance_left{ this->curve_left->get_max_domain() }; + float max_distance{ max_distance_left }; + if (this->curve_right.is_valid()) { + float max_distance_right{ this->curve_right->get_max_domain() }; + max_distance = max_distance_right > max_distance ? max_distance_right : max_distance; + } + min -= { max_distance, max_distance }; + max += { max_distance * 2.f, max_distance * 2.f }; + } + Rect2 bounds{ min, max - min }; + bool const changed{ bounds != get_bounds() }; + this->lock.unlock_shared(); + this->lock.lock_exclusive(); + set_bounds(bounds); + this->lock.unlock_exclusive(); + return changed; +} + +void TerrainModifierPath::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_READY: + update_bounds(); + set_notify_transform(true); + return; + case NOTIFICATION_TRANSFORM_CHANGED: + if (is_inside_tree()) { + if (!update_bounds()) { + push_changed(get_bounds()); + } + } + return; + case NOTIFICATION_CHILD_ORDER_CHANGED: + path_changed(); + return; + } +} + +float TerrainModifierPath::evaluate_line(Vector3 a, bool a_end, Vector3 b, bool b_end, Vector2 world_coordinate, float &out_dot, float &out_distance, float &out_percentage) { + Vector2 a2{ a.x, a.z }, b2{ b.x, b.z }; + Vector2 const relative_coordinate{ world_coordinate - a2 }; + Vector2 const difference2{ b2 - a2 }; + float w{ difference2.normalized().dot(relative_coordinate) / difference2.length() }; + Vector3 const difference{ b - a }; + Vector3 const closest_on_line{ a + difference * (w > 0 ? (w < 1 ? w : 1) : 0) }; + Vector2 const right{ -difference.z, difference.x }; + out_dot = right.normalized().dot(relative_coordinate); + out_distance = world_coordinate.distance_to({ closest_on_line.x, closest_on_line.z }); + out_percentage = w; + return a.y + (b.y - a.y) * w; +} + +float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) { + this->lock.lock_shared(); + if (this->curve_left_buffer.is_null() || this->points.size() <= 1) { + this->lock.unlock_shared(); + return before; + } + Ref right_curve{ this->curve_right_buffer }; + if (right_curve.is_null()) { + right_curve = this->curve_left_buffer; + } + float out_height{ before }; + long const count{ this->closed ? this->points.size() : this->points.size() - 1 }; + for (int i{ 0 }; i < count; i++) { + Vector3 const ipos{ this->points[i] }; + Vector3 const next_pos{ this->points[Math::wrapi(i + 1, 0, this->points.size())] }; + if (ipos == next_pos) { + continue; + } + float dot, distance, percentage; + bool const is_start{ !this->closed && i == 0 }, is_end{ !this->closed && i == count - 1 }; + float height{ evaluate_line(ipos, is_start, next_pos, is_end, world_coordinate, dot, distance, percentage) }; + float const left{ this->curve_left_buffer->sample(distance) }; + float const right{ right_curve->sample(distance) }; + float const ndot{ dot / distance }; + float separation{ ndot / 2.f + 0.5f }; + if (percentage > 0.f && percentage < 1.f) { + separation = Math::round(separation); + } + float const weight{ left * (1.f - separation) + right * separation }; + float const blended_height{ Math::lerp(out_height, height, weight) }; + out_height = blended_height; + } + this->lock.unlock_shared(); + return out_height; +} + +void TerrainModifierPath::path_changed() { + if (!is_inside_tree()) { + return; + } + this->lock.lock_exclusive(); + this->points.clear(); + this->min_height = INFINITY; + this->max_height = -INFINITY; + Vector3 last{ INFINITY, INFINITY, INFINITY }; + for (Variant var : get_children()) { + if (TerrainModifierPathPoint * point{ cast_to(var) }) { + Vector3 position{ point->get_global_position() }; + if (position != last) { + this->points.push_back(position); + if (position.y > this->max_height) { + this->max_height = position.y; + } + if (position.y < this->min_height) { + this->min_height = position.y; + } + } + } + last = var; + } + this->lock.unlock_exclusive(); + if (!update_bounds()) { + push_changed(get_bounds()); + } +} + +PackedStringArray TerrainModifierPath::get_configuration_warnings() const { + PackedStringArray warnings{ super_type::get_configuration_warnings() }; + if (this->curve_left.is_null()) { + warnings.push_back("curve_left is invalid, add a valid curve_left"); + } + return warnings; +} + +void TerrainModifierPath::set_curve_left(Ref curve) { + this->lock.lock_exclusive(); + if (curve.is_valid() && curve == this->curve_right) { + curve = curve->duplicate(); + } + if (Engine::get_singleton()->is_editor_hint()) { + if (this->curve_left.is_valid()) { + this->curve_left->disconnect_changed(callable_mp(this, &self_type::curves_changed)); + } + if (curve.is_valid()) { + curve->connect_changed(callable_mp(this, &self_type::curves_changed)); + } + } + this->curve_left = curve; + this->lock.unlock_exclusive(); + if (!curve.is_valid() && this->curve_right.is_valid()) { + curve = this->curve_right; + set_curve_right(nullptr); + } else { + curves_changed(); + update_configuration_warnings(); + } +} + +Ref TerrainModifierPath::get_curve_left() const { + return this->curve_left; +} + +void TerrainModifierPath::set_curve_right(Ref curve) { + this->lock.lock_exclusive(); + if (curve.is_valid() && curve == this->curve_left) { + curve = curve->duplicate(); + } + if (Engine::get_singleton()->is_editor_hint()) { + if (this->curve_right.is_valid()) { + this->curve_right->disconnect_changed(callable_mp(this, &self_type::curves_changed)); + } + if (curve.is_valid()) { + curve->connect_changed(callable_mp(this, &self_type::curves_changed)); + } + } + this->curve_right = curve; + this->lock.unlock_exclusive(); + curves_changed(); + update_configuration_warnings(); +} + +Ref TerrainModifierPath::get_curve_right() const { + return this->curve_right; +} + +void TerrainModifierComposite::update_sub_modifiers() { + for (TerrainModifier *mod : this->sub_modifiers) { + push_changed(mod->get_bounds()); + } + this->sub_modifiers.clear(); + for (Variant var : get_children()) { + if (TerrainModifier * mod{ cast_to(var) }) { + this->sub_modifiers.push_back(mod); + mod->set_terrain(get_terrain()); + push_changed(mod->get_bounds()); + } + } +} + +void TerrainModifierComposite::terrain_changed(Terrain *terrain) { + for (TerrainModifier *mod : this->sub_modifiers) { + mod->set_terrain(terrain); + } +} + +void TerrainModifierComposite::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_ENTER_TREE: + set_notify_transform(true); + if (!is_ready()) { + connect(sig_terrain_changed, callable_mp(this, &self_type::terrain_changed)); + } + return; + case NOTIFICATION_TRANSFORM_CHANGED: + for (TerrainModifier *mod : this->sub_modifiers) { + push_changed(mod->get_bounds()); + } + return; + case NOTIFICATION_CHILD_ORDER_CHANGED: + if (!is_ready()) { + return; + } + // fall through + case NOTIFICATION_READY: + update_sub_modifiers(); + return; + } +} + +float TerrainModifierComposite::evaluate_at(Vector2 world_coordinate, float before) { + float result{ 0.f }; + for (TerrainModifier *mod : sub_modifiers) { + result = mod->evaluate_at(world_coordinate, result); + } + return result + before; +} diff --git a/terrain_modifier.h b/terrain_modifier.h new file mode 100644 index 0000000..fc318fb --- /dev/null +++ b/terrain_modifier.h @@ -0,0 +1,136 @@ +#pragma once + +#include "core/object/object.h" +#include "core/variant/variant.h" +#include "macros.h" +#include "scene/3d/marker_3d.h" +#include "scene/resources/curve.h" +#include +class Terrain; + +class TerrainModifier : public Marker3D { + GDCLASS(TerrainModifier, Marker3D); + static void _bind_methods(); + +protected: + void _notification(int what); + +public: + void push_changed(Rect2 bounds); + virtual float evaluate_at(Vector2 world_coordinate, float before); + +private: + Vector3 thread_safe_global_position{}; + Terrain *terrain{ nullptr }; + Rect2 bounds{ { -INFINITY, -INFINITY }, { INFINITY, INFINITY } }; + +protected: + void set_bounds(Rect2 bounds); + +public: + static String sig_terrain_changed; + Rect2 get_bounds() const; + Vector3 get_thread_safe_global_position() const; + Terrain *get_terrain() const { + return this->terrain; + } + void set_terrain(Terrain *terrain) { + this->terrain = terrain; + emit_signal(sig_terrain_changed, terrain); + } +}; + +struct SharedMutex { + void lock_shared(); + void unlock_shared(); + void lock_exclusive(); + void unlock_exclusive(); + +private: + Mutex lock{}; + int shared_count{}; +}; + +class TerrainModifierDistance : public TerrainModifier { + GDCLASS(TerrainModifierDistance, TerrainModifier); + static void _bind_methods(); + void curves_changed(); + bool update_bounds(); + +protected: + void _notification(int what); + float distance_at(Vector2 const &world_coordinate); + +public: + float evaluate_at(Vector2 world_coordinate, float before) override; + PackedStringArray get_configuration_warnings() const override; + +private: + SharedMutex lock{}; + Ref distance_weight_curve{}; + Ref distance_weight_curve_buffer{}; + +public: + void set_distance_weight_curve(Ref curve); + Ref get_distance_weight_curve() const; +}; + +class TerrainModifierPathPoint : public Marker3D { + GDCLASS(TerrainModifierPathPoint, Marker3D); + static void _bind_methods(); + +protected: + void _notification(int what); + +public: + class TerrainModifierPath *path{ nullptr }; +}; + +class TerrainModifierPath : public TerrainModifier { + GDCLASS(TerrainModifierPath, TerrainModifier); + static void _bind_methods(); + void curves_changed(); + bool update_bounds(); + +protected: + void _notification(int what); + float evaluate_line(Vector3 a, bool a_end, Vector3 b, bool b_end, Vector2 world_coordinate, float &out_dot, float &out_distance, float &out_percentage); + +public: + float evaluate_at(Vector2 world_coordinate, float before) override; + void path_changed(); + PackedStringArray get_configuration_warnings() const override; + +private: + SharedMutex lock{}; + float min_height{}; + float max_height{}; + bool closed{ false }; + Vector points{}; + Ref curve_left_buffer{}; + Ref curve_left{}; + Ref curve_right_buffer{}; + Ref curve_right{}; + +public: + void set_curve_left(Ref curve); + Ref get_curve_left() const; + void set_curve_right(Ref curve); + Ref get_curve_right() const; +}; + +class TerrainModifierComposite : public TerrainModifier { + GDCLASS(TerrainModifierComposite, TerrainModifier); + static void _bind_methods() {} + void update_sub_modifiers(); + void terrain_changed(Terrain *terrain); + +protected: + void _notification(int what); + +public: + float evaluate_at(Vector2 world_coordinate, float before) override; + +private: + Vector sub_modifiers{}; +};