|
Mujoco KDL Wrapper
0.2.2
MuJoCo + KDL bridge for robot kinematics and dynamics
|
This tutorial builds a C++ simulation application in layers: compile a robot scene, add KDL control, add reset hooks, add objects and cameras, use the Simulate UI, record video, and then extend the scene to more robots and more complex task assets.
The snippets assume the package is built with MuJoCo Menagerie available, because the examples use the Kinova GEN3 arm and Robotiq 2F-85 gripper.
The wrapper has four layers. Keep them separate and the API stays simple:
| Layer | Type | Responsibility |
|---|---|---|
| Scene description | SceneSpec, RobotSpec, AttachmentSpec, SceneObject, CameraSpec | Describe what should be compiled into MuJoCo |
| Runtime environment | Env | Own mjModel/mjData, registered robots, and reset hooks |
| Robot control handle | Robot | KDL chain, joint maps, measured ports, command ports |
| Visualization/recording | Viewer, VideoRecorder | Interactive Simulate UI and offscreen MP4 recording |
| Python package | mj_kdl_wrapper | Python wrappers for the same scene, robot, reset, viewer, and recorder concepts |
The important ownership rule:
Most examples follow this flow:
Scenes are declared with SceneSpec. timestep, add_floor, and add_skybox have no defaults - they are choices the wrapper refuses to guess on your behalf. build_scene rejects timestep <= 0 at runtime. gravity_z defaults to Earth gravity (-9.81 m/s^2).
Env owns env.model and env.data. Call mj_kdl::cleanup(&env) when done.
You can also build raw pointers directly:
Prefer Env for applications that need reset hooks or registered robots.
build_scene() creates one MuJoCo mjSpec, attaches robot MJCF trees into it, adds global scene options, adds objects and cameras, then compiles the model. The robot list may be empty when the scene only contains objects.
Important fields:
Use RobotSpec::prefix for repeated robots. The prefix is applied during MJCF attachment so names stay unique in the compiled model:
Use RobotSpec::pos and RobotSpec::euler to place the robot root in the world. Euler angles are extrinsic XYZ degrees.
Use a consistent cleanup order:
If you created raw mjModel* and mjData* with build_scene(), free them with:
Do not call both cleanup(&env) and destroy_scene(env.model, env.data) for the same model/data. Env owns those pointers once init_env() succeeds.
Robot is the runtime handle for one controllable articulation. It stores the borrowed mjModel/mjData pointers, KDL chain, joint-name maps, measured ports, and command ports.
Registering with env_add_robot() lets reset(&env) sync the robot command ports after MuJoCo data is reset.
For position mode, write jnt_pos_cmd; update() copies it to MuJoCo actuator controls.
init_window_sim() starts MuJoCo Simulate in a render thread. Your loop still owns stepping, updating control, and task logic.
KDL dynamics work directly from the generated chain:
This is the core pattern used by ex_gravity_comp.
Attachments are MJCF assets attached under a body, site, or frame in the accumulated robot spec. They are applied in order, so mount -> sensor -> gripper chains are natural.
AttachTarget is a tagged pair of AttachKind { World, Body, Site, Frame } and an element name. Sites are the natural mount-point: the Kinova GEN3 MJCF ships pinch_site on the bracelet, with its own pos/quat that already encode the gripper offset and 180-degree flip. So the gripper attachment is just:
pos and euler on the spec are still available as offsets. For body and frame parents they ride on an intermediate frame, preserving the child's authored pose. For site parents they compose with the child root's authored pose so authored values are never silently dropped.
When the model has no suitable site, fall back to a body name and add the offset by hand:
Tell KDL about the attached tool when initializing the robot:
tool_body lumps the tool subtree inertia into the KDL chain. tcp_site adds a terminal frame from a MuJoCo site.
attachments is ordered. Each attachment is applied to the robot spec after all previous attachments, so later attach_to values may refer to bodies introduced by earlier attachments.
Both assets ship with the package; ex_table_pour runs this exact chain to hold a mug in the gripper.
For contact stability, add contact exclusions only when two attached bodies are known to overlap structurally:
Do not use exclusions to hide unstable task contacts; fix geometry, friction, or controller gains instead.
SceneObject supports primitive objects and MJCF-backed assets. Tables are just assets, not special first-class fields. SceneObject::attach_to accepts the same tagged AttachTarget as RobotSpec and AttachmentSpec. Inside build_scene the order is decorations -> objects (declaration order) -> robots -> cameras, so a robot or a later object may reference any prior object via its name or one of its sites.
SceneObject has no defaults for shape, size, rgba, mass, or friction. For MJCF-backed objects (when mjcf_path is set) those fields are ignored at runtime, so leaving them zero-initialised is fine. For primitives, build_scene runs explicit checks:
Condim is a typed enum (Tangential = 3, Torsional = 4, Rolling = 6) matching MuJoCo's contact-dimensionality integers. Default is Tangential.
scene.robots can be empty. Object-only scenes still compile and can be opened in the Simulate UI with init_window_sim(&viewer, model, data, "object scene").
After build_scene, an MJCF-backed SceneObject exposes its root body in the compiled scene under obj.name (i.e. the asset's internal root body name is rewritten so callers never need to know it). All other elements (sites, geoms, joints, child bodies) keep the obj.name + "_" prefix.
scene_object_site_name(obj, "site_name") returns the compiled name of a site authored inside the asset, for places that need a string at runtime:
Combined, this lets a robot sit on the tabletop without hand-threading heights:
A fixed object can also attach to the table body directly:
MuJoCo restricts freejoints to top-level bodies, so non-fixed primitives and asset roots with a freejoint must use AttachKind::World (the default). mj_compile reports the violation if this rule is broken.
Add fixed scene cameras through SceneSpec::cameras. CameraSpec requires pos and fovy; euler defaults to identity.
After compile, all cameras are visible to MuJoCo and include:
List them:
Use one in the viewer or recorder:
The Simulate UI also has its own live camera selector in the Rendering panel.
reset(&env) resets MuJoCo, runs your hook, forwards dynamics, then syncs all registered robots so stale commands do not hit the first post-reset step.
Put robot, object, controller, randomization, and task-specific state in the hook. Do not hide reset work in the control loop.
reset(&env) performs the reset in this order:
That order is deliberate. User hooks restore task state after the low-level MuJoCo reset, and robot ports are synchronized after the hook so the first post-reset update does not apply stale commands.
Use ResetContext when your hook needs direct MuJoCo access:
For randomized starts, generate random values in the hook and write them to MuJoCo before reset() returns.
The full viewer path is:
Useful wrapper-specific controls:
| Control | Behavior |
|---|---|
| , | slow the wrapper real-time factor |
| . | speed the wrapper real-time factor |
| Simulation -> RTF | current wrapper real-time factor |
| Simulation -> Recorder | path, camera, resolution, FPS, start/stop, status |
Recorder camera options include Current, Free, Tracking, and every fixed camera compiled into the model.
The right panel adds debug sections:
| Section | Behavior |
|---|---|
| Frames | per-body/site coordinate triads (RViz-style), with a Scale slider |
| Trace | Trace EE draws a trail following the robot TCP site |
| Perturb | Body shows the double-clicked selection; Drag = Camera/Force/Torque, then left-drag to apply (or Ctrl+right/left-drag) |
The Equality and Group enable sections are hidden by default; rebuild with -DSHOW_EQUALITY_PANEL=ON / -DSHOW_GROUP_PANEL=ON to restore them. These debug geoms appear in the live viewer only, not in recorded MP4s.
The recorder controls live in the left Simulation panel:
| Field | Meaning |
|---|---|
| Path | Output MP4 path, relative to the process working directory unless absolute |
| Camera | Current, Free, Tracking, or any compiled fixed camera |
| Resolution | 360p, 480p, 720p, or 1080p |
| FPS | Recording frame rate |
| Start rec | Opens EGL recorder and starts feeding frames |
| Stop rec | Closes ffmpeg and finalizes the MP4 |
| Rec | idle, recording, or failed |
Current means the recorder follows the Simulate viewer camera. A fixed camera records from that camera even if the live viewer is moved elsewhere.
When recording stops successfully, the terminal prints:
If the status changes to failed, check that ffmpeg is installed and the path is writable.
init_window_sim() owns a user scene that the render thread merges into every frame. Two helpers let you draw your own line geometry into it – the built-in use is a live polyline of recent end-effector positions, which makes it obvious at a glance whether the EE is tracking a commanded path:
Key points:
The motion-spec code generator wires this up automatically: declare a TRACE block in the ENVIRONMENT of a .robmot model (enabled, length, color) and every generated demo renders the trace with no hand-written loop code.
Interactive recording is available in the Simulate UI:
The terminal prints:
Headless recording uses the same VideoRecorder API:
This section assembles the pieces into a complete pick-place task. The goal is to pick a cube from one table location, move it to another location, open the gripper, and retreat. The full implementation is src/examples/ex_table_pick_place.cpp; the code below shows the structure you should reproduce in your own application.
The application has five parts:
Start with the gripper attachment. The Kinova pinch_site already encodes the tool offset and 180-degree flip, so no pos/euler are needed:
Add the table as an MJCF-backed SceneObject. The table asset origin is the tabletop surface center, so placing it at z = 0.7 makes the top surface 0.7 m.
Add a free cube. For a box, size is half-extents, so the world-frame center z is surface_z + half_height. Free objects must stay world-anchored (MuJoCo restricts freejoints to top-level bodies).
Assemble the scene. Set timestep, add_floor, and add_skybox explicitly, then mount the arm on the table's table_top site instead of writing surface_z into the robot position by hand:
The table asset defines a table_top site. Because MJCF-backed objects are prefixed when attached, get the compiled site name through the helper:
Now define task points from the table surface:
This makes the task robust if you move the table asset or swap it for another asset with the same site convention.
Initialize the robot with gripper inertia and TCP site:
Create KDL solvers:
q_min and q_max come from robot.joint_limits. Keep them in KDL::JntArray so IK respects the compiled model limits.
Use one orientation for the gripper and solve a sequence of poses:
Solve each waypoint seeded from the previous solution:
In production code, print the target names when IK fails. It saves time when a single pose is outside the workspace or has an impossible orientation.
Keep state configuration data-driven. Each row says where to move, how long to interpolate, how long to wait before forcing transition, and what the gripper should do.
On entry to a state, capture the measured joint position. Interpolate from that entry pose to the state target:
Transition when either the duration has elapsed and the joint error is small, or the timeout is reached:
Use KDL gravity plus joint PD:
The gripper actuator is model-specific. For the Robotiq Menagerie model, examples write the gripper command directly to its actuator control after update():
Reset must restore both the robot and task objects:
Because reset(&env) syncs registered robot command ports after this hook, the first control step after reset starts from the reset pose without stale torque or position commands.
Start the Simulate UI:
Use , and . to slow down or speed up the wrapper real-time factor. In the Simulation panel, the Recorder subsection can write an MP4 using Current, Free, Tracking, or any compiled fixed camera.
Before treating the example as working, check:
This is the architecture used by ex_table_pick_place: a small scene spec, a reset hook, precomputed IK waypoints, a data-driven state table, and one control law used consistently across states.
Use prefixes to disambiguate the second robot:
Then initialize two robot handles:
Each robot gets its own KDL chain and command ports, while both share the same MuJoCo model/data.
The included examples show how these pieces combine:
Read ../examples.md for behavior summaries and expected outputs.
For occasional changes, use the scene add/remove helpers. They rebuild the model, so this is for task setup and coarse changes, not per-frame object spawning.
Raw model/data form:
Env form:
After a rebuild, any cached MuJoCo IDs may be invalid. Recompute body IDs, joint IDs, site names, and KDL solvers that depend on the old model.
Use this checklist when a scene behaves incorrectly:
| Symptom | Check |
|---|---|
| Robot does not move in position mode | Model has actuators and kdl_to_mj_ctrl[i] >= 0 |
| First step after reset jumps | Robot is registered with env_add_robot() and reset uses reset(&env) |
| KDL gravity is wrong with a tool | ToolFrameSpec::tool_body points at the tool subtree root |
| TCP frame is wrong | ToolFrameSpec::tcp_site names an authored MuJoCo site |
| Object asset site not found | Use scene_object_site_name(object, "site") to account for prefixes |
| Recorder fails | BUILD_RECORDER=ON, EGL available, ffmpeg installed, output path writable |
| Camera missing in recorder list | Camera must exist in the compiled mjModel (get_camera_names(model)) |
Run the default build:
Run clang-tidy on wrapper code:
Run tests:
The vendored src/simulate_ui/simulate.cc is MuJoCo sample UI code with small wrapper UI additions. It is intentionally excluded from clang-tidy style cleanup so local changes stay reviewable against upstream.