Skip to content

Commit 8b339a3

Browse files
committed
[WIP] Iterate on making stiffness calculation more stable
1 parent 0fbec24 commit 8b339a3

File tree

5 files changed

+99
-63
lines changed

5 files changed

+99
-63
lines changed

doc_classes/RopeStiffnessParameters.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
Stiffness forces the rope to return to its resting position. The resting direction is downwards and affected by the the node's rotation.
1313
</member>
1414
<member name="stiffness_bend_curve" type="Curve" setter="set_stiffness_bend_curve" getter="get_stiffness_bend_curve">
15-
(Optional) Scale the stiffness individually per segment according to how far each segment is bent.[br]The [code]y[/code] value of the curve is multiplied with the stiffness. [code]x = 0[/code] corresponds to no bending and [code]x = 1[/code] corresponds to 180° bending in regard to the previous segment.[br]If unset, defaults to a quadratic curve.
15+
(Optional) Scale the stiffness individually per segment according to how far each segment is bent.[br]The [code]y[/code] value of the curve is multiplied with the stiffness. [code]x = 0[/code] corresponds to no bending and [code]x = 1[/code] corresponds to 180° bending in regard to the previous segment.[br]If unset, defaults to factor [code]1.0[/code].[br]Physically, there is a material-dependent limit how far an object can bend before bending properties (stiffness) change, e.g. a material might be less inclined to snap back into place at smaller bend angles, or a material might stop bending at some point, i.e. when it breaks. While this curve intends to accomodate these physical properties, it will never work fully realistically.
1616
</member>
1717
<member name="stiffness_curve" type="Curve" setter="set_stiffness_curve" getter="get_stiffness_curve">
1818
(Optional) Scale the stiffness along the rope. Useful to make the rope less erratic towards the end.
@@ -22,11 +22,11 @@
2222
</member>
2323
</members>
2424
<constants>
25-
<constant name="Bidirectional" value="0" enum="StiffnessMethod">
26-
Computes a forward and backward pass.[br]Produces overall better and more stable results than [enum Legacy]. Acts more sensitive to [member stiffness_bend_curve].
25+
<constant name="Forward" value="0" enum="StiffnessMethod">
26+
Computes a single forward pass from front to back. Produces stable results but may look unusual on "free" ropes ([member Rope.fixate_begin] disabled).
2727
</constant>
28-
<constant name="Legacy" value="1" enum="StiffnessMethod">
29-
Computes a single forward pass from front to back.[br]Produces decent results but may look unusual without [member Rope.fixate_begin] enabled. It could also behave more unstable in certain scenarios, i.e. jittery or erratic movement.[br]This was the default behavior before [code]v1.9[/code].
28+
<constant name="Bidirectional" value="1" enum="StiffnessMethod">
29+
(Experimental) Computes a forward and backward pass. Produces better looking results with "free" ropes ([member Rope.fixate_begin] disabled). Acts much more sensitive to [member stiffness] and tends to produce more unstable results. Reducing gravity may help against erratic, unnatural or perpetual movement.
3030
</constant>
3131
</constants>
3232
</class>

src/NativeRopeContext.cpp

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ static float get_point_perc(int64_t index, const PackedVector2Array& points)
1515
return points.size() > 0 ? static_cast<float>(index) / static_cast<float>(points.size() - 1) : 0;
1616
}
1717

18+
static Vector2 exp_decay(Vector2 value, Vector2 target, float damping_factor, double delta)
19+
{
20+
return value.lerp(target, (float)(1.0 - exp(-damping_factor * delta)));
21+
}
22+
1823
static Vector2 damp_vec(Vector2 value, float damping_factor, double delta)
1924
{
20-
return value.lerp(VECTOR_ZERO, (float)(1.0 - exp(-damping_factor * delta)));
25+
return exp_decay(value, VECTOR_ZERO, damping_factor, delta);
2126
}
2227

2328

