Unitree G1 MuJoCo worlds

Robot-agnostic MuJoCo worlds (tables, objects, fixed cameras) for k-valentin/unitree-g1-mujoco, the Unitree G1 simulation LeRobot loads as a Hub env. The robot repo resolves a world by name, merges it into its scene and does everything robot-specific (see "What the robot adds"); a world file knows nothing about the robot.

World What Task
pick_cylinder table with a free upright cylinder (r 2.5 cm, h 15 cm, 50 g) and a table_view camera Pick up the cylinder from the table with the right hand

Use

The robot repo takes a world through WORLD (config.yaml, make_env(world=...) or UNITREE_G1_MUJOCO_WORLD):

# by name, downloaded from this repo (only worlds/pick_cylinder/ is fetched)
UNITREE_G1_MUJOCO_WORLD=pick_cylinder
# pinned to a revision (tag, branch or commit), or from another repo
UNITREE_G1_MUJOCO_WORLD=k-valentin/unitree-g1-mujoco-worlds@<revision>:pick_cylinder
# a local checkout of this repo: every world is resolved from it by name
UNITREE_G1_MUJOCO_WORLDS_ROOT=/path/to/unitree-g1-mujoco-worlds UNITREE_G1_MUJOCO_WORLD=pick_cylinder
# a local folder with a world.xml, or a .xml file
UNITREE_G1_MUJOCO_WORLD=/path/to/my_world
from env import make_env  # the robot repo's env.py

env = make_env(world="pick_cylinder", world_randomize=0.02)

WORLD_GRASP (physics, the default, or attach), WORLD_RANDOMIZE and the other options are described in the robot repo's README.

Layout

worlds/<name>/world.xml     the MuJoCo content (required)
worlds/<name>/world.yaml    world facts (optional)
worlds/<name>/...           anything world.xml references with a relative path (meshes, textures)
tools/robot.py              finds the robot repo for the tools and tests
tools/grasp_diag.py         scripted pick_cylinder grasp with diagnostics
tools/thumb_map.py          AmazingHand thumb pose map used to pick the grasp's thumb poses
tests/                      contract checks of every world, and the pick_cylinder + G1 tests

The contract

  • World frame: x forward, y left, z up; the origin is on the floor where the robot spawns; the robot faces +x.
  • Every body with a free joint is an object: the robot resets it, jitters it and can grasp it. Name it.
  • Every <camera> of the world is published by the robot. Name it, and do not reuse a name the robot has (head_camera, left_wrist_cam, right_wrist_cam, global_view).
  • A world never names a robot body, site, mount, camera or collision bit: no <equality> to robot bodies, no *_ee_mount / *_tcp, no contype / conaffinity (plain MuJoCo defaults, 1 and 1). Names must not clash with the robot's (the robot repo rejects a world that reuses one) and defaults must live in a <default class=...>.
  • <option> is merged into the robot's (the world wins per attribute); <compiler> is dropped. Relative file= paths of meshes, textures, height fields and skins are resolved against the world's folder.
  • world.yaml holds world facts, with these optional keys only (the robot rejects unknown ones):
Key Meaning
support name of the box geom objects are jittered on, e.g. table_top; their edge stays at least 5 cm inside it
randomize default xy jitter in metres of the objects at start and on every reset (+-15 deg yaw too); the robot's WORLD_RANDOMIZE overrides it, 0 switches it off
tasks list of task strings

What the robot adds

When it merges a world, unitree-g1-mujoco ORs the AmazingHand collision bit (2) into the conaffinity of every colliding world geom, so hands touch world objects but never the robot, the floor or themselves; with WORLD_GRASP=attach it adds an inactive {side}_grasp_{body} weld from each hand mount to every object; it starts and resets the AmazingHands in their open pose; and it publishes the world's cameras.

pick_cylinder

