Skip to main content

alice_physics/
motor.rs

1//! Motors and PD Controllers
2//!
3//! Drive joints to target positions/velocities using proportional-derivative control.
4//! Deterministic: all computations use Fix128.
5
6use crate::joint::Joint;
7use crate::math::{Fix128, QuatFix, Vec3Fix};
8use crate::solver::RigidBody;
9
10/// Motor mode
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum MotorMode {
13    /// Disabled (no motor force)
14    Off,
15    /// Drive to target position (PD control)
16    Position,
17    /// Drive to target velocity (P control)
18    Velocity,
19}
20
21/// PD controller for a single joint axis
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct PdController {
24    /// Proportional gain (stiffness)
25    pub kp: Fix128,
26    /// Derivative gain (damping)
27    pub kd: Fix128,
28    /// Maximum output force/torque
29    pub max_force: Fix128,
30    /// Target position (for Position mode)
31    pub target_position: Fix128,
32    /// Target velocity (for Velocity mode)
33    pub target_velocity: Fix128,
34    /// Motor mode
35    pub mode: MotorMode,
36}
37
38impl PdController {
39    /// Create a new PD controller
40    #[inline]
41    #[must_use]
42    pub const fn new(kp: Fix128, kd: Fix128, max_force: Fix128) -> Self {
43        Self {
44            kp,
45            kd,
46            max_force,
47            target_position: Fix128::ZERO,
48            target_velocity: Fix128::ZERO,
49            mode: MotorMode::Off,
50        }
51    }
52
53    /// Set position target
54    pub fn set_position_target(&mut self, target: Fix128) {
55        self.target_position = target;
56        self.mode = MotorMode::Position;
57    }
58
59    /// Set velocity target
60    pub fn set_velocity_target(&mut self, target: Fix128) {
61        self.target_velocity = target;
62        self.mode = MotorMode::Velocity;
63    }
64
65    /// Disable the motor
66    pub fn disable(&mut self) {
67        self.mode = MotorMode::Off;
68    }
69
70    /// Compute control output
71    ///
72    /// Returns clamped force/torque value.
73    #[must_use]
74    pub fn compute(&self, current_position: Fix128, current_velocity: Fix128) -> Fix128 {
75        match self.mode {
76            MotorMode::Off => Fix128::ZERO,
77            MotorMode::Position => {
78                let error = self.target_position - current_position;
79                let vel_error = self.target_velocity - current_velocity;
80                let output = self.kp * error + self.kd * vel_error;
81                clamp(output, -self.max_force, self.max_force)
82            }
83            MotorMode::Velocity => {
84                let vel_error = self.target_velocity - current_velocity;
85                let output = self.kp * vel_error;
86                clamp(output, -self.max_force, self.max_force)
87            }
88        }
89    }
90}
91
92impl Default for PdController {
93    fn default() -> Self {
94        Self::new(
95            Fix128::from_int(100),  // kp = 100
96            Fix128::from_int(10),   // kd = 10
97            Fix128::from_int(1000), // max_force = 1000
98        )
99    }
100}
101
102/// Motor attached to a joint
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct JointMotor {
105    /// Index into joints array
106    pub joint_index: usize,
107    /// PD controller for the primary axis
108    pub controller: PdController,
109}
110
111impl JointMotor {
112    /// Create a new motor for the given joint
113    #[must_use]
114    pub const fn new(joint_index: usize, controller: PdController) -> Self {
115        Self {
116            joint_index,
117            controller,
118        }
119    }
120}
121
122/// 3-axis PD controller for ball joints / free rotation
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub struct PdController3D {
125    /// Per-axis proportional gains
126    pub kp: Vec3Fix,
127    /// Per-axis derivative gains
128    pub kd: Vec3Fix,
129    /// Maximum torque per axis
130    pub max_torque: Fix128,
131    /// Target orientation
132    pub target_rotation: QuatFix,
133    /// Target angular velocity
134    pub target_angular_velocity: Vec3Fix,
135    /// Motor mode
136    pub mode: MotorMode,
137}
138
139impl PdController3D {
140    /// Create a new 3-axis PD controller
141    #[inline]
142    #[must_use]
143    pub const fn new(kp: Vec3Fix, kd: Vec3Fix, max_torque: Fix128) -> Self {
144        Self {
145            kp,
146            kd,
147            max_torque,
148            target_rotation: QuatFix::IDENTITY,
149            target_angular_velocity: Vec3Fix::ZERO,
150            mode: MotorMode::Off,
151        }
152    }
153
154    /// Set rotation target
155    pub fn set_rotation_target(&mut self, target: QuatFix) {
156        self.target_rotation = target;
157        self.mode = MotorMode::Position;
158    }
159
160    /// Compute 3D torque output
161    #[must_use]
162    pub fn compute_torque(
163        &self,
164        current_rotation: QuatFix,
165        current_angular_velocity: Vec3Fix,
166    ) -> Vec3Fix {
167        match self.mode {
168            MotorMode::Off => Vec3Fix::ZERO,
169            MotorMode::Position => {
170                // Rotation error as axis-angle
171                let error_quat =
172                    shortest_arc(self.target_rotation.mul(current_rotation.conjugate()));
173                let error_axis = Vec3Fix::new(error_quat.x, error_quat.y, error_quat.z);
174                let two = Fix128::from_int(2);
175                let error_scaled =
176                    Vec3Fix::new(error_axis.x * two, error_axis.y * two, error_axis.z * two);
177
178                // PD control per axis
179                let vel_error = self.target_angular_velocity - current_angular_velocity;
180                let torque = Vec3Fix::new(
181                    self.kp.x * error_scaled.x + self.kd.x * vel_error.x,
182                    self.kp.y * error_scaled.y + self.kd.y * vel_error.y,
183                    self.kp.z * error_scaled.z + self.kd.z * vel_error.z,
184                );
185
186                // Clamp torque magnitude
187                let mag = torque.length();
188                if mag > self.max_torque && !mag.is_zero() {
189                    torque * (self.max_torque / mag)
190                } else {
191                    torque
192                }
193            }
194            MotorMode::Velocity => {
195                let vel_error = self.target_angular_velocity - current_angular_velocity;
196                let torque = Vec3Fix::new(
197                    self.kp.x * vel_error.x,
198                    self.kp.y * vel_error.y,
199                    self.kp.z * vel_error.z,
200                );
201
202                let mag = torque.length();
203                if mag > self.max_torque && !mag.is_zero() {
204                    torque * (self.max_torque / mag)
205                } else {
206                    torque
207                }
208            }
209        }
210    }
211}
212
213/// Apply all joint motors for one timestep
214///
215/// [`crate::PhysicsWorld::step`] calls this once per substep (with the substep
216/// `dt`) for the motors added with [`crate::PhysicsWorld::add_joint_motor`],
217/// ahead of the substep's position integration, so the motor acts as an
218/// external force / torque that the constraint solve then sees.
219///
220/// # Claims
221/// - The controller output is a force (or torque) `F`, clamped to
222///   `±max_force` by [`PdController::compute`]; the motor applies the impulse
223///   `F * dt` with equal and opposite halves on the joint's two bodies, so the
224///   pair's momentum (angular momentum for a hinge) is unchanged by it.
225/// - A `Joint::Hinge` motor's generalised coordinate is the relative twist
226///   angle about the hinge axis (`crate::joint::compute_twist_angle` of
227///   `rotation_b * rotation_a⁻¹` about body A's world hinge axis) and its rate
228///   is `(ω_b - ω_a) · axis`; the impulse goes to the bodies'
229///   `angular_velocity` through their world inverse inertia, as in
230///   [`crate::articulation::ArticulatedBody::apply_motors`].
231/// - Every other joint type keeps the centre-to-centre distance as its
232///   generalised coordinate and drives the bodies' linear `velocity` along the
233///   centre line (scaled by `inv_mass`).
234/// - A motor that is `Off`, names a joint index past the end of `joints`, or
235///   whose joint names a body past the end of `bodies` does nothing.
236pub fn apply_motors(motors: &[JointMotor], joints: &[Joint], bodies: &mut [RigidBody], dt: Fix128) {
237    for motor in motors {
238        if motor.controller.mode == MotorMode::Off {
239            continue;
240        }
241
242        if motor.joint_index >= joints.len() {
243            continue;
244        }
245
246        let joint = &joints[motor.joint_index];
247        let (body_a_idx, body_b_idx) = joint.bodies();
248
249        if body_a_idx >= bodies.len() || body_b_idx >= bodies.len() {
250            continue;
251        }
252
253        // Compute current state along joint axis
254        let body_a = bodies[body_a_idx];
255        let body_b = bodies[body_b_idx];
256
257        if let Joint::Hinge(hinge) = joint {
258            let axis = body_a.rotation.rotate_vec(hinge.local_axis_a);
259            let rel_quat = body_b.rotation.mul(body_a.rotation.conjugate());
260            let current_pos = crate::joint::compute_twist_angle(rel_quat, axis);
261            let current_vel = (body_b.angular_velocity - body_a.angular_velocity).dot(axis);
262
263            let torque = motor.controller.compute(current_pos, current_vel);
264            if torque.is_zero() {
265                continue;
266            }
267
268            let angular_impulse = axis * (torque * dt);
269            apply_angular_pair(bodies, body_a_idx, body_b_idx, angular_impulse);
270            continue;
271        }
272
273        let delta = body_b.position - body_a.position;
274        let current_pos = delta.length();
275        let rel_vel = body_b.velocity - body_a.velocity;
276        let current_vel = if current_pos.is_zero() {
277            Fix128::ZERO
278        } else {
279            rel_vel.dot(delta / current_pos)
280        };
281
282        let force = motor.controller.compute(current_pos, current_vel);
283
284        if force.is_zero() || current_pos.is_zero() {
285            continue;
286        }
287
288        let direction = delta / current_pos;
289        let impulse = direction * (force * dt);
290
291        if !body_a.inv_mass.is_zero() {
292            bodies[body_a_idx].velocity = bodies[body_a_idx].velocity - impulse * body_a.inv_mass;
293        }
294        if !body_b.inv_mass.is_zero() {
295            bodies[body_b_idx].velocity = bodies[body_b_idx].velocity + impulse * body_b.inv_mass;
296        }
297    }
298}
299
300/// Apply 3-axis rotation motors (`(joint index, controller)` pairs) for one
301/// timestep.
302///
303/// The world's counterpart of [`apply_motors`] for [`PdController3D`]:
304/// [`crate::PhysicsWorld::step`] calls it once per substep for the motors
305/// added with [`crate::PhysicsWorld::add_joint_motor_3d`].
306///
307/// # Claims
308/// - The controlled rotation is the joint's relative rotation
309///   `rotation_b * rotation_a⁻¹` and the controlled rate `ω_b - ω_a` (both in
310///   world frame, the convention of the hinge case of [`apply_motors`]); with a
311///   static body A at the identity this is body B's own world rotation.
312/// - The torque `τ` of [`PdController3D::compute_torque`] is applied as the
313///   angular impulse `τ dt`: `+I_b⁻¹ τ dt` on body B and `-I_a⁻¹ τ dt` on body
314///   A (world inverse inertia), skipping a body with zero `inv_mass`.
315/// - `Off` motors and out-of-range joint / body indices do nothing.
316pub(crate) fn apply_motors_3d(
317    motors: &[(usize, PdController3D)],
318    joints: &[Joint],
319    bodies: &mut [RigidBody],
320    dt: Fix128,
321) {
322    for (joint_index, controller) in motors {
323        if controller.mode == MotorMode::Off || *joint_index >= joints.len() {
324            continue;
325        }
326        let (body_a_idx, body_b_idx) = joints[*joint_index].bodies();
327        if body_a_idx >= bodies.len() || body_b_idx >= bodies.len() {
328            continue;
329        }
330        let body_a = bodies[body_a_idx];
331        let body_b = bodies[body_b_idx];
332        let rel_quat = body_b.rotation.mul(body_a.rotation.conjugate());
333        let rel_omega = body_b.angular_velocity - body_a.angular_velocity;
334        let torque = controller.compute_torque(rel_quat, rel_omega);
335        if torque == Vec3Fix::ZERO {
336            continue;
337        }
338        apply_angular_pair(bodies, body_a_idx, body_b_idx, torque * dt);
339    }
340}
341
342/// Add `angular_impulse` to body B and subtract it from body A, each through
343/// its world inverse inertia (a body with zero `inv_mass` is left alone).
344fn apply_angular_pair(
345    bodies: &mut [RigidBody],
346    body_a_idx: usize,
347    body_b_idx: usize,
348    angular_impulse: Vec3Fix,
349) {
350    let body_a = bodies[body_a_idx];
351    let body_b = bodies[body_b_idx];
352    if !body_a.inv_mass.is_zero() {
353        bodies[body_a_idx].angular_velocity =
354            body_a.angular_velocity - body_a.world_inv_inertia_apply(angular_impulse);
355    }
356    if !body_b.inv_mass.is_zero() {
357        bodies[body_b_idx].angular_velocity =
358            bodies[body_b_idx].angular_velocity + body_b.world_inv_inertia_apply(angular_impulse);
359    }
360}
361
362/// Return the representative of `q`'s rotation with `w >= 0`.
363///
364/// `q` and `-q` describe the same rotation, but the vector part of an error
365/// quaternion flips sign with it. Taking the `w >= 0` representative makes an
366/// error derived from the vector part (`2 * xyz`) point along the shortest arc
367/// whichever sign the inputs were stored with.
368#[inline]
369#[must_use]
370pub(crate) fn shortest_arc(q: QuatFix) -> QuatFix {
371    if q.w < Fix128::ZERO {
372        QuatFix::new(-q.x, -q.y, -q.z, -q.w)
373    } else {
374        q
375    }
376}
377
378#[inline]
379fn clamp(v: Fix128, min: Fix128, max: Fix128) -> Fix128 {
380    if v < min {
381        min
382    } else if v > max {
383        max
384    } else {
385        v
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use crate::joint::BallJoint;
393    #[cfg(not(feature = "std"))]
394    use alloc::{vec, vec::Vec};
395
396    #[test]
397    fn test_pd_position_control() {
398        let mut pd = PdController::new(
399            Fix128::from_int(10),
400            Fix128::from_int(1),
401            Fix128::from_int(100),
402        );
403        pd.set_position_target(Fix128::from_int(5));
404
405        let output = pd.compute(Fix128::ZERO, Fix128::ZERO);
406        // error = 5, output = 10*5 + 1*0 = 50
407        assert_eq!(output.hi, 50);
408    }
409
410    #[test]
411    fn test_pd_clamp() {
412        let mut pd = PdController::new(
413            Fix128::from_int(1000),
414            Fix128::ZERO,
415            Fix128::from_int(10), // max = 10
416        );
417        pd.set_position_target(Fix128::from_int(100));
418
419        let output = pd.compute(Fix128::ZERO, Fix128::ZERO);
420        assert_eq!(output.hi, 10, "Should be clamped to max_force");
421    }
422
423    #[test]
424    fn test_pd_off() {
425        let pd = PdController::new(
426            Fix128::from_int(10),
427            Fix128::from_int(1),
428            Fix128::from_int(100),
429        );
430        // Mode is Off by default
431        let output = pd.compute(Fix128::ZERO, Fix128::ZERO);
432        assert!(output.is_zero());
433    }
434
435    #[test]
436    fn test_velocity_mode() {
437        let mut pd = PdController::new(Fix128::from_int(10), Fix128::ZERO, Fix128::from_int(1000));
438        pd.set_velocity_target(Fix128::from_int(5));
439
440        let output = pd.compute(Fix128::ZERO, Fix128::ZERO);
441        // vel_error = 5, output = 10*5 = 50
442        assert_eq!(output.hi, 50);
443    }
444
445    #[test]
446    fn test_3d_controller() {
447        let mut pd = PdController3D::new(
448            Vec3Fix::from_int(100, 100, 100),
449            Vec3Fix::from_int(10, 10, 10),
450            Fix128::from_int(1000),
451        );
452        pd.set_rotation_target(QuatFix::IDENTITY);
453        pd.mode = MotorMode::Position;
454
455        let torque = pd.compute_torque(QuatFix::IDENTITY, Vec3Fix::ZERO);
456        // No error => zero torque
457        let mag = torque.length();
458        assert!(
459            mag < Fix128::from_ratio(1, 10),
460            "Zero error should give zero torque"
461        );
462    }
463
464    /// A (inv_mass ia) at origin、B (inv_mass ib) at (4, 0, 0)、Ball joint 0-1
465    fn motor_scene(ia: i64, ib: i64) -> (Vec<RigidBody>, Vec<Joint>) {
466        let mut a = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
467        let mut b = RigidBody::new_dynamic(Vec3Fix::from_int(4, 0, 0), Fix128::ONE);
468        a.inv_mass = Fix128::from_int(ia);
469        b.inv_mass = Fix128::from_int(ib);
470        let joints = vec![Joint::Ball(BallJoint::new(
471            0,
472            1,
473            Vec3Fix::ZERO,
474            Vec3Fix::ZERO,
475        ))];
476        (vec![a, b], joints)
477    }
478
479    #[test]
480    fn apply_motors_position_mode_pushes_along_joint_axis_by_force_dt_inv_mass() {
481        // kp 10 / kd 0 / max 100、target 距離 6、現在 4 → force = 10 * 2 = 20
482        // dt 1/4 → impulse 5 → B (inv 1) velocity += (5, 0, 0)、A static は不動
483        let (mut bodies, joints) = motor_scene(0, 1);
484        let mut pd = PdController::new(Fix128::from_int(10), Fix128::ZERO, Fix128::from_int(100));
485        pd.set_position_target(Fix128::from_int(6));
486        let motors = [JointMotor::new(0, pd)];
487        apply_motors(&motors, &joints, &mut bodies, Fix128::from_ratio(1, 4));
488        assert_eq!(bodies[1].velocity, Vec3Fix::from_int(5, 0, 0));
489        assert_eq!(bodies[0].velocity, Vec3Fix::ZERO);
490
491        // 目標が現在より近い (2) → force = -20 → B は A 側へ (-5, 0, 0)
492        let (mut closer, joints) = motor_scene(0, 1);
493        let mut pd_in = pd;
494        pd_in.set_position_target(Fix128::from_int(2));
495        apply_motors(
496            &[JointMotor::new(0, pd_in)],
497            &joints,
498            &mut closer,
499            Fix128::from_ratio(1, 4),
500        );
501        assert_eq!(closer[1].velocity, Vec3Fix::from_int(-5, 0, 0));
502
503        // 両 dynamic (inv 1 / inv 2): impulse 5 → A -= 5、B += 10 (運動量 = 質量比で分配)
504        let (mut both, joints) = motor_scene(1, 2);
505        apply_motors(
506            &[JointMotor::new(0, pd)],
507            &joints,
508            &mut both,
509            Fix128::from_ratio(1, 4),
510        );
511        assert_eq!(both[0].velocity, Vec3Fix::from_int(-5, 0, 0));
512        assert_eq!(both[1].velocity, Vec3Fix::from_int(10, 0, 0));
513
514        // max_force 8 で clamp → impulse 2
515        let (mut clamped, joints) = motor_scene(0, 1);
516        let mut pd_clamp =
517            PdController::new(Fix128::from_int(10), Fix128::ZERO, Fix128::from_int(8));
518        pd_clamp.set_position_target(Fix128::from_int(6));
519        apply_motors(
520            &[JointMotor::new(0, pd_clamp)],
521            &joints,
522            &mut clamped,
523            Fix128::from_ratio(1, 4),
524        );
525        assert_eq!(clamped[1].velocity, Vec3Fix::from_int(2, 0, 0));
526    }
527
528    #[test]
529    fn apply_motors_velocity_mode_uses_relative_velocity_along_axis() {
530        // kp 10、target vel 3、B が +x に 1 → vel_error 2 → force 20 → impulse 5 → B (6, 0, 0)
531        // 軸直交成分 (y) は current_vel に寄与しない
532        let (mut bodies, joints) = motor_scene(0, 1);
533        bodies[1].velocity = Vec3Fix::from_int(1, 7, 0);
534        let mut pd = PdController::new(Fix128::from_int(10), Fix128::ZERO, Fix128::from_int(100));
535        pd.set_velocity_target(Fix128::from_int(3));
536        apply_motors(
537            &[JointMotor::new(0, pd)],
538            &joints,
539            &mut bodies,
540            Fix128::from_ratio(1, 4),
541        );
542        assert_eq!(bodies[1].velocity, Vec3Fix::from_int(6, 7, 0));
543    }
544
545    #[test]
546    fn apply_motors_skips_off_out_of_range_zero_force_and_coincident_bodies() {
547        let (mut bodies, joints) = motor_scene(0, 1);
548        let mut pd = PdController::new(Fix128::from_int(10), Fix128::ZERO, Fix128::from_int(100));
549        pd.set_position_target(Fix128::from_int(6));
550        let dt = Fix128::from_ratio(1, 4);
551
552        // Off
553        let mut off = pd;
554        off.disable();
555        apply_motors(&[JointMotor::new(0, off)], &joints, &mut bodies, dt);
556        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
557        // joint index 範囲外
558        apply_motors(&[JointMotor::new(1, pd)], &joints, &mut bodies, dt);
559        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
560        // body index 範囲外 (joint が body 5 を参照)
561        let far = [Joint::Ball(BallJoint::new(
562            0,
563            5,
564            Vec3Fix::ZERO,
565            Vec3Fix::ZERO,
566        ))];
567        apply_motors(&[JointMotor::new(0, pd)], &far, &mut bodies, dt);
568        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
569        // 目標 = 現在距離 4 → force 0 → 不変
570        let mut at_target = pd;
571        at_target.set_position_target(Fix128::from_int(4));
572        apply_motors(&[JointMotor::new(0, at_target)], &joints, &mut bodies, dt);
573        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
574        // 同一点 (current_pos 0) → 方向が定義できず skip
575        bodies[1].position = Vec3Fix::ZERO;
576        apply_motors(&[JointMotor::new(0, pd)], &joints, &mut bodies, dt);
577        assert_eq!(bodies[1].velocity, Vec3Fix::ZERO);
578        // 通常 path は動く (上の skip が「何もしない」だけでない事の対照)
579        bodies[1].position = Vec3Fix::from_int(4, 0, 0);
580        apply_motors(&[JointMotor::new(0, pd)], &joints, &mut bodies, dt);
581        assert_eq!(bodies[1].velocity, Vec3Fix::from_int(5, 0, 0));
582    }
583
584    fn assert_vec_near(actual: Vec3Fix, expected: Vec3Fix, what: &str) {
585        let tol = Fix128::from_ratio(1, 1_000_000_000);
586        let d = actual - expected;
587        assert!(
588            d.x.abs() < tol && d.y.abs() < tol && d.z.abs() < tol,
589            "{what}: {actual:?} vs {expected:?}"
590        );
591    }
592
593    /// A quaternion of a turn about `Z` with `cos(θ/2) = 3/5`, `sin(θ/2) = 4/5`
594    /// (a unit quaternion: 9/25 + 16/25 = 1), stored with the sign `sign`.
595    fn turn_z(sign: i64) -> QuatFix {
596        let s = Fix128::from_int(sign);
597        QuatFix::new(
598            Fix128::ZERO,
599            Fix128::ZERO,
600            Fix128::from_ratio(4, 5) * s,
601            Fix128::from_ratio(3, 5) * s,
602        )
603    }
604
605    /// Position mode of the 3-axis controller with target identity and current
606    /// rotation `q = (0, 0, 4/5, 3/5)`: the error quaternion is `q⁻¹ =
607    /// (0, 0, -4/5, 3/5)`, whose vector part doubled is `(0, 0, -8/5)`. With
608    /// `kp = 10`, `kd = 1`, current `ω = (0, 0, 2)` and target `ω = 0` the torque
609    /// is `10·(-8/5) + 1·(-2) = -18` about `Z`. The same rotation stored as `-q`
610    /// (`w < 0`) must give the same torque (shortest arc, not the long way round).
611    /// With `max_torque = 9` the torque keeps its direction and is scaled to 9.
612    #[test]
613    fn controller_3d_position_mode_follows_the_shortest_arc_and_clamps_the_magnitude() {
614        let mut pd = PdController3D::new(
615            Vec3Fix::from_int(10, 10, 10),
616            Vec3Fix::from_int(1, 1, 1),
617            Fix128::from_int(100),
618        );
619        pd.set_rotation_target(QuatFix::IDENTITY);
620        let omega = Vec3Fix::from_int(0, 0, 2);
621        let expected = Vec3Fix::from_int(0, 0, -18);
622        assert_vec_near(pd.compute_torque(turn_z(1), omega), expected, "w > 0");
623        assert_vec_near(pd.compute_torque(turn_z(-1), omega), expected, "w < 0");
624
625        pd.max_torque = Fix128::from_int(9);
626        assert_vec_near(
627            pd.compute_torque(turn_z(-1), omega),
628            Vec3Fix::from_int(0, 0, -9),
629            "clamped",
630        );
631
632        pd.mode = MotorMode::Off;
633        assert_eq!(pd.compute_torque(turn_z(1), omega), Vec3Fix::ZERO);
634    }
635
636    /// Velocity mode of the 3-axis controller is `τ = kp ⊙ (ω_t - ω)` per axis:
637    /// `kp = (2, 3, 4)`, `ω_t = (1, 1, 1)`, `ω = (0, 1, 0)` gives `(2, 0, 4)`
638    /// (`|τ| = √20 < 100`, unclamped). With `kp = 10`, error `(3, 4, 0)` gives
639    /// `(30, 40, 0)` of magnitude 50; `max_torque = 25` halves it to `(15, 20, 0)`.
640    #[test]
641    fn controller_3d_velocity_mode_is_per_axis_proportional_and_clamped_by_magnitude() {
642        let mut pd = PdController3D::new(
643            Vec3Fix::from_int(2, 3, 4),
644            Vec3Fix::ZERO,
645            Fix128::from_int(100),
646        );
647        pd.target_angular_velocity = Vec3Fix::from_int(1, 1, 1);
648        pd.mode = MotorMode::Velocity;
649        assert_eq!(
650            pd.compute_torque(QuatFix::IDENTITY, Vec3Fix::from_int(0, 1, 0)),
651            Vec3Fix::from_int(2, 0, 4)
652        );
653
654        pd.kp = Vec3Fix::from_int(10, 10, 10);
655        pd.max_torque = Fix128::from_int(25);
656        pd.target_angular_velocity = Vec3Fix::from_int(3, 4, 0);
657        assert_vec_near(
658            pd.compute_torque(QuatFix::IDENTITY, Vec3Fix::ZERO),
659            Vec3Fix::from_int(15, 20, 0),
660            "clamped",
661        );
662    }
663
664    /// Two dynamic bodies at the identity with world inverse inertias 1 and 2
665    /// (isotropic), joined by a ball joint.
666    fn spinning_pair() -> (Vec<RigidBody>, Vec<Joint>) {
667        let (mut bodies, joints) = motor_scene(1, 1);
668        bodies[0].inv_inertia = Vec3Fix::from_int(1, 1, 1);
669        bodies[1].inv_inertia = Vec3Fix::from_int(2, 2, 2);
670        (bodies, joints)
671    }
672
673    /// `apply_motors_3d` controls the relative rate `ω_b - ω_a`: with
674    /// `kp = (2, 3, 4)`, `ω_t = (1, 1, 1)` and `ω_b - ω_a = (0, 1, 0)` the torque is
675    /// `(2, 0, 4)`; over `dt = 1/2` the impulse `(1, 0, 2)` goes `+I_b⁻¹` to B
676    /// (`ω_b = (0, 1, 0) + 2·(1, 0, 2) = (2, 1, 4)`) and `-I_a⁻¹` to A
677    /// (`ω_a = (-1, 0, -2)`). The pair's angular momentum
678    /// `I_a ω_a + I_b ω_b = (0, 1/2, 0)` is unchanged. A static A is left alone.
679    #[test]
680    fn apply_motors_3d_applies_equal_and_opposite_angular_impulses() {
681        let mut pd = PdController3D::new(
682            Vec3Fix::from_int(2, 3, 4),
683            Vec3Fix::ZERO,
684            Fix128::from_int(100),
685        );
686        pd.target_angular_velocity = Vec3Fix::from_int(1, 1, 1);
687        pd.mode = MotorMode::Velocity;
688        let dt = Fix128::from_ratio(1, 2);
689
690        let (mut bodies, joints) = spinning_pair();
691        bodies[1].angular_velocity = Vec3Fix::from_int(0, 1, 0);
692        apply_motors_3d(&[(0, pd)], &joints, &mut bodies, dt);
693        assert_eq!(bodies[0].angular_velocity, Vec3Fix::from_int(-1, 0, -2));
694        assert_eq!(bodies[1].angular_velocity, Vec3Fix::from_int(2, 1, 4));
695        let half = Fix128::from_ratio(1, 2);
696        let momentum = bodies[0].angular_velocity + bodies[1].angular_velocity * half;
697        assert_eq!(momentum, Vec3Fix::new(Fix128::ZERO, half, Fix128::ZERO));
698
699        let (mut fixed_a, joints) = spinning_pair();
700        fixed_a[0].inv_mass = Fix128::ZERO;
701        fixed_a[1].angular_velocity = Vec3Fix::from_int(0, 1, 0);
702        apply_motors_3d(&[(0, pd)], &joints, &mut fixed_a, dt);
703        assert_eq!(fixed_a[0].angular_velocity, Vec3Fix::ZERO);
704        assert_eq!(fixed_a[1].angular_velocity, Vec3Fix::from_int(2, 1, 4));
705    }
706
707    /// `apply_motors_3d` does nothing for an `Off` motor, a joint index past the
708    /// end, a joint naming a body past the end, or a rate already at target (zero
709    /// torque); the last call (same motor, rate off target) does act, so the skips
710    /// are not a motor that never runs.
711    #[test]
712    fn apply_motors_3d_skips_off_out_of_range_and_zero_torque() {
713        let mut pd = PdController3D::new(
714            Vec3Fix::from_int(1, 1, 1),
715            Vec3Fix::ZERO,
716            Fix128::from_int(100),
717        );
718        pd.target_angular_velocity = Vec3Fix::from_int(0, 0, 1);
719        pd.mode = MotorMode::Velocity;
720        let dt = Fix128::ONE;
721        let (mut bodies, joints) = spinning_pair();
722        let mut off = pd;
723        off.mode = MotorMode::Off;
724        apply_motors_3d(&[(0, off), (1, pd)], &joints, &mut bodies, dt);
725        let far = [Joint::Ball(BallJoint::new(
726            0,
727            5,
728            Vec3Fix::ZERO,
729            Vec3Fix::ZERO,
730        ))];
731        apply_motors_3d(&[(0, pd)], &far, &mut bodies, dt);
732        assert_eq!(bodies[0].angular_velocity, Vec3Fix::ZERO);
733        assert_eq!(bodies[1].angular_velocity, Vec3Fix::ZERO);
734
735        bodies[1].angular_velocity = Vec3Fix::from_int(0, 0, 1);
736        apply_motors_3d(&[(0, pd)], &joints, &mut bodies, dt);
737        assert_eq!(bodies[0].angular_velocity, Vec3Fix::ZERO);
738        assert_eq!(bodies[1].angular_velocity, Vec3Fix::from_int(0, 0, 1));
739
740        bodies[1].angular_velocity = Vec3Fix::ZERO;
741        apply_motors_3d(&[(0, pd)], &joints, &mut bodies, dt);
742        assert_eq!(bodies[0].angular_velocity, Vec3Fix::from_int(0, 0, -1));
743        assert_eq!(bodies[1].angular_velocity, Vec3Fix::from_int(0, 0, 2));
744    }
745
746    /// A hinge motor about `Y` between two dynamic bodies (world inverse inertia 1)
747    /// in velocity mode, `kp = 4`, target rate 1, both at rest: torque
748    /// `4·(1 - 0) = 4`, impulse over `dt = 1/4` is `1` about `Y`: `ω_b = (0, 1, 0)`,
749    /// `ω_a = (0, -1, 0)`. A target of `-10` with `max_force = 2` clamps the torque
750    /// to `-2` (impulse `-1/2`). A pair already turning at the target rate
751    /// (`ω_b - ω_a = 1`) gets zero torque and is left as it is.
752    #[test]
753    fn hinge_motor_drives_the_relative_rate_about_the_axis_and_clamps_below() {
754        let hinge = || {
755            Joint::Hinge(crate::joint::HingeJoint::new(
756                0,
757                1,
758                Vec3Fix::ZERO,
759                Vec3Fix::ZERO,
760                Vec3Fix::UNIT_Y,
761                Vec3Fix::UNIT_Y,
762            ))
763        };
764        let pair = || {
765            let (mut bodies, _) = motor_scene(1, 1);
766            bodies[0].inv_inertia = Vec3Fix::from_int(1, 1, 1);
767            bodies[1].inv_inertia = Vec3Fix::from_int(1, 1, 1);
768            bodies
769        };
770        let dt = Fix128::from_ratio(1, 4);
771        let mut pd = PdController::new(Fix128::from_int(4), Fix128::ZERO, Fix128::from_int(100));
772        pd.set_velocity_target(Fix128::ONE);
773
774        let mut bodies = pair();
775        apply_motors(&[JointMotor::new(0, pd)], &[hinge()], &mut bodies, dt);
776        assert_eq!(bodies[0].angular_velocity, Vec3Fix::from_int(0, -1, 0));
777        assert_eq!(bodies[1].angular_velocity, Vec3Fix::from_int(0, 1, 0));
778
779        let mut slow = pd;
780        slow.max_force = Fix128::from_int(2);
781        slow.set_velocity_target(Fix128::from_int(-10));
782        let mut bodies = pair();
783        apply_motors(&[JointMotor::new(0, slow)], &[hinge()], &mut bodies, dt);
784        let half = Fix128::from_ratio(1, 2);
785        assert_eq!(
786            bodies[0].angular_velocity,
787            Vec3Fix::new(Fix128::ZERO, half, Fix128::ZERO)
788        );
789        assert_eq!(
790            bodies[1].angular_velocity,
791            Vec3Fix::new(Fix128::ZERO, -half, Fix128::ZERO)
792        );
793
794        let mut bodies = pair();
795        bodies[1].angular_velocity = Vec3Fix::from_int(0, 1, 0);
796        apply_motors(&[JointMotor::new(0, pd)], &[hinge()], &mut bodies, dt);
797        assert_eq!(bodies[0].angular_velocity, Vec3Fix::ZERO);
798        assert_eq!(bodies[1].angular_velocity, Vec3Fix::from_int(0, 1, 0));
799    }
800}