@@ -67,6 +72,9 @@ void NativeRopeContext::load_context(Node2D* rope)
6772
collision_damping = rope->get("collision_damping");
6873
report_contact_points = rope->get("report_contact_points");
6974
resolve_collisions_while_constraining = rope->get("resolve_collisions_while_constraining");
75+
76+
if (data_buffer.size() < points.size())
77+
data_buffer.resize(points.size() * 2);
7078
}
7179

7280
void NativeRopeContext::simulate(double delta)
@@ -82,7 +90,16 @@ void NativeRopeContext::simulate(double delta)
8290
points[0] = rope->get_global_position();
8391
}
8492

93+
// Calculate new velocities and swap point buffers
8594
_simulate_velocities(delta);
95+
std::swap(oldpoints, points);
96+
97+
// Compute new point positions
98+
for (int64_t i = 0; i < points.size(); ++i)
99+
points[i] = oldpoints[i] + simulation_weights[i] * data_buffer[i];
100+
101+
// Constraint step
102+
_simulate_stiffness(delta);
86103
_constraint(delta);
87104

88105
if (!resolve_collisions_while_constraining)
@@ -107,41 +124,22 @@ void NativeRopeContext::_writeback()
107124

108125
void NativeRopeContext::_simulate_velocities(double delta)
109126
{
110-
const int first_idx = fixate_begin ? 1 : 0;
111-
const int size = (int)points.size();
112-
113-
// NOTE: The following is a little ugly but it reduces memory usage by reusing existing buffers.
114-
// It is also faster than allocating a new temporary buffer and also likely more cache efficient.
115-
116-
// oldpoints is no longer needed after initial velocity computation, so we reuse it to store velocities.
117-
PackedVector2Array& velocities = oldpoints;
118-
// Afterwards we use oldpoints to store the new points and finally swap points and oldpoints
119-
PackedVector2Array& new_points = oldpoints;
120-
121-
// Compute velocities and apply damping + gravity
122127
const bool use_damping_curve = damping_curve.is_valid() && damping_curve->get_point_count() > 0;
123128
const Vector2 frame_gravity = gravity_direction * static_cast<float>(gravity * delta);
124129

125-
for (int i = first_idx; i < size; ++i)
130+
// Init velocities using delta + damping + gravity
131+
for (int64_t i = 0; i < points.size(); ++i)
126132
{
127133
const Vector2 vel = points[i] - oldpoints[i];
128134
const float dampmult = use_damping_curve ? damping_curve->sample_baked(get_point_perc(i, points)) : 1.0f;
129-
velocities[i] = damp_vec(vel, damping * dampmult, delta);
130-
velocities[i] += frame_gravity;
135+
data_buffer[i] = damp_vec(vel, damping * dampmult, delta) + frame_gravity;
131136
}
132137

133138
// Apply other forces
134-
_simulate_wind(&velocities);
135-
_simulate_stiffness(delta, &velocities);
136-
137-
// Move points according to velocity
138-
for (int i = first_idx; i < size; ++i)
139-
new_points[i] = points[i] + simulation_weights[i] * velocities[i];
140-
141-
std::swap(oldpoints, points);
139+
_simulate_wind();
142140
}
143141

144-
void NativeRopeContext::_simulate_wind(PackedVector2Array* velocities) const
142+
void NativeRopeContext::_simulate_wind()
145143
{
146144
if (wind.is_null() || !wind->get_enable_wind() || wind->get_noise().is_null())
147145
return;
@@ -154,43 +152,54 @@ void NativeRopeContext::_simulate_wind(PackedVector2Array* velocities) const
154152
{
155153
const float noise = wind->get_noise()->get_noise_3d(points[i].x, points[i].y, time);
156154
const Vector2 noise_velocity = orth_strength * noise;
157-
(*velocities)[i] += wind_velocity + noise_velocity;
155+
data_buffer[i] += wind_velocity + noise_velocity;
158156
}
159157
}
160158