World frame as above, for a robot standing at pelvis height 0.74 m (the GR00T controller's default standing pose):

  • table top z = 0.755 m (0.04 m thick), x 0.20..0.80, y -0.40..0.40: the front edge is 0.20 m ahead of the pelvis. The hand's rest pose (arms at zero, TCP at z 0.835) has its lowest finger 1.3 cm above it;
  • cylinder r = 0.025 m, h = 0.15 m, 0.05 kg, centre (0.328, -0.071, 0.83): 0.328 m ahead of the pelvis, 0.071 m to the right and 0.09 m above it. Its contact parameters (friction 1.0, solref 0.008, priority 2) win over the finger pads' (priority 1). The AmazingHand's palm cavity is about 4 cm wide and its fingers span 10 cm in height, so a much thicker or shorter cylinder cannot be wrapped without the lowest finger touching the table;
  • table_view: fixed camera in front of the table, 0.65 m ahead and 0.7 m left of the cylinder, looking at it;
  • <option> is elliptic cones with impratio 10 and implicitfast (the grasp reference's settings); the timestep comes from the robot.

Clearance to the robot's collision geoms, arms at zero: bodies at least 5.5 cm from the table standing and 7 cm hanging on the elastic band (pelvis ~0.92 m), 10 cm from the cylinder.

Scripted side grasp (G1, right AmazingHand)

tools/grasp_diag.py runs a side grasp (fingers forward, palm facing the cylinder, thumb up) with first-contact, orientation and contact-force diagnostics. --physics is the tuned one, --lerobot closes through LeRobot's closure_to_q, --sweep runs the +-2 cm spawn grid, --world-grasp attach the kinematic grasp, --png-dir writes pictures.

With WORLD_GRASP=physics the right AmazingHand lifts the cylinder with finger contacts only: 25/25 on the grid, rise 8-9 cm, slip 1-2 mm, closed-stage normal forces of 10-30 N. It is a palm-and-thumb pinch with finger 3 at the cylinder's far side, not a wrap: the cylinder sits 7.5 cm along the fingers and 3 cm off the palm in the right_tcp frame, fingers 1-3 close to +-70 deg, the thumb from open (-80, -70) to (40, -70) deg over the last 60 % of the 1.2 s close, then a diagonal lift (+8 cm forward per 10 cm up) because the 5-DoF arm holds the palm level only on a slanted plane. The harness drives the arm through the bridge with LeRobot's default gains plus G1ArmIK gravity torques (the teleop's path) and tracks within 3 mm. The right TCP sits (0.075, 0.03, -0.02) m from the cylinder centre in the hand frame, at (0.33, -0.15, 0.86) in the world: that is the pose where the level palm clears the torso and the table, with the cylinder 7.7 cm further forward and 4.9 cm further right than the spawn (the harness moves it). At TCP (0.28, -0.09) the IK pose has the shoulder link inside the torso, which a teleported arm never notices and a PD-driven one cannot reach. The rest of the closed hand's cage is larger than the cylinder, so grasps nearer the palm or the fingertips tilt it. Not confirmed on hardware or in a teleop session.

WORLD_GRASP=attach is the fallback that makes pick-up data collectable without relying on the physical grasp: once an AmazingHand closes (closure >= 0.6) with the cylinder's centre in its grasp zone, the robot welds the cylinder to the hand at the current relative pose until the hand opens again (closure <= 0.4).

Add a world

  1. Create worlds/<name>/world.xml in the world frame above, with named free bodies and cameras and plain collision defaults. Put meshes and textures next to it and reference them with relative paths.
  2. Optionally add worlds/<name>/world.yaml (support, randomize, tasks).
  3. Check it with python -m unittest discover -s tests -k test_worlds (no robot needed) and by loading it: UNITREE_G1_MUJOCO_WORLDS_ROOT=$PWD UNITREE_G1_MUJOCO_WORLD=<name> python run_sim.py in the robot repo.

Tests

MUJOCO_GL=egl python -m unittest discover -s tests -v

tests/test_worlds.py needs only MuJoCo. tests/test_pick_cylinder.py and the tools use the robot repo: UNITREE_G1_MUJOCO_ROOT if set, else a full snapshot of k-valentin/unitree-g1-mujoco (meshes included, so set it to a local checkout whose meshes are real files). The scripted grasp and reach tests also need xr_teleoperate (its G1ArmIK) and are skipped without it; the LeRobot closure test needs lerobot.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support