From 9195e42743d10cf409ffbde68aa8153666a2b037 Mon Sep 17 00:00:00 2001 From: Marvin Ewald Date: Mon, 15 Jul 2024 23:05:00 +0200 Subject: [PATCH] Add property for constraining the distance between endpoints --- demo/addons/ropesim/Rope.gd | 7 ++++ demo/rope_examples/handles.tscn | 37 ++++++++++++++++++++ src/NativeRopeContext.cpp | 60 ++++++++++++++++++++++----------- src/NativeRopeContext.hpp | 1 + 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/demo/addons/ropesim/Rope.gd b/demo/addons/ropesim/Rope.gd index 5957c21..d5c6b76 100644 --- a/demo/addons/ropesim/Rope.gd +++ b/demo/addons/ropesim/Rope.gd @@ -23,6 +23,13 @@ signal on_point_count_changed() ## Overall rope length. Will be distributed uniformly among all segments. @export var rope_length: float = 100: set = _set_length +## Maximum euclidean distance between rope endpoints. Zero or negative for no limitation. +## This is an approximation and not 100% accurate. +## It is intended as a simple way to constraint the rope length when both endpoints are fixed by a RopeHandle. +## The actual length of the rope might differ depending on the number of constraint iterations. +## Fixed points in between are not taken into account. +@export var max_endpoint_distance: float = -1 + ## (Optional) Allows to distribute the length of rope segment in a non-uniform manner. ## Useful when certain parts of the rope should be more detailed than the rest. ## For example, if it is known that most movement happens at the beginning of the rope, a curve with diff --git a/demo/rope_examples/handles.tscn b/demo/rope_examples/handles.tscn index 031e3d6..ad77ead 100644 --- a/demo/rope_examples/handles.tscn +++ b/demo/rope_examples/handles.tscn @@ -150,3 +150,40 @@ script = ExtResource("3_h0p3k") num_segments = 20 rope_length = 200.0 metadata/_edit_group_ = true + +[node name="Label6" type="Label" parent="."] +offset_left = 304.0 +offset_top = 511.0 +offset_right = 823.0 +offset_bottom = 534.0 +text = "max_endpoint_distance limits the distance between both rope endpoints." + +[node name="Rope5" type="Node2D" parent="."] +position = Vector2(356, 591) +script = ExtResource("3_h0p3k") +max_endpoint_distance = 150.0 +fixate_begin = false + +[node name="RopeHandleBegin" type="Marker2D" parent="Rope5"] +position = Vector2(-10, -4) +script = ExtResource("1_n2oah") +rope_path = NodePath("..") +rope_position = 0.0 +strength = 1.0 + +[node name="RopeHandleEnd" type="Marker2D" parent="Rope5"] +position = Vector2(204, 22) +script = ExtResource("1_n2oah") +rope_path = NodePath("..") +strength = 1.0 + +[node name="Rope6" type="Node2D" parent="."] +position = Vector2(636, 564) +script = ExtResource("3_h0p3k") +max_endpoint_distance = 150.0 + +[node name="RopeHandleEnd" type="Marker2D" parent="Rope6"] +position = Vector2(160, 86) +script = ExtResource("1_n2oah") +rope_path = NodePath("..") +strength = 1.0 diff --git a/src/NativeRopeContext.cpp b/src/NativeRopeContext.cpp index d885187..011f1a3 100644 --- a/src/NativeRopeContext.cpp +++ b/src/NativeRopeContext.cpp @@ -39,6 +39,7 @@ void NativeRopeContext::load_context(Node2D* rope) gravity_direction = rope->get("gravity_direction"); damping = rope->get("damping"); stiffness = rope->get("stiffness"); + max_endpoint_distance = rope->get("max_endpoint_distance"); num_constraint_iterations = rope->get("num_constraint_iterations"); seg_lengths = rope->call("get_segment_lengths"); simulation_weights = rope->get("_simulation_weights"); @@ -151,30 +152,51 @@ void NativeRopeContext::_simulate_stiffness(PackedVector2Array* velocities) cons } } +static void constraint_segment(Vector2* point_a, Vector2* point_b, float weight_a, float weight_b, float seg_length) +{ + const Vector2 diff = *point_b - *point_a; + const float distance = diff.length(); + const float error = (seg_length - distance) * 0.5f; + const Vector2 dir = error * (diff / distance); + + // If one point has a weight < 1.0, the other point must compensate the difference in + // relation to its own weight. + // This is especially relevant with fixate_begin = true or with arbitrary weights = 0.0. + // In that case non-fixed point should be constrained by the whole error distance, not + // just half of it, because the other one can obviously not move. + // It actually works quite fine without this compensation, but this is more correct and + // produces better results. + *point_a -= (weight_a + weight_a * (1.0 - weight_b)) * dir; + *point_b += (weight_b + weight_b * (1.0 - weight_a)) * dir; +} + void NativeRopeContext::_constraint() { + const bool use_euclid_constraint = max_endpoint_distance > 0; + Vector2* first_point; + Vector2* last_point; + float euclid_constraint_first_weight; + float max_stretch_length_sqr; + + if (use_euclid_constraint) + { + first_point = &points[0]; + last_point = &points[(int)points.size() - 1]; + euclid_constraint_first_weight = fixate_begin ? 0.0 : 1.0; + max_stretch_length_sqr = max_endpoint_distance * max_endpoint_distance; + } + for (int _ = 0; _ < num_constraint_iterations; ++_) { - for (int i = 0; i < points.size() - 1; ++i) + if (use_euclid_constraint) { - const Vector2 diff = points[i + 1] - points[i]; - const float distance = diff.length(); - const float error = (seg_lengths[i] - distance) * 0.5f; - const Vector2 dir = error * (diff / distance); - const float weight = simulation_weights[i]; - const float next_weight = simulation_weights[i + 1]; - - // If one point has a weight < 1.0, the other point must compensate the difference in - // relation to its own weight. - // This is especially relevant with fixate_begin = true or with arbitrary weights = 0.0. - // In that case non-fixed point should be constrained by the whole error distance, not - // just half of it, because the other one can obviously not move. - // It actually works quite fine without this compensation, but this is more correct and - // produces better results. - // points[i] -= weight * dir; - // points[i + 1] += next_weight * dir; - points[i] -= (weight + weight * (1.0 - next_weight)) * dir; - points[i + 1] += (next_weight + next_weight * (1.0 - weight)) * dir; + const float rope_length_sqr = first_point->distance_squared_to(*last_point); + + if (rope_length_sqr > max_stretch_length_sqr) + constraint_segment(first_point, last_point, euclid_constraint_first_weight, 1.0, max_endpoint_distance); } + + for (int i = 0; i < points.size() - 1; ++i) + constraint_segment(&points[i], &points[i + 1], simulation_weights[i], simulation_weights[i + 1], seg_lengths[i]); } } diff --git a/src/NativeRopeContext.hpp b/src/NativeRopeContext.hpp index f98cef2..186516a 100644 --- a/src/NativeRopeContext.hpp +++ b/src/NativeRopeContext.hpp @@ -33,6 +33,7 @@ namespace godot Vector2 gravity_direction; float damping; float stiffness; + float max_endpoint_distance; int num_constraint_iterations; Ref damping_curve; bool fixate_begin;