Skip to content

Commit ca22971

Browse files
committed
Improve stiffness simulation
- Fix strange behavior with `RopeHandle`s - Implement bidirectional stiffness computation - Add more settings to fine-tune stiffness behavior
1 parent 3fffe3a commit ca22971

3 files changed

Lines changed: 100 additions & 24 deletions

File tree

demo/addons/ropesim/Rope.gd

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ class_name Rope
44

55
# TODO: Split line rendering into a separate node
66

7+
enum StiffnessMethod {
8+
## Computes a single forward pass from front to back.
9+
## Produces decent results but may look unusual without [member fixate_begin] enabled.
10+
## It could also behave unstable in certain scenarios, i.e. jittery or erratic movement. [br]
11+
## This was the default behavior before v1.9.
12+
Legacy,
13+
## Computes a forward and backward pass.
14+
## Produces overall better and more stable results than [member StiffnessMethod.Legacy].
15+
## Acts more sensitive to [member stiffness_bend_curve].
16+
Bidirectional,
17+
}
18+
719
## Triggered when the rope has been registered at the NativeRopeServer.
820
signal on_registered()
921

@@ -37,6 +49,20 @@ signal on_point_count_changed()
3749
## Might not produce 100% realistic results with fixed points.
3850
@export var stiffness: float = 0.0
3951

52+
## Determines which method to use for stiffness simulation.
53+
@export var stiffness_method: StiffnessMethod = StiffnessMethod.Bidirectional
54+
55+
## (Optional) Scale the stiffness along the rope.
56+
## If the rope behaves erratic,
57+
@export var stiffness_curve: Curve
58+
59+
## (Optional) Scale the stiffness individually per segment according to how far each segment is bent.
60+
## The y value of the curve is multiplied with the stiffness.
61+
## x = 0 corresponds to no bending and x = 1 corresponds to 180° bending in regard to the previous
62+
## segment.
63+
## If unset, defaults to a quadratic curve.
64+
@export var stiffness_bend_curve: Curve
65+
4066
## Gravity
4167
@export var gravity: float = 100
4268

@@ -450,7 +476,6 @@ func _set_seg_dist(value: Curve) -> void:
450476
segment_length_distribution.changed.connect(update_segments)
451477
update_segments()
452478

453-
454479
static func _resize_with_default(array: Variant, new_size: int, default: Variant) -> void:
455480
@warning_ignore("unsafe_method_access")
456481
var oldsize: int = array.size()

src/NativeRopeContext.cpp

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const Vector2 VECTOR_ZERO = Vector2(); // NOLINT(cert-err58-cpp)
1010
const Vector2 VECTOR_DOWN = Vector2(0, 1); // NOLINT(cert-err58-cpp)
1111

1212

13-
static float get_point_perc(int index, const PackedVector2Array& points)
13+
static float get_point_perc(int64_t index, const PackedVector2Array& points)
1414
{
1515
return points.size() > 0 ? static_cast<float>(index) / static_cast<float>(points.size() - 1) : 0;
1616
}
@@ -54,6 +54,9 @@ void NativeRopeContext::load_context(Node2D* rope)
5454
wind = rope->get("wind");
5555
damping = rope->get("damping");
5656
stiffness = rope->get("stiffness");
57+
stiffness_method = static_cast<StiffnessMethod>(static_cast<int>(rope->get("stiffness_method")));
58+
stiffness_curve = rope->get("stiffness_curve");
59+
stiffness_bend_curve = rope->get("stiffness_bend_curve");
5760
max_endpoint_distance = rope->get("max_endpoint_distance");
5861
num_constraint_iterations = rope->get("num_constraint_iterations");
5962
seg_lengths = rope->call("get_segment_lengths");
@@ -132,7 +135,7 @@ void NativeRopeContext::_simulate_velocities(double delta)
132135

133136
// Apply other forces
134137
_simulate_wind(&velocities);
135-
_simulate_stiffness(&velocities);
138+
_simulate_stiffness(delta, &velocities);
136139

137140
// Move points according to velocity
138141
for (int i = first_idx; i < size; ++i)
@@ -158,46 +161,83 @@ void NativeRopeContext::_simulate_wind(PackedVector2Array* velocities) const
158161
}
159162
}
160163

