diff --git a/TODO.org b/TODO.org index 6c6957a..354478f 100644 --- a/TODO.org +++ b/TODO.org @@ -1,9 +1,8 @@ - [x] Cache chunk meshes - [ ] Noise modifier -- [ ] Min/Max modifier - [ ] Only load defined chunks - [ ] More accurate modifier bounds -- [x] Separate terrain module into it's own repository +- [ ] Separate terrain module into it's own repository - [ ] Texturing step - [ ] Use Semaphores in mesh gen multi-threading - [ ] Separate source files for terrain modifiers @@ -12,3 +11,4 @@ - [x] Stop processing terrain after generation is complete - [x] Don't start generation threads if all meshes are cached - [ ] Scaling modifiers +- [x] Rewrite line modifier diff --git a/register_types.cpp b/register_types.cpp index 150ee0c..c2a9aa1 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -13,7 +13,6 @@ void initialize_terrain_module(ModuleInitializationLevel p_level) { ClassDB::register_abstract_class(); ClassDB::register_class(); ClassDB::register_class(); - ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); } diff --git a/shared_mutex.cpp b/shared_mutex.cpp new file mode 100644 index 0000000..f52564a --- /dev/null +++ b/shared_mutex.cpp @@ -0,0 +1,55 @@ +#include "shared_mutex.h" +#include "core/error/error_macros.h" + +void SharedMutex::lock_shared() { + this->lock.lock(); + this->shared_count++; + this->lock.unlock(); +} + +void SharedMutex::unlock_shared() { + this->lock.lock(); + this->shared_count--; + ERR_FAIL_COND_EDMSG(this->shared_count < 0, "SharedMutex::unlock_shared, shared_count < 0"); + 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(); +} + +bool SharedMutex::is_locked() { + if (this->lock.try_lock()) { + bool locked{ this->shared_count != 0 }; + this->lock.unlock(); + return locked; + } else { + return false; + } +} + +SharedMutex::LockExclusive::LockExclusive(SharedMutex &mutex) : mtx{ mutex } { + this->mtx.lock_exclusive(); +} + +SharedMutex::LockExclusive::~LockExclusive() { + this->mtx.unlock_exclusive(); +} + +SharedMutex::LockShared::LockShared(SharedMutex &mutex) : mtx{ mutex } { + this->mtx.lock_shared(); +} + +SharedMutex::LockShared::~LockShared() { + this->mtx.unlock_shared(); +} diff --git a/shared_mutex.h b/shared_mutex.h new file mode 100644 index 0000000..3f47f99 --- /dev/null +++ b/shared_mutex.h @@ -0,0 +1,39 @@ +#pragma once + +#include "core/os/mutex.h" + +struct SharedMutex { + void lock_shared(); + void unlock_shared(); + void lock_exclusive(); + void unlock_exclusive(); + bool is_locked(); + +private: + BinaryMutex lock{}; + unsigned shared_count{}; + +public: + class LockExclusive { + SharedMutex &mtx; + + public: + explicit LockExclusive(SharedMutex &mtx); + ~LockExclusive(); + LockExclusive() = delete; + LockExclusive(LockExclusive &) = delete; + LockExclusive(LockExclusive &&) = delete; + LockExclusive &operator=(LockExclusive &) = delete; + }; + class LockShared { + SharedMutex &mtx; + + public: + explicit LockShared(SharedMutex &mtx); + ~LockShared(); + LockShared() = delete; + LockShared(LockShared &) = delete; + LockShared(LockShared &&) = delete; + LockShared &operator=(LockShared &) = delete; + }; +}; diff --git a/terrain_modifier.cpp b/terrain_modifier.cpp index f8ada28..3af6a2e 100644 --- a/terrain_modifier.cpp +++ b/terrain_modifier.cpp @@ -4,6 +4,7 @@ #include "core/variant/variant.h" #include "macros.h" #include "scene/main/node.h" +#include "scene/resources/curve.h" #include "terrain/terrain.h" #include @@ -22,6 +23,7 @@ void TerrainModifier::_notification(int what) { set_notify_transform(true); } this->thread_safe_global_position = get_global_position(); + return; case NOTIFICATION_TRANSFORM_CHANGED: this->thread_safe_global_position = get_global_position(); return; @@ -55,32 +57,6 @@ 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"); } @@ -89,29 +65,32 @@ void TerrainModifierDistance::curves_changed() { if (!update_bounds()) { push_changed(get_bounds()); } - this->lock.lock_exclusive(); + SharedMutex::LockExclusive exclusive{ this->lock }; 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() }; + bool changed{ false }; 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 }; + { + 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; } - bool const changed{ before != bounds }; - this->lock.unlock_shared(); - this->lock.lock_exclusive(); - set_bounds(bounds); - this->lock.unlock_exclusive(); + { + SharedMutex::LockExclusive exclusive{ this->lock }; + set_bounds(bounds); + } + return changed; } @@ -139,21 +118,17 @@ float TerrainModifierDistance::distance_at(Vector2 const &world_coordinate) { } float TerrainModifierDistance::evaluate_at(Vector2 world_coordinate, float before) { - this->lock.lock_shared(); + SharedMutex::LockShared shared{ this->lock }; 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; } @@ -166,17 +141,18 @@ PackedStringArray TerrainModifierDistance::get_configuration_warnings() const { } 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)); + { + 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; } - this->distance_weight_curve = curve; - this->lock.unlock_exclusive(); curves_changed(); update_configuration_warnings(); } @@ -185,78 +161,69 @@ 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() { + { + 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()); } - 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; + 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; } - 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; + float const domain{ this->curve_right->get_max_domain() }; + margin = domain > margin ? domain : margin; } - min -= { max_distance, max_distance }; - max += { max_distance * 2.f, max_distance * 2.f }; + // 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); } - 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; } @@ -265,107 +232,140 @@ void TerrainModifierPath::_notification(int what) { default: return; case NOTIFICATION_READY: - update_bounds(); + children_changed(); set_notify_transform(true); + update_bounds(); return; case NOTIFICATION_TRANSFORM_CHANGED: if (is_inside_tree()) { - if (!update_bounds()) { - push_changed(get_bounds()); - } + path_changed(); } return; case NOTIFICATION_CHILD_ORDER_CHANGED: - path_changed(); + if (is_ready()) { + children_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(); + 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; } - 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; + 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; } - this->lock.unlock_shared(); - return out_height; + // 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() { - 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; - } - } + 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()); } - last = var; } - this->lock.unlock_exclusive(); + 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; } @@ -383,11 +383,12 @@ void TerrainModifierPath::set_curve_left(Ref curve) { } } this->curve_left = curve; - this->lock.unlock_exclusive(); if (!curve.is_valid() && this->curve_right.is_valid()) { - curve = this->curve_right; + this->curve_left = this->curve_right; + this->lock.unlock_exclusive(); set_curve_right(nullptr); } else { + this->lock.unlock_exclusive(); curves_changed(); update_configuration_warnings(); } @@ -398,20 +399,21 @@ Ref TerrainModifierPath::get_curve_left() const { } 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)); + { + SharedMutex::LockExclusive exclusive{ this->lock }; + if (curve.is_valid() && curve == this->curve_left) { + curve = curve->duplicate(); } - if (curve.is_valid()) { - curve->connect_changed(callable_mp(this, &self_type::curves_changed)); + 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->curve_right = curve; - this->lock.unlock_exclusive(); curves_changed(); update_configuration_warnings(); } diff --git a/terrain_modifier.h b/terrain_modifier.h index fc318fb..daaaf89 100644 --- a/terrain_modifier.h +++ b/terrain_modifier.h @@ -4,7 +4,9 @@ #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; @@ -40,17 +42,6 @@ public: } }; -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(); @@ -75,17 +66,6 @@ public: 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(); @@ -94,19 +74,18 @@ class TerrainModifierPath : public TerrainModifier { 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(); + void children_changed(); PackedStringArray get_configuration_warnings() const override; private: SharedMutex lock{}; - float min_height{}; - float max_height{}; - bool closed{ false }; - Vector points{}; + Path3D *path{ nullptr }; + Transform3D global_path_transform{}; + Ref path_buffer{}; Ref curve_left_buffer{}; Ref curve_left{}; Ref curve_right_buffer{};