161-
void NativeRopeContext::_simulate_stiffness(double delta, PackedVector2Array* velocities) const
159+
void NativeRopeContext::_simulate_stiffness(double delta)
162160
{
163-
if (stiffness_params->get_stiffness() <= 0)
161+
if (stiffness_params.is_null() || stiffness_params->get_stiffness() <= 0)
164162
return;
165163

166164
const auto num_points = points.size();
165+
uint8_t num_passes = 1; // Averaging the force in bidirectional stiffness seems to make the rope more stable
167166

168-
if (stiffness_params->get_stiffness_method() == RopeStiffnessParameters::Legacy)
167+
// Clear buffer as it is used for accumulating stiffness forces.
168+
for (int64_t i = 0; i < num_points; ++i)
169+
data_buffer[i] = VECTOR_ZERO;
170+
171+
if (stiffness_params->get_stiffness_method() == RopeStiffnessParameters::Forward)
169172
{
170173
const Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized();
171-
_simulate_stiffness_segment(delta, velocities, 1, num_points, parent_seg_dir); // Start at second and stop at last point
174+
_simulate_stiffness_segment(delta, 1, num_points, parent_seg_dir); // Start at second and stop at last point
172175
}
173176
else
174177
{
175178
const Vector2 end_parent = (points[num_points - 2] - points[num_points - 1]).normalized();
179+
num_passes = 2;
176180

177181
if (fixate_begin)
178182
{
179183
// TODO: Make direction configurable, e.g. an enum to select the mode ("transform", "gravity", "custom").
180184
const Vector2 parent_seg_dir = rope->get_global_transform().basis_xform(VECTOR_DOWN).normalized();
181-
_simulate_stiffness_segment(delta, velocities, 1, num_points, parent_seg_dir); // Start at second and stop at last point
182-
_simulate_stiffness_segment(delta, velocities, num_points - 2, 0, end_parent); // Start at second last and stop before first point (because it's fixed)
185+
_simulate_stiffness_segment(delta, 1, num_points, parent_seg_dir); // Start at second and stop at last point
186+
_simulate_stiffness_segment(delta, num_points - 2, 0, end_parent); // Start at second last and stop before first point (because it's fixed)
183187
}
184188
else
185189
{
186190
const Vector2 begin_parent = (points[1] - points[0]).normalized();
187-
_simulate_stiffness_segment(delta, velocities, 1, num_points, begin_parent); // Start at second and stop at last point
188-
_simulate_stiffness_segment(delta, velocities, num_points - 2, -1, end_parent); // Start at second last and stop at first point
191+
_simulate_stiffness_segment(delta, 1, num_points, begin_parent); // Start at second and stop at last point
192+
_simulate_stiffness_segment(delta, num_points - 2, -1, end_parent); // Start at second last and stop at first point
189193
}
190194
}
195+
196+
const float stiffness_damping = stiffness_params->get_stiffness_damping();
197+
198+
for (int64_t i = 0; i < num_points; ++i)
199+
points[i] += damp_vec(data_buffer[i] / num_passes, stiffness_damping, delta);
191200
}
192201

