Generic motion retargeting for any humanoid URDF and terrain mesh.
This is a re-implementation of the OmniRetarget method. OmniRetargeting is a flexible motion retargeting system that converts ordered human/source target positions to any humanoid robot operating on any terrain mesh. Unlike specialized retargeting systems, OmniRetargeting automatically adapts to different robot morphologies and terrain types.
OmniRetargeting uses a source-agnostic architecture that supports multiple motion data formats through a registry system.
- SMPL-X: Human body model motion data
- Custom sources: Easily add new source adapters
from omniretargeting import OmniRetargeter
from omniretargeting.data_sources import create_data_source
# Create a data source (automatically uses registry)
data_source = create_data_source(
source_type="smplx",
motion_file="path/to/motion.npz",
source_config={"model_directory": "/path/to/models"}
)
# Create retargeter
retargeter = OmniRetargeter(
robot_urdf_path="robot.urdf",
terrain_mesh_path="terrain.obj",
joint_mapping={"Pelvis": "torso_link", ...},
robot_height=1.6
)
# Retarget motion (batch mode)
scale, robot_motion = retargeter.retarget_motion(data_source)
# Or stream mode for frame-by-frame processing
for robot_frame in retargeter.retarget_stream(data_source):
# Process each frame
passSee docs/ADDING_SOURCE_ADAPTERS.md for a guide on implementing new source adapters.
install from source:
git clone <https://github.com/project-instinct/omniretargeting>
cd omniretargeting
pip install -e .For development with testing:
pip install -e ".[dev,test]"from omniretargeting import OmniRetargeter, load_robot_config
from omniretargeting.data_sources.smplx import SmplxDataSource
from pathlib import Path
# Load a robot profile. The default profiles currently use SMPL-X target names.
cfg = load_robot_config("robot_models/unitree_g1/unitree_g1.json")
# Load source motion as MotionData. SMPL-X is the implemented adapter today.
source = SmplxDataSource(
motion_file=Path("path/to/motion_stageii.npz"),
model_directory="path/to/smplx/models",
gender="neutral",
target_names_override=cfg.get("source_target_names"),
betas=cfg.get("smplx_betas"),
)
motion = source.load()
retargeter = OmniRetargeter(
robot_urdf_path=cfg["urdf_path"],
terrain_mesh_path="path/to/terrain.obj",
joint_mapping=cfg["joint_mapping"],
robot_height=cfg.get("robot_height"),
source_target_names=motion.target_names,
height_estimation=cfg.get("height_estimation"),
base_orientation=cfg.get("base_orientation"),
retargeting=cfg.get("retargeting"),
link_offset_config=cfg.get("link_offset_config"),
)
source_to_robot_scale, retargeted_motion = retargeter.retarget_motion(
motion,
enable_terrain_scaling=True,
visualize_trajectory=False,
)
print(f"Source-to-robot scale factor: {source_to_robot_scale}")
print(f"Retargeted motion shape: {retargeted_motion.shape}") # (T, 7 + DOF)For a ready-to-run setup, omniretargeting ships with robot profiles under
robot_models/. These profiles contain robot assets, target-to-link mappings,
height/orientation helpers, link offsets, and retargeting settings.
from omniretargeting import OmniRetargeter, load_robot_config
cfg = load_robot_config("robot_models/unitree_g1/unitree_g1.json")
retargeter = OmniRetargeter(
robot_urdf_path=cfg["urdf_path"],
terrain_mesh_path="path/to/terrain.obj",
joint_mapping=cfg["joint_mapping"],
robot_height=cfg.get("robot_height"),
source_target_names=cfg.get("source_target_names"),
height_estimation=cfg.get("height_estimation"),
base_orientation=cfg.get("base_orientation"),
retargeting=cfg.get("retargeting"),
link_offset_config=cfg.get("link_offset_config"),
)The retargeting core consumes either a MotionData object, a DataSource, or a numpy array of target positions with shape (T, J, 3):
- T: Number of frames
- J: Number of ordered source targets
- 3: (x, y, z) coordinates in world frame
MotionData can also carry target_names, optional root orientations/translations, framerate, source height, and source-specific metadata:
from omniretargeting import MotionData
motion = MotionData(
positions=positions, # (T, J, 3)
target_names=["Pelvis", "Head"], # optional but recommended
framerate=30.0,
human_height=1.72,
)SMPL-X is currently the implemented source adapter. It returns MotionData through SmplxDataSource.load() and can read:
1. Pre-processed files (.npy):
from omniretargeting.data_sources.smplx import SmplxDataSource
motion = SmplxDataSource(motion_file=Path("trajectory.npy")).load()2. Pre-processed files (.npz with 'global_joint_positions'):
motion = SmplxDataSource(
motion_file=Path("trajectory.npz"),
model_directory="/path/to/smplx/models",
).load()
# Looks for 'global_joint_positions' key for positions
# Looks for 'full_pose' and 'root_orient' keys for orientations3. Raw SMPL-X-NG files (stageii.npz):
Raw SMPL-X-NG files contain SMPL-X parameters, not joint positions. Keys include:
'gender','surface_model_type','mocap_frame_rate''trans','poses','betas''root_orient','pose_body','pose_hand','pose_jaw','pose_eye'
To load these, provide the SMPL-X model path:
motion = SmplxDataSource(
motion_file=Path("HumanEva_S3_Jog_1_stageii.npz"),
model_directory="/path/to/smplx/models",
gender="neutral",
).load()For compatibility, omniretargeting.utils.load_smplx_trajectory() still returns (positions, orientations); new code should prefer SmplxDataSource and MotionData.
A dictionary mapping source target names or IDs (keys) to robot body/link names (values) as they appear in the URDF:
joint_mapping = {
"Pelvis": "pelvis",
"L_Hip": "left_hip_roll_link",
"R_Hip": "right_hip_roll_link",
"Spine1": "waist_yaw_link",
"L_Knee": "left_knee_link",
"R_Knee": "right_knee_link",
"L_Ankle": "left_ankle_roll_link",
"R_Ankle": "right_ankle_roll_link",
"L_Shoulder": "left_shoulder_roll_link",
"R_Shoulder": "right_shoulder_roll_link",
"L_Elbow": "left_elbow_link",
"R_Elbow": "right_elbow_link",
"L_Wrist": "left_wrist_yaw_link",
"R_Wrist": "right_wrist_yaw_link",
}For the current SMPL-X adapter, the default target ordering is:
Pelvis, L_Hip, R_Hip, Spine1, L_Knee, R_Knee, Spine2, L_Ankle, R_Ankle,
Spine3, L_Foot, R_Foot, Neck, L_Collar, R_Collar, Head, L_Shoulder,
R_Shoulder, L_Elbow, R_Elbow, L_Wrist, R_Wrist
Pass the corresponding order to OmniRetargeter(source_target_names=...). Any key in joint_mapping must be present in source_target_names, and each mapped value must match a body name in the robot URDF; unresolved robot body entries are filtered out with a warning at initialization.
Supports common mesh formats:
.obj(Wavefront OBJ).stl(STL mesh).ply(Polygon File Format).gltf/.glb(glTF)
Optional Terrain Scaling: the terrain mesh is unscaled by default. If enable_terrain_scaling=True is passed to retarget_motion() (or --output-scaled-terrain is set on the CLI), OmniRetargeting computes a source-to-robot scale factor from the robot/source height ratio and retargets against the scaled source motion and scaled mesh.
Standard URDF format for humanoid robots. The system automatically:
- Detects robot height from the default pose (overridable via
robot_height) - Reads joint limits and types from the URDF
- Loads visual meshes for (optional) visualization
source_to_robot_scale, retargeted_motion = retargeter.retarget_motion(
motion,
framerate=30.0,
enable_scene_scaling=True,
)source_to_robot_scale:1.0by default, or the computed robot/source height ratio whenenable_scene_scaling=True.retargeted_motion: Numpy array of shape(T, 7 + DOF)containing:[0:3]: Root position (x, y, z)[3:7]: Root quaternion in wxyz order (MuJoCo convention)[7:]: Joint angles in radians
python -m omniretargeting.main --output my_motion.npz ... writes a .npz
containing the following keys (the output filename is also normalized to end
with _retargeted.npz if it doesn't already):
| Key | Shape | Description |
|---|---|---|
framerate |
scalar | Motion framerate (from file or --framerate). |
joint_names |
(DOF,) |
Robot joint names (excluding the floating base). |
joint_pos |
(T, DOF) |
Joint angles in radians. |
base_pos_w |
(T, 3) |
Root position in world frame. |
base_quat_w |
(T, 4) |
Root quaternion in world frame (wxyz). |
If --output-scaled-terrain is provided, the scaled terrain mesh used for
retargeting is exported to that path as well. If --scaled-objects DIR is
provided and the source adapter exposes an object mesh, the CLI also exports a
scaled object mesh plus per-frame object poses into that directory.
retargeter = OmniRetargeter(
robot_urdf_path=robot_urdf,
terrain_mesh_path=terrain_mesh,
joint_mapping=joint_mapping,
robot_height=1.8 # Override auto-detected height
)The CLI is driven by a per-robot JSON profile. The URDF path, joint mapping, and retargeting settings all come from the profile — the CLI does not accept a separate URDF argument.
New workflows should use --source-config, which moves source-specific options
into a YAML file instead of requiring many CLI flags.
SMPL-X example
python -m omniretargeting.main \
--robot-config robot_models/unitree_g1/unitree_g1.json \
--source-config config_templates/smplx_template.yaml \
--terrain /path/to/terrain.obj \
--output /path/to/output.npz \
--output-scaled-terrain /path/to/scaled-terrain.obj \
--framerate 30 \
--penetration-resolver xyz_nudgeOMOMO / object-interaction example
python -m omniretargeting.main \
--robot-config robot_models/unitree_g1/unitree_g1.json \
--source-config config_templates/omomo_floorlamp_example.yaml \
--terrain /path/to/terrain.obj \
--output /path/to/output.npz \
--output-scaled-terrain /path/to/scaled-terrain.obj \
--scaled-objects /path/to/scaled-objects \
--save-video /path/to/output.mp4The legacy flags still work for existing scripts and tests, but they now emit
DeprecationWarnings and should be migrated to --source-config over time.
python -m omniretargeting.main \
--robot-config robot_models/unitree_g1/unitree_g1.json \
--motion /path/to/motion_stageii.npz \
--model-dir /path/to/smplx/models \
--terrain /path/to/terrain.obj \
--output /path/to/output.npzMain arguments:
| Flag | Default | Description |
|---|---|---|
--robot-config |
robot_models/unitree_g1/unitree_g1.json |
Path to robot profile JSON. |
--source-config |
None |
Recommended YAML source configuration file. See config_templates/. |
--output |
(required) | Output .npz path (normalized to end in _retargeted.npz). |
--terrain |
flat ground | Path to terrain mesh; a default flat terrain is generated if omitted. |
--output-scaled-terrain |
None |
Enables scene scaling and exports the scaled terrain mesh. |
--scaled-objects |
None |
Directory for scaled object mesh exports and per-frame object poses when the source adapter provides them. |
--framerate |
auto / 30 | Motion framerate; auto-detected from the source file when possible. |
--vis |
off | Launch a MuJoCo viewer on the retargeted motion. |
--save-video PATH |
off | Render the retargeted motion to video (requires imageio[ffmpeg], and MUJOCO_GL=egl/osmesa for headless). |
--replace-cylinders-with-capsules |
off | Swap cylinder collision geoms for capsules (IsaacLab/PhysX convention). |
--penetration-resolver {hard_constraint,xyz_nudge} |
xyz_nudge |
Contact handling mode; overrides the value in the profile. |
Legacy source-loading flags:
| Flag | Status | Description |
|---|---|---|
--source |
deprecated | Source entry name or source type from the robot profile. |
--motion |
deprecated | Legacy path to source motion file. |
--source-options |
deprecated | Legacy JSON object with adapter-specific options. |
--model-dir |
deprecated | Legacy adapter model directory, e.g. SMPL-X model files. |
--smplx_motion |
deprecated alias | Legacy alias for --motion. |
--smplx_model_dir |
deprecated alias | Legacy alias for --model-dir. |
Keep one JSON profile per humanoid robot (for example under
robot_models/<robot_name>/). Relative urdf_path values are resolved against
the profile file's directory.
Current shipped profiles use a flat schema:
name– optional profile name, used in log outputurdf_path– required, path to the robot URDF (relative to the profile file)joint_mapping– required, source target name → robot body namerobot_height– optional override for auto-detected robot heightsource_target_names/smplx_joint_names– optional custom source target orderingheight_estimation– source target names andhead_top_offsetused to estimate source heightbase_orientation– source target names used to estimate root orientation (pelvis,left_hip,right_hip,spine)link_offset_config– optional robot-link local offsets for mapped link target pointsretargeting– solver settings forwarded toGenericInteractionRetargeter:collision_detection_thresholdterrain_sample_pointsreplace_cylinders_with_capsulespenetration_resolver:"hard_constraint"or"xyz_nudge"foot_stabilization: nested block (seerobot_models/unitree_g1/unitree_g1.json) that controls the post-processing XYZ-nudge pass (enabled,clearance,surface_clearance,contact_clearance,xy_correction_gain, smoothing windows, wall-contact thresholds, etc.)
load_robot_config() also accepts the newer nested profile shape with robot, retargeting.solver, active_source, and source entries. The loader normalizes both shapes into the same keys used above.
# Check if joint mapping is valid
missing_joints = retargeter.validate_joint_mapping()
if missing_joints:
print(f"Warning: Missing joints: {missing_joints}")
# Get robot information
print(f"Robot DOF: {retargeter.get_robot_dof()}")
print(f"Joint names: {retargeter.get_joint_names()}")pytest tests/Main class for motion retargeting (defined in omniretargeting/core.py).
OmniRetargeter(
robot_urdf_path,
terrain_mesh_path,
joint_mapping,
robot_height=None,
source_target_names=None,
height_estimation=None,
base_orientation=None,
retargeting=None,
link_offset_config=None,
)retarget_motion(motion, base_orientations=None, base_translations=None, framerate=None, visualize_trajectory=True, enable_terrain_scaling=False)→(source_to_robot_scale, retargeted_motion)get_robot_dof()→intget_joint_names()→List[str]validate_joint_mapping()→List[str](robot body names fromjoint_mappingthat are missing from the URDF)
from omniretargeting import load_robot_config
cfg = load_robot_config("robot_models/unitree_g1/unitree_g1.json")Loads a robot profile JSON, resolves urdf_path relative to the profile file, and normalizes legacy flat and nested profile fields. Raises if no non-empty mapping is available.
from pathlib import Path
from omniretargeting.data_sources.smplx import SmplxDataSource
motion = SmplxDataSource(
motion_file=Path("motion_stageii.npz"),
model_directory="/path/to/smplx/models",
gender="neutral",
).load()Returns MotionData. SMPL-X joint orientations, when available, are stored in motion.metadata["joint_orientations"] as wxyz quaternions.
Declared in pyproject.toml / setup.py:
- numpy, scipy, matplotlib, tqdm
- torch
- trimesh, smplx, jinja2
- mujoco (≥3.7 for URDF
strippath=falsedefault) - viser, yourdfpy, robot_descriptions
- cvxpy, libigl, tyro
- open3d, pyvista
OmniRetargeting adapts the interaction-mesh retargeting approach from the holosoma_retargeting project to work with generic robots and terrains:
- Source-to-Robot Scaling (optional): Computes the robot/source height ratio and uses it to scale source motion and the terrain mesh before retargeting (enabled by
enable_terrain_scaling=Trueor--output-scaled-terrain). - Generic Robot Support: Works with any URDF through automatic model loading, body-name validation, and auto-detected height.
- Interaction Mesh: Builds a tetrahedral interaction mesh from mapped source targets and terrain sample points.
- Optimization: Per-frame SQP optimization with Laplacian-deformation objective, joint limits, and a target base-orientation term for smoothness.
- Collision / Penetration Handling: Two modes selectable via
retargeting.penetration_resolver:hard_constraint– penetration inequalities inside the SQP.xyz_nudge– post-optimization foot stabilization that projects probe points out of the terrain and smooths XY drift (seefoot_stabilizationin the profile).
- Joint Limits: Respects robot joint limits throughout.
- Coordinate-system alignment: Source adapters should provide positions in the project world frame. The current SMPL-X adapter assumes trajectories are already in a +Z-up world frame.
- Foot stabilization tuning: The
xyz_nudgeresolver is effective on flat and mildly uneven terrain but may need per-robot tuning (foot_stabilizationblock in the profile) for complex scenes with walls. - Object interaction (v1):
- Objects are represented as concatenated point clouds
(T, N, 3)without per-object identity. - No explicit robot-object collision constraints (relies on Laplacian preservation only).
- Non-convex objects sampled as points may cause issues if penetration constraints are added in future versions (convex hull of points != actual object geometry).
- No per-object Laplacian weights or metadata tracking.
- Adapters must provide object points in world frame; core does not handle object pose transformation.
- Objects are represented as concatenated point clouds
We welcome contributions! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License. See LICENSE for the full text.
This repository is a re-implementation of the OmniRetarget method. If you use this code in your research, please cite the original paper:
@article{yang2025omniretarget,
title={OmniRetarget: Interaction-Preserving Data Generation for Humanoid Whole-Body Loco-Manipulation and Scene Interaction},
author={Yang, Lujie and Huang, Xiaoyu and Wu, Zhen and Kanazawa, Angjoo and Abbeel, Pieter and Sferrazza, Carmelo and Liu, C. Karen and Duan, Rocky and Shi, Guanya},
journal={arXiv preprint arXiv:2509.26633},
year={2025},
url={https://arxiv.org/abs/2509.26633}
}