Skip to main content

rapier3d_mjcf/loader/
runtime.rs

1//! Post-insertion runtime helpers attached to [`MjcfRobotHandles`]:
2//! gravity compensation, contact-hook construction, mocap-keyframe
3//! application, actuator control, and sensor reading.
4
5use mjcf_rs::extras::Keyframe;
6
7use rapier3d::dynamics::{
8    GenericJoint, ImpulseJointHandle, ImpulseJointSet, JointAxis, MultibodyIndex,
9    MultibodyJointHandle, MultibodyJointSet, RigidBody, RigidBodySet, RigidBodyType,
10};
11use rapier3d::math::{Pose, Real, Rotation, Vector};
12
13use crate::hooks::{MjcfContactHooks, PairOverride};
14
15use super::handles::MjcfRobotHandles;
16use super::types::{MjcfDofKind, MjcfQposDof, MjcfRobot, SensorObjectRef};
17
18impl<H> MjcfRobotHandles<H> {
19    /// Apply per-body gravity compensation as an explicit per-step force.
20    ///
21    /// When `body.gravcomp ∈ {0, 1}`, the loader sets `gravity_scale`
22    /// directly so the user has nothing to do. For *fractional* values
23    /// the loader's `gravity_scale = 1 - gravcomp` is only exact while
24    /// the body's mass doesn't change; calling this helper each step
25    /// instead applies a force that exactly cancels `gravcomp * mass *
26    /// gravity` against the current `mass`.
27    ///
28    /// The helper takes over from the loader's static reduction by first
29    /// resetting `gravity_scale` to 1 (otherwise the two compensations
30    /// would stack — for `gravcomp = 1` that means a body that floats
31    /// upward at `g` instead of standing still). It also resets the
32    /// body's accumulated user force before adding its own contribution,
33    /// so successive per-step calls don't pile up across frames. If you
34    /// rely on `add_force` for other things on a `gravcomp` body, you'll
35    /// need to re-apply those after `apply_gravity_compensation`.
36    ///
37    /// Call once per frame before `pipeline.step`.
38    pub fn apply_gravity_compensation(
39        &self,
40        bodies: &mut RigidBodySet,
41        robot: &MjcfRobot,
42        gravity: Vector,
43    ) {
44        for (i, b) in robot.bodies.iter().enumerate() {
45            if b.gravcomp <= 0.0 {
46                continue;
47            }
48            let Some(handle) = self.bodies.get(i).and_then(|h| h.as_ref()) else {
49                continue;
50            };
51            if let Some(rb) = bodies.get_mut(handle.body) {
52                if rb.gravity_scale() != 1.0 {
53                    rb.set_gravity_scale(1.0, false);
54                }
55                rb.reset_forces(false);
56                // Use the MJCF-declared mass: `rb.mass()` returns 0 until
57                // the rapier pipeline runs `update_world_mass_properties`,
58                // which means the first call (before any step) would
59                // silently fail to compensate.
60                let force = -gravity * (b.gravcomp as Real) * b.mass;
61                rb.add_force(force, true);
62            }
63        }
64    }
65
66    /// Build a [`MjcfContactHooks`] honoring this robot's `<contact><exclude>`
67    /// and `<contact><pair>` entries. The returned hook can be passed to
68    /// `pipeline.step(&hooks, ...)`.
69    ///
70    /// If the robot has no contact entries the returned hook is empty;
71    /// installing it has no effect (other than a per-pair function-pointer
72    /// dispatch).
73    pub fn contact_hooks(&self, robot: &MjcfRobot) -> MjcfContactHooks {
74        let mut hooks = MjcfContactHooks::new();
75
76        // <exclude>: forbid contacts between every collider attached to body1
77        // and every collider attached to body2.
78        for ex in &robot.contact_excludes {
79            let Some(b1) = robot.body_name_to_idx.get(&ex.body1).copied() else {
80                log::warn!("<contact><exclude>: unknown body1 `{}`", ex.body1);
81                continue;
82            };
83            let Some(b2) = robot.body_name_to_idx.get(&ex.body2).copied() else {
84                log::warn!("<contact><exclude>: unknown body2 `{}`", ex.body2);
85                continue;
86            };
87            let Some(h1) = self.bodies.get(b1).and_then(|b| b.as_ref()) else {
88                continue;
89            };
90            let Some(h2) = self.bodies.get(b2).and_then(|b| b.as_ref()) else {
91                continue;
92            };
93            for c1 in &h1.colliders {
94                for c2 in &h2.colliders {
95                    hooks.exclude(c1.handle, c2.handle);
96                }
97            }
98        }
99
100        // <pair>: per-pair friction / margin overrides between two named geoms.
101        for p in &robot.contact_pairs {
102            let Some(&(b1, gi1)) = robot.geom_name_to_collider.get(&p.geom1) else {
103                log::warn!("<contact><pair>: unknown geom1 `{}`", p.geom1);
104                continue;
105            };
106            let Some(&(b2, gi2)) = robot.geom_name_to_collider.get(&p.geom2) else {
107                log::warn!("<contact><pair>: unknown geom2 `{}`", p.geom2);
108                continue;
109            };
110            let Some(h1) = self
111                .bodies
112                .get(b1)
113                .and_then(|b| b.as_ref())
114                .and_then(|b| b.colliders.get(gi1))
115            else {
116                continue;
117            };
118            let Some(h2) = self
119                .bodies
120                .get(b2)
121                .and_then(|b| b.as_ref())
122                .and_then(|b| b.colliders.get(gi2))
123            else {
124                continue;
125            };
126            let ov = PairOverride {
127                friction: p.friction.map(|f| f[0] as Real),
128                margin: p.margin.map(|m| m as Real),
129            };
130            hooks.add_override(h1.handle, h2.handle, ov);
131        }
132
133        hooks
134    }
135
136    /// Apply a keyframe's `mpos`/`mquat` to mocap bodies. Joint coordinates
137    /// (`qpos`/`qvel`) require knowledge of generalized-coordinate ordering
138    /// and are best applied through [`MjcfRobot`] in impulse-joint mode
139    /// (where each body has a global pose), or via the multibody set in
140    /// multibody-joint mode.
141    pub fn apply_mocap_keyframe(
142        &self,
143        bodies: &mut RigidBodySet,
144        robot: &MjcfRobot,
145        key: &Keyframe,
146    ) {
147        // mpos/mquat are concatenated arrays — 3 floats per mocap body.
148        let mocap_bodies: Vec<usize> = robot
149            .bodies
150            .iter()
151            .enumerate()
152            .skip(1)
153            .filter_map(|(i, b)| {
154                if !b.is_intermediate
155                    && robot.bodies[i].body.body_type() == RigidBodyType::KinematicPositionBased
156                {
157                    Some(i)
158                } else {
159                    None
160                }
161            })
162            .collect();
163        for (k, idx) in mocap_bodies.iter().enumerate() {
164            let pos = if key.mpos.len() >= 3 * (k + 1) {
165                Vector::new(
166                    key.mpos[3 * k] as Real,
167                    key.mpos[3 * k + 1] as Real,
168                    key.mpos[3 * k + 2] as Real,
169                )
170            } else {
171                continue;
172            };
173            let q = if key.mquat.len() >= 4 * (k + 1) {
174                Rotation::from_xyzw(
175                    key.mquat[4 * k + 1] as Real,
176                    key.mquat[4 * k + 2] as Real,
177                    key.mquat[4 * k + 3] as Real,
178                    key.mquat[4 * k] as Real,
179                )
180            } else {
181                Rotation::IDENTITY
182            };
183            if let Some(handle) = self.bodies.get(*idx).and_then(|b| b.as_ref())
184                && let Some(rb) = bodies.get_mut(handle.body)
185            {
186                rb.set_position(Pose::from_parts(pos, q), true);
187            }
188        }
189    }
190}
191
192/// The world pose encoded by a free joint's 7 `qpos` (`x y z` + `w x y z`
193/// quaternion, MuJoCo's wxyz order), with the loader's length `scale` applied
194/// to the translation and the loader's `shift` pre-multiplied so the floating
195/// base lands in the same frame as the rest of the model.
196fn free_pose(qp: &[f64], scale: Real, shift: Pose) -> Pose {
197    let translation = Vector::new(qp[0] as Real, qp[1] as Real, qp[2] as Real) * scale;
198    let rotation = Rotation::from_xyzw(qp[4] as Real, qp[5] as Real, qp[6] as Real, qp[3] as Real);
199    shift * Pose::from_parts(translation, rotation)
200}
201
202/// A hinge/slide DoF's target rapier joint coordinate from its single `qpos`
203/// scalar. MuJoCo stores the absolute coordinate, so we subtract `<joint ref>`
204/// (and scale lengths) to reach rapier's frame-relative convention — the same
205/// transform the serial-joint builder applies to the joint limits.
206fn scalar_coord(dof: &MjcfQposDof, qp: &[f64]) -> Real {
207    match dof.kind {
208        MjcfDofKind::Hinge => qp[0] as Real - dof.reference,
209        MjcfDofKind::Slide => (qp[0] as Real - dof.reference) * dof.scale,
210        _ => 0.0,
211    }
212}
213
214/// The target relative rotation of a ball DoF from its 4 `qpos` (wxyz).
215fn ball_rotation(qp: &[f64]) -> Rotation {
216    Rotation::from_xyzw(qp[1] as Real, qp[2] as Real, qp[3] as Real, qp[0] as Real)
217}
218
219impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
220    /// Apply a keyframe's full state (`qpos`, `qvel`, and `mpos`/`mquat`) to a
221    /// robot inserted through [`insert_using_multibody_joints`](MjcfRobot::insert_using_multibody_joints).
222    ///
223    /// Joint coordinates are written to the multibody's generalized
224    /// coordinates (then propagated through forward-kinematics so the rapier
225    /// rigid-bodies match), and a floating base's `qpos` sets its root body's
226    /// world pose. Call this once after insertion to start the model in a
227    /// declared keyframe (e.g. MuJoCo's `home`).
228    ///
229    /// `qvel` is written to the generalized velocities of articulated joints
230    /// and as the world-frame linear/angular velocity of a floating base.
231    /// Mocap bodies are handled exactly as [`apply_mocap_keyframe`](Self::apply_mocap_keyframe).
232    pub fn apply_keyframe(
233        &self,
234        bodies: &mut RigidBodySet,
235        multibody_joints: &mut MultibodyJointSet,
236        robot: &MjcfRobot,
237        key: &Keyframe,
238    ) {
239        self.apply_mocap_keyframe(bodies, robot, key);
240
241        // Multibodies whose root pose / joint coordinates changed: re-run
242        // forward-kinematics on each once at the end.
243        let mut touched: Vec<MultibodyIndex> = Vec::new();
244        let touch = |touched: &mut Vec<MultibodyIndex>, idx: MultibodyIndex| {
245            if !touched.contains(&idx) {
246                touched.push(idx);
247            }
248        };
249
250        let mut qpos_i = 0;
251        let mut qvel_i = 0;
252        for dof in &robot.qpos_dofs {
253            let qpos = key.qpos.get(qpos_i..qpos_i + dof.kind.qpos_width());
254            let qvel = key.qvel.get(qvel_i..qvel_i + dof.kind.qvel_width());
255            qpos_i += dof.kind.qpos_width();
256            qvel_i += dof.kind.qvel_width();
257
258            if dof.kind == MjcfDofKind::Free {
259                let Some(handle) = self.bodies.get(dof.body).and_then(|b| b.as_ref()) else {
260                    continue;
261                };
262                if let Some(rb) = bodies.get_mut(handle.body) {
263                    if let Some(qp) = qpos {
264                        rb.set_position(free_pose(qp, dof.scale, robot.base_shift), true);
265                    }
266                    if let Some(qv) = qvel {
267                        let lin =
268                            Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale;
269                        let ang = Vector::new(qv[3] as Real, qv[4] as Real, qv[5] as Real);
270                        rb.set_linvel(lin, true);
271                        rb.set_angvel(ang, true);
272                    }
273                }
274                if let Some(link) = multibody_joints.rigid_body_link(handle.body) {
275                    touch(&mut touched, link.multibody);
276                }
277                continue;
278            }
279
280            // Articulated DoF (hinge / slide / ball): write to the multibody's
281            // generalized coordinates.
282            let Some(jidx) = dof.joint else { continue };
283            let Some(jh) = self.joints.get(jidx) else {
284                continue;
285            };
286            // `joint` is `None` for a joint that was dropped as a loop closure.
287            let Some(handle) = jh.joint else { continue };
288            if let Some(link) = multibody_joints.rigid_body_link(jh.link2) {
289                touch(&mut touched, link.multibody);
290            }
291            let Some((mb, link_id)) = multibody_joints.get_mut(handle) else {
292                continue;
293            };
294
295            // Compute the displacement (target − current) under an immutable
296            // borrow, then apply it under a mutable one.
297            let (assembly_id, ndofs, disp) = {
298                let Some(link) = mb.link(link_id) else {
299                    continue;
300                };
301                let joint = link.joint();
302                let coords = joint.coords();
303                let disp = qpos.map(|qp| match dof.kind {
304                    MjcfDofKind::Hinge => vec![scalar_coord(dof, qp) - coords[3]],
305                    MjcfDofKind::Slide => vec![scalar_coord(dof, qp) - coords[0]],
306                    MjcfDofKind::Ball => {
307                        // Reach the target relative rotation from the current
308                        // one: `from_scaled_axis(disp) * joint_rot == target`.
309                        let delta = ball_rotation(qp) * joint.joint_rot().inverse();
310                        let sa = delta.to_scaled_axis();
311                        vec![sa.x, sa.y, sa.z]
312                    }
313                    MjcfDofKind::Free => vec![],
314                });
315                (link.assembly_id(), joint.ndofs(), disp)
316            };
317            if let Some(disp) = disp
318                && let Some(link) = mb.link_mut(link_id)
319            {
320                link.joint.apply_displacement(&disp);
321            }
322            if let Some(qv) = qvel {
323                let scale = if dof.kind == MjcfDofKind::Slide {
324                    dof.scale
325                } else {
326                    1.0
327                };
328                let mut vels = mb.generalized_velocity_mut();
329                for k in 0..ndofs.min(qv.len()) {
330                    vels[assembly_id + k] = qv[k] as Real * scale;
331                }
332            }
333        }
334
335        for idx in touched {
336            if let Some(mb) = multibody_joints.get_multibody_mut(idx) {
337                mb.forward_kinematics(bodies, true);
338                mb.update_rigid_bodies(bodies, false);
339            }
340        }
341    }
342}
343
344impl MjcfRobotHandles<ImpulseJointHandle> {
345    /// Apply a keyframe's `qpos` (and a floating base's `qvel`) to a robot
346    /// inserted through [`insert_using_impulse_joints`](MjcfRobot::insert_using_impulse_joints).
347    ///
348    /// In the impulse-joint path every body carries a global pose, so the
349    /// keyframe is realized by running forward-kinematics over the joint tree
350    /// (anchored at the unchanged fixed/welded roots, or at a floating base set
351    /// from its `qpos`) and writing each body's resulting world pose. The joint
352    /// constraints are already satisfied by those poses.
353    ///
354    /// Only a floating base's `qvel` is applied (as world-frame body velocity);
355    /// per-joint `qvel` has no generalized-coordinate home in this path and is
356    /// ignored. Mocap bodies are handled as [`apply_mocap_keyframe`](Self::apply_mocap_keyframe).
357    pub fn apply_keyframe(&self, bodies: &mut RigidBodySet, robot: &MjcfRobot, key: &Keyframe) {
358        self.apply_mocap_keyframe(bodies, robot, key);
359
360        // Per-joint relative transform from the keyframe; identity where a
361        // joint carries no qpos (synthesized welds keep the bodies at rest).
362        let mut joint_tf: Vec<Pose> = vec![Pose::default(); robot.joints.len()];
363        // Forward-kinematics world poses, seeded with the load-time poses so
364        // fixed/welded roots and the world body keep their place.
365        let mut world: Vec<Pose> = robot.bodies.iter().map(|b| *b.body.position()).collect();
366        // Floating-base velocities, applied after the pose write-back.
367        let mut free_vels: Vec<(usize, Vector, Vector)> = Vec::new();
368
369        let mut qpos_i = 0;
370        let mut qvel_i = 0;
371        for dof in &robot.qpos_dofs {
372            let qpos = key.qpos.get(qpos_i..qpos_i + dof.kind.qpos_width());
373            let qvel = key.qvel.get(qvel_i..qvel_i + dof.kind.qvel_width());
374            qpos_i += dof.kind.qpos_width();
375            qvel_i += dof.kind.qvel_width();
376
377            match dof.kind {
378                MjcfDofKind::Free => {
379                    if let Some(qp) = qpos {
380                        world[dof.body] = free_pose(qp, dof.scale, robot.base_shift);
381                    }
382                    if let Some(qv) = qvel {
383                        let lin =
384                            Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale;
385                        let ang = Vector::new(qv[3] as Real, qv[4] as Real, qv[5] as Real);
386                        free_vels.push((dof.body, lin, ang));
387                    }
388                }
389                MjcfDofKind::Hinge | MjcfDofKind::Slide => {
390                    if let (Some(jidx), Some(qp)) = (dof.joint, qpos) {
391                        let q = scalar_coord(dof, qp);
392                        joint_tf[jidx] = if dof.kind == MjcfDofKind::Hinge {
393                            Pose::from_rotation(Rotation::from_axis_angle(Vector::X, q))
394                        } else {
395                            Pose::from_translation(Vector::X * q)
396                        };
397                    }
398                }
399                MjcfDofKind::Ball => {
400                    if let (Some(jidx), Some(qp)) = (dof.joint, qpos) {
401                        joint_tf[jidx] = Pose::from_rotation(ball_rotation(qp));
402                    }
403                }
404            }
405        }
406
407        // Forward-kinematics: `robot.joints` is in topological order, so each
408        // joint's parent body is already positioned. This mirrors rapier's
409        // multibody `body_to_parent`: world₂ = world₁ · frame1 · q · frame2⁻¹.
410        for (jidx, j) in robot.joints.iter().enumerate() {
411            world[j.link2] = world[j.link1]
412                * j.joint.local_frame1
413                * joint_tf[jidx]
414                * j.joint.local_frame2.inverse();
415        }
416
417        for (i, handle) in self.bodies.iter().enumerate() {
418            if let Some(handle) = handle
419                && let Some(rb) = bodies.get_mut(handle.body)
420            {
421                rb.set_position(world[i], true);
422            }
423        }
424        for (body, lin, ang) in free_vels {
425            if let Some(handle) = self.bodies.get(body).and_then(|b| b.as_ref())
426                && let Some(rb) = bodies.get_mut(handle.body)
427            {
428                rb.set_linvel(lin, true);
429                rb.set_angvel(ang, true);
430            }
431        }
432    }
433}
434
435impl MjcfRobotHandles<ImpulseJointHandle> {
436    /// Apply per-actuator control values to the impulse joints they drive.
437    ///
438    /// `ctrl` is a flat array of one scalar per actuator, in the order of
439    /// [`MjcfRobot::actuators`] (and [`MjcfRobotHandles::actuators`]). The
440    /// per-actuator mapping uses MJCF semantics:
441    ///
442    /// - `<motor>` — a force/torque input. We emulate the constant-force
443    ///   semantics by aiming the motor at a very large velocity and
444    ///   capping the applied force at `|ctrl * gear|`.
445    /// - `<position>` → `set_motor_position(ctrl, kp, kv)`.
446    /// - `<velocity>` → `set_motor_velocity(ctrl, kv)`.
447    /// - `<damper>` → zero velocity target with damping `gainprm[0] * |ctrl|`.
448    /// - everything else (`general`, `intvelocity`, …) is left to the user
449    ///   — the metadata is preserved on `Self::actuators`.
450    pub fn apply_controls(&self, joints: &mut ImpulseJointSet, ctrl: &[Real]) {
451        self.apply_controls_scaled(joints, ctrl, 1.0);
452    }
453
454    /// Like [`apply_controls`](Self::apply_controls) but uniformly scales every
455    /// actuator's strength by `gain_scale` (gains and force limits; see
456    /// `configure_actuator_motor`). `gain_scale < 1` softens the actuation —
457    /// e.g. to ease servo-driven moves that would otherwise saturate and snap.
458    pub fn apply_controls_scaled(
459        &self,
460        joints: &mut ImpulseJointSet,
461        ctrl: &[Real],
462        gain_scale: Real,
463    ) {
464        for (i, ah) in self.actuators.iter().enumerate() {
465            let Some(handle) = ah.joint else { continue };
466            let Some(joint) = joints.get_mut(handle, true) else {
467                continue;
468            };
469            let u = ctrl.get(i).copied().unwrap_or(0.0);
470            configure_actuator_motor(&mut joint.data, &ah.actuator, u, gain_scale);
471        }
472    }
473}
474
475impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
476    /// Apply per-actuator control values to the multibody joints they drive.
477    ///
478    /// This is the multibody-path counterpart of
479    /// [`apply_controls`](MjcfRobotHandles::<ImpulseJointHandle>::apply_controls):
480    /// it reproduces MuJoCo's "actuation" by writing each actuator's motor
481    /// onto the multibody link it drives. The per-actuator interpretation is
482    /// identical (see `configure_actuator_motor`); call it once per frame
483    /// before `pipeline.step`.
484    ///
485    /// `ctrl` is a flat array of one scalar per actuator, in the order of
486    /// [`MjcfRobot::actuators`] (and [`MjcfRobotHandles::actuators`]).
487    /// Actuators whose joint was dropped from the multibody chain (a loop
488    /// closure) are skipped.
489    ///
490    /// Each driven body is woken so a steady control input keeps holding the
491    /// pose even after the multibody would otherwise have gone to sleep
492    /// (mirroring the impulse path, which wakes through the joint graph).
493    pub fn apply_controls_multibody(
494        &self,
495        bodies: &mut RigidBodySet,
496        multibody_joints: &mut MultibodyJointSet,
497        ctrl: &[Real],
498    ) {
499        self.apply_controls_multibody_scaled(bodies, multibody_joints, ctrl, 1.0);
500    }
501
502    /// Like [`apply_controls_multibody`](Self::apply_controls_multibody) but
503    /// uniformly scales every actuator's strength by `gain_scale` (gains and
504    /// force limits; see `configure_actuator_motor`). `gain_scale < 1` softens
505    /// the actuation, so a servo-driven move (e.g. easing between keyframes)
506    /// ramps in instead of saturating to its force limit and arriving instantly.
507    pub fn apply_controls_multibody_scaled(
508        &self,
509        bodies: &mut RigidBodySet,
510        multibody_joints: &mut MultibodyJointSet,
511        ctrl: &[Real],
512        gain_scale: Real,
513    ) {
514        for (i, ah) in self.actuators.iter().enumerate() {
515            // `H` is `Option<MultibodyJointHandle>` here, so `ah.joint` is
516            // `Option<Option<_>>`: the outer `None` means "no actuator joint",
517            // the inner `None` means "joint dropped as a loop closure".
518            let Some(Some(handle)) = ah.joint else {
519                continue;
520            };
521            let Some((mb, link_id)) = multibody_joints.get_mut(handle) else {
522                continue;
523            };
524            let Some(link) = mb.links_mut().nth(link_id) else {
525                continue;
526            };
527            let u = ctrl.get(i).copied().unwrap_or(0.0);
528            configure_actuator_motor(&mut link.joint.data, &ah.actuator, u, gain_scale);
529            let rb = link.rigid_body_handle();
530            if let Some(body) = bodies.get_mut(rb) {
531                body.wake_up(true);
532            }
533        }
534    }
535}
536
537/// Write the joint-motor configuration for one MJCF actuator driving one
538/// rapier joint, given the control value `u`. Shared by the impulse- and
539/// multibody-joint control paths. MJCF semantics:
540///
541/// - `<motor>` → constant generalized force `u * gear` (emulated with a
542///   far-target velocity motor capped at `|u * gear|`).
543/// - `<position>` → `set_motor_position(u, kp, kv)`.
544/// - `<velocity>` → `set_motor_velocity(u, kv)`.
545/// - `<damper>` → zero-velocity target with damping `gainprm[0] * |u|`.
546/// - `<general>` with `biastype="affine"` → a position/velocity servo. The
547///   affine model `force = gainprm0·u + biasprm0 + biasprm1·q + biasprm2·q̇`
548///   matches a spring `stiffness·(target − q) − damping·q̇` with
549///   `stiffness = −biasprm1`, `damping = −biasprm2`, and
550///   `target = (gainprm0·u + biasprm0) / stiffness`. This is what holds e.g.
551///   the flybody legs in their commanded pose.
552/// - `<general>` with a non-affine bias → constant force `gainprm0·u * gear`
553///   (same emulation as `<motor>`).
554/// - other kinds (`intvelocity`, muscle, …) are left untouched.
555///
556/// The joint's free axis is the angular (hinge/ball) or linear (slide) X axis
557/// after the per-joint basis rotation applied in `build_serial_joint`; we
558/// configure both and let the locked one's motor be a no-op.
559fn configure_actuator_motor(
560    data: &mut GenericJoint,
561    actuator: &mjcf_rs::extras::Actuator,
562    u: Real,
563    gain_scale: Real,
564) {
565    use mjcf_rs::extras::ActuatorKind;
566
567    let ax = JointAxis::AngX;
568    let lin_ax = JointAxis::LinX;
569    // MuJoCo's `<position>` defaults `kp` to 1 when it isn't set (directly or
570    // through a `<default>` class), not 0. Defaulting to 0 here would give a
571    // zero-gain — i.e. completely limp — servo, which is what made e.g. the
572    // shadow hand's many `kp`-less finger actuators unable to hold/move their
573    // joints. `kv` (velocity gain) does default to 0 in MuJoCo.
574    //
575    // `gain_scale` uniformly scales the actuator's strength: the servo gains
576    // (`kp`/`kv`, or an affine `<general>`'s stiffness/damping — target pose
577    // unchanged) and the force cap (`forcerange`) and constant forces. Below 1
578    // the actuator pushes more softly, so a servo-driven move (e.g. snapping
579    // between keyframes) eases in instead of saturating to its force limit and
580    // arriving almost instantly. `1.0` reproduces the model as authored.
581    let kp = actuator.kp.unwrap_or(1.0) as Real * gain_scale;
582    let kv = actuator.kv.unwrap_or_default() as Real * gain_scale;
583    let gear = actuator.gear[0] as Real;
584    let base_force_max = actuator
585        .force_range
586        .map(|r| r[1].abs() as Real)
587        .unwrap_or(Real::INFINITY);
588    // Scale the force cap too (an unbounded `INFINITY` cap stays unbounded —
589    // and avoids `INFINITY * 0` = `NaN` for a zero scale).
590    let force_max = if base_force_max.is_finite() {
591        base_force_max * gain_scale
592    } else {
593        base_force_max
594    };
595
596    // Emulate a constant generalized force: aim the motor at a far-away
597    // velocity in the force's direction and cap the impulse at the force
598    // magnitude. With stiffness = damping = 0, the motor saturates at its
599    // max_force every step (the target is never reached), so the net effect
600    // is a constant force along the joint axis.
601    let apply_constant_force = |data: &mut GenericJoint, force: Real| {
602        let max = force.abs().min(force_max);
603        let far_vel = if force >= 0.0 { 1.0e9 } else { -1.0e9 };
604        data.set_motor_velocity(ax, far_vel, 0.0);
605        data.set_motor_velocity(lin_ax, far_vel, 0.0);
606        data.set_motor_max_force(ax, max);
607        data.set_motor_max_force(lin_ax, max);
608    };
609
610    match actuator.kind {
611        ActuatorKind::Motor => apply_constant_force(data, u * gear * gain_scale),
612        ActuatorKind::Position => {
613            data.set_motor_position(ax, u, kp, kv);
614            data.set_motor_position(lin_ax, u, kp, kv);
615            // Apply the actuator's `forcerange` as the motor's max force.
616            // `set_motor_position` leaves `max_force` untouched, so without this
617            // the servo inherits whatever joint construction left it at (e.g. the
618            // `frictionloss` passed to `motor_max_force` when building the joint),
619            // which starves position servos — the joint can't deliver enough
620            // torque to hold against gravity. `force_max` is `INFINITY` when no
621            // `forcerange` is given, which leaves the motor unbounded (a no-op).
622            data.set_motor_max_force(ax, force_max);
623            data.set_motor_max_force(lin_ax, force_max);
624        }
625        ActuatorKind::Velocity => {
626            data.set_motor_velocity(ax, u, kv);
627            data.set_motor_velocity(lin_ax, u, kv);
628            data.set_motor_max_force(ax, force_max);
629            data.set_motor_max_force(lin_ax, force_max);
630        }
631        ActuatorKind::Damper => {
632            let damp =
633                actuator.gainprm.first().copied().unwrap_or(0.0) as Real * u.abs() * gain_scale;
634            data.set_motor_velocity(ax, 0.0, damp);
635            data.set_motor_velocity(lin_ax, 0.0, damp);
636            data.set_motor_max_force(ax, force_max);
637            data.set_motor_max_force(lin_ax, force_max);
638        }
639        ActuatorKind::General => {
640            let g0 = actuator.gainprm.first().copied().unwrap_or(0.0) as Real;
641            let b0 = actuator.biasprm.first().copied().unwrap_or(0.0) as Real;
642            let b1 = actuator.biasprm.get(1).copied().unwrap_or(0.0) as Real;
643            let b2 = actuator.biasprm.get(2).copied().unwrap_or(0.0) as Real;
644            let affine = actuator.bias_type.as_deref() == Some("affine");
645            // A position servo needs a real restoring term (`-biasprm1·q`,
646            // i.e. stiffness > 0); without it the affine bias is just a
647            // constant + velocity-damping force.
648            if affine && -b1 > 0.0 {
649                let stiffness = -b1;
650                let damping = -b2;
651                // Target uses the unscaled stiffness so the commanded pose is
652                // unchanged; only the gains (force) scale.
653                let target = (g0 * u + b0) / stiffness;
654                data.set_motor_position(ax, target, stiffness * gain_scale, damping * gain_scale);
655                data.set_motor_position(
656                    lin_ax,
657                    target,
658                    stiffness * gain_scale,
659                    damping * gain_scale,
660                );
661                data.set_motor_max_force(ax, force_max);
662                data.set_motor_max_force(lin_ax, force_max);
663            } else {
664                // `gaintype="fixed"`, non-restoring bias: constant force
665                // `gainprm0·u` through the transmission.
666                apply_constant_force(data, g0 * u * gear * gain_scale);
667            }
668        }
669        ActuatorKind::IntVelocity | ActuatorKind::Other => {
670            // Not driven automatically — user-controlled.
671        }
672    }
673}
674
675/// One scalar / vector / quaternion reading from a sensor.
676#[derive(Copy, Clone, Debug, PartialEq)]
677pub enum MjcfSensorValue {
678    /// One scalar.
679    Scalar(Real),
680    /// 3-vector.
681    Vector3(Vector),
682    /// Unit quaternion.
683    Quat(Rotation),
684}
685
686impl MjcfRobot {
687    /// Look up a keyframe by its `<key name="…">`. Returns `None` if no
688    /// keyframe with that name exists.
689    pub fn keyframe_by_name(&self, name: &str) -> Option<&Keyframe> {
690        self.keyframes
691            .iter()
692            .find(|k| k.name.as_deref() == Some(name))
693    }
694
695    /// Build a per-actuator control vector that commands the given keyframe's
696    /// pose, in the order of [`Self::actuators`].
697    ///
698    /// This is what lets a keyframe *hold* under actuation instead of being
699    /// dragged back to the zero configuration: driving the actuators with a
700    /// plain zero vector (the obvious default) makes every position servo pull
701    /// its joint to 0, undoing the keyframe. Each entry is resolved as:
702    ///
703    /// - the keyframe's explicit `ctrl[i]` when it provides one (MuJoCo's own
704    ///   commanded input for the pose), otherwise
705    /// - for an actuator driving a 1-DoF (hinge/slide) joint, that joint's
706    ///   keyframe `qpos` in rapier coordinates (the natural position-servo
707    ///   target), otherwise
708    /// - `0`.
709    ///
710    /// Pass the result to [`apply_controls`](MjcfRobotHandles::<rapier3d::dynamics::ImpulseJointHandle>::apply_controls)
711    /// / [`apply_controls_multibody`](MjcfRobotHandles::<Option<MultibodyJointHandle>>::apply_controls_multibody).
712    pub fn keyframe_controls(&self, key: &Keyframe) -> Vec<Real> {
713        // Per-joint position target (rapier coordinates) derived from `qpos`.
714        let mut joint_target: Vec<Option<Real>> = vec![None; self.joints.len()];
715        let mut qpos_i = 0;
716        for dof in &self.qpos_dofs {
717            if let Some(jidx) = dof.joint
718                && let Some(&q) = key.qpos.get(qpos_i)
719            {
720                let target = match dof.kind {
721                    MjcfDofKind::Hinge => Some(q as Real - dof.reference),
722                    MjcfDofKind::Slide => Some((q as Real - dof.reference) * dof.scale),
723                    // Ball/free are multi-DoF; no single scalar servo target.
724                    _ => None,
725                };
726                if let Some(t) = target {
727                    joint_target[jidx] = Some(t);
728                }
729            }
730            qpos_i += dof.kind.qpos_width();
731        }
732
733        self.actuators
734            .iter()
735            .enumerate()
736            .map(|(i, a)| {
737                if let Some(&c) = key.ctrl.get(i) {
738                    c as Real
739                } else {
740                    a.joint_index
741                        .and_then(|j| joint_target.get(j).copied().flatten())
742                        .unwrap_or(0.0)
743                }
744            })
745            .collect()
746    }
747
748    /// Read a sensor's current value from the rapier state. Returns `None`
749    /// for sensors the loader doesn't know how to evaluate.
750    pub fn read_sensor(
751        &self,
752        sensor_idx: usize,
753        bodies: &RigidBodySet,
754        handles: &MjcfRobotHandles<impl Copy>,
755        gravity: Vector,
756    ) -> Option<MjcfSensorValue> {
757        let binding = self.sensors.get(sensor_idx)?;
758        let s = &binding.sensor;
759        let body_handle_of = |b: usize| -> Option<&RigidBody> {
760            handles
761                .bodies
762                .get(b)
763                .and_then(|h| h.as_ref())
764                .and_then(|h| bodies.get(h.body))
765        };
766        match (s.kind.as_str(), &binding.object) {
767            ("framepos", SensorObjectRef::Body(b))
768            | ("framepos", SensorObjectRef::Site { body: b, .. }) => {
769                Some(MjcfSensorValue::Vector3(body_handle_of(*b)?.translation()))
770            }
771            ("framequat", SensorObjectRef::Body(b)) => {
772                Some(MjcfSensorValue::Quat(*body_handle_of(*b)?.rotation()))
773            }
774            ("framelinvel", SensorObjectRef::Body(b)) => {
775                Some(MjcfSensorValue::Vector3(body_handle_of(*b)?.linvel()))
776            }
777            ("frameangvel", SensorObjectRef::Body(b)) => {
778                Some(MjcfSensorValue::Vector3(body_handle_of(*b)?.angvel()))
779            }
780            ("velocimeter", SensorObjectRef::Body(b))
781            | ("velocimeter", SensorObjectRef::Site { body: b, .. }) => {
782                Some(MjcfSensorValue::Vector3(body_handle_of(*b)?.linvel()))
783            }
784            ("gyro", SensorObjectRef::Body(b))
785            | ("gyro", SensorObjectRef::Site { body: b, .. }) => {
786                let rb = body_handle_of(*b)?;
787                // Express angular velocity in the body's local frame.
788                let inv_rot = rb.rotation().inverse();
789                Some(MjcfSensorValue::Vector3(inv_rot * rb.angvel()))
790            }
791            ("subtreemass", SensorObjectRef::Body(b)) => {
792                let rb = body_handle_of(*b)?;
793                Some(MjcfSensorValue::Scalar(rb.mass()))
794            }
795            ("subtreecom", SensorObjectRef::Body(b)) => {
796                let rb = body_handle_of(*b)?;
797                let local_com = rb.mass_properties().local_mprops.local_com;
798                Some(MjcfSensorValue::Vector3(*rb.position() * local_com))
799            }
800            ("clock", _) => {
801                // The user is expected to keep their own simulation time.
802                let _ = gravity;
803                Some(MjcfSensorValue::Scalar(0.0))
804            }
805            _ => None,
806        }
807    }
808}