193-
void NativeRopeContext::_simulate_stiffness_segment(double /*delta*/, PackedVector2Array* velocities, int64_t idx_from, int64_t idx_stop, Vector2 last_stiffness_dir) const
202+
void NativeRopeContext::_simulate_stiffness_segment(double delta, int64_t idx_from, int64_t idx_stop, Vector2 last_stiffness_dir)
194203
{
195204
// NOTE: oldpoints should not be used here, see comments in simulate_velocities().
196205

@@ -200,16 +209,12 @@ void NativeRopeContext::_simulate_stiffness_segment(double /*delta*/, PackedVec
200209
const auto stiffness_bend_curve = stiffness_params->get_stiffness_bend_curve();
201210
const bool use_stiffness_curve = stiffness_curve.is_valid() && stiffness_curve->get_point_count() > 0;
202211
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;
212+
const bool iterating_forward = idx_from <= idx_stop;
213+
const int64_t idx_step = iterating_forward ? 1 : -1;
214+
const int64_t seglength_idx_offset = iterating_forward ? -1 : 0;
204215

205216
for (int64_t i = idx_from; i != idx_stop; i += idx_step)
206217
{
207-
// NOTE: Asked a physicist to confirm this computation is physically accurate.
208-
// He mentioned that, while it is technically correct, there is a material-dependent limit
209-
// how far an object can bend before bending properties (stiffness) changes.
210-
// E.g. a material might be less inclined to snap back into place at smaller bend angles, or
211-
// a material might stop bending at some point, i.e. when it breaks.
212-
//
213218
// | parent_seg_dir ---> parent_seg_dir.orthogonal()
214219
// | \
215220
// V \ seg_dir
@@ -218,22 +223,21 @@ void NativeRopeContext::_simulate_stiffness_segment(double /*delta*/, PackedVec
218223
// V V
219224
// / force_dir
220225
// V
221-
//
222-
const auto last_idx = i - idx_step;
226+
227+
const int64_t last_idx = i - idx_step;
223228
const Vector2 seg_dir = (points[i] - points[last_idx]).normalized();
224229
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);
230+
const float bend_factor = Math::absf(angle / (float)Math_PI);
226231
const float scale_curve_pos = get_point_perc(idx_step >= 0 ? i : (points.size() - 1 - i), points);
227232
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);
233+
const float bend_scale = use_bend_curve ? stiffness_bend_curve->sample_baked(bend_factor) : 1.0f;
230234

231-
// The force directs orthogonal to the current segment
232-
const Vector2 force_dir = Math::sign(-angle) * seg_dir.orthogonal();
233-
force += force_dir * stiffness * scale * bend_scale;
235+
const Vector2 target_pos = points[last_idx] + last_stiffness_dir * seg_lengths[i + seglength_idx_offset];
236+
const Vector2 diff = target_pos - points[i];
237+
force += exp_decay(VECTOR_ZERO, diff, stiffness * scale * bend_scale, delta);
234238

235-
// `simulation_weights` will be applied later in `_simulate_velocities()`
236-
(*velocities)[i] += force;
239+
// In bidirectional mode points need to be buffered to prevent the first pass affecting the second
240+
data_buffer[i] += force * simulation_weights[i];
237241

238242
// Carry velocity over to the next segment. `simulation_weights` must be applied now to not carry over a wrong value.
239243
force *= simulation_weights[i];

src/NativeRopeContext.hpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@ class NativeRopeContext // NOLINT(cppcoreguidelines-special-member-functions)
2121
bool validate() const;
2222

2323
protected:
24+
/// Reads point data from `points` and `oldpoints` and writes velocities to `data_buffer`.
2425
void _simulate_velocities(double delta);
25-
void _simulate_wind(PackedVector2Array* velocities) const;
26-
void _simulate_stiffness(double delta, PackedVector2Array* velocities) const;
27-
void _simulate_stiffness_segment(double delta, PackedVector2Array* velocities, int64_t idx_from, int64_t idx_stop, Vector2 last_stiffness_dir) const;
26+
27+
/// Reads point data from `points` and writes velocities to `data_buffer`.
28+
void _simulate_wind();
29+
30+
/// Reads point data from `data_buffer` and writes new points to `points`.
31+
void _simulate_stiffness(double delta);
32+
void _simulate_stiffness_segment(double delta, int64_t idx_from, int64_t idx_stop, Vector2 last_stiffness_dir);
33+
34+
/// Resolves collisions. Reads and writes to `points`.
2835
void _resolve_collisions(double delta, bool disable_contact_reporting);
36+
37+
/// Constrains points iteratively. Reads and writes to `points`.
2938
void _constraint(double delta);
3039
void _writeback();
3140

@@ -36,6 +45,12 @@ class NativeRopeContext // NOLINT(cppcoreguidelines-special-member-functions)
3645
PackedVector2Array _contact_points;
3746

3847
public:
48+
/// Internal buffer for point and velocity calculations. Will be resized on demand to be at
49+
/// least as large as the current points vector. It will not be cleared between different ropes.
50+
/// It is needed mostly for storing intermediate results during bidirectional stiffness
51+
/// calculation without overwriting `points` and `oldpoints`.
52+
PackedVector2Array data_buffer;
53+
3954
Node2D* rope = nullptr;
4055
PackedVector2Array points;
4156
PackedVector2Array oldpoints;

src/RopeStiffnessParameters.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ void RopeStiffnessParameters::set_stiffness_bend_curve(Ref<Curve> curve)
4747
emit_changed();
4848
}
4949

50+
float RopeStiffnessParameters::get_stiffness_damping() const
51+
{
52+
return stiffness_damping;
53+
}
54+
55+
void RopeStiffnessParameters::set_stiffness_damping(float value)
56+
{
57+
stiffness_damping = value;
58+
emit_changed();
59+
}
60+
5061

5162
void RopeStiffnessParameters::_bind_methods()
5263
{
@@ -58,12 +69,15 @@ void RopeStiffnessParameters::_bind_methods()
5869
ClassDB::bind_method(D_METHOD("set_stiffness_curve", "curve"), &RopeStiffnessParameters::set_stiffness_curve);
5970
ClassDB::bind_method(D_METHOD("get_stiffness_bend_curve"), &RopeStiffnessParameters::get_stiffness_bend_curve);
6071
ClassDB::bind_method(D_METHOD("set_stiffness_bend_curve", "curve"), &RopeStiffnessParameters::set_stiffness_bend_curve);
72+
ClassDB::bind_method(D_METHOD("get_stiffness_damping"), &RopeStiffnessParameters::get_stiffness_damping);
73+
ClassDB::bind_method(D_METHOD("set_stiffness_damping", "value"), &RopeStiffnessParameters::set_stiffness_damping);
6174

6275
ADD_PROPERTY(PropertyInfo(Variant::INT, "stiffness_method", PROPERTY_HINT_ENUM, stiffness_method_strings), "set_stiffness_method", "get_stiffness_method");
6376
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "stiffness"), "set_stiffness", "get_stiffness");
6477
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stiffness_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_stiffness_curve", "get_stiffness_curve");
6578
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stiffness_bend_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_stiffness_bend_curve", "get_stiffness_bend_curve");
79+
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "stiffness_damping"), "set_stiffness_damping", "get_stiffness_damping");
6680

