From f422cdabaf5cc3887b85896f1602aeb52ec66663 Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 19 Apr 2026 15:29:19 +0200 Subject: [PATCH] chore: cleanup, separated modifier files --- TODO.org | 2 +- register_types.cpp | 3 + terrain_chunk.cpp | 2 - terrain_modifier.cpp | 423 --------------------------------- terrain_modifier.h | 77 ------ terrain_modifier_composite.cpp | 56 +++++ terrain_modifier_composite.h | 19 ++ terrain_modifier_distance.cpp | 107 +++++++++ terrain_modifier_distance.h | 28 +++ terrain_modifier_path.cpp | 263 ++++++++++++++++++++ terrain_modifier_path.h | 37 +++ 11 files changed, 514 insertions(+), 503 deletions(-) create mode 100644 terrain_modifier_composite.cpp create mode 100644 terrain_modifier_composite.h create mode 100644 terrain_modifier_distance.cpp create mode 100644 terrain_modifier_distance.h create mode 100644 terrain_modifier_path.cpp create mode 100644 terrain_modifier_path.h diff --git a/TODO.org b/TODO.org index 354478f..5c1a944 100644 --- a/TODO.org +++ b/TODO.org @@ -5,7 +5,7 @@ - [ ] Separate terrain module into it's own repository - [ ] Texturing step - [ ] Use Semaphores in mesh gen multi-threading -- [ ] Separate source files for terrain modifiers +- [x] 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 diff --git a/register_types.cpp b/register_types.cpp index c2a9aa1..3b6ab82 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -4,6 +4,9 @@ #include "terrain/terrain.h" #include "terrain/terrain_chunk.h" #include "terrain/terrain_modifier.h" +#include "terrain/terrain_modifier_composite.h" +#include "terrain/terrain_modifier_distance.h" +#include "terrain/terrain_modifier_path.h" void initialize_terrain_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { diff --git a/terrain_chunk.cpp b/terrain_chunk.cpp index aa8f22a..0da3b8c 100644 --- a/terrain_chunk.cpp +++ b/terrain_chunk.cpp @@ -1,6 +1,4 @@ #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" diff --git a/terrain_modifier.cpp b/terrain_modifier.cpp index 3af6a2e..aa4c190 100644 --- a/terrain_modifier.cpp +++ b/terrain_modifier.cpp @@ -1,12 +1,8 @@ #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 "scene/resources/curve.h" #include "terrain/terrain.h" -#include String TerrainModifier::sig_terrain_changed{ "terrain_changed" }; @@ -56,422 +52,3 @@ Rect2 TerrainModifier::get_bounds() const { Vector3 TerrainModifier::get_thread_safe_global_position() const { return this->thread_safe_global_position; } - -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()); - } - SharedMutex::LockExclusive exclusive{ this->lock }; - this->distance_weight_curve_buffer = this->distance_weight_curve.is_valid() ? this->distance_weight_curve->duplicate(true) : nullptr; -} - -bool TerrainModifierDistance::update_bounds() { - bool changed{ false }; - Rect2 bounds{}; - { - SharedMutex::LockShared shared{ this->lock }; - Rect2 const before{ get_bounds() }; - Vector3 position{ get_thread_safe_global_position() }; - bounds.position = { position.x, position.z }; - bounds.size = { 0, 0 }; - 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 }; - } - changed = before != bounds; - } - { - SharedMutex::LockExclusive exclusive{ this->lock }; - set_bounds(bounds); - } - - 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) { - SharedMutex::LockShared shared{ this->lock }; - if (this->distance_weight_curve_buffer.is_null()) { - return before; - } - float const distance{ distance_at(world_coordinate) }; - if (distance >= this->distance_weight_curve_buffer->get_max_domain()) { - 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) }; - 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) { - { - SharedMutex::LockExclusive exclusive{ this->lock }; - 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; - } - curves_changed(); - update_configuration_warnings(); -} - -Ref TerrainModifierDistance::get_distance_weight_curve() const { - return this->distance_weight_curve; -} - -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() { - { - SharedMutex::LockExclusive exclusive{ this->lock }; - 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; - } - if (!update_bounds()) { - push_changed(get_bounds()); - } -} - -bool TerrainModifierPath::update_bounds() { - bool changed{ false }; - Rect2 bounds{}; - { - // calculate the bounds, no need to make an exclusive lock if we can avoid it - SharedMutex::LockShared shared{ this->lock }; - if (this->path == nullptr) { - return false; - } - // which of the two curves is the furthest reaching - float margin{ 0.f }; - if (this->curve_left.is_valid()) { - float const domain{ this->curve_left->get_max_domain() }; - margin = domain > margin ? domain : margin; - } - if (this->curve_right.is_valid()) { - float const domain{ this->curve_right->get_max_domain() }; - margin = domain > margin ? domain : margin; - } - // alias some known data - Transform3D curve_transform{ this->path->get_global_transform() }; - PackedVector3Array const &baked_points{ this->path->get_curve()->get_baked_points() }; - // find the highest and lowest x and z values - Vector2 min{}, max{}; - for (int i{ 0 }; i < baked_points.size(); i += 10) { - Vector3 point{ baked_points[i] }; - point = { - curve_transform.basis.get_column(0) * point.x + - curve_transform.basis.get_column(1) * point.y + - curve_transform.basis.get_column(2) * point.z + - curve_transform.origin - }; - min = min.min({ point.x, point.z }); - max = max.max({ point.x, point.z }); - } - // extend found min and max with margins - min -= { margin, margin }; - max += Vector2{ margin, margin }; - // calculate bounds and check if any change is made - bounds = { min, max - min }; - changed = bounds != get_bounds(); - } - if (changed) { - // ensure we have an exclusive lock before writing thread-shared data - SharedMutex::LockExclusive exclusive{ this->lock }; - set_bounds(bounds); - } - return changed; -} - -void TerrainModifierPath::_notification(int what) { - switch (what) { - default: - return; - case NOTIFICATION_READY: - children_changed(); - set_notify_transform(true); - update_bounds(); - return; - case NOTIFICATION_TRANSFORM_CHANGED: - if (is_inside_tree()) { - path_changed(); - } - return; - case NOTIFICATION_CHILD_ORDER_CHANGED: - if (is_ready()) { - children_changed(); - } - return; - } -} - -float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) { - SharedMutex::LockShared shared{ this->lock }; - if (this->path == nullptr) { - print_error("no path"); - return before; - } - if (this->curve_left_buffer.is_null()) { - print_error("no curves"); - return before; - } - if (this->path_buffer.is_null()) { - print_error("no path buffer"); - return before; - } - if (this->path_buffer->get_point_count() <= 1) { - print_error("path buffer functionally empty"); - return before; - } - Ref right_curve{ this->curve_right_buffer }; - if (right_curve.is_null()) { - right_curve = this->curve_left_buffer; - } - Transform3D const inv_global_transform{ this->global_path_transform.inverse() }; - // convert world coordinate from 2d world to 3d path-local space - Vector3 relative_position{ - world_coordinate.x - this->global_path_transform.origin.x, 0.f, world_coordinate.y - this->global_path_transform.origin.z - }; - relative_position = { - inv_global_transform.basis.get_column(0) * relative_position.x + - inv_global_transform.basis.get_column(1) * relative_position.y + - inv_global_transform.basis.get_column(2) * relative_position.z - }; - // find the offset of the point closest to the world coordinate ... - real_t const offset{ this->path_buffer->get_closest_offset(relative_position) }; - // ... and fetch the corresponding transform - Transform3D const curve_point{ this->path_buffer->sample_baked_with_rotation(offset) }; - Vector3 global_curve_position{ - curve_point.origin.x * this->global_path_transform.basis.get_column(0) + - curve_point.origin.y * this->global_path_transform.basis.get_column(1) + - curve_point.origin.z * this->global_path_transform.basis.get_column(2) + - this->global_path_transform.origin - }; - // extract and xz position from sampled transform - Vector2 const world_curve_point{ Vector2{ global_curve_position.x, global_curve_position.z } }; - // calculate the xz distance from the curve - float const distance{ world_curve_point.distance_to(world_coordinate) }; - // exit early if we know for sure this point should not be affected by the path - if (distance > this->curve_left_buffer->get_max_domain() && distance > right_curve->get_max_domain()) { - return before; - } - // extract right direction and extract xz coordinates - Vector3 right_direction{ curve_point.basis.get_column(0) }; - right_direction = { - right_direction.x * this->global_path_transform.basis.get_column(0) + - right_direction.y * this->global_path_transform.basis.get_column(1) + - right_direction.z * this->global_path_transform.basis.get_column(2) - }; - Vector2 const right2d{ Vector2{ right_direction.x, right_direction.z }.normalized() }; - // fetch the left and right curve weights according to the distance - float const left_weight{ this->curve_left_buffer->sample(distance) }; - float const right_weight{ right_curve->sample(distance) }; - // calculate xz dot product, normalized to the distance - float const dot{ right2d.dot(world_coordinate - world_curve_point) / distance }; - // use the dot product to calculate the ratio between the left and right weights ... - float const right_left_ratio{ (dot + 1.f) / 2.f }; - // ... and use that as the t-value in a lerp between left and right weights - float const weight{ Math::lerp(left_weight, right_weight, right_left_ratio) }; - // which then is the t-value of the final lerp from the unchanged height to the curve's height at this point - return Math::lerp(before, global_curve_position.y, weight); -} - -void TerrainModifierPath::path_changed() { - print_line("Path changed"); - { - SharedMutex::LockExclusive exclusive{ this->lock }; - if (this->path) { - this->path_buffer = this->path->get_curve()->duplicate_deep(); - this->path_buffer->sample_baked(0.0).hash(); - this->global_path_transform = this->path->get_global_transform(); - print_line("path len:", this->path_buffer->get_point_count()); - } - } - update_configuration_warnings(); - if (!update_bounds()) { - push_changed(get_bounds()); - } -} - -void TerrainModifierPath::children_changed() { - if (!is_inside_tree()) { - return; - } - { - SharedMutex::LockExclusive exclusive{ this->lock }; - if (this->path) { - this->path->disconnect("curve_changed", callable_mp(this, &self_type::path_changed)); - } - for (Variant var : get_children()) { - if (Path3D * path{ cast_to(var) }) { - print_line("path found"); - this->path = path; - this->path->connect("curve_changed", callable_mp(this, &self_type::path_changed)); - break; - } - } - } - path_changed(); -} - -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"); - } - if (this->path == nullptr) { - warnings.push_back("path is unassigned"); - } - 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; - if (!curve.is_valid() && this->curve_right.is_valid()) { - this->curve_left = this->curve_right; - this->lock.unlock_exclusive(); - set_curve_right(nullptr); - } else { - this->lock.unlock_exclusive(); - curves_changed(); - update_configuration_warnings(); - } -} - -Ref TerrainModifierPath::get_curve_left() const { - return this->curve_left; -} - -void TerrainModifierPath::set_curve_right(Ref curve) { - { - SharedMutex::LockExclusive exclusive{ this->lock }; - 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; - } - 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 index daaaf89..a09275b 100644 --- a/terrain_modifier.h +++ b/terrain_modifier.h @@ -1,12 +1,7 @@ #pragma once #include "core/object/object.h" -#include "core/variant/variant.h" -#include "macros.h" #include "scene/3d/marker_3d.h" -#include "scene/3d/path_3d.h" -#include "scene/resources/curve.h" -#include "shared_mutex.h" #include class Terrain; @@ -41,75 +36,3 @@ public: emit_signal(sig_terrain_changed, terrain); } }; - -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 TerrainModifierPath : public TerrainModifier { - GDCLASS(TerrainModifierPath, TerrainModifier); - static void _bind_methods(); - void curves_changed(); - bool update_bounds(); - -protected: - void _notification(int what); - -public: - float evaluate_at(Vector2 world_coordinate, float before) override; - void path_changed(); - void children_changed(); - PackedStringArray get_configuration_warnings() const override; - -private: - SharedMutex lock{}; - Path3D *path{ nullptr }; - Transform3D global_path_transform{}; - Ref path_buffer{}; - 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{}; -}; diff --git a/terrain_modifier_composite.cpp b/terrain_modifier_composite.cpp new file mode 100644 index 0000000..e40b6d7 --- /dev/null +++ b/terrain_modifier_composite.cpp @@ -0,0 +1,56 @@ +#include "terrain_modifier_composite.h" +#include "terrain/terrain.h" + +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_composite.h b/terrain_modifier_composite.h new file mode 100644 index 0000000..b2dbe27 --- /dev/null +++ b/terrain_modifier_composite.h @@ -0,0 +1,19 @@ +#pragma once + +#include "terrain/terrain_modifier.h" + +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{}; +}; diff --git a/terrain_modifier_distance.cpp b/terrain_modifier_distance.cpp new file mode 100644 index 0000000..c9bdb79 --- /dev/null +++ b/terrain_modifier_distance.cpp @@ -0,0 +1,107 @@ +#include "terrain_modifier_distance.h" +#include "core/typedefs.h" +#include "macros.h" + +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()); + } + SharedMutex::LockExclusive exclusive{ this->lock }; + this->distance_weight_curve_buffer = this->distance_weight_curve.is_valid() ? this->distance_weight_curve->duplicate(true) : nullptr; +} + +bool TerrainModifierDistance::update_bounds() { + bool changed{ false }; + Rect2 bounds{}; + { + SharedMutex::LockShared shared{ this->lock }; + Rect2 const before{ get_bounds() }; + Vector3 position{ get_thread_safe_global_position() }; + bounds.position = { position.x, position.z }; + bounds.size = { 0, 0 }; + 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 }; + } + changed = before != bounds; + } + { + SharedMutex::LockExclusive exclusive{ this->lock }; + set_bounds(bounds); + } + + 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) { + SharedMutex::LockShared shared{ this->lock }; + if (this->distance_weight_curve_buffer.is_null()) { + return before; + } + float const distance{ distance_at(world_coordinate) }; + if (distance >= this->distance_weight_curve_buffer->get_max_domain()) { + return before; + } + float const weight_offset{ 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) }; + 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) { + { + SharedMutex::LockExclusive exclusive{ this->lock }; + 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; + } + curves_changed(); + update_configuration_warnings(); +} + +Ref TerrainModifierDistance::get_distance_weight_curve() const { + return this->distance_weight_curve; +} diff --git a/terrain_modifier_distance.h b/terrain_modifier_distance.h new file mode 100644 index 0000000..2cd6ed0 --- /dev/null +++ b/terrain_modifier_distance.h @@ -0,0 +1,28 @@ +#pragma once + +#include "terrain/shared_mutex.h" +#include "terrain/terrain_modifier.h" + +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; +}; diff --git a/terrain_modifier_path.cpp b/terrain_modifier_path.cpp new file mode 100644 index 0000000..523728a --- /dev/null +++ b/terrain_modifier_path.cpp @@ -0,0 +1,263 @@ +#include "terrain_modifier_path.h" +#include "macros.h" + +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() { + { + SharedMutex::LockExclusive exclusive{ this->lock }; + 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; + } + if (!update_bounds()) { + push_changed(get_bounds()); + } +} + +bool TerrainModifierPath::update_bounds() { + bool changed{ false }; + Rect2 bounds{}; + { + // calculate the bounds, no need to make an exclusive lock if we can avoid it + SharedMutex::LockShared shared{ this->lock }; + if (this->path == nullptr) { + return false; + } + // which of the two curves is the furthest reaching + float margin{ 0.f }; + if (this->curve_left.is_valid()) { + float const domain{ this->curve_left->get_max_domain() }; + margin = domain > margin ? domain : margin; + } + if (this->curve_right.is_valid()) { + float const domain{ this->curve_right->get_max_domain() }; + margin = domain > margin ? domain : margin; + } + // alias some known data + Transform3D curve_transform{ this->path->get_global_transform() }; + PackedVector3Array const &baked_points{ this->path->get_curve()->get_baked_points() }; + // find the highest and lowest x and z values + Vector2 min{}, max{}; + for (int i{ 0 }; i < baked_points.size(); i += 10) { + Vector3 point{ baked_points[i] }; + point = { + curve_transform.basis.get_column(0) * point.x + + curve_transform.basis.get_column(1) * point.y + + curve_transform.basis.get_column(2) * point.z + + curve_transform.origin + }; + min = min.min({ point.x, point.z }); + max = max.max({ point.x, point.z }); + } + // extend found min and max with margins + min -= { margin, margin }; + max += Vector2{ margin, margin }; + // calculate bounds and check if any change is made + bounds = { min, max - min }; + changed = bounds != get_bounds(); + } + if (changed) { + // ensure we have an exclusive lock before writing thread-shared data + SharedMutex::LockExclusive exclusive{ this->lock }; + set_bounds(bounds); + } + return changed; +} + +void TerrainModifierPath::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_READY: + children_changed(); + set_notify_transform(true); + update_bounds(); + return; + case NOTIFICATION_TRANSFORM_CHANGED: + if (is_inside_tree()) { + path_changed(); + } + return; + case NOTIFICATION_CHILD_ORDER_CHANGED: + if (is_ready()) { + children_changed(); + } + return; + } +} + +float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) { + SharedMutex::LockShared shared{ this->lock }; + if (this->path == nullptr) { + print_error("no path"); + return before; + } + if (this->curve_left_buffer.is_null()) { + print_error("no curves"); + return before; + } + if (this->path_buffer.is_null()) { + print_error("no path buffer"); + return before; + } + if (this->path_buffer->get_point_count() <= 1) { + print_error("path buffer functionally empty"); + return before; + } + Ref right_curve{ this->curve_right_buffer }; + if (right_curve.is_null()) { + right_curve = this->curve_left_buffer; + } + Transform3D const inv_global_transform{ this->global_path_transform.inverse() }; + // convert world coordinate from 2d world to 3d path-local space + Vector3 relative_position{ + world_coordinate.x - this->global_path_transform.origin.x, 0.f, world_coordinate.y - this->global_path_transform.origin.z + }; + relative_position = { + inv_global_transform.basis.get_column(0) * relative_position.x + + inv_global_transform.basis.get_column(1) * relative_position.y + + inv_global_transform.basis.get_column(2) * relative_position.z + }; + // find the offset of the point closest to the world coordinate ... + real_t const offset{ this->path_buffer->get_closest_offset(relative_position) }; + // ... and fetch the corresponding transform + Transform3D const curve_point{ this->path_buffer->sample_baked_with_rotation(offset) }; + Vector3 global_curve_position{ + curve_point.origin.x * this->global_path_transform.basis.get_column(0) + + curve_point.origin.y * this->global_path_transform.basis.get_column(1) + + curve_point.origin.z * this->global_path_transform.basis.get_column(2) + + this->global_path_transform.origin + }; + // extract and xz position from sampled transform + Vector2 const world_curve_point{ Vector2{ global_curve_position.x, global_curve_position.z } }; + // calculate the xz distance from the curve + float const distance{ world_curve_point.distance_to(world_coordinate) }; + // exit early if we know for sure this point should not be affected by the path + if (distance > this->curve_left_buffer->get_max_domain() && distance > right_curve->get_max_domain()) { + return before; + } + // extract right direction and extract xz coordinates + Vector3 right_direction{ curve_point.basis.get_column(0) }; + right_direction = { + right_direction.x * this->global_path_transform.basis.get_column(0) + + right_direction.y * this->global_path_transform.basis.get_column(1) + + right_direction.z * this->global_path_transform.basis.get_column(2) + }; + Vector2 const right2d{ Vector2{ right_direction.x, right_direction.z }.normalized() }; + // fetch the left and right curve weights according to the distance + float const left_weight{ this->curve_left_buffer->sample(distance) }; + float const right_weight{ right_curve->sample(distance) }; + // calculate xz dot product, normalized to the distance + float const dot{ right2d.dot(world_coordinate - world_curve_point) / distance }; + // use the dot product to calculate the ratio between the left and right weights ... + float const right_left_ratio{ (dot + 1.f) / 2.f }; + // ... and use that as the t-value in a lerp between left and right weights + float const weight{ Math::lerp(left_weight, right_weight, right_left_ratio) }; + // which then is the t-value of the final lerp from the unchanged height to the curve's height at this point + return Math::lerp(before, global_curve_position.y, weight); +} + +void TerrainModifierPath::path_changed() { + print_line("Path changed"); + { + SharedMutex::LockExclusive exclusive{ this->lock }; + if (this->path) { + this->path_buffer = this->path->get_curve()->duplicate_deep(); + this->path_buffer->sample_baked(0.0).hash(); + this->global_path_transform = this->path->get_global_transform(); + print_line("path len:", this->path_buffer->get_point_count()); + } + } + update_configuration_warnings(); + if (!update_bounds()) { + push_changed(get_bounds()); + } +} + +void TerrainModifierPath::children_changed() { + if (!is_inside_tree()) { + return; + } + { + SharedMutex::LockExclusive exclusive{ this->lock }; + if (this->path) { + this->path->disconnect("curve_changed", callable_mp(this, &self_type::path_changed)); + } + for (Variant var : get_children()) { + if (Path3D * path{ cast_to(var) }) { + print_line("path found"); + this->path = path; + this->path->connect("curve_changed", callable_mp(this, &self_type::path_changed)); + break; + } + } + } + path_changed(); +} + +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"); + } + if (this->path == nullptr) { + warnings.push_back("path is unassigned"); + } + 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; + if (!curve.is_valid() && this->curve_right.is_valid()) { + this->curve_left = this->curve_right; + this->lock.unlock_exclusive(); + set_curve_right(nullptr); + } else { + this->lock.unlock_exclusive(); + curves_changed(); + update_configuration_warnings(); + } +} + +Ref TerrainModifierPath::get_curve_left() const { + return this->curve_left; +} + +void TerrainModifierPath::set_curve_right(Ref curve) { + { + SharedMutex::LockExclusive exclusive{ this->lock }; + 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; + } + curves_changed(); + update_configuration_warnings(); +} + +Ref TerrainModifierPath::get_curve_right() const { + return this->curve_right; +} diff --git a/terrain_modifier_path.h b/terrain_modifier_path.h new file mode 100644 index 0000000..2799f24 --- /dev/null +++ b/terrain_modifier_path.h @@ -0,0 +1,37 @@ +#pragma once + +#include "scene/3d/path_3d.h" +#include "terrain/shared_mutex.h" +#include "terrain/terrain_modifier.h" + +class TerrainModifierPath : public TerrainModifier { + GDCLASS(TerrainModifierPath, TerrainModifier); + static void _bind_methods(); + void curves_changed(); + bool update_bounds(); + +protected: + void _notification(int what); + +public: + float evaluate_at(Vector2 world_coordinate, float before) override; + void path_changed(); + void children_changed(); + PackedStringArray get_configuration_warnings() const override; + +private: + SharedMutex lock{}; + Path3D *path{ nullptr }; + Transform3D global_path_transform{}; + Ref path_buffer{}; + 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; +};