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}