Lizhi Yang, Fengze Xie*, Yuyang Wang*, Lu Gan, Yisong Yue, Aaron D. Ames
* Equal contribution.
[Website] [Paper] [Video] [Live demo]
v0.1: a human interface (the policy follows a PICO headset's body tracking
directly, with all 24 joints or five points), a pretrained policy
for the G1, X2 Ultra and T2, g1_amber for a G1 with weak waist motors, a held
actuator delay in training for robustness to latency on hardware, teleop running
entirely on the G1's own computer (deploy/onboard/), and
updated X2 Ultra and T2 models (vendor gains, Booster's motor model).
Disclaimer. MorphMimic builds on SuperMimic (Liao, Shao, Long, Zakka, Horowitz, Abbeel and Sreenath, UC Berkeley; citation below). The base algorithm is our own implementation of that paper, written from the paper alone. It is not the authors' code, and they have not reviewed or endorsed it or our extensions. Any mistakes are ours.
SuperMimic trains a humanoid motion-tracking policy by backpropagating through a learned, differentiable world model. MorphMimic trains several robots at once: the Unitree G1, the AgiBot X2 Ultra and the Booster T2, simulated in mjlab (MuJoCo Warp). The world model, the policy, or both can be shared between the robots. Every robot is mapped to the G1's 29-joint layout, and the networks get a morphology vector (gains, limits, limb lengths, masses) that tells them which body they are driving.
The policy takes two kinds of goal:
- motion: a full reference motion of the robot (all joints and bodies).
- human: a human's motion, given as the 24 SMPL joints of a uniform skeleton over the
next 10 frames. It can use all 24 joints (
full) or only the pelvis, wrists and ankles (five, what a VR headset with two controllers and two ankle trackers gives you). A PICO headset's body tracking goes straight into the policy, with no retargeting. The human encoder is distilled from the motion interface and feeds the same decoder, so one policy serves both.
No state estimation. The policy reads only the IMU (orientation and angular velocity) and the joint encoders. Its goals do not depend on where the robot is: the motion goal's path is in the reference's own heading frame, and the human skeleton is in its own heading frame plus the human chest's orientation relative to the robot's torso. You do not need odometry, motion capture or a base-velocity estimator. Only the critics, which are used only in training, see the robot's offset from the reference.
If you do have the robot's pose (from motion capture or a state estimator), the anchor mode gives the motion goal relative to the robot itself, so the policy corrects its drift from the reference path.
src/morphmimic/
data/ SEED CSV -> 50 Hz npz, SOMA BVH -> human sidecars, clip filters,
subsets, GPU motion library
env/ mjlab tracking env (multi-clip command, reset sampling, observations)
model/ state layout, analytic reward heads, world model, policy, critic,
human goal features
robots/ G1, X2 Ultra and T2: registry, translator to the 29-joint layout,
morphology vector, vendored X2 and T2 models and their actuators
train/ replay buffer, collector, imagination, trainer, multi-robot sharing
eval/ benchmark, drift diagnostic, policy loader
scripts/ convert_seed, convert_human, check_human, build_subsets, train, eval,
drift, play, render
tools/soma_retargeter/ retargets the SEED motions to the X2 and the T2
deploy/
export_onnx.py training checkpoint -> policy.onnx + <robot>.json
export_ckpt.py training checkpoint -> inference-only checkpoint
run.py one-command deployment: the policy node plus the sim or the G1
sim2sim/ policy node (onnxruntime) + CPU MuJoCo sim node, over ZMQ
vr_teleop/ PICO headset reader, human skeleton and GMR targets, streamer
hardware/ the real G1 on the same protocol as the sim, safety layer
onboard/ everything on the G1's own computer, PICO in a container
tools/ headset-free test sessions for teleop
You need an NVIDIA GPU for training and uv.
git clone --recursive git@github.com:lzyang2000/morphmimic.git && cd morphmimic
uv sync # Python 3.11, mjlab 1.6.0, torch (cu128)
uv sync --group export --group teleop # adds onnxruntime, pyzmq and GMR for deploymentwarp-lang is pinned below 1.18 because 1.18 needs a CUDA 13 driver. Drivers with
CUDA 12.9 (575 or newer) work.
Training uses the BONES-SEED
motion dataset. The commands below use $DATA for wherever you keep it.
G1. Download g1/csv/<session>/*.csv and seed_metadata_v003.csv. Then convert the
clips to 50 Hz npz and build the subsets (about 5 minutes with 96 workers, about 91 GB
on disk):
uv run python -m morphmimic.scripts.convert_seed \
--csv-dir $DATA/seed/g1/csv --metadata $DATA/seed/seed_metadata_v003.csv \
--out-dir $DATA/seed_g1_npz50 --workers 96
uv run python -m morphmimic.scripts.build_subsets \
--manifest $DATA/seed_g1_npz50/manifest.csv --out-dir $DATA/seed_g1_npz50/subsetsThis writes subsets/{tiny,1h,7h,40h,all}.yaml. The filters keep 129,398 clips
(262.8 h).
Human sidecars (for the human interface). Download SEED's SOMA uniform-skeleton BVH
files (soma_uniform/bvh/<session>/*.bvh) and convert them to 50 Hz sidecars of the 24
SMPL joints, one per clip, in human50/ next to the clips. All three robots use the
same clips, so the G1's sidecars serve the X2 and the T2 too:
uv run python -m morphmimic.scripts.convert_human --npz-root $DATA/seed_g1_npz50 \
--bvh-root $DATA/seed/soma_uniform/bvh --workers 96
uv run python -m morphmimic.scripts.check_human \
--subset-yaml $DATA/seed_g1_npz50/subsets/1h.yaml # sidecars against their clipsX2 Ultra and T2. SEED has no retarget for these robots, so we retarget the SOMA BVH
files with NVIDIA's SOMA Retargeter
(commit cbdb2b4). The scripts in tools/soma_retargeter/ handle the steps around it:
stage_subset.pylinks the BVH files of a G1 subset into one folder.assemble_robots.pybuilds the retargeter's robot folder: its own X2 target with an effector fix, and our hand-written T2 target, both with the simulator's soft joint limits.- Fill the
__IMPORT__,__EXPORT__and__ROBOTS__placeholders inbatch_x2.jsonorbatch_t2.jsonwith absolute paths, and run the retargeter'sapp/bvh_to_csv_converter.py --config <that file>. For the full set, split the work withshard_stage.pyandlaunch_shards.sh, then check the output withvalidate_csv.py. - Convert the CSVs, and build the subsets from the G1's clip lists so every robot has the same clips:
for r in x2 t2; do
uv run python -m morphmimic.scripts.convert_seed --robot $r \
--csv-dir $DATA/seed_${r}_csv --metadata $DATA/seed/seed_metadata_v004.csv \
--out-dir $DATA/seed_${r}_npz50 --workers 64
uv run python -m morphmimic.scripts.build_subsets \
--manifest $DATA/seed_${r}_npz50/manifest.csv --out-dir $DATA/seed_${r}_npz50/subsets \
--clips-from $DATA/seed_g1_npz50/subsets/all.yaml
doneThe defaults follow SuperMimic's Table A.5: 4096 envs per GPU, H=32 imagined steps, k=8, γ=0.99, λ=0.95, learning rate 2e-4 with cosine decay. Multi-robot runs put one robot on each GPU, so they need three GPUs on one machine.
G1=$DATA/seed_g1_npz50/subsets/all.yaml
X2=$DATA/seed_x2_npz50/subsets/all.yaml
T2=$DATA/seed_t2_npz50/subsets/all.yaml
COMMON="--kp-interface --kp-student copy --no-kp-preview-mix \
--human-root $DATA/seed_g1_npz50/human50 --library-dtype float16 \
--iterations 60000 --ckpt-every 4000 --log-dir logs"COMMON is our current recipe. It trains the human interface alongside the motion
interface. The human encoder learns from the motion encoder (--kp-student copy): it
copies the motion interface's action and pulls its latent towards the motion latent, on
its own imagined states. Half of its samples see all 24 joints and half see only the
five (--kp-masking mixed, the default), and it always sees the full 10-frame preview
(--no-kp-preview-mix).
Unified: one world model and one policy for all three robots.
uv run python -m morphmimic.scripts.train --robots g1 x2 t2 \
--subset-yamls $G1 $X2 $T2 --share all --gpu-ids "[0,1,2]" $COMMON --run-name unifiedOur own G1 has weak waist roll and pitch motors, so we train with --robots g1_amber x2 t2 instead (same subset yamls). See Robot models. The policy we run on
our G1 comes from that run.
One world model, a policy per robot.
uv run python -m morphmimic.scripts.train --robots g1 x2 t2 \
--subset-yamls $G1 $X2 $T2 --share wm --gpu-ids "[0,1,2]" $COMMON --run-name shared_wmOne policy, a world model per robot.
uv run python -m morphmimic.scripts.train --robots g1 x2 t2 \
--subset-yamls $G1 $X2 $T2 --share policy --gpu-ids "[0,1,2]" $COMMON --run-name shared_policySingle robot (its own world model and policy, one GPU).
uv run python -m morphmimic.scripts.train --robot g1 --subset-yaml $G1 --gpu 0 \
$COMMON --run-name single_g1 # or --robot x2 / t2 with $X2 / $T2Notes:
--kp-interfaceadds the human interface and needs the human sidecars (--human-root). Leave out the four human flags to train the motion interface only.--kp-driftand--kp-jitteradd slow drift and per-joint noise to the human encoder's input. Both are off by default because a three-robot policy trained with them chattered on our real G1.- A multi-robot run writes each robot's checkpoints to
logs/<run>/<robot>/(ckpt_<it>.ptandlatest.pt). - To resume, rerun the same command with
--resume logs/<run>/latest.pt(single robot) or--resume logs/<run>(multi-robot). - For a quick try, use the 1 h subset (
subsets/1h.yaml) and drop--library-dtype. - Memory at 4096 envs: about 28 GB per GPU on the 1 h subset and 51 to 58 GB on the full set. With the full set, each robot's process also needs about 120 to 130 GB of host RAM.
- The first iterations compile the updates into CUDA graphs, which takes a few minutes.
--compile noneruns eagerly instead. - By default, training adds an actuator delay of 0 to 20 ms, drawn once per episode and
then held, because a real robot's latency is steady (
--actuator-delay-max-ms,--actuator-delay-fixed). With the earlier default, 0 to 40 ms redrawn every physics step (--no-actuator-delay-fixed), our three-robot policy kept stepping after a step on the real G1 once its command latency rose by a few ms.--no-actuator-delayturns the delay off. - The recipe stops at 60,000 iterations. The cosine decay still runs over its default
250,000 (
--lr-decay-iters), so the learning rate is still high there. Our later checkpoints of the same run (78,000 and on) swayed while standing on the G1, and the training metrics did not show it. --wandblogs to Weights & Biases, usingWANDB_ENTITYandWANDB_PROJECT(defaultmorphmimic) from the environment.uv run python -m morphmimic.scripts.train --helplists every option.
By default the motion goal ignores where the robot is, so the policy cannot correct
drift from the reference path. --goal-anchor robot gives the reference anchor (torso)
position and orientation relative to the robot's own anchor instead. Drift then shows up
in the goal and the policy steers back onto the path. The cost is that the policy needs
the robot's world position and heading at deploy time, from motion capture or a state
estimator.
uv run python -m morphmimic.scripts.train --robots g1 x2 t2 \
--subset-yamls $G1 $X2 $T2 --share all --gpu-ids "[0,1,2]" \
--goal-anchor robot --library-dtype float16 --iterations 40000 --ckpt-every 4000 \
--log-dir logs --run-name unified_anchor- The anchor mode trains the motion interface only. It refuses
--kp-interface, because the human goals have no position. - A checkpoint keeps its anchor mode. Resuming or exporting it with the other mode fails with a message naming the flag to use.
- Export with
--goal-anchor robot(step 1 below); the anchor goes into each<robot>.json. Run it with--interface motion. - The sim node sends the floating-base position and yaw with every state message, so
the anchor mode works in sim2sim as is. On hardware the pose comes from a
PoseSource(deploy/hardware/pose_source.py). The G1 node has none yet, so it sends no pose and the policy node stops rather than act without one.
The benchmark plays every clip of a subset once from its first frame, with a deterministic policy and no randomisation, and writes a per-clip table (completion, tracking errors, drift):
uv run python -m morphmimic.scripts.eval --ckpt logs/unified/g1/ckpt_60000.pt \
--subset-yaml $G1 --num-envs 1024 # the motion interface
uv run python -m morphmimic.scripts.eval --ckpt logs/unified/g1/ckpt_60000.pt \
--subset-yaml $G1 --interface human --human-points full # or fivescripts.play and scripts.render show a checkpoint on a clip in mjlab; --help lists
their options.
The X2 Ultra runs AgiBot's own joint gains with MuJoCo's built-in PD. The T2 runs
Booster's deployment gains with a model of Booster's motors, whose torque falls off with
speed as in Booster's own training. The earlier release of this repository (commit
aa40fe4) used a DC-motor model for the X2 and other gains for the T2, so its numbers
will not reproduce exactly with this code.
g1_amber is the G1 with the waist roll and pitch damping raised from 1.81 to 3.0, for
a robot with weak waist motors (like ours). Nothing else changes: the body, the
reference motions and the deployment are the G1's. On the robot, run it with --robot g1_amber --hardware g1.
Deployment does not need the training stack. A policy process (numpy, onnx and onnxruntime only) and a robot process talk over ZMQ. The robot process is either CPU MuJoCo (sim2sim) or the real G1 (sim2real); the policy process is the same for both.
The policy we run on our G1 is attached to the
v0.1 release. It comes
from the unified recipe with --robots g1_amber x2 t2, at 60,000 iterations, exported
as in step 1. The one policy.onnx drives the G1 (as g1_amber), the X2 Ultra and the
T2, through either interface.
curl -LO https://github.com/lzyang2000/morphmimic/releases/download/v0.1/morphmimic_policy.zip
unzip morphmimic_policy.zip
(cd morphmimic_policy && sha256sum -c SHA256SUMS)
W=morphmimic_policyUse $W wherever the steps below use an export, and skip step 1. Pass --robot g1_amber for the G1 (--robot g1_amber --hardware g1 on the robot), or --robot x2 /
--robot t2.
The policy is provided as is, with no warranty and no support. We have tested it on a real G1 only, where we picked this checkpoint; your G1 may behave differently. On the X2 Ultra and the T2 it has run only in the sim: their sim2real tests are waiting for the robots. Start any robot hung or held, and keep the remote ready to damp it.
Export a trained run to ONNX. A multi-robot run gives one policy.onnx for all three
robots and both interfaces, plus one <robot>.json per robot (its normalisers,
morphology vector and constants):
uv run --group export python -m deploy.export_onnx logs/unified out/unified --iteration 60000A single-robot checkpoint (logs/single_g1/ckpt_60000.pt) exports the same way. Before
writing anything, the export checks that the ONNX model reproduces the PyTorch actions.
This writes the robot's MuJoCo model and the reference clips you want to play. It needs mjlab and a GPU; everything after it runs on the CPU.
uv run python -m deploy.sim2sim.export_assets --robot g1 \
--subset-yaml $DATA/seed_g1_npz50/subsets/1h.yaml \
--clips walk_ff_loop_315_R_001__A257 --out assets--clips takes any clip names in that subset. Use the robot's own subset
(seed_x2_npz50 for --robot x2, and so on).
W=out/unified # the export from step 1
uv run --group export python -m deploy.run --deploy $W --assets assets \
--robot g1 --clip walk_ff_loop_315_R_001__A257 --interface motion --viewer native--interface motiontracks the whole reference.--interface human --human-points full(orfive) tracks the clip's human motion instead; pass its sidecar with--human-clip $DATA/seed_g1_npz50/human50/<session>/<clip>.npz.--viewer nativeopens MuJoCo's viewer. Without a display, useMUJOCO_GL=egl ... --video out.mp4.--delay-ms 20adds actuator delay.- At the end, the run prints whether the clip completed, the joint and body errors, and the drift.
You need a PICO 4 (or 4 Pro) headset with its two controllers, two PICO Motion Trackers on your ankles, and a Linux PC on the same Wi-Fi.
One-time setup:
bash deploy/install_pico.shbuilds the PICO SDK binding into.venv. After that, useuv sync --group teleop --inexact, because a plainuv syncremoves the SDK.- On the PC, install and start the
XRoboToolkit PC service
(a
.deb, needs sudo). - On the headset, enable developer mode and install the XRoboToolkit app. Pair and calibrate the ankle trackers in PICO's Motion Tracker app.
- In XRoboToolkit, enter the PC's IP address, tick Head and Controller, select Send, and set Pico Motion Tracker to Full body.
Then run the streamer and the sim in two terminals:
# Terminal 1: headset -> the operator on the uniform skeleton at 50 Hz (no retargeting)
uv run --group teleop python -m deploy.vr_teleop.streamer --target human
# Terminal 2: policy and sim, the human interface
uv run --group export --group teleop python -m deploy.run --deploy $W \
--assets assets --robot x2 --teleop --interface human --human-points full --viewer native--human-points five uses only your pelvis, wrists and ankles. For the motion
interface, the streamer retargets you to the robot with GMR instead
(streamer --robot x2, then --interface motion).
Controls (A and B on the right controller, X and Y on the left):
| Press | Mode | What happens |
|---|---|---|
| A+B+X+Y | OFF | Calibrate and go to IDLE. Stand upright and still. |
| A+X | IDLE | POSE: the robot follows you. Stand roughly as the robot stands first: the robot blends into your pose over 0.5 s. |
| A+X | POSE | Back to IDLE: the robot returns to its standing pose. |
| hold left Menu | POSE | Pause: the robot holds its current pose. Release to continue. |
| A+B+X+Y | IDLE or POSE | Stop: back to OFF. The robot returns to its standing pose and ignores you until you calibrate again. |
The robot never damps from the headset. In OFF and IDLE it holds its standing pose; only the robot's remote damps it (The real G1). A button combo fires after it is held for 0.1 s, once per press.
When you enter POSE, your heading is aligned to the robot's. Where you stand in the room
does not matter. If the stream stops for 3 s, the robot goes to IDLE. With GMR
(--target gmr), --human-height 1.7 on the streamer sets your height for the
retargeting (default 1.8 m); the human target measures you at calibration instead.
Without a headset. Record a session once with streamer.py --record session.npz
and replay it for any robot and either interface (--replay session.npz). You can also
make a GMR session from a G1 clip:
uv run --group teleop python -m deploy.tools.make_gmr_session wave.npz \
--clip $DATA/seed_g1_npz50/230713/wave_R_001__A430.npz
MUJOCO_GL=egl uv run --group export --group teleop python -m deploy.run \
--deploy $W --assets assets --robot t2 --teleop --replay wave.npz \
--max-steps 700 --video t2_wave.mp4The generated session calibrates at 1 s, enters POSE at 4 s, plays the clip, then returns to IDLE.
In our paper, the shared policy from the 40,000-iteration unified run, exported as in step 1, dances, walks and follows a teleoperator on a physical G1.
By default, everything runs on the G1's own computer and the headset talks to the robot
directly; see deploy/onboard/. The streamer (the PICO
service and streamer.py) can also run on any machine on the robot's network, and the
policy node reads it with --streamer tcp://<host>:5556. The steps below drive the
robot from a PC on its network instead.
Once, on the PC connected to the robot:
bash deploy/install_unitree_sdk.sh # builds the Unitree SDK binding into .venv (sudo for apt)Then, with the robot powered on and hung or held, add --hardware g1 --net <iface> to
the same command. Start with neither --clip nor --teleop, so the policy tracks the
neutral stand. --max-steps 1500 ends each policy run after 30 s:
uv run --group export --group teleop python -m deploy.run --deploy $W \
--robot g1 --hardware g1 --net <iface> --interface motion --max-steps 1500Add --assets assets --clip <name> to play a motion, or --teleop --interface human --human-points full to follow the headset (with the streamer on --target human).
--record ticks.npz saves every policy tick: the measured joint positions and
velocities, the IMU, and the joint targets sent. The robot's remote controls the node:
| Button | What happens |
|---|---|
| START | From damping, move to the standing pose over 2 s and hold it. |
| A | Start the policy. Without teleop it holds the neutral stand, facing the robot's heading. |
| Y | With a clip, while the policy stands: play the clip, then return to the stand. |
| B | Damp at the current pose (START stands again). |
| SELECT | Damp and exit (Ctrl-C does the same). |
- On hardware a clip always starts from the stand. Each Y turns the clip to the robot's
heading and blends into its first frame over 2 s (
--from-stand Schanges this). After the clip, the robot blends back to the stand in 1 s. A Y while a clip plays is ignored. Pick a clip that starts and ends standing still. - The gains are the training gains from
g1.json.--kp-scalescales only the stiffness. - If no action arrives for 1 s, the robot does not damp: the targets blend to the PD stand, and the policy restarts on the stand and takes over again with its first action.
- After
--max-steps Nsteps, the robot blends back to the PD stand over 2 s and holds it. A runs the policy again. - Keep the policy machine otherwise idle. A busy CPU can stall the policy.
- The policy can run on another machine: run
policy_node.py --bind-host 0.0.0.0 --robot-host <robot pc>there, andg1_node.py --policy-host <policy pc> --bind-host 0.0.0.0on the robot side.
To try a hardware clip in the sim first, pass --from-stand 2. The robot starts on the
stand, facing --start-yaw degrees, and Y is pressed at each --cue-s time. The run
ends 2 s after the last clip:
uv run --group export --group teleop python -m deploy.run --deploy $W --assets assets \
--robot g1 --clip <name> --interface motion --from-stand 2 --start-yaw 137 \
--cue-s 1 --viewer nativeThe base method is SuperMimic's. These are the places where we filled a gap or chose differently:
- The world model's reward and observation heads are computed analytically, which the
paper allows. Its termination head is the only learned part. By default it is a
four-parameter monotone hazard on the height and tilt margins (
--termination:hazard, an MLP withlearned, or the fixed analyticsoft). - The world model works in the pelvis frame and also gets the gravity direction and
body heights as inputs, as in SuperTrack (
--wm-frame headingfor the yaw-only frame without them). - The critic and the world model have LayerNorm after each hidden layer
(
--no-critic-layer-norm,--no-wm-layer-norm), and the critic's targets are clamped at R_max / (1 - γ) = 10, the largest possible return (--no-critic-target-clip). Without these, our full-set runs diverged. - The first 500 iterations train only the world model (
--wm-only-iters 0to skip). Imagined rollouts start only where they cannot reach the end of a clip (--no-avoid-clip-end). - Episodes end at the last frame of their clip or after 10 s, whichever comes first.
The reset sampler therefore normalises each bin's return by the best return reachable
in that time (
--reset-norm actualnormalises by the steps actually run). - The MLPs run in bfloat16 autocast, about 1.5 times faster than TF32 in our runs
(
--no-bf16). - Goals do not depend on the robot's position, so the policy runs from the IMU and joint encoders alone. This is built into the code, not a flag.
- Training randomises the actuator delay (0 to 20 ms, drawn once per episode and held)
for robustness to latency on the real robot (
--no-actuator-delay).
train --help lists every option.
The code is under the Apache License 2.0. The vendored robot models under
src/morphmimic/robots/ and the submodules in third_party/ keep their own licences.
@misc{yang2026morphmimic,
title = {{MorphMimic}: Morphology-Conditioned World Model and Policy Sharing Across Humanoids},
author = {Yang, Lizhi and Xie, Fengze and Wang, Yuyang and Gan, Lu and Yue, Yisong and Ames, Aaron D.},
year = {2026},
note = {Technical report},
howpublished = {\url{https://lzyang2000.github.io/morphmimic/}}
}MorphMimic builds on:
@inproceedings{liao2026supermimic,
title = {SuperMimic: Scaling Humanoid Whole-Body Control with One Differentiable World Model},
author = {Liao and Shao and Long and Zakka and Horowitz and Abbeel and Sreenath},
booktitle = {Conference on Robot Learning (CoRL)},
year = {2026},
eprint = {2610.28480},
archivePrefix = {arXiv}
}We also use mjlab,
BONES-SEED,
SOMA Retargeter,
GMR,
XRoboToolkit, and the X2 Ultra and T2 models vendored
under src/morphmimic/robots/ with their licences.