Mujoco KDL Wrapper  0.2.2
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 <GLFW/glfw3.h>
9#include <kdl/chain.hpp>
10#include <kdl/frames.hpp>
11#include <kdl/jntarray.hpp>
12#include <cstdio>
13#include <cstring>
14#include <functional>
15#include <sstream>
16#include <string>
17#include <vector>
18#include <chrono>
19
20namespace mj_kdl {
21
22/**
23 * @ingroup grp_logging
24 * Log verbosity level. Each level includes all levels below it:
25 * NONE - nothing printed.
26 * INFO - informational messages only (scene/chain construction progress).
27 * WARN - INFO + recoverable warnings (e.g. fallback to headless mode).
28 * ERROR - all messages, including errors that cause functions to fail. Default.
29 */
30enum class LogLevel { NONE = 0, INFO = 1, WARN = 2, ERROR = 3 };
31
32/** @ingroup grp_logging
33 * Library-wide log verbosity (inline so one shared instance across all TUs). */
35
36/** @ingroup grp_logging
37 * Set the library-wide log verbosity. */
38inline void set_log_level(LogLevel level) { g_log_level = level; }
39/** @ingroup grp_logging
40 * Get the library-wide log verbosity. */
42
43} // namespace mj_kdl
44
45/* @ingroup grp_logging
46 * Internal logging macros, exposed so wrapper users (examples, tests, and
47 * downstream code) can emit messages through the same stream/level filter.
48 * MJ_LOG_ is the primitive; LOG_INFO/LOG_WARN/LOG_ERROR are the entry points.
49 * `expr` may use << to build the message: LOG_INFO("count=" << n).
50 *
51 * Defined at file scope (not inside the mj_kdl namespace) because macros are
52 * not namespaced; the MJ_ prefix avoids collisions.
53 */
54#define MJ_FILENAME_ (::strrchr(__FILE__, '/') ? ::strrchr(__FILE__, '/') + 1 : __FILE__)
55
56#define MJ_LOG_(lvl_enum, color, label, expr) \
57 do { \
58 if (::mj_kdl::g_log_level >= ::mj_kdl::LogLevel::lvl_enum) { \
59 std::ostringstream _mj_oss; \
60 _mj_oss << expr; /* NOLINT(bugprone-macro-parentheses) */ \
61 std::fprintf( \
62 stderr, \
63 color "[mj_kdl " label "] %s:%d (%s): %s\033[0m\n", \
64 MJ_FILENAME_, \
65 __LINE__, \
66 __func__, \
67 _mj_oss.str().c_str() \
68 ); \
69 } \
70 } while (0)
71
72#define LOG_INFO(expr) MJ_LOG_(INFO, "", "INFO ", expr)
73#define LOG_WARN(expr) MJ_LOG_(WARN, "\033[33m", "WARN ", expr)
74#define LOG_ERROR(expr) MJ_LOG_(ERROR, "\033[31m", "ERROR", expr)
75
76namespace mj_kdl {
77
78/**
79 * @ingroup grp_types
80 * Kind of element an AttachTarget references in the accumulated scene spec.
81 * World selects the worldbody (the default); Body, Site, and Frame each look
82 * up a named element of the corresponding type.
83 */
84enum class AttachKind { World, Body, Site, Frame };
85
86/**
87 * @ingroup grp_types
88 * Where to attach a robot, object, or attachment in the accumulated scene spec.
89 * Tagged so exactly one alternative is encoded; defaulting to World keeps
90 * callers that omit attach_to anchored to the worldbody.
91 * For Site, the site's own pos/quat becomes the placement frame and the
92 * accompanying pos/euler are an additional offset on top of it (matches MJCF).
93 */
95{
97 const char *name = nullptr; // ignored when kind == World
98};
99
100/**
101 * @ingroup grp_types
102 * One link in an ordered attachment chain for a robot.
103 * An attachment is any MJCF body (end effector, mount, FT sensor, tool, additional arm
104 * on a mobile base, etc.) attached under a named element in the accumulated robot spec.
105 * Attachments are applied in declaration order; attach_to may reference any body, site,
106 * or frame present after all prior attachments have been applied.
107 */
109{
110 const char *mjcf_path = nullptr; // MJCF file for this attachment
111 AttachTarget attach_to; // parent in root or prior attachment (default: world)
112 const char *prefix = ""; // element name prefix (avoids name conflicts)
113 double pos[3] = { 0, 0, 0 }; // position offset [m]
114 double euler[3] = { 0, 0, 0 }; // extrinsic XYZ Euler offset [degrees]
115
116 /* Contact exclusion pairs registered by attach_to_spec(). */
117 std::vector<std::pair<std::string, std::string>> contact_exclusions; // (body1, body2) pairs
118};
119
120/**
121 * @ingroup grp_types
122 * One robot in a scene: a root MJCF (arm, mobile base, ...) with an ordered attachment
123 * chain and a placement target.
124 *
125 * attachments is applied in order by build_scene() / attach_to_spec(): each entry's
126 * attach_to may reference any body, site, or frame in the accumulated spec (root + all
127 * prior attachments). This naturally supports: fixed arm, arm+gripper, arm+mount+FT+
128 * gripper, mobile base, mobile manipulator (base root, arm as first attachment), etc.
129 *
130 * attach_to selects where the robot root is placed in the scene; the default is the
131 * worldbody. Set it to e.g. { AttachKind::Site, "table_mount" } to place the robot on
132 * a tabletop site exported by a prior scene object. pos/euler are offsets in the
133 * resolved parent frame.
134 *
135 * path is the root MJCF passed to build_scene(). prefix must be unique per robot
136 * in multi-robot scenes.
137 */
139{
140 const char *path = nullptr; // root MJCF path
141 const char *prefix = ""; // element name prefix
142 AttachTarget attach_to; // placement parent (default: world)
143 double pos[3] = { 0, 0, 0 }; // offset in parent frame [m]
144 double euler[3] = { 0, 0, 0 }; // extrinsic XYZ Euler offset [degrees]
145 std::vector<AttachmentSpec> attachments; // ordered attachment chain; empty = none
146};
147
148/** @ingroup grp_types
149 * Shape type for scene objects. Unspecified is the sentinel value;
150 * build_scene rejects a primitive SceneObject whose shape is Unspecified. */
152
153/**
154 * @ingroup grp_types
155 * Contact-friction dimensionality, matching MuJoCo's `condim` integer values.
156 * Tangential (3) - sliding friction only (default).
157 * Torsional (4) - +torsion about the contact normal.
158 * Rolling (6) - +torsion and +rolling resistance.
159 * Values 1 (frictionless) and 2 (1D friction) exist in MuJoCo but are
160 * uncommon; if needed, pass `static_cast<Condim>(1)` etc.
161 */
162enum class Condim : int { Tangential = 3, Torsional = 4, Rolling = 6 };
163
164/**
165 * @ingroup grp_types
166 * A free-floating or fixed rigid body to place in the scene.
167 *
168 * size:
169 * BOX - half-extents (x, y, z)
170 * SPHERE - {radius, 0, 0}
171 * CYLINDER - {radius, half-length, 0}
172 * Ignored when mjcf_path is set.
173 *
174 * attach_to:
175 * Parent in the accumulated scene spec. Default is the worldbody. A child
176 * object must appear after its parent in SceneSpec::objects.
177 * MuJoCo constraint: a body that carries a freejoint must be a direct child
178 * of the worldbody. So a non-fixed primitive (fixed == false) and any
179 * mjcf_path asset whose root body owns a freejoint must use AttachKind::World.
180 * Fixed primitives and articulated subtrees (no freejoint on the root) may
181 * use any kind. mj_compile reports the violation if this rule is broken.
182 *
183 * pos:
184 * Offset in the resolved parent frame. For MJCF assets, this is the placement
185 * frame for the asset's first root body.
186 *
187 * fixed:
188 * If true the body is welded to its parent (no freejoint); useful for
189 * static obstacles or fixtures. Ignored when mjcf_path is set.
190 *
191 * Fields without an inline default (size, rgba, mass, friction) must be set
192 * explicitly by the caller. They are arbitrary visual/material/dynamic
193 * choices, not neutral identities, so the API refuses to invent placeholder
194 * values.
195 */
197{
198 std::string name;
199 std::string mjcf_path; // optional MJCF asset; when set, shape/size/mass/friction are ignored
200 AttachTarget attach_to; // placement parent (default: world)
201 Shape shape = Shape::Unspecified; // required for primitives; rejected at build time if not set
202 double size[3]; // half-extents (BOX) / {radius, 0, 0} (SPHERE) / {radius, half-len, 0} (CYL)
203 double pos[3] = { 0.0, 0.0, 0.0 }; // offset in resolved parent frame [m]
204 float rgba[4]; // [r, g, b, a]; required for primitives
205 bool fixed = false;
206 double mass; // [kg]; required for primitives
208 double friction[3]; // [slide, spin, roll]; required for primitives
209};
210
211/**
212 * @ingroup grp_types
213 * A named fixed camera to add to the world body of the scene.
214 * After build_scene() the camera is accessible by name via get_camera_names()
215 * and can be activated on a Viewer or VideoRecorder with use_camera().
216 *
217 * pos and fovy have no defaults: there is no neutral camera position or
218 * field of view, so the caller must specify both. euler defaults to identity.
219 */
221{
222 std::string name;
223 double pos[3]; // world-frame position [m]
224 double euler[3] = { 0.0, 0.0, 0.0 }; // extrinsic XYZ Euler [degrees]
225 double fovy; // vertical field of view [degrees]
226};
227
228/** @ingroup grp_types
229 * Full scene description passed to build_scene().
230 * timestep, add_floor, and add_skybox have no defaults: the caller must
231 * choose a physics step and an explicit yes/no for each decoration so the
232 * resulting scene is never silently misconfigured. gravity_z defaults to
233 * Earth gravity. */
235{
236 std::vector<RobotSpec> robots;
237 double timestep; // required; suggested 0.002 [s]
238 double gravity_z = -9.81; // Earth gravity [m/s^2]
239 bool add_floor; // required; checker groundplane geom
240 bool add_skybox; // required; gradient sky + directional light
241 std::vector<SceneObject> objects;
242 std::vector<CameraSpec> cameras; // static world cameras added to worldbody
243};
244
245/**
246 * @ingroup grp_types
247 * Logical force-torque sensor backed by MuJoCo's separate <force> and <torque>
248 * sensors. If force_sensor/torque_sensor are omitted, init_robot_from_mjcf()
249 * resolves "{name}_force" and "{name}_torque".
250 */
252{
253 const char *name = nullptr; // logical wrapper name
254 const char *force_sensor = nullptr; // MuJoCo <force> sensor name
255 const char *torque_sensor = nullptr; // MuJoCo <torque> sensor name
256 const char *frame_site = nullptr; // optional site that defines the sensor frame
257};
258
259/**
260 * @ingroup grp_types
261 * Runtime force-torque sensor state. The wrench is updated by update().
262 */
264{
265 std::string name;
266 std::string force_sensor;
267 std::string torque_sensor;
268 std::string frame_site;
269
270 int force_adr = -1;
271 int torque_adr = -1;
273
274 KDL::Wrench wrench = KDL::Wrench::Zero();
275};
276
277/**
278 * @ingroup grp_types
279 * Optional tool/end-effector description used while building the KDL chain.
280 *
281 * tool_body names the root of the attached tool subtree whose mass/inertia is
282 * lumped into the arm dynamics. tcp_site names an authored MuJoCo site that
283 * becomes the KDL terminal frame for FK/IK (takes priority when set). When
284 * the model has no suitable site, tcp_frame provides an equivalent manual
285 * transform expressed in the tip body's local frame.
286 * For the prefixed Robotiq 2F-85 this is typically {"g_base", "g_pinch"}.
287 */
289{
290 const char *tool_body = nullptr;
291 const char *tcp_site = nullptr; // MuJoCo site name (takes priority)
292 KDL::Frame tcp_frame = KDL::Frame::Identity(); // manual TCP in tip frame (fallback)
293 std::vector<ForceTorqueSensorSpec> ft_sensors;
294};
295
296/**
297 * @ingroup grp_types
298 * Joint-space control mode for update().
299 * POSITION - writes jnt_pos_cmd to actuator ctrl inputs.
300 * TORQUE - writes jnt_trq_cmd to qfrc_applied (generalized forces).
301 */
302enum class CtrlMode { POSITION, TORQUE };
303
304/**
305 * @ingroup grp_types
306 * Runtime handle for one KDL-tracked articulation inside a MuJoCo scene.
307 * model/data are borrowed (never freed by cleanup()); call destroy_scene() separately.
308 *
309 * Workflow:
310 * 1. Call init_robot_from_mjcf() - populates configuration and sizes port vectors to n_joints.
311 * 2. Each control step: read *_msr ports (updated by update()), fill *_cmd ports,
312 * call update() to apply commands to MuJoCo and read back sensor state.
313 */
314struct Robot
315{
316 /* Configuration - set once by init_robot() / init_from_mjcf(). */
317 mjModel *model = nullptr;
318 mjData *data = nullptr;
319 KDL::Chain chain;
320 KDL::Frame tip_T_tcp = KDL::Frame::Identity();
321 bool has_tcp_frame = false;
322 std::string tcp_site;
323 int n_joints = 0;
324 std::vector<std::string> joint_names;
325 std::vector<std::pair<double, double>> joint_limits;
326 std::vector<ForceTorqueSensor> ft_sensors;
327
328 /* Ports - read/written each control cycle. */
330 bool paused = false;
331 std::vector<double> jnt_pos_msr; // [rad] - measured joint positions (written by update())
332 std::vector<double> jnt_vel_msr; // [rad/s] - measured joint velocities (written by update())
333 std::vector<double> jnt_trq_msr; // [Nm] - actuator output torques (written by update())
334 std::vector<double> jnt_pos_cmd; // [rad] - position setpoints (POSITION mode)
335 std::vector<double> jnt_trq_cmd; // [Nm] - torque commands (TORQUE mode)
336
337 /* Internal state - populated by init_robot() / init_from_mjcf(). */
338 std::vector<int> kdl_to_mj_qpos; // KDL index -> MuJoCo qpos address
339 std::vector<int> kdl_to_mj_dof; // KDL index -> MuJoCo dof address
340 std::vector<int> kdl_to_mj_ctrl; // KDL index -> MuJoCo ctrl index (-1 if none)
341};
342
343/**
344 * @ingroup grp_viewer
345 * GLFW window and MuJoCo visualization state for the manual render loop.
346 * Created by init_window(); freed by cleanup(Viewer *).
347 */
348struct Viewer
349{
350 GLFWwindow *window = nullptr;
351 mjvScene scn{};
352 mjvCamera cam{};
353 mjvOption opt{};
354 mjvPerturb pert{};
355 mjrContext con{};
356 /* Real-time factor controlling simulation speed in step()/tick().
357 * 1.0 = real-time (default), 2.0 = 2x faster, 0.5 = half speed.
358 * Keyboard: ',' slows down, '.' speeds up in both viewer modes.
359 * 0.0 means run as fast as possible (no sleep). */
360 double realtime_factor = 1.0;
361 /* internal: real-time pacing state used by tick(). */
362 std::chrono::steady_clock::time_point _tick_t{};
363 /* internal: non-null when init_window_sim() is used; holds SimUiState*. */
364 void *_sim_ui = nullptr;
365};
366
367/**
368 * @ingroup grp_recorder
369 * Standard output resolution presets for init_video_recorder().
370 * Each maps to a 16:9 frame size at the named quality level.
371 */
372enum class VideoResolution {
373 R360p = 360, // 640 x 360
374 R480p = 480, // 854 x 480
375 R720p = 720, // 1280 x 720
376 R1080p = 1080, // 1920 x 1080
377 R2K = 1440, // 2560 x 1440
378 R4K = 2160, // 3840 x 2160
379};
380
381/**
382 * @ingroup grp_recorder
383 * Headless video recorder. Renders frames to an EGL offscreen buffer and
384 * pipes raw RGB data to an ffmpeg process, producing an H.264 MP4 without a
385 * display server or GLFW window.
386 *
387 * Requirements: EGL (libegl-dev) and ffmpeg available in PATH.
388 *
389 * Typical usage:
390 *
391 * VideoRecorder vr;
392 * init_video_recorder(&vr, model, "sim.mp4", VideoResolution::R1080p);
393 * vr.cam.azimuth = 135; vr.cam.elevation = -20; vr.cam.distance = 2.5;
394 *
395 * for (int i = 0; i < steps; ++i) {
396 * mj_step(model, data);
397 * record_frame(&vr, model, data);
398 * }
399 *
400 * cleanup(&vr);
401 */
403{
404 mjvCamera cam{}; // camera configuration; modify freely between frames
405 mjvOption opt{}; // rendering options; modify freely between frames
406 void *_impl = nullptr; // opaque EGL + ffmpeg state
407};
408
409struct Env;
410
411/** @ingroup grp_env
412 * Options controlling an environment reset. */
414{
415 int keyframe = 0; // keyframe index to use when available
416 bool use_keyframe = true; // fall back to mj_resetData when false or invalid
417};
418
419/** @ingroup grp_env
420 * Information returned by reset(). */
422{
423 bool used_keyframe = false;
424 int keyframe = -1;
425};
426
427/** @ingroup grp_env
428 * Runtime context passed to Env::on_reset after MuJoCo data has been reset and
429 * before mj_forward() and robot command-port synchronisation. */
431{
432 Env *env = nullptr;
433 mjModel *model = nullptr;
434 mjData *data = nullptr;
435 const ResetOptions *options = nullptr;
436 ResetInfo *info = nullptr;
437};
438
439using ResetHook = std::function<void(ResetContext *)>;
440
441/** @ingroup grp_env
442 * Runtime environment instance: declarative SceneSpec plus compiled MuJoCo
443 * model/data and Robot handles that should be synchronised after reset.
444 *
445 * Env owns model/data created by init_env(); registered Robot pointers are
446 * borrowed and are never deleted by cleanup(Env *). Robot::model/data remain
447 * borrowed aliases for compatibility with the existing robot-centric API.
448 */
449struct Env
450{
452 mjModel *model = nullptr;
453 mjData *data = nullptr;
454 std::vector<Robot *> robots;
456};
457
458/**
459 * @ingroup grp_scene
460 * Save the compiled model to an MJCF XML file for later reloading via build_scene().
461 * Must be called with the model returned by the most recent build_scene() call -
462 * MuJoCo only retains the last compiled model's XML internally.
463 * Typical use: build a combined scene (dual-arm, arm+gripper, ...) once, save it,
464 * then reload via build_scene() in subsequent runs to skip all build steps.
465 * @param model Model to save; must be the most recently compiled model.
466 * @param path Output path for the MJCF XML file.
467 * @return true on success.
468 */
469bool save_model_xml(const mjModel *model, const char *path);
470
471/**
472 * @ingroup grp_robot
473 * Build KDL chain from a compiled MuJoCo model and optional tool/TCP metadata.
474 *
475 * If tool->tcp_site is set, that authored site becomes the KDL terminal frame
476 * for FK/IK. The joint count and MuJoCo joint/actuator maps still cover only
477 * the controllable joints from base_body to tip_body.
478 * Pass tool = nullptr (the default) for an arm with no attached tool.
479 */
481 Robot *r,
482 mjModel *model,
483 mjData *data,
484 const char *base_body,
485 const char *tip_body,
486 const char *prefix = "",
487 const ToolFrameSpec *tool = nullptr
488);
489
490/** @ingroup grp_robot Find a configured logical force-torque sensor by name. */
491const ForceTorqueSensor *find_ft_sensor(const Robot *r, const char *name);
492
493/**
494 * @ingroup grp_robot
495 * Per-joint torque/force saturation limit in KDL joint order, read from the
496 * MuJoCo actuator forcerange (`mjModel::actuator_forcerange`). For each KDL
497 * joint, the returned bound is symmetric: max(|lo|, |hi|) of the driving
498 * actuator's forcerange. Joints with no driving actuator
499 * (`kdl_to_mj_ctrl[i] == -1`) or an unlimited actuator (`actuator_forcelimited`
500 * false) fall back to `fallback`.
501 *
502 * @param r Initialized robot (init_robot_from_mjcf() already called).
503 * @param fallback Bound used for joints without a force-limited actuator;
504 * default is a large, effectively non-limiting value.
505 * @return One entry per KDL joint, same order as r->joint_names.
506 */
507std::vector<double> joint_force_limits(const Robot *r, double fallback = 1e6);
508
509/**
510 * @ingroup grp_scene
511 * Apply one attachment to an arm spec using the MuJoCo spec API (mjs_attach).
512 * Parses a->mjcf_path, attaches its first root body under a->attach_to with the given
513 * pos/euler offset, prefixes all element names with a->prefix, and registers contact
514 * exclusions via mjs_addExclude. Can be called repeatedly to build a chain: each
515 * subsequent a->attach_to may reference any body added by prior calls.
516 * @param[in,out] robot_spec Accumulated robot spec to attach into.
517 * @param[in] a Attachment; a->mjcf_path must not be null.
518 * @return true on success.
519 */
520bool attach_to_spec(mjSpec *robot_spec, const AttachmentSpec *a);
521
522/**
523 * @ingroup grp_scene
524 * Build a MuJoCo scene from one or more robots using the MuJoCo spec API.
525 * This is the primary scene-building function.
526 *
527 * For each RobotSpec: mj_parseXML loads the root MJCF, then attach_to_spec() applies
528 * each entry in RobotSpec::attachments in order (mount, sensor, gripper, etc.),
529 * and mjs_attach places the complete robot spec at the given position. A single
530 * mj_compile produces the final model -- no intermediate XML files are written.
531 *
532 * @param[out] out_model Newly allocated MuJoCo model; caller frees via destroy_scene().
533 * @param[out] out_data Newly allocated MuJoCo data; caller frees via destroy_scene().
534 * @param[in] spec Scene description: robots (with attachment chains), table,
535 * objects, timestep, gravity, floor, skybox.
536 * @return true on success.
537 */
538bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec);
539
540/**
541 * @ingroup grp_scene
542 * Free a model/data pair allocated by any scene-building function.
543 * @param[in] model Model to free (may be null).
544 * @param[in] data Data to free (may be null).
545 */
546void destroy_scene(mjModel *model, mjData *data);
547
548/**
549 * @ingroup grp_env
550 * Build a runtime environment from a declarative SceneSpec.
551 * The resulting model/data are owned by env and freed by cleanup(Env *).
552 */
553bool init_env(Env *env, const SceneSpec *spec);
554
555/**
556 * @ingroup grp_env
557 * Register a Robot handle to be synchronised after environment reset.
558 * The robot is borrowed; the Env does not delete or clean it up.
559 */
560void env_add_robot(Env *env, Robot *robot);
561
562/**
563 * @ingroup grp_env
564 * Reset the whole environment to a keyframe/default state, call Env::on_reset
565 * for user-specific robot/object/task restoration, then sync all registered
566 * Robot command ports and clear stale robot forces.
567 */
568ResetInfo reset(Env *env, const ResetOptions *options = nullptr);
569
570
571/**
572 * @ingroup grp_viewer
573 * Open a GLFW window and initialise MuJoCo visualization contexts.
574 * Must be called after init_robot() or init_from_mjcf().
575 * @param[out] v Viewer to initialise; must be zero-initialised before call.
576 * @param[in] r Robot whose model drives the rendering context.
577 * @param[in] title Window title string.
578 * @param[in] width Window width in pixels.
579 * @param[in] height Window height in pixels.
580 * @return true on success, false if GLFW or MuJoCo context creation fails.
581 */
583 Viewer *v,
584 Robot *r,
585 const char *title = "MuJoCo",
586 int width = 1280,
587 int height = 720
588);
589
590/**
591 * @ingroup grp_viewer
592 * Open the full MuJoCo simulate UI (panels, physics controls, joint viewer)
593 * in a background render thread, then return so the caller can drive the
594 * physics loop with tick().
595 *
596 * Use this instead of init_window() when you want the simulate UI panels
597 * alongside a user-owned loop. tick() automatically acquires the render
598 * mutex, steps physics, and handles pause / perturbation / speed controls.
599 *
600 * Note: the render thread owns the GLFW window; on Linux (X11 / Wayland)
601 * this works correctly. Not supported on macOS.
602 *
603 * @param[out] v Viewer to initialise; freed by cleanup(Viewer *).
604 * @param[in] r Robot to simulate. r is registered globally; pass the same
605 * Robot to every subsequent step() call so that keyboard and
606 * mouse perturbation callbacks operate on the correct model.
607 * Only one (Viewer, Robot) pair may be active at a time.
608 * @param[in] title Label shown in the window title bar (default "MuJoCo").
609 * @return true on success.
610 */
611bool init_window_sim(Viewer *v, Robot *r, const char *title = "MuJoCo");
612
613/**
614 * @ingroup grp_viewer
615 * Open the simulate UI for a robot-less model/data pair (e.g. a Scene or Env
616 * with no Robot). Physics, camera and pause are handled by the UI directly.
617 * @return true on success.
618 */
619bool init_window_sim(Viewer *v, mjModel *m, mjData *d, const char *title = "MuJoCo");
620
621/**
622 * @ingroup grp_viewer
623 * Reset the viewer's user-scene geom count to 0.
624 * Call once per frame before appending trace segments with add_trace_segment().
625 * No-op when v is not backed by an init_window_sim() window (e.g. headless).
626 * @param[in,out] v Viewer initialised by init_window_sim().
627 */
629
630/**
631 * @ingroup grp_viewer
632 * Append a single line segment to the viewer's user scene. Thread-safe.
633 * The render thread merges the user scene into each frame automatically.
634 * Silently drops the segment once the user-scene geom buffer is full.
635 * No-op when v is not backed by an init_window_sim() window (e.g. headless).
636 * @param[in,out] v Viewer initialised by init_window_sim().
637 * @param[in] a Segment start point (world frame) [m].
638 * @param[in] b Segment end point (world frame) [m].
639 * @param[in] rgba Optional [r, g, b, a] colour; nullptr -> warm orange.
640 */
642 Viewer *v,
643 const KDL::Vector &a,
644 const KDL::Vector &b,
645 const float rgba[4] = nullptr
646);
647
648/**
649 * @ingroup grp_robot
650 * Zero all Robot fields. Does not free model or data; call destroy_scene() for that.
651 * @param[in,out] r Robot to tear down.
652 */
653void cleanup(Robot *r);
654
655/**
656 * @ingroup grp_env
657 * Destroy model/data owned by Env and clear borrowed Robot registrations.
658 * Registered Robot objects are not deleted.
659 */
660void cleanup(Env *env);
661
662/**
663 * @ingroup grp_viewer
664 * Release the GLFW window and MuJoCo visualization contexts owned by v.
665 * @param[in,out] v Viewer to tear down; all pointers set to null afterwards.
666 */
668
669/**
670 * @ingroup grp_recorder
671 * Initialise a headless EGL video recorder.
672 * Creates an EGL context, an offscreen render target, and launches an ffmpeg
673 * process (H.264/MP4) via a pipe. The MuJoCo model is used to size the scene
674 * and initialise the rendering context; it must remain valid until cleanup().
675 *
676 * @param vr VideoRecorder to initialise; freed by cleanup(VideoRecorder*).
677 * @param model MuJoCo model for the rendering context.
678 * @param out_path Output MP4 path (e.g. "sim.mp4").
679 * @param width Frame width in pixels (default 1280).
680 * @param height Frame height in pixels (default 720).
681 * @param fps Playback frame rate (default 60).
682 * @return true on success; false if EGL init or ffmpeg launch fails.
683 */
685 VideoRecorder *vr,
686 mjModel *model,
687 const char *out_path,
688 int width = 1280,
689 int height = 720,
690 int fps = 60
691);
692
693/**
694 * @ingroup grp_recorder
695 * Convenience overload: initialise a VideoRecorder using a named resolution preset.
696 * Frame width is derived from the preset at 16:9 aspect ratio.
697 *
698 * @param vr VideoRecorder to initialise.
699 * @param model MuJoCo model.
700 * @param out_path Output MP4 path.
701 * @param resolution VideoResolution preset (e.g. VideoResolution::R1080p).
702 * @param fps Playback frame rate (default 60).
703 * @return true on success.
704 */
706 VideoRecorder *vr,
707 mjModel *model,
708 const char *out_path,
709 VideoResolution resolution,
710 int fps = 60
711);
712
713/**
714 * @ingroup grp_recorder
715 * Render the current simulation state and write one frame to the video stream.
716 * Call mj_step() (or equivalent) before each record_frame() call.
717 *
718 * @param vr VideoRecorder initialised by init_video_recorder().
719 * @param model MuJoCo model.
720 * @param data MuJoCo data (current state).
721 * @return true on success; false on render or pipe write error.
722 */
723bool record_frame(VideoRecorder *vr, mjModel *model, mjData *data);
724
725/**
726 * @ingroup grp_recorder
727 * Flush the ffmpeg pipe, finalise the MP4, and release all EGL resources.
728 * After this call vr->_impl is null and the VideoRecorder may be discarded.
729 *
730 * @param vr VideoRecorder to tear down.
731 */
733
734/**
735 * @ingroup grp_robot
736 * Advance one physics timestep.
737 *
738 * Headless (no viewer active): calls mj_step() and returns true.
739 * GUI (init_window_sim() was called): advances physics, renders, syncs to real
740 * time, and polls GLFW events -- exactly what tick() used to do. Returns false
741 * once the user closes the window.
742 *
743 * This replaces the old headless/GUI split:
744 *
745 * // Before:
746 * if (headless) { mj_kdl::step(&r); }
747 * else if (!mj_kdl::tick(&v, m, d)) break;
748 *
749 * // After:
750 * if (!mj_kdl::step(&r)) break;
751 *
752 * Coupling note: in GUI mode the keyboard and mouse perturbation callbacks
753 * operate on the Robot registered via init_window_sim(). Always pass the
754 * same Robot to both init_window_sim() and step(); passing a different Robot
755 * causes perturbation forces to be applied to the wrong model.
756 *
757 * @param[in,out] s Simulation state; must be the Robot passed to init_window_sim().
758 * @return true while the window is open (or always true in headless mode).
759 */
760bool step(Robot *s);
761
762/**
763 * @ingroup grp_robot
764 * Advance the simulation by n timesteps.
765 * Returns false immediately if the viewer window is closed mid-sequence.
766 * @param[in,out] s Simulation state.
767 * @param[in] n Number of steps.
768 */
769bool step_n(Robot *s, int n);
770
771/**
772 * @ingroup grp_viewer
773 * Model/data overload of step() for multi-robot or no-robot GUI loops.
774 * Equivalent to the former tick(Viewer*, mjModel*, mjData*).
775 * @param[in,out] v Viewer initialised by init_window() or init_window_sim().
776 * @param[in] m Shared MuJoCo model.
777 * @param[in] d Shared MuJoCo data.
778 * @return true while the window is open; false once the user closes it.
779 */
780bool step(Viewer *v, mjModel *m, mjData *d);
781
782/**
783 * @ingroup grp_viewer
784 * Returns true if the viewer window is open and not scheduled for closing.
785 * @param[in] v Viewer created by init_window().
786 */
787bool is_running(const Viewer *v);
788
789/**
790 * @ingroup grp_viewer
791 * Render the current simulation frame to the viewer window.
792 * @param[in,out] v Viewer created by init_window().
793 * @param[in] r Robot whose model and data are rendered.
794 * @return true if the window is still open after rendering.
795 */
796bool render(Viewer *v, const Robot *r);
797
798/**
799 * @ingroup grp_viewer
800 * Render the current simulation frame to the viewer window.
801 * Model/data overload -- use when no single Robot owns the scene (e.g. multi-robot).
802 * @param[in,out] v Viewer created by init_window().
803 * @param[in] m MuJoCo model.
804 * @param[in] d MuJoCo data.
805 * @return true if the window is still open after rendering.
806 */
807bool render(Viewer *v, mjModel *m, mjData *d);
808
809/**
810 * @ingroup grp_robot
811 * One control cycle: read MuJoCo into *_msr, then apply *_cmd to MuJoCo.
812 * Read step: qpos -> jnt_pos_msr, qvel -> jnt_vel_msr, qfrc_actuator -> jnt_trq_msr.
813 * Apply step: POSITION -> data->ctrl,
814 * TORQUE -> qfrc_applied; also sets ctrl = qpos to neutralize
815 * position actuators (zeroes kp*(ctrl-qpos) restoring force).
816 * Joints with kdl_to_mj_ctrl[i] == -1 are skipped for ctrl writes.
817 */
818void update(Robot *r);
819
820/**
821 * @ingroup grp_robot
822 * Write KDL joint positions into MuJoCo qpos (KDL chain order -> MuJoCo addresses).
823 * @param[in,out] r Robot with a valid data pointer.
824 * @param[in] q Joint positions in KDL chain order; size must equal r->n_joints.
825 * @param[in] call_forward If true (default), calls mj_forward() after writing qpos
826 * so that body poses and sensor data are updated immediately.
827 */
828void set_joint_pos(Robot *r, const KDL::JntArray &q, bool call_forward = true);
829
830/**
831 * @ingroup grp_robot
832 * Teleport a free-floating body to a world-frame position and optionally a
833 * world-frame orientation, then zero its velocity.
834 * body_name must identify a body that owns a mjJNT_FREE joint.
835 * quat is MuJoCo convention [w, x, y, z]; pass nullptr to keep identity orientation.
836 * @param[in,out] model MuJoCo model.
837 * @param[in,out] data MuJoCo data.
838 * @param[in] body_name Name of the free-floating body to teleport.
839 * @param[in] pos World-frame position [x, y, z].
840 * @param[in] quat World-frame orientation [w, x, y, z], or nullptr for identity.
841 */
843 mjModel *model,
844 mjData *data,
845 const char *body_name,
846 const double pos[3],
847 const double *quat = nullptr
848);
849
850/**
851 * @ingroup grp_scene
852 * Add an object to the scene by appending it to spec->objects and rebuilding
853 * the model. The old model/data are freed; new ones replace them.
854 * Any Robot handles sharing the old model/data become stale - call init_robot()
855 * again on the new model/data after this call.
856 * @param[in,out] model Current model pointer; updated to new model on success.
857 * @param[in,out] data Current data pointer; updated to new data on success.
858 * @param[in,out] spec Scene spec; obj is appended to spec->objects.
859 * @param[in] obj Object to add.
860 * @return true on success; model/data and spec->objects unchanged on failure.
861 */
862bool scene_add_object(mjModel **model, mjData **data, SceneSpec *spec, const SceneObject &obj);
863
864/**
865 * @ingroup grp_scene
866 * Env overload: adds obj, rebuilds, and re-initialises all robots registered in env.
867 * env->model, env->data, and each Robot's model/data pointers are updated automatically.
868 * @return true on success; env unchanged on failure.
869 */
870bool scene_add_object(Env *env, const SceneObject &obj);
871
872/**
873 * @ingroup grp_scene
874 * Remove a named object from the scene by erasing it from spec->objects and
875 * rebuilding the model. The old model/data are freed; new ones replace them.
876 * Any Robot handles sharing the old model/data become stale - call init_robot()
877 * again on the new model/data after this call.
878 * @param[in,out] model Current model pointer; updated to new model on success.
879 * @param[in,out] data Current data pointer; updated to new data on success.
880 * @param[in,out] spec Scene spec; named object removed from spec->objects.
881 * @param[in] name Name of the object to remove.
882 * @return true on success; false if name not found or rebuild fails.
883 */
884bool scene_remove_object(mjModel **model, mjData **data, SceneSpec *spec, const std::string &name);
885
886/**
887 * @ingroup grp_scene
888 * Env overload: removes the named object, rebuilds, and re-initialises all robots registered
889 * in env. env->model, env->data, and each Robot's model/data pointers are updated automatically.
890 * @return true on success; false if name not found or rebuild fails.
891 */
892bool scene_remove_object(Env *env, const std::string &name);
893
894/**
895 * @ingroup grp_scene
896 * Return the compiled MuJoCo name for a site inside an MJCF-backed SceneObject.
897 * build_scene() prefixes all MJCF asset element names with obj.name + "_".
898 */
899std::string scene_object_site_name(const SceneObject &obj, const char *site_name);
900
901/**
902 * @ingroup grp_scene
903 * Read a named MuJoCo site as a world-frame KDL frame.
904 * Calls mj_forward() before reading site_xpos/site_xmat.
905 */
906bool get_site_frame(const mjModel *model, mjData *data, const char *site_name, KDL::Frame *out);
907
908/**
909 * @ingroup grp_scene
910 * Read a named MuJoCo body as a world-frame KDL frame.
911 * Calls mj_forward() before reading xpos/xmat.
912 */
913bool get_body_frame(const mjModel *model, mjData *data, const char *body_name, KDL::Frame *out);
914
915/**
916 * @ingroup grp_scene
917 * Read a joint's position (qpos) in physical units (rad or m), by joint name.
918 * If the name is not a joint, it is treated as an actuator name and resolved to
919 * its transmission joint (direct joint, or the first tendon-wrapped joint).
920 */
921bool get_joint_position(const mjModel *model, mjData *data, const char *name, double *out);
922
923/**
924 * @ingroup grp_scene
925 * Return the names of all cameras in a compiled model.
926 * Includes cameras from robot MJCFs (e.g. the Kinova wrist camera) and any
927 * cameras added via SceneSpec::cameras.
928 */
929std::vector<std::string> get_camera_names(const mjModel *model);
930
931/**
932 * @ingroup grp_viewer
933 * Switch the viewer to a named fixed camera defined in the model.
934 * Works for both init_window() and init_window_sim() paths.
935 * Pass nullptr or an empty string to return to the free camera.
936 * @return true if the camera name was found; false if not found (viewer unchanged).
937 */
938bool use_camera(Viewer *v, const mjModel *model, const char *name);
939
940/**
941 * Configure the viewer's free orbit camera.
942 */
944 Viewer *v,
945 double distance,
946 double azimuth,
947 double elevation,
948 const std::array<double, 3> &lookat
949);
950
951/**
952 * @ingroup grp_recorder
953 * Switch the video recorder to a named fixed camera defined in the model.
954 * @return true if the camera name was found; false if not found (recorder unchanged).
955 */
956bool use_camera(VideoRecorder *vr, const mjModel *model, const char *name);
957
958/**
959 * Internal spec-building helpers.
960 *
961 * These are used internally by build_scene() but are exposed here for advanced
962 * callers that construct mjSpec objects directly. They are not part of the
963 * stable public API and may change between releases.
964 */
965
966/**
967 * @ingroup grp_advanced
968 * Add a sky gradient texture and overhead directional light to spec.
969 * Corresponds to SceneSpec::add_skybox.
970 */
971void add_skybox_to_spec(mjSpec *spec);
972
973/**
974 * @ingroup grp_advanced
975 * Add a checker groundplane texture, material, and floor plane geom to spec.
976 * Corresponds to SceneSpec::add_floor.
977 */
978void add_floor_to_spec(mjSpec *spec);
979
980/**
981 * @ingroup grp_advanced
982 * Add free-floating or fixed rigid bodies to the world body of spec.
983 * @param spec MuJoCo spec to modify.
984 * @param objects List of objects to add.
985 */
986void add_objects_to_spec(mjSpec *spec, const std::vector<SceneObject> &objects);
987
988/**
989 * @ingroup grp_advanced
990 * Compile spec into a model and create its data buffer.
991 * spec is always deleted (on success and failure).
992 * @param[in] spec MuJoCo spec to compile; always freed by this call.
993 * @param[out] out_model Newly allocated model on success; null on failure.
994 * @param[out] out_data Newly allocated data on success; null on failure.
995 * @return true on success.
996 */
997bool compile_and_make_data(mjSpec *spec, mjModel **out_model, mjData **out_data);
998
999/**
1000 * @ingroup grp_advanced
1001 * Load MuJoCo decoder plugins (STL, OBJ, ...) once at first use.
1002 * Required for external mesh decoder plugin libraries.
1003 * Called automatically by all scene-building functions; call explicitly only
1004 * when building a scene via raw mjSpec APIs without going through the library.
1005 */
1007
1008} // namespace mj_kdl
void add_skybox_to_spec(mjSpec *spec)
bool compile_and_make_data(mjSpec *spec, mjModel **out_model, mjData **out_data)
void add_objects_to_spec(mjSpec *spec, const std::vector< SceneObject > &objects)
void add_floor_to_spec(mjSpec *spec)
void ensure_plugins_loaded()
void env_add_robot(Env *env, Robot *robot)
ResetInfo reset(Env *env, const ResetOptions *options=nullptr)
bool init_env(Env *env, const SceneSpec *spec)
void set_log_level(LogLevel level)
LogLevel get_log_level()
LogLevel g_log_level
bool 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, mjModel *model, mjData *data)
void set_body_pose(mjModel *model, mjData *data, const char *body_name, const double pos[3], const double *quat=nullptr)
bool step_n(Robot *s, int n)
std::vector< double > joint_force_limits(const Robot *r, double fallback=1e6)
bool step(Robot *s)
void cleanup(Robot *r)
bool init_robot_from_mjcf(Robot *r, mjModel *model, mjData *data, const char *base_body, const char *tip_body, const char *prefix="", const ToolFrameSpec *tool=nullptr)
void set_joint_pos(Robot *r, const KDL::JntArray &q, bool call_forward=true)
void update(Robot *r)
const ForceTorqueSensor * find_ft_sensor(const Robot *r, const char *name)
bool get_site_frame(const mjModel *model, mjData *data, const char *site_name, KDL::Frame *out)
bool get_body_frame(const mjModel *model, mjData *data, const char *body_name, KDL::Frame *out)
bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
bool attach_to_spec(mjSpec *robot_spec, const AttachmentSpec *a)
bool scene_add_object(mjModel **model, mjData **data, SceneSpec *spec, const SceneObject &obj)
bool get_joint_position(const mjModel *model, mjData *data, const char *name, double *out)
bool scene_remove_object(mjModel **model, mjData **data, SceneSpec *spec, const std::string &name)
void destroy_scene(mjModel *model, mjData *data)
std::vector< std::string > get_camera_names(const mjModel *model)
bool save_model_xml(const mjModel *model, const char *path)
std::string scene_object_site_name(const SceneObject &obj, const char *site_name)
bool init_window_sim(Viewer *v, Robot *r, const char *title="MuJoCo")
bool use_camera(Viewer *v, const mjModel *model, const char *name)
bool init_window(Viewer *v, Robot *r, const char *title="MuJoCo", int width=1280, int height=720)
void clear_trace(Viewer *v)
bool render(Viewer *v, const Robot *r)
void add_trace_segment(Viewer *v, const KDL::Vector &a, const KDL::Vector &b, const float rgba[4]=nullptr)
bool is_running(const Viewer *v)
std::function< void(ResetContext *)> ResetHook
void set_free_camera(Viewer *v, double distance, double azimuth, double elevation, const std::array< double, 3 > &lookat)
std::vector< std::pair< std::string, std::string > > contact_exclusions
ResetHook on_reset
std::vector< Robot * > robots
const ResetOptions * options
std::vector< AttachmentSpec > attachments
std::vector< ForceTorqueSensor > ft_sensors
std::vector< int > kdl_to_mj_ctrl
std::vector< int > kdl_to_mj_qpos
std::vector< int > kdl_to_mj_dof
std::vector< double > jnt_pos_msr
std::string tcp_site
std::vector< double > jnt_pos_cmd
std::vector< std::pair< double, double > > joint_limits
std::vector< double > jnt_vel_msr
std::vector< double > jnt_trq_cmd
std::vector< double > jnt_trq_msr
std::vector< std::string > joint_names
KDL::Frame tip_T_tcp
std::vector< RobotSpec > robots
std::vector< SceneObject > objects
std::vector< CameraSpec > cameras
std::vector< ForceTorqueSensorSpec > ft_sensors
GLFWwindow * window
std::chrono::steady_clock::time_point _tick_t