From 31a99860666fa7e0caf4335f3e68fe4f991a10e1 Mon Sep 17 00:00:00 2001 From: Sara Date: Fri, 27 Feb 2026 18:01:14 +0100 Subject: [PATCH] feat: WIP terrain path modifiers --- modules/terrain/register_types.cpp | 2 + modules/terrain/terrain_modifier.cpp | 251 +++++++++++++++++++++++---- modules/terrain/terrain_modifier.h | 51 +++++- project/scenes/terrain_test.tscn | 58 +++---- 4 files changed, 288 insertions(+), 74 deletions(-) diff --git a/modules/terrain/register_types.cpp b/modules/terrain/register_types.cpp index 3aaff6ba..deaa9de4 100644 --- a/modules/terrain/register_types.cpp +++ b/modules/terrain/register_types.cpp @@ -12,6 +12,8 @@ void initialize_terrain_module(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_abstract_class(); ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); } diff --git a/modules/terrain/terrain_modifier.cpp b/modules/terrain/terrain_modifier.cpp index cffb239e..30674478 100644 --- a/modules/terrain/terrain_modifier.cpp +++ b/modules/terrain/terrain_modifier.cpp @@ -1,13 +1,12 @@ #include "terrain_modifier.h" #include "core/config/engine.h" +#include "core/math/math_funcs.h" #include "core/variant/variant.h" #include "macros.h" #include "terrain/terrain.h" #include -void TerrainModifier::_bind_methods() { - BIND_PROPERTY(Variant::FLOAT, blend_distance); -} +void TerrainModifier::_bind_methods() {} void TerrainModifier::_notification(int what) { switch (what) { @@ -24,18 +23,6 @@ void TerrainModifier::_notification(int what) { } } -float TerrainModifier::blend(float under, float over) { - float const difference{ under - over }; - float const distance{ Math::abs(difference) }; - // .25 because we need half of each half of the blend range to be used - float const center_distance{ this->blend_distance == 0.f ? 0.f : this->blend_distance * 0.25f - distance / this->blend_distance }; - if (center_distance <= 0.f) { - return over; - } - float const smooth_center_distance{ center_distance * center_distance }; - return over + smooth_center_distance; -} - void TerrainModifier::push_changed(Rect2 area) { if (this->terrain) { this->terrain->push_changed(area); @@ -44,22 +31,25 @@ void TerrainModifier::push_changed(Rect2 area) { float TerrainModifier::evaluate_at(Vector2 world_coordinate, float before) { Vector3 const global_position{ get_thread_safe_global_position() }; - world_coordinate -= { global_position.x, global_position.z }; - return blend(before, 0.0); + 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 TerrainModifier::set_blend_distance(float value) { - this->blend_distance = value; -} - -float TerrainModifier::get_blend_distance() const { - return this->blend_distance; -} - void SharedMutex::lock_shared() { this->lock.lock(); this->shared_count++; @@ -91,11 +81,6 @@ void TerrainModifierDistance::_bind_methods() { } void TerrainModifierDistance::curves_changed() { - this->lock.lock_exclusive(); - if (this->distance_weight_curve.is_valid()) { - this->distance_weight_curve->bake(); - } - this->lock.unlock_exclusive(); if (!update_bounds()) { push_changed(get_bounds()); } @@ -117,11 +102,7 @@ bool TerrainModifierDistance::update_bounds() { this->lock.unlock_shared(); this->lock.lock_exclusive(); bool const changed{ before != bounds }; - if (changed) { - set_bounds(bounds); - push_changed(before); - push_changed(bounds); - } + set_bounds(bounds); this->lock.unlock_exclusive(); return changed; } @@ -164,7 +145,7 @@ float TerrainModifierDistance::evaluate_at(Vector2 world_coordinate, float befor std::clamp(distance, this->distance_weight_curve->get_min_domain(), this->distance_weight_curve->get_max_domain()) }; float const weight{ this->distance_weight_curve->sample(weight_offset) }; - float out{ weight <= 0.f ? before : Math::lerp(before, blend(before, get_thread_safe_global_position().y), weight) }; + float out{ weight <= 0.f ? before : Math::lerp(before, get_thread_safe_global_position().y, weight) }; this->lock.unlock_shared(); return out; @@ -197,3 +178,201 @@ void TerrainModifierDistance::set_distance_weight_curve(Ref curve) { 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: + if ((this->path = cast_to(get_parent()))) { + this->path->path_changed(); + } + return; + case NOTIFICATION_READY: + set_notify_transform(true); + return; + case NOTIFICATION_TRANSFORM_CHANGED: + if (this->path) { + 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()); + } +} + +bool TerrainModifierPath::update_bounds() { + Vector2 min{}, max{}; + this->lock.lock_shared(); + if (this->points.is_empty() || this->curve_left.is_null() || this->curve_right.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_right{ this->curve_right->get_max_domain() }; + float max_distance{ max_distance_left > max_distance_right ? max_distance_left : max_distance_right }; + min -= { max_distance, max_distance }; + max += { max_distance, max_distance }; + } + Rect2 bounds{ min, max - min }; + bool const changed{ bounds != get_bounds() }; + this->lock.unlock_shared(); + set_bounds(bounds); + 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; + } +} + +float TerrainModifierPath::evaluate_line(Vector3 a, Vector3 b, Vector2 world_coordinate, float &out_dot, float &out_distance) { + Vector2 a2{ a.x, a.z }, b2{ b.x, b.z }; + Vector2 const relative_coordinate{ world_coordinate - a2 }; + Vector2 const difference2{ b2 - a2 }; + float const 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 }); + if (a.y > b.y) { + return a.y + (b.y - a.y) * (w > 0 ? w : 0); + } else { + return a.y + (b.y - a.y) * (w < 1 ? w : 1); + } +} + +float TerrainModifierPath::evaluate_at(Vector2 world_coordinate, float before) { + this->lock.lock_shared(); + if (this->curve_left.is_null() || this->curve_right.is_null() || this->points.size() <= 1) { + this->lock.unlock_shared(); + return before; + } + float out_score{ -INFINITY }; + float out_height{ 0 }; + for (int i{ 0 }; i < this->points.size(); i++) { + if (this->closed || i < this->points.size() - 1) { + float dot, distance; + float height{ evaluate_line(this->points[i], this->points[Math::wrapi(i + 1, 0, this->points.size())], world_coordinate, dot, distance) }; + float left{ this->curve_left->sample(distance) }; + float right{ this->curve_right->sample(distance) }; + float ndot{ dot / distance }; + float separation{ ndot / 2.f + 0.5f }; + float weight{ left * (1 - separation) + right * separation }; + float blended_height{ Math::lerp(before, height, weight) }; + float score{ weight - (Math::abs(ndot) == 1) * 100000.f }; + if (score > out_score) { + out_score = score; + out_height = blended_height; + } + } + } + this->lock.unlock_shared(); + return out_height; +} + +void TerrainModifierPath::path_changed() { + this->lock.lock_exclusive(); + this->points.clear(); + Vector3 last{ INFINITY, INFINITY, INFINITY }; + for (Variant var : get_children()) { + if (TerrainModifierPathPoint * point{ cast_to(var) }) { + if (var != last) { + this->points.push_back(point->get_global_position()); + } + } + 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("distance_weight_curve is invalid, add a valid curve_left"); + } + if (this->curve_right.is_null()) { + warnings.push_back("distance_weight_curve is invalid, add a valid curve_right"); + } + return warnings; +} + +void TerrainModifierPath::set_curve_left(Ref curve) { + this->lock.lock_exclusive(); + 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(); + 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 (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; +} diff --git a/modules/terrain/terrain_modifier.h b/modules/terrain/terrain_modifier.h index 4e1bfd41..92087eb8 100644 --- a/modules/terrain/terrain_modifier.h +++ b/modules/terrain/terrain_modifier.h @@ -14,25 +14,22 @@ class TerrainModifier : public Marker3D { protected: void _notification(int what); - float blend(float under, float over); void push_changed(Rect2 bounds); public: virtual float evaluate_at(Vector2 world_coordinate, float before); private: - float blend_distance{ 10.0 }; Vector3 thread_safe_global_position{}; Terrain *terrain{ nullptr }; Rect2 bounds{ { -INFINITY, -INFINITY }, { INFINITY, INFINITY } }; protected: - GET_SET_FNS(Rect2, bounds); + void set_bounds(Rect2 bounds); + Rect2 get_bounds() const; public: Vector3 get_thread_safe_global_position() const; - void set_blend_distance(float value); - float get_blend_distance() const; GET_SET_FNS(Terrain *, terrain); }; @@ -55,7 +52,7 @@ class TerrainModifierDistance : public TerrainModifier { protected: void _notification(int what); - virtual float distance_at(Vector2 const &world_coordinate); + float distance_at(Vector2 const &world_coordinate); public: float evaluate_at(Vector2 world_coordinate, float before) override; @@ -69,3 +66,45 @@ 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, Vector3 b, Vector2 world_coordinate, float &out_dot, float &out_distance); + +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{}; + 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; +}; diff --git a/project/scenes/terrain_test.tscn b/project/scenes/terrain_test.tscn index 690d47a7..d3e2d6e7 100644 --- a/project/scenes/terrain_test.tscn +++ b/project/scenes/terrain_test.tscn @@ -12,25 +12,20 @@ sky_material = SubResource("ProceduralSkyMaterial_kbmr5") background_mode = 2 sky = SubResource("Sky_w3uoq") -[sub_resource type="Curve" id="Curve_nonsf"] -_limits = [0.0, 1.0, 0.0, 500.0] -_data = [Vector2(47.722435, 1), 0.0, -9.816581e-05, 0, 0, Vector2(353.09595, 0.40317744), -0.0059316778, -0.0059316778, 0, 0, Vector2(500, 0), 0.0, 0.0, 0, 0] +[sub_resource type="Curve" id="Curve_kbmr5"] +_limits = [0.0, 1.0, 0.0, 100.0] +_data = [Vector2(0, 1), 0.0, -0.0015643721, 0, 0, Vector2(60.370926, 0.60930693), -0.018071167, -0.018071167, 0, 0, Vector2(100, 0), 0.0, 0.0, 0, 0] point_count = 3 [sub_resource type="Curve" id="Curve_w3uoq"] -_limits = [0.0, 1.0, 0.0, 500.0] -_data = [Vector2(0, 1), 0.0, -9.816581e-05, 0, 0, Vector2(269.62408, 0.60529476), -0.00438691, -0.00438691, 0, 0, Vector2(500, 0), 0.0, 0.0, 0, 0] -point_count = 3 - -[sub_resource type="Curve" id="Curve_kbmr5"] -_limits = [0.0, 1.0, 0.0, 300.0] -_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(126.071, 0.5549462), -0.009107173, -0.009107173, 0, 0, Vector2(186.00266, 0.15958571), -0.004071936, -0.004071936, 0, 0, Vector2(300, 0), 0.0, 0.0, 0, 0] -point_count = 4 +_limits = [0.0, 1.0, 0.0, 200.0] +_data = [Vector2(0, 1), 0.0, -0.005, 0, 1, Vector2(200, 0), -0.005, 0.0, 1, 0] +point_count = 2 [sub_resource type="Curve" id="Curve_chm2y"] _limits = [0.0, 1.0, 0.0, 300.0] -_data = [Vector2(0, 1), -0.005856493, -0.0037222188, 0, 0, Vector2(300, 0), 7.00571e-05, -0.05797184, 0, 0] -point_count = 2 +_data = [Vector2(0, 1), -0.005856493, -0.0020244052, 0, 0, Vector2(195.24551, 0.17229712), -0.0043557375, -0.0043557375, 0, 0, Vector2(300, 0), 0.00031304389, -0.05797184, 0, 0] +point_count = 3 [sub_resource type="BoxMesh" id="BoxMesh_kbmr5"] @@ -44,29 +39,28 @@ side_length = 1000 chunk_size = 100 thread_count = 5 -[node name="TerrainModifierDistance2" type="TerrainModifierDistance" parent="Terrain" unique_id=2110821264] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 856.484, -141.91985, 376.6767) -blend_distance = 0.0 -distance_weight_curve = SubResource("Curve_nonsf") +[node name="TerrainModifierPath" type="TerrainModifierPath" parent="Terrain" unique_id=462259542] +transform = Transform3D(2.7896824, 0, 0, 0, 1, 0, 0, 0, 3.9856973, 154.71588, 151.32993, 365.46173) +curve_left = SubResource("Curve_kbmr5") +curve_right = SubResource("Curve_w3uoq") -[node name="TerrainModifierDistance5" type="TerrainModifierDistance" parent="Terrain" unique_id=54251754] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 587.8701, -100.01076, 96.289734) -blend_distance = 0.0 -distance_weight_curve = SubResource("Curve_w3uoq") +[node name="TerrainModifierPathPoint" type="TerrainModifierPathPoint" parent="Terrain/TerrainModifierPath" unique_id=1975236067] +transform = Transform3D(0.9999999, 0, 0, 0, 1, 0, 0, 0, 0.9999999, -6.678116, -33.99875, -74.15768) -[node name="TerrainModifierDistance4" type="TerrainModifierDistance" parent="Terrain" unique_id=961725906] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 945.23486, -114.01094, 837.39813) -blend_distance = 0.0 -distance_weight_curve = SubResource("Curve_nonsf") +[node name="TerrainModifierPathPoint5" type="TerrainModifierPathPoint" parent="Terrain/TerrainModifierPath" unique_id=2007122252] +transform = Transform3D(0.99999994, 0, 0, 0, 1, 0, 0, 0, 0.99999994, 41.63815, 4.6532288, -38.86402) -[node name="TerrainModifierDistance" type="TerrainModifierDistance" parent="Terrain" unique_id=1885116624] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 111.654144, 74.591, 740.1567) -blend_distance = 0.0 -distance_weight_curve = SubResource("Curve_kbmr5") +[node name="TerrainModifierPathPoint2" type="TerrainModifierPathPoint" parent="Terrain/TerrainModifierPath" unique_id=88875414] +transform = Transform3D(0.99999994, 0, 0, 0, 1, 0, 0, 0, 0.99999994, 4.2666435, -27.249336, 5.506424) -[node name="TerrainModifierDistance3" type="TerrainModifierDistance" parent="Terrain" unique_id=1846439541] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 260.8238, 202.32297, 899.09283) -blend_distance = 0.0 +[node name="TerrainModifierPathPoint3" type="TerrainModifierPathPoint" parent="Terrain/TerrainModifierPath" unique_id=910243114] +transform = Transform3D(-0.08673841, 0, 0.9962309, 0, 1, 0, -0.9962308, 0, -0.08673839, 77.83667, 84.083954, 12.383522) + +[node name="TerrainModifierPathPoint4" type="TerrainModifierPathPoint" parent="Terrain/TerrainModifierPath" unique_id=738726374] +transform = Transform3D(-0.08673839, 0, 0.9962309, 0, 1, 0, -0.9962308, 0, -0.08673841, 124.05687, -50.373947, -21.531578) + +[node name="TerrainModifierDistance8" type="TerrainModifierDistance" parent="Terrain" unique_id=1993490768] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 784.91595, 251.11382, 135.92102) distance_weight_curve = SubResource("Curve_chm2y") [node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1089775425]