Skip to main content

box3d_rust/joint/
parallel.rs

1// Port of parallel_joint.c: public accessors, torque reporting, and the
2// prepare/warm-start/solve simulation functions.
3//
4// The parallel joint keeps body A's and body B's z-axes collinear (a 2-DOF
5// angular constraint about the perpendicular x/y axes) via a soft spring.
6// Unlike distance/weld, `b3SolveParallelJoint` takes no `useBias` parameter ΓÇö
7// the joint is always driven through its own softness.
8//
9// SPDX-FileCopyrightText: 2025 Erin Catto
10// SPDX-License-Identifier: MIT
11
12use super::{get_joint_sim_check_type, get_joint_sim_check_type_ref, JointSim, JointType};
13use crate::body::{body_flags, BodyState, IDENTITY_BODY_STATE};
14use crate::core::NULL_INDEX;
15use crate::id::JointId;
16use crate::math_functions::{
17    add, add_mm, blend2, cross, det, dot, dot_quat, inv_mul_quat, length2, length_squared2, mul_mv,
18    mul_quat, mul_sv, negate_quat, rotate_vector, solve2, sub, sub2, Mat2, Vec2, Vec3, VEC3_AXIS_X,
19    VEC3_AXIS_Y,
20};
21use crate::solver::{make_soft, StepContext};
22use crate::solver_set::AWAKE_SET;
23use crate::world::World;
24
25/// (b3ParallelJoint_SetSpringHertz)
26pub fn parallel_joint_set_spring_hertz(world: &mut World, joint_id: JointId, hertz: f32) {
27    crate::recording::with_recording(world, |rec| {
28        rec.write_parallel_joint_set_spring_hertz(joint_id, hertz);
29    });
30    debug_assert!(hertz >= 0.0);
31    get_joint_sim_check_type(world, joint_id, JointType::Parallel)
32        .parallel_mut()
33        .hertz = hertz;
34}
35
36/// (b3ParallelJoint_GetSpringHertz)
37pub fn parallel_joint_get_spring_hertz(world: &World, joint_id: JointId) -> f32 {
38    get_joint_sim_check_type_ref(world, joint_id, JointType::Parallel)
39        .parallel()
40        .hertz
41}
42
43/// (b3ParallelJoint_SetSpringDampingRatio)
44pub fn parallel_joint_set_spring_damping_ratio(
45    world: &mut World,
46    joint_id: JointId,
47    damping_ratio: f32,
48) {
49    crate::recording::with_recording(world, |rec| {
50        rec.write_parallel_joint_set_spring_damping_ratio(joint_id, damping_ratio);
51    });
52    debug_assert!(damping_ratio >= 0.0);
53    get_joint_sim_check_type(world, joint_id, JointType::Parallel)
54        .parallel_mut()
55        .damping_ratio = damping_ratio;
56}
57
58/// (b3ParallelJoint_GetSpringDampingRatio)
59pub fn parallel_joint_get_spring_damping_ratio(world: &World, joint_id: JointId) -> f32 {
60    get_joint_sim_check_type_ref(world, joint_id, JointType::Parallel)
61        .parallel()
62        .damping_ratio
63}
64
65/// (b3ParallelJoint_SetMaxTorque)
66pub fn parallel_joint_set_max_torque(world: &mut World, joint_id: JointId, max_force: f32) {
67    crate::recording::with_recording(world, |rec| {
68        rec.write_parallel_joint_set_max_torque(joint_id, max_force);
69    });
70    debug_assert!(max_force >= 0.0);
71    get_joint_sim_check_type(world, joint_id, JointType::Parallel)
72        .parallel_mut()
73        .max_torque = max_force;
74}
75
76/// (b3ParallelJoint_GetMaxTorque)
77pub fn parallel_joint_get_max_torque(world: &World, joint_id: JointId) -> f32 {
78    get_joint_sim_check_type_ref(world, joint_id, JointType::Parallel)
79        .parallel()
80        .max_torque
81}
82
83/// (b3GetParallelJointTorque)
84///
85/// C takes `(b3World* world, b3JointSim* base)`; the port only needs
86/// `world->inv_h`, taken by value so callers that already hold `base` as a
87/// mutable borrow of `world` (e.g. a future joint-type dispatcher) don't hit
88/// a double-borrow of `world`.
89pub fn get_parallel_joint_torque(inv_h: f32, base: &mut JointSim) -> Vec3 {
90    let joint = base.parallel_mut();
91
92    let rel_q = inv_mul_quat(joint.quat_a, joint.quat_b);
93    joint.perp_axis_x = mul_sv(
94        0.5,
95        rotate_vector(
96            joint.quat_a,
97            add(mul_sv(rel_q.s, VEC3_AXIS_X), cross(rel_q.v, VEC3_AXIS_X)),
98        ),
99    );
100    joint.perp_axis_y = mul_sv(
101        0.5,
102        rotate_vector(
103            joint.quat_a,
104            add(mul_sv(rel_q.s, VEC3_AXIS_Y), cross(rel_q.v, VEC3_AXIS_Y)),
105        ),
106    );
107
108    let angular_impulse = blend2(
109        joint.perp_impulse.x,
110        joint.perp_axis_x,
111        joint.perp_impulse.y,
112        joint.perp_axis_y,
113    );
114    mul_sv(inv_h, angular_impulse)
115}
116
117/// (b3PrepareParallelJoint)
118pub fn prepare_parallel_joint(world: &World, base: &mut JointSim, context: &StepContext) {
119    debug_assert!(base.type_ == JointType::Parallel);
120
121    let id_a = base.body_id_a;
122    let id_b = base.body_id_b;
123
124    let body_a = &world.bodies[id_a as usize];
125    let body_b = &world.bodies[id_b as usize];
126
127    debug_assert!(body_a.set_index == AWAKE_SET || body_b.set_index == AWAKE_SET);
128
129    let body_sim_a =
130        &world.solver_sets[body_a.set_index as usize].body_sims[body_a.local_index as usize];
131    let body_sim_b =
132        &world.solver_sets[body_b.set_index as usize].body_sims[body_b.local_index as usize];
133
134    base.inv_mass_a = body_sim_a.inv_mass;
135    base.inv_mass_b = body_sim_b.inv_mass;
136    base.inv_i_a = body_sim_a.inv_inertia_world;
137    base.inv_i_b = body_sim_b.inv_inertia_world;
138
139    let inv_inertia_sum = add_mm(base.inv_i_a, base.inv_i_b);
140    base.fixed_rotation = det(inv_inertia_sum) < 1000.0 * f32::MIN_POSITIVE;
141
142    let local_frame_a_q = base.local_frame_a.q;
143    let local_frame_b_q = base.local_frame_b.q;
144
145    let index_a = if body_a.set_index == AWAKE_SET {
146        body_a.local_index
147    } else {
148        NULL_INDEX
149    };
150    let index_b = if body_b.set_index == AWAKE_SET {
151        body_b.local_index
152    } else {
153        NULL_INDEX
154    };
155
156    let quat_a = mul_quat(body_sim_a.transform.q, local_frame_a_q);
157    let quat_b = mul_quat(body_sim_b.transform.q, local_frame_b_q);
158    let rel_q = inv_mul_quat(quat_a, quat_b);
159
160    // These are needed for warm starting
161    let perp_axis_x = mul_sv(
162        0.5,
163        rotate_vector(
164            quat_a,
165            add(mul_sv(rel_q.s, VEC3_AXIS_X), cross(rel_q.v, VEC3_AXIS_X)),
166        ),
167    );
168    let perp_axis_y = mul_sv(
169        0.5,
170        rotate_vector(
171            quat_a,
172            add(mul_sv(rel_q.s, VEC3_AXIS_Y), cross(rel_q.v, VEC3_AXIS_Y)),
173        ),
174    );
175
176    let joint = base.parallel_mut();
177    joint.index_a = index_a;
178    joint.index_b = index_b;
179    joint.quat_a = quat_a;
180    joint.quat_b = quat_b;
181    joint.perp_axis_x = perp_axis_x;
182    joint.perp_axis_y = perp_axis_y;
183
184    joint.softness = make_soft(joint.hertz, joint.damping_ratio, context.h);
185
186    if !context.enable_warm_starting {
187        joint.perp_impulse = Vec2 { x: 0.0, y: 0.0 };
188    }
189}
190
191/// (b3WarmStartParallelJoint)
192pub fn warm_start_parallel_joint(base: &mut JointSim, states: &mut [BodyState]) {
193    debug_assert!(base.type_ == JointType::Parallel);
194
195    let i_a = base.inv_i_a;
196    let i_b = base.inv_i_b;
197
198    let joint = base.parallel_mut();
199
200    let mut state_a = if joint.index_a == NULL_INDEX {
201        IDENTITY_BODY_STATE
202    } else {
203        states[joint.index_a as usize]
204    };
205    let mut state_b = if joint.index_b == NULL_INDEX {
206        IDENTITY_BODY_STATE
207    } else {
208        states[joint.index_b as usize]
209    };
210
211    let mut w_a = state_a.angular_velocity;
212    let mut w_b = state_b.angular_velocity;
213
214    let angular_impulse = blend2(
215        joint.perp_impulse.x,
216        joint.perp_axis_x,
217        joint.perp_impulse.y,
218        joint.perp_axis_y,
219    );
220
221    w_a = sub(w_a, mul_mv(i_a, angular_impulse));
222    w_b = add(w_b, mul_mv(i_b, angular_impulse));
223
224    if state_a.flags & body_flags::DYNAMIC_FLAG != 0 {
225        state_a.angular_velocity = w_a;
226        states[joint.index_a as usize] = state_a;
227    }
228
229    if state_b.flags & body_flags::DYNAMIC_FLAG != 0 {
230        state_b.angular_velocity = w_b;
231        states[joint.index_b as usize] = state_b;
232    }
233}
234
235/// (b3SolveParallelJoint)
236pub fn solve_parallel_joint(base: &mut JointSim, context: &StepContext, states: &mut [BodyState]) {
237    let i_a = base.inv_i_a;
238    let i_b = base.inv_i_b;
239    let fixed_rotation = base.fixed_rotation;
240
241    let joint = base.parallel_mut();
242
243    let mut state_a = if joint.index_a == NULL_INDEX {
244        IDENTITY_BODY_STATE
245    } else {
246        states[joint.index_a as usize]
247    };
248    let mut state_b = if joint.index_b == NULL_INDEX {
249        IDENTITY_BODY_STATE
250    } else {
251        states[joint.index_b as usize]
252    };
253
254    let mut w_a = state_a.angular_velocity;
255    let mut w_b = state_b.angular_velocity;
256
257    let quat_a = mul_quat(state_a.delta_rotation, joint.quat_a);
258    let mut quat_b = mul_quat(state_b.delta_rotation, joint.quat_b);
259
260    if dot_quat(quat_a, quat_b) < 0.0 {
261        // this keeps the rotation angle in the range [-pi, pi]
262        quat_b = negate_quat(quat_b);
263    }
264
265    let rel_q = inv_mul_quat(quat_a, quat_b);
266
267    if !fixed_rotation && joint.max_torque > 0.0 {
268        let c = Vec2 {
269            x: rel_q.v.x,
270            y: rel_q.v.y,
271        };
272        let bias = Vec2 {
273            x: joint.softness.bias_rate * c.x,
274            y: joint.softness.bias_rate * c.y,
275        };
276        let mass_scale = joint.softness.mass_scale;
277        let impulse_scale = joint.softness.impulse_scale;
278
279        // Collinearity constraint as 2-by-2
280        let perp_axis_x = mul_sv(
281            0.5,
282            rotate_vector(
283                quat_a,
284                add(mul_sv(rel_q.s, VEC3_AXIS_X), cross(rel_q.v, VEC3_AXIS_X)),
285            ),
286        );
287        let perp_axis_y = mul_sv(
288            0.5,
289            rotate_vector(
290                quat_a,
291                add(mul_sv(rel_q.s, VEC3_AXIS_Y), cross(rel_q.v, VEC3_AXIS_Y)),
292            ),
293        );
294        joint.perp_axis_x = perp_axis_x;
295        joint.perp_axis_y = perp_axis_y;
296
297        let inv_inertia_sum = add_mm(i_a, i_b);
298        let kxx = dot(perp_axis_x, mul_mv(inv_inertia_sum, perp_axis_x));
299        let kyy = dot(perp_axis_y, mul_mv(inv_inertia_sum, perp_axis_y));
300        let kxy = dot(perp_axis_x, mul_mv(inv_inertia_sum, perp_axis_y));
301
302        let k = Mat2 {
303            cx: Vec2 { x: kxx, y: kxy },
304            cy: Vec2 { x: kxy, y: kyy },
305        };
306
307        let w_rel = sub(w_b, w_a);
308        let cdot = Vec2 {
309            x: dot(w_rel, perp_axis_x),
310            y: dot(w_rel, perp_axis_y),
311        };
312
313        let max_impulse = context.h * joint.max_torque;
314        let old_impulse = joint.perp_impulse;
315        let cdot_plus_bias = Vec2 {
316            x: cdot.x + bias.x,
317            y: cdot.y + bias.y,
318        };
319        let sol = solve2(k, cdot_plus_bias);
320        let mut delta_impulse = Vec2 {
321            x: -mass_scale * sol.x - impulse_scale * old_impulse.x,
322            y: -mass_scale * sol.y - impulse_scale * old_impulse.y,
323        };
324        joint.perp_impulse = Vec2 {
325            x: old_impulse.x + delta_impulse.x,
326            y: old_impulse.y + delta_impulse.y,
327        };
328        if length_squared2(joint.perp_impulse) > max_impulse * max_impulse {
329            let s = max_impulse / length2(joint.perp_impulse);
330            joint.perp_impulse = Vec2 {
331                x: s * joint.perp_impulse.x,
332                y: s * joint.perp_impulse.y,
333            };
334        }
335
336        delta_impulse = sub2(joint.perp_impulse, old_impulse);
337
338        let angular_impulse = blend2(delta_impulse.x, perp_axis_x, delta_impulse.y, perp_axis_y);
339        w_a = sub(w_a, mul_mv(i_a, angular_impulse));
340        w_b = add(w_b, mul_mv(i_b, angular_impulse));
341    }
342
343    if state_a.flags & body_flags::DYNAMIC_FLAG != 0 {
344        state_a.angular_velocity = w_a;
345        states[joint.index_a as usize] = state_a;
346    }
347
348    if state_b.flags & body_flags::DYNAMIC_FLAG != 0 {
349        state_b.angular_velocity = w_b;
350        states[joint.index_b as usize] = state_b;
351    }
352}