6781
BIND_ENUM_CONSTANT(Bidirectional);
68-
BIND_ENUM_CONSTANT(Legacy);
82+
BIND_ENUM_CONSTANT(Forward);
6983
}

src/RopeStiffnessParameters.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class RopeStiffnessParameters : public Resource // NOLINT(cppcoreguidelines-spe
1212
public:
1313
enum StiffnessMethod : int8_t // NOLINT(cppcoreguidelines-use-enum-class)
1414
{
15+
Forward,
1516
Bidirectional,
16-
Legacy,
1717
};
1818

1919
static constexpr const char* stiffness_method_strings = "Bidirectional,Legacy";
@@ -31,6 +31,9 @@ class RopeStiffnessParameters : public Resource // NOLINT(cppcoreguidelines-spe
3131
Ref<Curve> get_stiffness_bend_curve() const;
3232
void set_stiffness_bend_curve(Ref<Curve> curve);
3333

34+
float get_stiffness_damping() const;
35+
void set_stiffness_damping(float value);
36+
3437
protected:
3538
static void _bind_methods();
3639

@@ -39,8 +42,8 @@ class RopeStiffnessParameters : public Resource // NOLINT(cppcoreguidelines-spe
3942
float stiffness = 0.0;
4043
Ref<Curve> stiffness_curve;
4144
Ref<Curve> stiffness_bend_curve;
45+
float stiffness_damping = 0.0;
4246
// TODO: Stiffness direction for back and front
43-
// TODO: Stiffness damping
4447
};
4548

4649
}

0 commit comments

Comments
 (0)