#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; }