Mujoco KDL Wrapper  0.5.1
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
mj_kdl_wrapper.hpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: MIT
2 * Copyright (c) 2026 Vamsi Kalagaturu
3 * See LICENSE for details. */
4
5#pragma once
6
7#include <mujoco/mujoco.h>
8#include <kdl/chain.hpp>
9#include <kdl/frames.hpp>
10#include <kdl/jntarray.hpp>
11#include <array>
12#include <cmath>
13#include <cstdint>
14#include <cstdio>
15#include <cstring>
16#include <deque>
17#include <functional>
18#include <memory>
19#include <sstream>
20#include <string>
21#include <utility>
22#include <vector>
23#include <chrono>
24
25namespace mj_kdl {
26
27/**
28 * @ingroup grp_logging
29 * Least severe message printed: INFO prints everything (default), WARN warnings and errors,
30 * ERROR errors only, NONE nothing.
31 */
32enum class LogLevel { INFO = 0, WARN = 1, ERROR = 2, NONE = 3 };
33
34/** @ingroup grp_logging
35 * Library-wide log threshold (inline so one shared instance across all TUs). */
37
38/** @ingroup grp_logging
39 * Set the library-wide log verbosity. */
40inline void set_log_level(LogLevel level) { g_log_level = level; }
41/** @ingroup grp_logging
42 * Get the library-wide log verbosity. */
44
45} // namespace mj_kdl
46
47// Log through the library's threshold; expr may stream: MJ_LOG_INFO("count=" << n).
48#define MJ_FILENAME_ (::strrchr(__FILE__, '/') ? ::strrchr(__FILE__, '/') + 1 : __FILE__)
49
50#define MJ_LOG_(lvl_enum, color, label, expr) \
51 do { \
52 if (::mj_kdl::LogLevel::lvl_enum >= ::mj_kdl::g_log_level) { \
53 std::ostringstream _mj_oss; \
54 _mj_oss << expr; /* NOLINT(bugprone-macro-parentheses) */ \
55 std::fprintf( \
56 stderr, \
57 color "[mj_kdl " label "] %s:%d (%s): %s\033[0m\n", \
58 MJ_FILENAME_, \
59 __LINE__, \
60 __func__, \
61 _mj_oss.str().c_str() \
62 ); \
63 } \
64 } while (0)
65
66#define MJ_LOG_INFO(expr) MJ_LOG_(INFO, "", "INFO ", expr)
67#define MJ_LOG_WARN(expr) MJ_LOG_(WARN, "\033[33m", "WARN ", expr)
68#define MJ_LOG_ERROR(expr) MJ_LOG_(ERROR, "\033[31m", "ERROR", expr)
69
70namespace mj_kdl {
71
72/**
73 * @ingroup grp_types
74 * Result of a call that can fail on its input: empty error = success. The error is the same
75 * text the call logs.
76 */
77struct Status
78{
79 std::string error;
80
81 explicit operator bool() const { return error.empty(); }
82};
83
84/**
85 * @ingroup grp_types
86 * Kind of element an AttachTarget references in the accumulated scene spec.
87 * World selects the worldbody (the default); Body, Site, and Frame each look
88 * up a named element of the corresponding type.
89 */
90enum class AttachKind { World, Body, Site, Frame };
91
92/**
93 * @ingroup grp_types
94 * Where to attach a robot, object, or attachment in the accumulated scene spec.
95 * Tagged so exactly one alternative is encoded; defaulting to World keeps
96 * callers that omit attach_to anchored to the worldbody.
97 * For Site, the site's own pos/quat becomes the placement frame and the
98 * accompanying pos/quat are an additional offset on top of it (matches MJCF).
99 */
101{
103 std::string name; // ignored when kind == World
104};
105
106/**
107 * @ingroup grp_types
108 * Joint-space control mode. Each mode drives its own actuator, in its own actuator group:
109 * POSITION - jnt_pos_cmd to a position servo's ctrl.
110 * TORQUE - jnt_trq_cmd to a motor's ctrl.
111 * VELOCITY - jnt_vel_cmd to a velocity actuator's ctrl.
112 */
114
115/**
116 * @ingroup grp_types
117 * A control mode for RobotSpec::modes or AttachmentSpec::modes. joints empty = every joint the
118 * robot's or attachment's own MJCF actuates. kv is the VELOCITY actuator's gain [N m s/rad],
119 * required for VELOCITY.
120 */
122{
124 std::vector<std::string> joints;
125 double kv = 0.0;
126};
127
128/**
129 * @ingroup grp_types
130 * One link in an ordered attachment chain for a robot.
131 * An attachment is any MJCF body (end effector, mount, FT sensor, tool, additional arm
132 * on a mobile base, etc.) attached under a named element in the accumulated robot spec.
133 * Attachments are applied in declaration order; attach_to may reference any body, site,
134 * or frame present after all prior attachments have been applied.
135 * modes: control modes for its own joints, in groups of their own so it switches apart from its
136 * robot; empty by default, unlike RobotSpec::modes.
137 */
139{
140 std::string mjcf_path; // MJCF file for this attachment
141 AttachTarget attach_to; // body, site or frame of root or prior attachment
142 std::string prefix; // element name prefix (avoids name conflicts)
143 double pos[3] = { 0, 0, 0 }; // position offset [m]
144 double quat[4] = { 0, 0, 0, 1 }; // orientation offset [x, y, z, w]
145 std::vector<CtrlModeSpec> modes; // {} = native mode only
146
147 /* Contact exclusion pairs registered by build_scene(). */
148 std::vector<std::pair<std::string, std::string>> contact_exclusions; // (body1, body2) pairs
149};
150
151/**
152 * @ingroup grp_types
153 * One robot in a scene: a root MJCF (arm, mobile base, ...) with an ordered attachment
154 * chain and a placement target.
155 *
156 * attachments is applied in order by build_scene(): each entry's
157 * attach_to may reference any body, site, or frame in the accumulated spec (root + all
158 * prior attachments). This naturally supports: fixed arm, arm+gripper, arm+mount+FT+
159 * gripper, mobile base, mobile manipulator (base root, arm as first attachment), etc.
160 *
161 * attach_to selects where the robot root is placed in the scene; the default is the
162 * worldbody. Set it to e.g. { AttachKind::Site, "table_mount" } to place the robot on
163 * a tabletop site exported by a prior scene object. pos/quat are offsets in the
164 * resolved parent frame.
165 *
166 * path is the root MJCF passed to build_scene(). prefix must be unique per robot
167 * in multi-robot scenes.
168 *
169 * modes lists the control modes the robot offers beyond the one its own actuators give
170 * (a `<position>` servo gives POSITION, a `<motor>` gives TORQUE); by default every robot also gets
171 * TORQUE. build_scene() adds one actuator per extra mode on each listed joint and puts each mode
172 * in its own actuator group, switched with set_control_mode(). These are the robot's own joints;
173 * an attachment's take modes through AttachmentSpec::modes. With no joint list, joints that
174 * cannot take modes are skipped. Actuator groups 1-30 are reserved for this: group
175 * 1 + 3 * owner + mode, where the owners are the robots in order and then every attachment with
176 * modes in order, ten at most. The robot's MJCF must not assign actuator groups itself.
177 */
179{
180 std::string path; // root MJCF path
181 std::string prefix; // element name prefix
182 AttachTarget attach_to; // placement parent (default: world)
183 double pos[3] = { 0, 0, 0 }; // offset in parent frame [m]
184 double quat[4] = { 0, 0, 0, 1 }; // orientation offset [x, y, z, w]
185 std::vector<AttachmentSpec> attachments; // ordered attachment chain; empty = none
186 std::vector<CtrlModeSpec> modes = { CtrlModeSpec{} }; // TORQUE; {} = native mode only
187};
188
189/** @ingroup grp_types
190 * Shape type for scene objects. Unspecified is the sentinel value;
191 * build_scene rejects a primitive SceneObject whose shape is Unspecified. */
193
194/**
195 * @ingroup grp_types
196 * Contact-friction dimensionality, matching MuJoCo's `condim` integer values.
197 * Tangential (3) - sliding friction only (default).
198 * Torsional (4) - +torsion about the contact normal.
199 * Rolling (6) - +torsion and +rolling resistance.
200 * Values 1 (frictionless) and 2 (1D friction) exist in MuJoCo but are
201 * uncommon; if needed, pass `static_cast<Condim>(1)` etc.
202 */
203enum class Condim : int { Tangential = 3, Torsional = 4, Rolling = 6 };
204
205/**
206 * @ingroup grp_types
207 * A free-floating or fixed rigid body to place in the scene.
208 *
209 * Names: an MJCF asset's elements keep their authored names, with prefix prepended when set;
210 * its root body is renamed to name. The same asset used twice needs distinct prefixes, or
211 * the build fails on the repeated names.
212 *
213 * size:
214 * BOX - half-extents (x, y, z)
215 * SPHERE - {radius, 0, 0}
216 * CYLINDER - {radius, half-length, 0}
217 * Ignored when mjcf_path is set.
218 *
219 * attach_to:
220 * Parent in the accumulated scene spec. Default is the worldbody. A child
221 * object must appear after its parent in SceneSpec::objects.
222 * MuJoCo constraint: a body that carries a freejoint must be a direct child
223 * of the worldbody. So a non-fixed primitive (fixed == false) and any
224 * mjcf_path asset whose root body owns a freejoint must use AttachKind::World.
225 * Fixed primitives and articulated subtrees (no freejoint on the root) may
226 * use any kind. mj_compile reports the violation if this rule is broken.
227 *
228 * pos:
229 * Offset in the resolved parent frame. For MJCF assets, this is the placement
230 * frame for the asset's first root body.
231 *
232 * fixed:
233 * If true the body is welded to its parent (no freejoint); useful for
234 * static obstacles or fixtures. If false, an MJCF asset whose root has no joint
235 * gets a freejoint named `<name>_free`.
236 *
237 * size, rgba, mass and friction start unset (NaN) and must be set explicitly
238 * by the caller; build_scene() fails on a primitive that leaves one unset.
239 * They are arbitrary visual/material/dynamic choices, not neutral identities,
240 * so the API refuses to invent placeholder values. A fixed primitive may leave
241 * mass unset.
242 *
243 * rgba / has_rgba:
244 * A primitive's colour, required. On an MJCF asset it is optional: with
245 * has_rgba set, every geom under the asset's root body takes it, so a scene
246 * can state the colour an object is drawn in without editing the asset.
247 */
249{
250 std::string name; // the object's root body takes this name
251 std::string mjcf_path; // optional MJCF asset; when set, shape/size/mass/friction are ignored
252 std::string prefix; // prepended to the asset's element names; empty = as authored
253 AttachTarget attach_to; // placement parent (default: world)
254 Shape shape = Shape::Unspecified; // required for primitives; rejected at build time if not set
255 // half-extents (BOX) / {radius, 0, 0} (SPHERE) / {radius, half-len, 0} (CYL)
256 double size[3] = { NAN, NAN, NAN };
257 double pos[3] = { 0.0, 0.0, 0.0 }; // offset in resolved parent frame [m]
258 double quat[4] = { 0.0, 0.0, 0.0, 1.0 }; // orientation offset [x, y, z, w]
259 float rgba[4] = { NAN, NAN, NAN, NAN }; // [r, g, b, a]; required for primitives
260 bool has_rgba = false; // rgba is set; recolours an asset's geoms
261 bool fixed = false;
262 double mass = NAN; // [kg]; required for non-fixed primitives
264 double friction[3] = { NAN, NAN, NAN }; // [slide, spin, roll]; required for primitives
265};
266
267/**
268 * @ingroup grp_types
269 * A frame to mark on a body of the assembled scene, as a MuJoCo site.
270 *
271 * Sites are what a model's own frames become: the scene states where they sit, and the
272 * runtime can then address and draw them. Added after every robot and object, so `body`
273 * may name anything the assembled scene holds. A site the asset already declares under
274 * this name is left alone -- the asset's own is authoritative.
275 */
277{
278 std::string body; // body to add the site to, by name
279 std::string name; // site name, unique within the scene
280 double pos[3] = { 0.0, 0.0, 0.0 }; // offset in the body frame [m]
281 double quat[4] = { 0.0, 0.0, 0.0, 1.0 }; // orientation in the body frame [x, y, z, w]
282};
283
284/**
285 * @ingroup grp_types
286 * A named fixed camera to add to the world body of the scene.
287 * After build_scene() the camera is accessible by name (mj_name2id(model, mjOBJ_CAMERA, name))
288 * and can be activated on a Viewer with use_camera(), or on a VideoRecorder through vr.cam.
289 *
290 * pos and fovy start unset (NaN): there is no neutral camera position or field of view, so
291 * the caller must specify both, and build_scene() fails otherwise. quat defaults to identity.
292 */
294{
295 std::string name;
296 std::string body; // anchor body; empty = worldbody
297 double pos[3] = { NAN, NAN, NAN }; // position in the anchor body's frame [m]
298 double quat[4] = { 0.0, 0.0, 0.0, 1.0 }; // orientation [x, y, z, w]
299 double fovy = NAN; // vertical field of view [degrees]
300};
301
302/** @ingroup grp_types
303 * Full scene description passed to build_scene().
304 * timestep (unset: NaN), add_floor, and add_skybox have no defaults: the caller must
305 * choose a physics step and an explicit yes/no for each decoration so the
306 * resulting scene is never silently misconfigured. gravity_z defaults to
307 * Earth gravity. */
309{
310 std::vector<RobotSpec> robots;
311 double timestep = NAN; // required; suggested 0.002 [s]
312 double gravity_z = -9.81; // Earth gravity [m/s^2]
313 bool add_floor; // required; checker groundplane geom
314 double floor_z = 0.0; // floor plane height in the world frame [m]
315 bool add_skybox; // required; gradient sky + directional light
316 std::vector<SceneObject> objects;
317 std::vector<SiteSpec> sites; // frames marked on the assembled scene's bodies
318 std::vector<CameraSpec> cameras; // static world cameras added to worldbody
319};
320
321/**
322 * @ingroup grp_types
323 * Logical force-torque sensor backed by MuJoCo's separate `<force>` and `<torque>`
324 * sensors. If force_sensor/torque_sensor are omitted, init_robot_from_mjcf()
325 * resolves "{name}_force" and "{name}_torque".
326 */
328{
329 std::string name; // logical wrapper name
330 std::string force_sensor; // MuJoCo <force> sensor name
331 std::string torque_sensor; // MuJoCo <torque> sensor name
332 std::string frame_site; // optional site that defines the sensor frame
333};
334
335/**
336 * @ingroup grp_types
337 * What a force-torque sensor measures. reset() assigns a fresh one, so every field here is reset.
338 *
339 * The wrench is the load on the sensor, as a physical F/T sensor reports it: a tool hanging below
340 * reads its weight pointing down. MuJoCo's `<force>`/`<torque>` sensors give the opposite (the
341 * parent's force on the child), so the reading is their negation.
342 */
344{
345 KDL::Wrench wrench = KDL::Wrench::Zero(); // updated by update(Env *)
346};
347
348/** @ingroup grp_types
349 * A resolved force-torque sensor: its names and addresses, plus the reading. */
351{
352 std::string name;
353 std::string force_sensor;
354 std::string torque_sensor;
355 std::string frame_site;
356
357 int force_adr = -1;
358 int torque_adr = -1;
360};
361
362/**
363 * @ingroup grp_types
364 * Optional tool/end-effector description used while building the KDL chain.
365 *
366 * tool_body names the root of the attached tool subtree whose mass/inertia is
367 * lumped into the arm dynamics, once, at the tool's pose at init (a 2F-85's
368 * finger opening then is what the chain keeps). tcp_site names an authored MuJoCo site that
369 * becomes the KDL terminal frame for FK/IK (takes priority when set). When
370 * the model has no suitable site, tcp_frame provides an equivalent manual
371 * transform expressed in the tip body's local frame.
372 * For the prefixed Robotiq 2F-85 this is {"g_base_mount", "g_pinch"}: the mount carries mass too.
373 */
375{
376 std::string tool_body;
377 std::string tcp_site; // MuJoCo site name (takes priority)
378 KDL::Frame tcp_frame = KDL::Frame::Identity(); // manual TCP in tip frame (fallback)
379 std::vector<ForceTorqueSensorSpec> ft_sensors;
380};
381
382
383/**
384 * @ingroup grp_types
385 * A robot's control ports. Sized once by init_robot_*(); reset() rewrites their values in place,
386 * so a pointer to an element stays valid until the robot is re-initialised or destroyed.
387 */
389{
391 std::vector<double> jnt_pos_msr; // [rad] measured joint positions (update())
392 std::vector<double> jnt_vel_msr; // [rad/s] measured joint velocities (update())
393 std::vector<double> jnt_trq_msr; // [Nm] active mode's qfrc_actuator (update())
394 std::vector<double> jnt_pos_cmd; // [rad] position setpoints (POSITION)
395 std::vector<double> jnt_vel_cmd; // [rad/s] velocity setpoints (VELOCITY)
396 std::vector<double> jnt_trq_cmd; // [Nm] torque commands (TORQUE)
397 std::vector<uint8_t> jnt_saturated; // 1 if the command was clamped to ctrlrange (update())
398};
399
400struct Env;
401struct RobotInternals;
402
403/**
404 * @ingroup grp_types
405 * One KDL-tracked articulation in an Env. init_robot_from_mjcf() / init_robot_from_chain()
406 * configure it and register it with the Env, which then reads, commands and resets it.
407 * Registered by address, so it is neither copied nor moved.
408 */
410{
411 /* Configuration - set by init_robot_from_mjcf() / init_robot_from_chain(). */
412 mjModel *model = nullptr; // the Env's; updated on a rebuild
413 mjData *data = nullptr;
416 bool has_tcp_frame = false;
417 std::string tcp_site;
418 int n_joints = 0;
419 std::vector<std::string> joint_names;
420 std::vector<std::pair<double, double>> joint_limits; // [lo, hi]; +-inf for unlimited joints
421 std::vector<ForceTorqueSensor> ft_sensors;
422 bool paused = false; // step() leaves physics alone
423
426 Robot(const Robot &) = delete;
427 Robot &operator=(const Robot &) = delete;
428
429 std::unique_ptr<RobotInternals> _impl; // internal
430};
431
432/**
433 * @ingroup grp_viewer
434 * The simulate UI window of an Env. Opened by open_viewer(); closed by cleanup(Env *).
435 */
436struct Viewer
437{
438 mjvCamera cam{};
439 /* Real-time factor for pace_realtime(): 1.0 = real time, 0.5 = half speed, 0.0 = uncapped.
440 * The ',' and '.' keys adjust it. */
441 double realtime_factor = 1.0;
442 double fps = 0.0; // the window's frame rate, read-only, set by step()
443 std::chrono::steady_clock::time_point _tick_t{}; // internal: pacing
444 void *_sim_ui = nullptr; // internal: SimUiState*, open
445};
446
447/**
448 * @ingroup grp_recorder
449 * Standard output resolution presets for init_video_recorder().
450 * Each maps to a 16:9 frame size at the named quality level.
451 */
452enum class VideoResolution {
453 R360p = 360, // 640 x 360
454 R480p = 480, // 854 x 480
455 R720p = 720, // 1280 x 720
456 R1080p = 1080, // 1920 x 1080
457 R2K = 1440, // 2560 x 1440
458 R4K = 2160, // 3840 x 2160
459};
460
461/**
462 * @ingroup grp_recorder
463 * Headless video recorder. Renders frames to an EGL offscreen buffer and
464 * pipes raw RGB data to an ffmpeg process, producing an H.264 MP4 without a
465 * display server or GLFW window.
466 *
467 * Each recorder renders on a thread of its own: record_frame() and render_rgb() take a
468 * snapshot of the scene on the caller's thread and return, so a control loop pays for the
469 * snapshot, not the render. cam and opt are read at the snapshot.
470 *
471 * Requirements: EGL (libegl-dev) and ffmpeg available in PATH.
472 *
473 * Typical usage:
474 *
475 * VideoRecorder vr;
476 * init_video_recorder(&vr, env.model, "sim.mp4", VideoResolution::R1080p);
477 * vr.cam.azimuth = 135; vr.cam.elevation = -20; vr.cam.distance = 2.5;
478 *
479 * for (int i = 0; i < steps; ++i) {
480 * step(&env);
481 * record_frame(&vr, &env);
482 * }
483 *
484 * cleanup(&vr);
485 */
487{
488 mjvCamera cam{}; // camera configuration; modify freely between frames
489 mjvOption opt{}; // rendering options; modify freely between frames
490 void *_impl = nullptr; // opaque EGL + ffmpeg state
491};
492
493/** @ingroup grp_env
494 * Options controlling an environment reset. */
496{
497 int keyframe = 0; // keyframe index to use when available
498 bool use_keyframe = true; // fall back to mj_resetData when false or invalid
499};
500
501/** @ingroup grp_env
502 * Information returned by reset(). */
504{
505 bool used_keyframe = false;
506 int keyframe = -1;
507};
508
509/** @ingroup grp_env
510 * Runtime context passed to Env::on_reset, which runs after everything is re-seeded: it may
511 * prime commands, and a robot it moves needs its commands set too. */
513{
514 Env *env = nullptr;
515 mjModel *model = nullptr;
516 mjData *data = nullptr;
517 const ResetOptions *options = nullptr;
518 ResetInfo *info = nullptr;
519};
520
521using ResetHook = std::function<void(ResetContext *)>;
522
523/* Scene slots: each derives from what it reads or commands, which reset() assigns afresh. */
524
525/** @ingroup grp_scene
526 * What a scene joint slot reads. */
528{
529 double position = 0.0;
530 double velocity = 0.0;
531 std::uint64_t seq = 0; // bumped by each read
532};
533
534/** @ingroup grp_scene
535 * A joint the world model reads that no Robot samples: a gripper mimic, an object hinge. */
537{
538 std::string name;
539 int qpos_adr = -1;
540 int dof_adr = -1;
541};
542
543/** @ingroup grp_scene
544 * What a free-body slot reads. */
546{
548 std::uint64_t seq = 0; // bumped by each read
549};
550
551/** @ingroup grp_scene
552 * A free body: its freejoint's qpos is the body pose in the world frame. */
554{
555 std::string name;
556 int qpos_adr = -1;
557};
558
559/** @ingroup grp_scene
560 * What a wrench slot commands. */
565
566/** @ingroup grp_scene
567 * A body a controller pushes: the wrench is applied as xfrc_applied, at the body's centre
568 * of mass. */
570{
571 std::string name;
572 int body_id = -1;
573};
574
575/** @ingroup grp_scene
576 * What an actuator slot commands. */
578{
579 double command = 0.0; // in ctrl units
580 bool saturated = false; // command was clamped to ctrlrange (update(Env *))
581};
582
583/** @ingroup grp_scene
584 * An actuator a controller commands directly, outside the KDL chain: a gripper drive. */
586{
587 std::string name;
588 int ctrl_id = -1;
589};
590
591/**
592 * @ingroup grp_scene
593 * Every non-robot primitive of the scene, read and applied by update(Env *).
594 * Deques: a pointer to a slot stays valid as more are bound.
595 */
597{
598 const mjModel *model = nullptr;
599 std::deque<SceneJointSlot> joints;
600 std::deque<SceneFreeBodySlot> free_bodies;
601 std::deque<SceneWrenchSlot> wrenches;
602 std::deque<SceneActuatorSlot> actuators;
603};
604
605struct EnvInternals;
606
607/** @ingroup grp_env
608 * The simulation: the compiled scene and everything that reads, commands or shows it.
609 * init_env() builds it; step(), update() and reset() drive it; cleanup(Env *) frees it.
610 * Robots are borrowed: registered by init_robot_from_mjcf() / init_robot_from_chain(), never
611 * deleted here. Not copied or moved (robots and slots point into it).
612 */
613struct Env
614{
616 mjModel *model = nullptr;
617 mjData *data = nullptr;
618 std::vector<Robot *> robots;
619 SceneState scene; // bind slots with bind_scene_*(&env.scene, ...)
620 Viewer viewer; // closed until open_viewer()
622 // Takes each compiled pair; returns the pair to run on, which the Env never frees.
623 std::function<std::pair<mjModel *, mjData *>(mjModel *, mjData *)> adopt;
624
627 Env(const Env &) = delete;
628 Env &operator=(const Env &) = delete;
629
630 std::unique_ptr<EnvInternals> _impl; // internal
631};
632
633/**
634 * @ingroup grp_scene
635 * Save a model to an MJCF XML file, including runtime changes to its real-valued fields.
636 * Works for any live model from build_scene() or init_env() (adopted ones too), and for the
637 * last model loaded with mj_loadXML. Typical use: build a combined scene once, save it, reload
638 * it later.
639 * @param model Model to save.
640 * @param path Output path for the MJCF XML file.
641 * @return an empty Status on success, else the error.
642 */
643Status save_model_xml(const mjModel *model, const char *path);
644
645/**
646 * @ingroup grp_robot
647 * Build KDL chain from a compiled MuJoCo model and optional tool/TCP metadata.
648 *
649 * If tool->tcp_site is set, that authored site becomes the KDL terminal frame
650 * for FK/IK. The joint count and MuJoCo joint/actuator maps still cover only
651 * the controllable joints from base_body to tip_body.
652 * Pass tool = nullptr (the default) for an arm with no attached tool.
653 * prefix is prepended to every name the call resolves: base_body, tip_body, the tool body,
654 * TCP site and F/T sensor names, e.g. ("base_link", "bracelet_link", "r2_") is the second arm.
655 * Registers r with env, which reads, commands and resets it from then on; on failure r is left
656 * as it was.
657 */
659 Robot *r,
660 Env *env,
661 const char *base_body,
662 const char *tip_body,
663 const char *prefix = "",
664 const ToolFrameSpec *tool = nullptr
665);
666
667/**
668 * @ingroup grp_robot
669 * Adopt an externally built KDL chain and wire it to a compiled MuJoCo model.
670 *
671 * Same as init_robot_from_mjcf(), except the chain is supplied rather than derived
672 * from the model: use this when the chain comes from the scene description that also
673 * produced the MJCF, so the solvers compute with the authored dynamics. The chain is
674 * taken as given - no tool inertia is lumped onto it, since such a chain already
675 * carries its tool as explicit segments.
676 *
677 * joint_names lists the MuJoCo joint names in KDL chain order, one per chain joint;
678 * they drive the same qpos/dof/ctrl index maps init_robot_from_mjcf() builds. prefix is
679 * prepended to each joint name and F/T sensor name. tool is used only to resolve FT sensors;
680 * tool->tool_body and tool->tcp_site are ignored. Registers r with env, as init_robot_from_mjcf().
681 */
683 Robot *r,
684 Env *env,
685 const KDL::Chain &chain,
686 const std::vector<std::string> &joint_names,
687 const char *prefix = "",
688 const ToolFrameSpec *tool = nullptr
689);
690
691/**
692 * @ingroup grp_robot
693 * Per-joint torque/force saturation limit in KDL joint order for the actuator
694 * of the robot's `ctrl_mode`: max(|lo|, |hi|) of its `forcerange` times |gear|,
695 * and in TORQUE mode at most its `ctrlrange` times |gear|. Joints with no
696 * actuator for that mode or an unlimited one fall back to `fallback`.
697 *
698 * @param r Initialized robot (init_robot_from_mjcf() already called).
699 * @param fallback Bound used for joints without a force-limited actuator;
700 * default is a large, effectively non-limiting value.
701 * @return One entry per KDL joint, same order as r->joint_names.
702 */
703std::vector<double> joint_force_limits(const Robot *r, double fallback = 1e6);
704
705/**
706 * @ingroup grp_scene
707 * Build a MuJoCo scene from one or more robots using the MuJoCo spec API.
708 * This is the primary scene-building function.
709 *
710 * For each RobotSpec: mj_parseXML loads the root MJCF, then mjs_attach applies
711 * each entry in RobotSpec::attachments in order (mount, sensor, gripper, etc.),
712 * and mjs_attach places the complete robot spec at the given position. A single
713 * mj_compile produces the final model -- no intermediate XML files are written.
714 *
715 * @param[out] out_model Newly allocated MuJoCo model; caller frees via destroy_scene().
716 * @param[out] out_data Newly allocated MuJoCo data; caller frees via destroy_scene().
717 * @param[in] spec Scene description: robots (with attachment chains), objects,
718 * sites, cameras, timestep, gravity, floor, skybox.
719 * @return an empty Status on success, else the error.
720 */
721Status build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec);
722
723/**
724 * @ingroup grp_scene
725 * Free a model/data pair allocated by any scene-building function.
726 * @param[in] model Model to free (may be null).
727 * @param[in] data Data to free (may be null).
728 */
729void destroy_scene(mjModel *model, mjData *data);
730
731/**
732 * @ingroup grp_env
733 * Build the scene from spec into env, which owns the model/data until cleanup(Env *).
734 * Env::on_reset and Env::adopt set beforehand are kept.
735 */
736Status init_env(Env *env, const SceneSpec *spec);
737
738/**
739 * @ingroup grp_env
740 * Reset everything env holds: MuJoCo data to the keyframe (or the model default); every
741 * registered robot's ports (holding the reset pose, a requested ctrl_mode kept) and F/T readings,
742 * and every scene slot, re-seeded; then Env::on_reset; then measurements read from the result.
743 * The simulate UI's reset button does the same.
744 */
745ResetInfo reset(Env *env, const ResetOptions *options = nullptr);
746
747/**
748 * @ingroup grp_viewer
749 * Open the simulate UI (panels, physics controls) on env, rendered on a background thread;
750 * step() then drives physics, pause, perturbation and recording. Linux (X11 / Wayland) only.
751 * @return an error when there is no display or the window cannot be created.
752 */
753Status open_viewer(Env *env, const char *title = "MuJoCo");
754
755/**
756 * @ingroup grp_viewer
757 * Reset the viewer's user-scene geom count to 0.
758 * Call once per frame before appending trace segments with add_trace_segment().
759 * No-op while the viewer is closed (e.g. headless).
760 * @param[in,out] v An Env's viewer.
761 */
763
764/**
765 * @ingroup grp_viewer
766 * Append a single line segment to the viewer's user scene. Thread-safe.
767 * The render thread merges the user scene into each frame automatically.
768 * Silently drops the segment once the user-scene geom buffer is full.
769 * No-op while the viewer is closed (e.g. headless).
770 * @param[in,out] v An Env's viewer.
771 * @param[in] a Segment start point (world frame) [m].
772 * @param[in] b Segment end point (world frame) [m].
773 * @param[in] rgba Optional [r, g, b, a] colour; nullptr -> warm orange.
774 */
776 Viewer *v,
777 const KDL::Vector &a,
778 const KDL::Vector &b,
779 const float rgba[4] = nullptr
780);
781
782/**
783 * @ingroup grp_viewer
784 * Append a world-space arrow to the viewer's user scene. Thread-safe.
785 * Shares the user scene with add_trace_segment(), so clear_trace() clears both
786 * and one call per frame is enough for either.
787 * dir need not be normalised; a zero-length dir draws nothing.
788 * Silently drops the arrow once the user-scene geom buffer is full.
789 * No-op while the viewer is closed (e.g. headless).
790 * @param[in,out] v An Env's viewer.
791 * @param[in] from Arrow tail (world frame) [m].
792 * @param[in] dir Direction the arrow points; normalised internally.
793 * @param[in] length Arrow length [m].
794 * @param[in] rgba Optional [r, g, b, a] colour; nullptr -> warm orange.
795 */
797 Viewer *v,
798 const KDL::Vector &from,
799 const KDL::Vector &dir,
800 double length,
801 const float rgba[4] = nullptr
802);
803
804/**
805 * @ingroup grp_robot
806 * Unregister r from its Env and clear it. The Env keeps running without it.
807 */
808void cleanup(Robot *r);
809
810/**
811 * @ingroup grp_env
812 * Close the viewer, free the model/data and forget the robots (which are not deleted).
813 */
814void cleanup(Env *env);
815
816/**
817 * @ingroup grp_recorder
818 * Initialise a headless EGL video recorder.
819 * Creates an EGL context and launches an ffmpeg process (H.264/MP4) via a pipe. The render
820 * context is made from the Env at its first frame and remade after the Env's model is rebuilt.
821 *
822 * @param vr VideoRecorder to initialise; freed by cleanup(VideoRecorder*).
823 * @param model MuJoCo model whose default free camera vr->cam starts from.
824 * @param out_path Output MP4 path (e.g. "sim.mp4").
825 * @param width Frame width in pixels (default 1280).
826 * @param height Frame height in pixels (default 720).
827 * @param fps Playback frame rate (default 60).
828 * @return an error if EGL init or ffmpeg launch fails.
829 */
831 VideoRecorder *vr,
832 mjModel *model,
833 const char *out_path,
834 int width = 1280,
835 int height = 720,
836 int fps = 60
837);
838
839/**
840 * @ingroup grp_recorder
841 * Convenience overload: initialise a VideoRecorder using a named resolution preset.
842 * Frame width is derived from the preset at 16:9 aspect ratio.
843 *
844 * @param vr VideoRecorder to initialise.
845 * @param model MuJoCo model.
846 * @param out_path Output MP4 path.
847 * @param resolution VideoResolution preset (e.g. VideoResolution::R1080p).
848 * @param fps Playback frame rate (default 60).
849 * @return an empty Status on success, else the error.
850 */
852 VideoRecorder *vr,
853 mjModel *model,
854 const char *out_path,
855 VideoResolution resolution,
856 int fps = 60
857);
858
859/**
860 * @ingroup grp_recorder
861 * Snapshot env's current state as the next video frame; it is rendered and encoded on the
862 * recorder's thread. Waits only while the previous frame has not been picked up.
863 *
864 * @param vr VideoRecorder initialised by init_video_recorder().
865 * @param env Env to render.
866 * @return false once rendering or the pipe to ffmpeg has failed; every frame is recorded.
867 */
869
870/**
871 * @ingroup grp_recorder
872 * Initialise offscreen rendering only: EGL context, no ffmpeg process and no output file.
873 * Use with render_rgb() to grab frames; the camera is vr->cam, as for a recording, and
874 * cleanup(VideoRecorder*) frees it. An open vr is freed first.
875 *
876 * @param vr VideoRecorder to initialise; freed by cleanup(VideoRecorder*).
877 * @param model MuJoCo model whose default free camera vr->cam starts from.
878 * @param width Frame width in pixels.
879 * @param height Frame height in pixels.
880 * @return an error if EGL init fails.
881 */
882Status init_offscreen(VideoRecorder *vr, mjModel *model, int width, int height);
883
884/**
885 * @ingroup grp_recorder
886 * Render env's current state into a caller-owned top-down RGB8 buffer
887 * of width*height*3 bytes.
888 *
889 * @param vr VideoRecorder initialised by init_offscreen() or init_video_recorder().
890 * @param env Env to render.
891 * @param out Destination buffer, width*height*3 bytes.
892 * @return true on success.
893 */
894bool render_rgb(VideoRecorder *vr, Env *env, std::uint8_t *out);
895
896/**
897 * @ingroup grp_recorder
898 * Snapshot env's current state and return; done gets the top-down RGB8 frame on the recorder's
899 * thread, valid only during the call. Waits only while the previous frame has not been picked up.
900 *
901 * @param vr VideoRecorder initialised by init_offscreen() or init_video_recorder().
902 * @param env Env to render.
903 * @param done Receives width*height*3 bytes.
904 * @return false once rendering has failed.
905 */
906bool render_rgb(VideoRecorder *vr, Env *env, std::function<void(const std::uint8_t *rgb)> done);
907
908/**
909 * @ingroup grp_recorder
910 * Flush the ffmpeg pipe, finalise the MP4, and release all EGL resources.
911 * After this call vr->_impl is null and the VideoRecorder may be discarded.
912 *
913 * @param vr VideoRecorder to tear down.
914 */
916
917/**
918 * @ingroup grp_env
919 * Advance env one timestep: mj_step2() then mj_step1() (MuJoCo's split mj_step()). Afterwards
920 * joint state, body frames and position/velocity sensors all describe the new state; force and
921 * acceleration sensors describe the step just taken. With the viewer open it also honours its
922 * pause, perturbation and recording, and does nothing while every robot is paused.
923 * @return false once the viewer window is closed; always true headless.
924 */
925bool step(Env *env);
926
927/**
928 * @ingroup grp_viewer
929 * Sleep out this step's share of wall time at the viewer's real-time factor. step() never
930 * sleeps; a loop that paces itself reads Viewer::realtime_factor instead. No-op headless.
931 */
933
934/**
935 * @ingroup grp_viewer
936 * Returns true while the viewer window is open.
937 * @param[in] v An Env's viewer.
938 */
939bool is_running(const Viewer *v);
940
941/**
942 * @ingroup grp_viewer
943 * Whether a key is currently held down in the viewer's window.
944 *
945 * The simulate UI opened by open_viewer() owns its GLFW window on the
946 * render thread, so a caller driving physics on its own thread must not call
947 * glfwGetKey() itself. This reads the key state that the UI's own key callback
948 * records, which is safe from any thread.
949 *
950 * Keys the UI consumes for itself (',' and '.' for the speed control) are
951 * reported like any other.
952 *
953 * @param[in] v Viewer, or nullptr.
954 * @param[in] glfw_key A GLFW key code, e.g. GLFW_KEY_UP.
955 * @return true while the key is held; false for a nullptr or headless viewer,
956 * or an out-of-range key code.
957 */
958bool key_pressed(const Viewer *v, int glfw_key);
959
960/**
961 * @ingroup grp_viewer
962 * Claim a key for the caller, so the simulate UI never acts on it.
963 *
964 * The UI binds keys of its own: the left and right arrows scrub the history,
965 * escape restores the free camera, space pauses. A caller
966 * that drives a robot with those keys would otherwise fight the UI for them.
967 * A captured key is still reported by key_pressed(); it is only withheld from
968 * the UI's own handler.
969 *
970 * @param[in,out] v An Env's viewer, or nullptr.
971 * @param[in] glfw_key A GLFW key code, e.g. GLFW_KEY_LEFT.
972 * @param[in] capture true to claim the key, false to give it back.
973 */
974void capture_key(Viewer *v, int glfw_key, bool capture = true);
975
976/**
977 * @ingroup grp_env
978 * One control cycle for everything env holds: read, then apply.
979 * Robots: qpos -> jnt_pos_msr, qvel -> jnt_vel_msr, qfrc_actuator -> jnt_trq_msr (only the active
980 * mode's actuators produce force, so this is the drive torque in every mode), F/T wrenches; then
981 * the active mode's command to its actuators' ctrl (gear applied, ctrlrange clamped). If
982 * ctrl_mode was changed directly, the switch runs first (set_control_mode()). Outside TORQUE,
983 * gravity on the bodies a robot's joints carry is compensated (body_gravcomp), so a servo holds
984 * its setpoint; in TORQUE the command carries gravity itself.
985 * Scene slots: joints and free bodies sampled from qpos/qvel; wrenches into xfrc_applied (zeros
986 * included), actuator commands into ctrl.
987 */
988void update(Env *env);
989
990/**
991 * @ingroup grp_robot
992 * Switch the robot to mode: seed the new actuators so nothing jumps, enable their group,
993 * disable the robot's other mode groups. @return an error if the robot has no actuator for mode.
994 */
996
997/**
998 * @ingroup grp_robot
999 * The same switch for a robot driven outside a Robot chain (e.g. through env.scene):
1000 * robot is its index in SceneSpec::robots. @return an error if it has no actuators for mode.
1001 */
1002Status set_control_mode(Env *env, int robot, CtrlMode mode);
1003
1004/**
1005 * @ingroup grp_scene
1006 * Resolve a scalar joint by name and keep a slot for it.
1007 * Rejects an unknown name, a free or ball joint, and a name already bound.
1008 * @return the slot, or nullptr on failure. The address stays valid across further binds.
1009 */
1010SceneJointSlot *bind_scene_joint(SceneState *s, const char *joint_name);
1011
1012/**
1013 * @ingroup grp_scene
1014 * Resolve a free-floating body by name and keep a slot for its pose.
1015 * Rejects an unknown name, a body that owns no mjJNT_FREE joint, and a name already bound.
1016 * @return the slot, or nullptr on failure. The address stays valid across further binds.
1017 */
1019
1020/**
1021 * @ingroup grp_scene
1022 * Resolve a body by name and keep a slot for the wrench applied to it.
1023 * Rejects an unknown name and a name already bound.
1024 * @return the slot, or nullptr on failure. The address stays valid across further binds.
1025 */
1027
1028/**
1029 * @ingroup grp_scene
1030 * Resolve the actuator named, or the one driving the joint of that name, and keep a slot for
1031 * its command. Rejects a name that reaches no actuator, and a name already bound.
1032 * @return the slot, or nullptr on failure. The address stays valid across further binds.
1033 */
1035
1036/**
1037 * @ingroup grp_robot
1038 * Write KDL joint positions into MuJoCo qpos (KDL chain order -> MuJoCo addresses). Frames
1039 * read afterwards follow the new positions.
1040 * @param[in,out] r Registered robot.
1041 * @param[in] q Joint positions in KDL chain order; size must equal r->n_joints.
1042 */
1044
1045/**
1046 * @ingroup grp_env
1047 * Teleport a free-floating body to a world-frame position and optionally a
1048 * world-frame orientation, then zero its velocity.
1049 * body_name must identify a body that owns a mjJNT_FREE joint.
1050 * quat is [x, y, z, w] like every quaternion in this API; nullptr sets identity.
1051 */
1053 Env *env,
1054 const char *body_name,
1055 const double pos[3],
1056 const double *quat = nullptr
1057);
1058
1059/**
1060 * @ingroup grp_scene
1061 * Append obj to env->spec.objects and rebuild. Time, and qpos/qvel/act/ctrl of every joint and
1062 * actuator whose name survives, carry over; robots, scene slots, recorders and the viewer follow
1063 * the new model; a slot whose name is gone is unbound and skipped.
1064 * @return an error on failure (a robot or F/T sensor that no longer resolves), env unchanged.
1065 */
1067
1068/**
1069 * @ingroup grp_scene
1070 * Remove the named object from env->spec.objects and rebuild, as scene_add_object() does.
1071 * @return an error if name is not found or the rebuild fails, env unchanged.
1072 */
1073Status scene_remove_object(Env *env, const std::string &name);
1074
1075/**
1076 * @ingroup grp_scene
1077 * Read a named MuJoCo site as a world-frame KDL frame. Recomputes the kinematics only when the
1078 * state has changed since they were last computed (a direct qpos write included).
1079 */
1080bool get_site_frame(Env *env, const char *site_name, KDL::Frame *out);
1081
1082/**
1083 * @ingroup grp_scene
1084 * Read a named MuJoCo body as a world-frame KDL frame, as get_site_frame() does.
1085 */
1086bool get_body_frame(Env *env, const char *body_name, KDL::Frame *out);
1087
1088/**
1089 * @ingroup grp_viewer
1090 * Switch the viewer to a named fixed camera defined in the model.
1091 * Pass nullptr or an empty string to return to the free camera.
1092 * @return true if the camera name was found; false if not found (viewer unchanged).
1093 */
1094bool use_camera(Viewer *v, const mjModel *model, const char *name);
1095
1096/**
1097 * @ingroup grp_viewer
1098 * Configure the viewer's free orbit camera.
1099 */
1101 Viewer *v,
1102 double distance,
1103 double azimuth,
1104 double elevation,
1105 const std::array<double, 3> &lookat
1106);
1107
1108} // namespace mj_kdl
static Frame Identity()
static Wrench Zero()
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
void set_body_pose(Env *env, const char *body_name, const double pos[3], const double *quat=nullptr)
Status init_env(Env *env, const SceneSpec *spec)
void update(Env *env)
bool step(Env *env)
void set_log_level(LogLevel level)
LogLevel get_log_level()
LogLevel g_log_level
Status init_offscreen(VideoRecorder *vr, mjModel *model, int width, int height)
Status init_video_recorder(VideoRecorder *vr, mjModel *model, const char *out_path, int width=1280, int height=720, int fps=60)
bool record_frame(VideoRecorder *vr, Env *env)
bool render_rgb(VideoRecorder *vr, Env *env, std::uint8_t *out)
Status set_control_mode(Robot *r, CtrlMode mode)
Status init_robot_from_mjcf(Robot *r, Env *env, const char *base_body, const char *tip_body, const char *prefix="", const ToolFrameSpec *tool=nullptr)
std::vector< double > joint_force_limits(const Robot *r, double fallback=1e6)
void set_joint_pos(Robot *r, const KDL::JntArray &q)
void cleanup(Robot *r)
Status init_robot_from_chain(Robot *r, Env *env, const KDL::Chain &chain, const std::vector< std::string > &joint_names, const char *prefix="", const ToolFrameSpec *tool=nullptr)
bool get_body_frame(Env *env, const char *body_name, KDL::Frame *out)
Status build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
Status scene_remove_object(Env *env, const std::string &name)
SceneWrenchSlot * bind_scene_wrench(SceneState *s, const char *body_name)
bool get_site_frame(Env *env, const char *site_name, KDL::Frame *out)
void destroy_scene(mjModel *model, mjData *data)
SceneActuatorSlot * bind_scene_actuator(SceneState *s, const char *name)
Status scene_add_object(Env *env, const SceneObject &obj)
SceneFreeBodySlot * bind_scene_free_body(SceneState *s, const char *body_name)
Status save_model_xml(const mjModel *model, const char *path)
SceneJointSlot * bind_scene_joint(SceneState *s, const char *joint_name)
Status open_viewer(Env *env, const char *title="MuJoCo")
void capture_key(Viewer *v, int glfw_key, bool capture=true)
bool use_camera(Viewer *v, const mjModel *model, const char *name)
void pace_realtime(Env *env)
void add_overlay_arrow(Viewer *v, const KDL::Vector &from, const KDL::Vector &dir, double length, const float rgba[4]=nullptr)
void clear_trace(Viewer *v)
bool key_pressed(const Viewer *v, int glfw_key)
void add_trace_segment(Viewer *v, const KDL::Vector &a, const KDL::Vector &b, const float rgba[4]=nullptr)
void set_free_camera(Viewer *v, double distance, double azimuth, double elevation, const std::array< double, 3 > &lookat)
bool is_running(const Viewer *v)
std::function< void(ResetContext *)> ResetHook
std::vector< CtrlModeSpec > modes
std::vector< std::pair< std::string, std::string > > contact_exclusions
std::vector< std::string > joints
std::function< std::pair< mjModel *, mjData * >(mjModel *, mjData *)> adopt
ResetHook on_reset
std::unique_ptr< EnvInternals > _impl
std::vector< Robot * > robots
SceneState scene
Env & operator=(const Env &)=delete
Env(const Env &)=delete
const ResetOptions * options
std::vector< double > jnt_vel_cmd
std::vector< double > jnt_trq_msr
std::vector< double > jnt_pos_cmd
std::vector< double > jnt_vel_msr
std::vector< double > jnt_pos_msr
std::vector< uint8_t > jnt_saturated
std::vector< double > jnt_trq_cmd
std::vector< AttachmentSpec > attachments
std::vector< CtrlModeSpec > modes
std::vector< ForceTorqueSensor > ft_sensors
std::string tcp_site
Robot & operator=(const Robot &)=delete
std::unique_ptr< RobotInternals > _impl
std::vector< std::pair< double, double > > joint_limits
std::vector< std::string > joint_names
Robot(const Robot &)=delete
KDL::Frame tip_T_tcp
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
std::vector< SiteSpec > sites
std::vector< CameraSpec > cameras
std::deque< SceneJointSlot > joints
std::deque< SceneWrenchSlot > wrenches
std::deque< SceneFreeBodySlot > free_bodies
std::deque< SceneActuatorSlot > actuators
const mjModel * model
std::string error
std::vector< ForceTorqueSensorSpec > ft_sensors
std::chrono::steady_clock::time_point _tick_t