#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() {} 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()); } } 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 }; } this->lock.unlock_shared(); this->lock.lock_exclusive(); bool const changed{ before != bounds }; 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.is_null()) { this->lock.unlock_shared(); return before; } float const distance{ distance_at(world_coordinate) }; if (distance >= this->distance_weight_curve->get_max_domain()) { this->lock.unlock_shared(); return before; } float const weight_offset{ 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, 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: 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, bool a_end, Vector3 b, bool b_end, 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 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_end) { w = w > 0 ? w : 0; } if (b_end) { w = w < 1 ? w : 1; } 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.is_null() || this->curve_right.is_null() || this->points.size() <= 1) { this->lock.unlock_shared(); return before; } float const max_distance{ this->curve_left->get_max_domain() > this->curve_right->get_max_domain() ? this->curve_left->get_max_domain() : this->curve_right->get_max_domain() }; float out_score{ -INFINITY }; float out_height{ 0 }; long const count{ this->closed ? this->points.size() : this->points.size() - 1 }; for (int i{ 0 }; i < count; i++) { float dot, distance; float const height{ evaluate_line(this->points[i], !this->closed && i == 0, this->points[Math::wrapi(i + 1, 0, this->points.size())], !this->closed && i == count - 1, world_coordinate, dot, distance) }; float const left{ this->curve_left->sample(distance) }; float const right{ this->curve_right->sample(distance) }; float const ndot{ dot / distance }; float separation{ ndot / 2.f + 0.5f }; if (closed || (i > 0 && i < count - 1)) { separation = Math::round(separation); } float const weight{ left * (1 - separation) + right * separation }; float const blended_height{ Math::lerp(before, height, weight) }; float const score{ weight - (1 - Math::abs(ndot)) * 2.f - (distance / max_distance) }; 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(); 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("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; }