161-
void NativeRopeContext::_simulate_stiffness(PackedVector2Array* velocities) const
164+
void NativeRopeContext::_simulate_stiffness(double delta, PackedVector2Array* velocities) const
162165
{
163-
// NOTE: oldpoints should not be used here, see comments in simulate_velocities().
164-
165166
if (stiffness <= 0)
166167
return;
167168

168-
Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized();
169-
Vector2 last_stiffness_force;
169+
const auto num_points = points.size();
170+
171+
if (stiffness_method == StiffnessMethod::Legacy)
172+
{
173+
const Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized();
174+
_simulate_stiffness_segment(delta, velocities, 1, num_points, parent_seg_dir); // Start at second and stop at last point
175+
}
176+
else
177+
{
178+
const Vector2 end_parent = (points[num_points - 2] - points[num_points - 1]).normalized();
179+
180+
if (fixate_begin)
181+
{
182+
// TODO: Make direction configurable, e.g. an enum to select the mode ("transform", "gravity", "custom").
183+
const Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized();
184+
_simulate_stiffness_segment(delta, velocities, 1, num_points, parent_seg_dir); // Start at second and stop at last point
185+
_simulate_stiffness_segment(delta, velocities, num_points - 2, 0, end_parent); // Start at second last and stop before first point (because it's fixed)
186+
}
187+
else
188+
{
189+
const Vector2 begin_parent = (points[1] - points[0]).normalized();
190+
_simulate_stiffness_segment(delta, velocities, 1, num_points, begin_parent); // Start at second and stop at last point
191+
_simulate_stiffness_segment(delta, velocities, num_points - 2, -1, end_parent); // Start at second last and stop at first point
192+
}
193+
}
194+
}
195+
196+
void NativeRopeContext::_simulate_stiffness_segment(double /*delta*/, PackedVector2Array* velocities, int64_t idx_from, int64_t idx_stop, Vector2 last_stiffness_dir) const
197+
{
198+
// NOTE: oldpoints should not be used here, see comments in simulate_velocities().
170199

171-
for (int i = 1; i < points.size(); ++i)
200+
Vector2 force;
201+
const bool use_stiffness_curve = stiffness_curve.is_valid() && stiffness_curve->get_point_count() > 0;
202+
const bool use_bend_curve = stiffness_bend_curve.is_valid() && stiffness_bend_curve->get_point_count() > 0;
203+
const int64_t idx_step = idx_from <= idx_stop ? 1 : -1;
204+
205+
for (int64_t i = idx_from; i != idx_stop; i += idx_step)
172206
{
173207
// NOTE: Asked a physicist to confirm this computation is physically accurate.
174208
// He mentioned that, while it is technically correct, there is a material-dependent limit
175209
// how far an object can bend before bending properties (stiffness) changes.
176210
// E.g. a material might be less inclined to snap back into place at smaller bend angles, or
177211
// a material might stop bending at some point, i.e. when it breaks.
178-
// This implementation should probably suffice in most cases, but a more advanced
179-
// implementation would include curves for stiffness in relation to segment position and
180-
// for stiffness in relation to bend angle.
181212
//
182213
// | parent_seg_dir ---> parent_seg_dir.orthogonal()
183214
// | \
184215
// V \ seg_dir
185216
// \ seg_dir V
186-
// \
187-
// V
188-
const Vector2 seg_dir = (points[i] - points[i - 1]).normalized();
189-
const float angle = seg_dir.angle_to(parent_seg_dir);
217+
// \ / force_dir
218+
// V V
219+
// / force_dir
220+
// V
221+
//
222+
const auto last_idx = i - idx_step;
223+
const Vector2 seg_dir = (points[i] - points[last_idx]).normalized();
224+
const float angle = seg_dir.angle_to(last_stiffness_dir); // angle is signed and can be used to determine the force direction
225+
const float bend_factor = Math::absf(angle / 3.1415f);
226+
const float scale_curve_pos = get_point_perc(idx_step >= 0 ? i : (points.size() - 1 - i), points);
227+
const float scale = use_stiffness_curve ? stiffness_curve->sample_baked(scale_curve_pos) : 1.0f;
228+
// If no curve specified, use quadratic scale by default
229+
const float bend_scale = (use_bend_curve ? stiffness_bend_curve->sample_baked(bend_factor) : bend_factor * bend_factor);
190230

191231
// The force directs orthogonal to the current segment
192-
const Vector2 force_dir = seg_dir.orthogonal();
232+
const Vector2 force_dir = Math::sign(-angle) * seg_dir.orthogonal();
233+
force += force_dir * stiffness * scale * bend_scale;
193234

194-
// Scale the force the further the segment bends.
195-
// angle is signed and can be used to determine the force direction
196-
last_stiffness_force += force_dir * (-angle / 3.1415f) * stiffness;
197-
parent_seg_dir = seg_dir;
235+
// `simulation_weights` will be applied later in `_simulate_velocities()`
236+
(*velocities)[i] += force;
198237

199-
// Update velocity
200-
(*velocities)[i] += last_stiffness_force;
238+
// Carry velocity over to the next segment. `simulation_weights` must be applied now to not carry over a wrong value.
239+
force *= simulation_weights[i];
240+
last_stiffness_dir = seg_dir;
201241
}
202242
}
203243

src/NativeRopeContext.hpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
namespace godot {
99

10+
// Should match Rope.gd
11+
enum class StiffnessMethod : int8_t
12+
{
13+
Legacy,
14+
Bidirectional
15+
};
16+
1017
// Caches properties of a rope node and performs simulation.
1118
// TODO: Could be used as base class in the future to manage rope data in C++ and not in GDScript.
1219
class NativeRopeContext // NOLINT(cppcoreguidelines-special-member-functions)
@@ -22,7 +29,8 @@ class NativeRopeContext // NOLINT(cppcoreguidelines-special-member-functions)
2229
protected:
2330
void _simulate_velocities(double delta);
2431
void _simulate_wind(PackedVector2Array* velocities) const;
25-
void _simulate_stiffness(PackedVector2Array* velocities) const;
32+
void _simulate_stiffness(double delta, PackedVector2Array* velocities) const;
33+
void _simulate_stiffness_segment(double delta, PackedVector2Array* velocities, int64_t idx_from, int64_t idx_stop, Vector2 last_stiffness_dir) const;
2634
void _resolve_collisions(double delta, bool disable_contact_reporting);
2735
void _constraint(double delta);
2836
void _writeback();
@@ -45,13 +53,16 @@ class NativeRopeContext // NOLINT(cppcoreguidelines-special-member-functions)
4553
float damping = 0.0;
4654
Ref<Curve> damping_curve;
4755
float stiffness = 0.0;
56+
Ref<Curve> stiffness_curve;
57+
Ref<Curve> stiffness_bend_curve;
4858
float max_endpoint_distance = 0.0;
4959
int num_constraint_iterations = 0;
5060

5161
float collision_radius = 1.0;
5262
float collision_damping = 0.0;
5363
int collision_mask = 0;
5464

65+
StiffnessMethod stiffness_method = StiffnessMethod::Bidirectional;
5566
bool fixate_begin = true;
5667
bool resolve_to_begin = false;
5768
bool resolve_to_end = false;

0 commit comments

Comments
 (0)