rapier3d_mjcf/loader/types.rs
1//! Plain-data types describing the loaded robot before insertion: bodies,
2//! joints, equality constraints, actuator/sensor bindings, and the
3//! [`MjcfRobot`] container that ties them together.
4
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8use mjcf_rs::contact::{ContactExclude as MjcfContactExclude, ContactPair as MjcfContactPair};
9use mjcf_rs::extras::{Actuator as MjcfActuator, Keyframe, Sensor as MjcfSensor};
10use mjcf_rs::model::BodyId;
11
12use rapier3d::dynamics::{GenericJoint, RigidBody};
13use rapier3d::geometry::{Collider, SharedShape};
14use rapier3d::math::{Pose, Real, Vector};
15
16/// One body from the MJCF model materialized as a rapier rigid-body and its colliders.
17#[derive(Clone, Debug)]
18pub struct MjcfBody {
19 /// Original MJCF body name (or a synthesized name for intermediate bodies).
20 pub name: Option<String>,
21 /// `true` if this body was added to materialize one of several joints
22 /// declared inside a single MJCF `<body>`.
23 pub is_intermediate: bool,
24 /// Index of the original MJCF body in [`Model::bodies`](mjcf_rs::model::Model::bodies)
25 /// (the same index is used for both the body itself and any
26 /// intermediates introduced for its joint chain). The world body has
27 /// id 0.
28 pub mjcf_body_id: BodyId,
29 /// The rapier rigid-body.
30 pub body: RigidBody,
31 /// Colliders attached to this rigid-body.
32 pub colliders: Vec<Collider>,
33 /// `<body gravcomp>` value (0 = no compensation, 1 = full compensation).
34 /// Recorded so [`MjcfRobotHandles::apply_gravity_compensation`](super::MjcfRobotHandles::apply_gravity_compensation)
35 /// can re-apply the exact force per step for fractional values.
36 pub gravcomp: f64,
37 /// Mass derived from the MJCF model (`<inertial>` or derived from
38 /// geoms when `inertiafromgeom` requests it). Zero when the body has
39 /// no inertial information — e.g. intermediate spacer bodies, or
40 /// when [`MjcfLoaderOptions::apply_imported_mass_props`](super::MjcfLoaderOptions::apply_imported_mass_props)
41 /// is `false`.
42 ///
43 /// [`MjcfRobotHandles::apply_gravity_compensation`](super::MjcfRobotHandles::apply_gravity_compensation)
44 /// reads this directly rather than calling `RigidBody::mass()`,
45 /// because rapier's `mass()` returns 0 until the body has gone through
46 /// one step (the `additional_mass_properties` doesn't combine into
47 /// the queryable `local_mprops` until `update_world_mass_properties`
48 /// runs).
49 pub mass: Real,
50 /// Render-only meshes derived from MJCF `<geom>` elements with
51 /// `contype = conaffinity = 0`. Populated only when
52 /// [`MjcfLoaderOptions::create_colliders_from_visual_shapes`](super::MjcfLoaderOptions::create_colliders_from_visual_shapes)
53 /// is `false` — that flag controls whether visual geoms become
54 /// colliders; when it's off, the geometry is preserved here instead
55 /// so the user can hand it to a renderer.
56 pub visual_meshes: Vec<MjcfVisualMesh>,
57}
58
59/// A render-only mesh declared by an MJCF `<geom>` (typically a
60/// `<geom type="mesh">` with `contype = conaffinity = 0`). The loader
61/// surfaces these so the caller can render them attached to the parent
62/// body without inserting them into the physics collider set.
63#[derive(Clone, Debug)]
64pub struct MjcfVisualMesh {
65 /// Shape, ready to be handed to a renderer.
66 pub shape: SharedShape,
67 /// Pose of the mesh in the body's local frame.
68 pub local_pose: Pose,
69 /// Resolved color, prioritized in this order: `<geom rgba>` over the
70 /// MJCF `<material rgba>` over the OBJ MTL diffuse color. `None`
71 /// when no source set one — the caller's fallback applies.
72 pub rgba: Option<[f32; 4]>,
73 /// Per-vertex texture coordinates parallel to the shape's vertex
74 /// buffer. `Some` only for mesh geoms loaded from an asset that
75 /// carried UV data; `None` for primitives and for meshes whose
76 /// source didn't include texture coordinates.
77 pub uvs: Option<Vec<[f32; 2]>>,
78 /// Per-vertex normals parallel to the shape's vertex buffer. Computed
79 /// from the geometry (crease-aware smoothing, honoring the asset's
80 /// `smoothnormal`) the way MuJoCo generates mesh normals — faceted
81 /// sources like STL carry no usable shared-vertex normals of their own.
82 /// `Some` for mesh geoms; `None` for primitives. When present, a
83 /// renderer should use these directly instead of recomputing flat
84 /// per-face normals, so meshes render smooth-shaded like MuJoCo's
85 /// viewer rather than faceted.
86 pub normals: Option<Vec<[f32; 3]>>,
87 /// Resolved filesystem path to a 2D color texture. Pulled from the
88 /// MJCF `<material texture=…>` when set, otherwise from the OBJ
89 /// MTL's `map_Kd`. `None` for geoms that aren't textured.
90 pub texture: Option<PathBuf>,
91 /// PBR shading parameters resolved from the geom's `<material>`. `None`
92 /// when the geom references no material — the renderer then keeps its own
93 /// default shading rather than being forced to a metallic-roughness look.
94 pub material: Option<MjcfRenderMaterial>,
95}
96
97/// PBR shading parameters resolved from an MJCF `<material>`, expressed in the
98/// metallic-roughness model a modern renderer consumes. MuJoCo's legacy Phong
99/// `specular`/`shininess` are folded in here too: `reflectance` carries the
100/// specular intensity, and `roughness` falls back to `1 − shininess` when the
101/// material doesn't set `roughness` explicitly.
102#[derive(Copy, Clone, Debug, PartialEq)]
103pub struct MjcfRenderMaterial {
104 /// Metallic factor in `[0, 1]` (MuJoCo `metallic`; 0 = dielectric).
105 pub metallic: f32,
106 /// Surface roughness in `[0, 1]` (MuJoCo `roughness`, or `1 − shininess`).
107 pub roughness: f32,
108 /// Dielectric specular reflectance in `[0, 1]` (MuJoCo `specular`), which a
109 /// renderer maps to the F0 Fresnel term.
110 pub reflectance: f32,
111 /// Emissive color `material.rgb × emission`; `[0, 0, 0]` for non-emissive.
112 pub emissive: [f32; 3],
113}
114
115/// One joint from the MJCF model materialized as a rapier `GenericJoint`.
116#[derive(Clone, Debug)]
117pub struct MjcfJoint {
118 /// MJCF joint name (`None` for synthesized fixed joints connecting
119 /// no-joint bodies to their parent).
120 pub name: Option<String>,
121 /// Index of the parent rapier body in [`MjcfRobot::bodies`].
122 pub link1: usize,
123 /// Index of the child rapier body in [`MjcfRobot::bodies`].
124 pub link2: usize,
125 /// The joint description.
126 pub joint: GenericJoint,
127 /// MJCF `<joint damping>` value, preserved separately from the motor so
128 /// the multibody insertion path can route it through per-DoF damping
129 /// (which is numerically more stable than motor damping under stiff
130 /// loads, and naturally covers all 3 DoFs of a ball joint instead of
131 /// only the motorised AngX).
132 pub damping_per_dof: Real,
133 /// MJCF `<joint armature>` value (reflected rotor inertia). On the
134 /// multibody insertion path this is added to the generalized mass-matrix
135 /// diagonal of each of the joint's DoFs, matching MuJoCo's joint-space
136 /// semantics. It is deliberately **not** baked into the link's spatial
137 /// inertia tensor: doing so makes the inertia extremely anisotropic
138 /// (huge along the joint axis, ~0 across it) and the multibody mass
139 /// matrix ill-conditioned.
140 pub armature_per_dof: Real,
141 /// MJCF `<joint stiffness>` (passive spring). On the multibody path this
142 /// can be integrated implicitly in the generalized dynamics (added to the
143 /// mass-matrix diagonal as `dt²·k` with a force `-k·(q − ref)`), which is
144 /// numerically stable for stiff springs on low-inertia links — unlike the
145 /// explicit position motor used otherwise. `0` if the joint has no spring.
146 pub spring_stiffness_per_dof: Real,
147 /// Rest position of the spring (`springref − ref`, in rapier's joint
148 /// coordinate convention). Used for both the explicit-stiffness spring and
149 /// the `springdamper` spring below.
150 pub spring_ref: Real,
151 /// MJCF `<joint springdamper="timeconst dampratio">` (both positive). Unlike
152 /// `spring_stiffness_per_dof`/`damping_per_dof` — which are physical
153 /// coefficients known up front — a `springdamper` requests a spring with a
154 /// given *time constant and damping ratio*, so its stiffness and damping
155 /// must be computed from the assembled joint-space inertia (MuJoCo's
156 /// `dof_invweight0`). The multibody insertion path resolves it in a
157 /// post-assembly pass; a valid `springdamper` overrides the explicit
158 /// stiffness/damping (which are then left at 0 here).
159 pub springdamper: Option<(Real, Real)>,
160}
161
162/// One `<equality>` constraint materialized as an extra rapier joint.
163#[derive(Clone, Debug)]
164pub struct MjcfEqualityJoint {
165 /// `name` attribute, if any.
166 pub name: Option<String>,
167 /// First body's index in [`MjcfRobot::bodies`].
168 pub link1: usize,
169 /// Second body's index in [`MjcfRobot::bodies`].
170 pub link2: usize,
171 /// Whether the constraint is active (mapped to `set_enabled`).
172 pub active: bool,
173 /// The joint description.
174 pub joint: GenericJoint,
175}
176
177/// One `<equality><joint>` coupling, resolved to a *linear* relation between
178/// two of [`MjcfRobot::joints`]: `q2 = coeff·q1 + offset` (rapier joint
179/// coordinates). Higher-order `polycoef` terms are unsupported. Applied as a
180/// multibody DoF coupling at insertion time.
181#[derive(Copy, Clone, Debug)]
182pub struct MjcfJointCoupling {
183 /// Index of the first (independent) joint in [`MjcfRobot::joints`].
184 pub joint1: usize,
185 /// Index of the second (dependent) joint in [`MjcfRobot::joints`].
186 pub joint2: usize,
187 /// Linear coupling coefficient (`polycoef[1]`).
188 pub coeff: Real,
189 /// Constant offset (`polycoef[0]`).
190 pub offset: Real,
191 /// Whether the constraint is active.
192 pub active: bool,
193}
194
195/// `<actuator>` ready to drive a rapier joint motor.
196#[derive(Clone, Debug)]
197pub struct MjcfActuatorBinding {
198 /// Original actuator metadata.
199 pub actuator: MjcfActuator,
200 /// Index into [`MjcfRobot::joints`] of the joint this actuator drives,
201 /// or `None` if the actuator references something rapier doesn't model.
202 pub joint_index: Option<usize>,
203}
204
205/// `<sensor>` definition resolved against the rapier handles. The simulation
206/// itself is up to the user — call [`MjcfRobot::read_sensor`] to query.
207#[derive(Clone, Debug)]
208pub struct MjcfSensorBinding {
209 /// Original sensor metadata.
210 pub sensor: MjcfSensor,
211 /// Resolved object reference, when applicable.
212 pub object: SensorObjectRef,
213}
214
215/// Resolved subject of a sensor.
216#[derive(Clone, Debug, Default)]
217pub enum SensorObjectRef {
218 /// No (or unrecognized) object reference.
219 #[default]
220 None,
221 /// Resolved to a body (rapier index in [`MjcfRobot::bodies`]).
222 Body(usize),
223 /// Resolved to a joint (rapier index in [`MjcfRobot::joints`]).
224 Joint(usize),
225 /// Resolved to a site (body index, site index in that body's `sites`).
226 Site {
227 /// Rapier body index this site belongs to.
228 body: usize,
229 /// Index into the original MJCF body's `sites` list (the loader does
230 /// not create rapier objects for sites).
231 site: usize,
232 },
233}
234
235/// The MJCF joint kind backing one keyframe `qpos`/`qvel` slot. Decides how
236/// many `qpos` / `qvel` scalars the slot consumes and how they are written to
237/// the rapier model.
238#[derive(Copy, Clone, Debug, PartialEq, Eq)]
239pub enum MjcfDofKind {
240 /// 6-DoF free / floating base. Consumes 7 `qpos` (xyz position + wxyz
241 /// quaternion, world frame) and 6 `qvel` (linear + angular). Maps to a
242 /// free rapier body — there is no rapier joint, so the world pose is
243 /// written directly (see [`MjcfQposDof::body`]).
244 Free,
245 /// 3-DoF ball. Consumes 4 `qpos` (wxyz quaternion) and 3 `qvel`.
246 Ball,
247 /// 1-DoF revolute. Consumes 1 `qpos` (angle) and 1 `qvel`.
248 Hinge,
249 /// 1-DoF prismatic. Consumes 1 `qpos` (length) and 1 `qvel`.
250 Slide,
251}
252
253impl MjcfDofKind {
254 /// Number of `qpos` scalars this DoF consumes.
255 pub fn qpos_width(self) -> usize {
256 match self {
257 MjcfDofKind::Free => 7,
258 MjcfDofKind::Ball => 4,
259 MjcfDofKind::Hinge | MjcfDofKind::Slide => 1,
260 }
261 }
262
263 /// Number of `qvel` scalars this DoF consumes.
264 pub fn qvel_width(self) -> usize {
265 match self {
266 MjcfDofKind::Free => 6,
267 MjcfDofKind::Ball => 3,
268 MjcfDofKind::Hinge | MjcfDofKind::Slide => 1,
269 }
270 }
271}
272
273/// One MJCF joint's slot in a keyframe's `qpos` / `qvel` arrays, resolved to
274/// where it must be written in the rapier model.
275///
276/// The entries in [`MjcfRobot::qpos_dofs`] are ordered exactly as MuJoCo lays
277/// out `qpos` (and `qvel`): joints in kinematic-tree order — bodies in
278/// declaration order, and within a body its joints in declaration order.
279/// Welds and other 0-DoF couplings contribute nothing, matching MuJoCo. So
280/// reading a keyframe is a single left-to-right walk of this list, advancing a
281/// `qpos` cursor by [`MjcfDofKind::qpos_width`] (and a `qvel` cursor by
282/// [`MjcfDofKind::qvel_width`]) per entry.
283#[derive(Clone, Debug)]
284pub struct MjcfQposDof {
285 /// The joint kind — decides the `qpos`/`qvel` width and how the slot maps
286 /// onto rapier.
287 pub kind: MjcfDofKind,
288 /// Index into [`MjcfRobot::joints`] of the rapier joint this slot drives.
289 /// `None` for [`MjcfDofKind::Free`], which has no rapier joint — its pose
290 /// is written to [`Self::body`] directly.
291 pub joint: Option<usize>,
292 /// Index into [`MjcfRobot::bodies`] of the child body this slot moves. For
293 /// [`MjcfDofKind::Free`] this is the floating body whose world pose `qpos`
294 /// sets.
295 pub body: usize,
296 /// MJCF `<joint ref>` (the joint's zero offset). Subtracted from `qpos`
297 /// to convert MuJoCo's absolute joint coordinate to rapier's
298 /// frame-relative one, matching the limit convention in the serial-joint
299 /// builder. A length (pre-scale) for [`MjcfDofKind::Slide`]; unused for
300 /// `Free`/`Ball`.
301 pub reference: Real,
302 /// Length scale (`MjcfLoaderOptions::scale`) applied to `Slide` coordinates
303 /// and to the `Free` base translation, matching the scaling the loader
304 /// applied to the model geometry.
305 pub scale: Real,
306}
307
308/// A robot loaded from an MJCF file: a flat list of bodies, joints, and
309/// extras.
310#[derive(Clone, Debug, Default)]
311pub struct MjcfRobot {
312 /// Optional `<mujoco model="…">` name.
313 pub name: Option<String>,
314 /// All rapier rigid-bodies created from the MJCF model. Index 0 is
315 /// reserved for the implicit world body but is never inserted into the
316 /// rapier `RigidBodySet`. Original MJCF body indices map 1:1 onto
317 /// entries with `is_intermediate == false`.
318 pub bodies: Vec<MjcfBody>,
319 /// All joints connecting one body to another.
320 pub joints: Vec<MjcfJoint>,
321 /// Extra joints created from `<equality>` constraints.
322 pub equality_joints: Vec<MjcfEqualityJoint>,
323 /// `<equality><joint>` couplings between two joints' coordinates.
324 pub joint_couplings: Vec<MjcfJointCoupling>,
325 /// Actuator bindings.
326 pub actuators: Vec<MjcfActuatorBinding>,
327 /// Sensor bindings.
328 pub sensors: Vec<MjcfSensorBinding>,
329 /// Keyframes preserved from the model.
330 pub keyframes: Vec<Keyframe>,
331 /// Layout that maps a keyframe's `qpos` / `qvel` arrays onto
332 /// [`Self::joints`] / [`Self::bodies`], in MuJoCo's generalized-coordinate
333 /// order. Drives [`MjcfRobotHandles::apply_keyframe`](super::MjcfRobotHandles::apply_keyframe).
334 pub qpos_dofs: Vec<MjcfQposDof>,
335 /// The `MjcfLoaderOptions::shift` the loader applied to every body pose.
336 /// Re-applied when a keyframe writes a free body's absolute world pose so
337 /// the floating base lands in the same frame as the rest of the model.
338 pub base_shift: Pose,
339 /// Resolved gravity vector (`<option gravity>`).
340 pub gravity: Vector,
341 /// Map from MJCF body name to index in [`Self::bodies`].
342 pub body_name_to_idx: HashMap<String, usize>,
343 /// Map from MJCF geom name to a `(body_index, collider_index_in_body)` pair.
344 pub geom_name_to_collider: HashMap<String, (usize, usize)>,
345 /// Map from MJCF joint name to index in [`Self::joints`].
346 pub joint_name_to_idx: HashMap<String, usize>,
347 /// `<contact><exclude>` entries (preserved verbatim).
348 pub contact_excludes: Vec<MjcfContactExclude>,
349 /// `<contact><pair>` entries (preserved verbatim).
350 pub contact_pairs: Vec<MjcfContactPair>,
351}