Downloads · 30 days
41
100% of all-time downloads
k-valentin/unitree-g1-mujoco
unitree-g1-mujoco is a machine learning model from k-valentin. Use it for the machine learning task on the model card, and read the license before you ship it in a product. It is set up for lerobot.
This fork of lerobot/unitree-g1-mujoco defaults to the 23dof (rev10) body with the Pollen Robotics AmazingHand end effector and a D455 pan/tilt head (one published head camera). Body (29dof/23dof), end effector (rubbe…
Downloads · 30 days
41
100% of all-time downloads
All-time downloads
41
Public
Repo size
77 MB
Likes
0
Public
Click a slice to open those files.
.stl71.9 MB · 98%
From the Hugging Face model README
This fork of lerobot/unitree-g1-mujoco defaults to the 23dof (rev_1_0) body with the
Pollen Robotics AmazingHand end effector and a D455 pan/tilt head (one published head
camera). Body (29dof/23dof), end effector (rubber_hand, none, dex1, dex3,
amazing_hand), head mount (fixed/pan_tilt) and head sensor (d435i/d455) combine freely: sim/mjcf/compose.py composes
the selected scene at load time from a bare body and reusable parts files. This is the model
repository loaded dynamically by LeRobot through env.py.
The existing make_env() entry point, body motor commands, body state DDS
topics, simulation stepping and ZMQ image message format are preserved.
Files. Every model is one bare body plus parts files:
| File | What | Origin |
|---|---|---|
assets/g1_29dof.xml, assets/g1_23dof.xml | bodies without hands, bare wrist inertia | extracted from Unitree's MJCF |
assets/end_effectors/rubber_hand.xml | Unitree's stock passive hands | extracted from Unitree's 29dof MJCF |
assets/end_effectors/dex3.xml | Dex3-1 hands | extracted from Unitree's Dex3 MJCF + URDF |
assets/end_effectors/dex1.xml | Dex1-1 grippers | generated by tools/build_dex1_model.py from reference/hiw500/ |
assets/end_effectors/amazing_hand.xml | AmazingHands | generated by tools/build_amazing_hand.py from assets/amazing_hand/ |
assets/heads/pan_tilt.xml | pan/tilt head mount (the fixed mount is the body's own) | from the lab URDF (g1_comp.urdf) |
assets/sensors/d435i.xml | RealSense D435i head camera (stock G1) | D435i color stream optics |
assets/sensors/d455.xml | RealSense D455 head camera | D455 color stream optics |
assets/scene.xml | floor, lights and global camera every model is included into | upstream |
An end-effector parts file defines left_ee_mount / right_ee_mount bodies in the 29dof
wrist_yaw_link frame, with the meshes, defaults, actuators, sensors and equality constraints they
use; a head mount defines a body tree in the 23dof torso_link frame with a head_sensor_mount, and
a sensor file a head_sensor body in that mount's frame. The extracted files name
their source; Unitree's full models are in this repo's history (commit 1801640).
Generated files are checked by ToolsTests and must not be edited by hand.
config.yaml defaults to BODY: 23dof, END_EFFECTOR: amazing_hand and
HEAD_MOUNT: pan_tilt, HEAD_SENSOR: d455. The 23dof body models the rev_1_0 hardware, which has no
waist_roll/waist_pitch and no wrist_pitch/wrist_yaw per arm (6 fewer joints than the
29dof body).
BODY | Base model | Body motors | mode_machine |
|---|---|---|---|
23dof (default) | assets/g1_23dof.xml | 23 | 4 |
29dof | assets/g1_29dof.xml | 29 | 2 |
The DDS LowState_/LowCmd_ messages always carry 35 motor slots. The 23dof
body publishes only slots 0-12, 15-19, 22-26; the 6 slots it has no joint
for (13, 14, 20, 21, 27, 28, i.e. waist_roll/waist_pitch and each arm's
wrist_pitch/wrist_yaw) are left at the SDK default q=dq=tau=0, and any
command written to them is ignored. This mapping lives in
sim/unitree_sdk2py_bridge.py's JOINT_SLOTS.
To use the 23dof body from LeRobot:
lerobot-record \
--robot.type=unitree_g1 \
--robot.is_simulation=true \
--robot.sim_env_repo_id=k-valentin/unitree-g1-mujoco \
...
Direct users of this repository select it with make_env(body="23dof") (or
BODY: 23dof in config.yaml).
End effectors are named after the hardware; each one mounts on either body. config.yaml
defaults to END_EFFECTOR: amazing_hand. Finger counts, effort limits and the published
camera list follow the selection automatically.
| Selection | Parts (from) | Actuated joints per side | Cameras |
|---|---|---|---|
rubber_hand | Unitree's stock passive hand | none | head |
none | bare wrist (no parts) | none | head |
dex1 | Dex1-1 gripper | 2 fingers (DDS) | head + both wrists |
dex3 | Dex3-1 hand (with Dex1's wrist cameras) | 7 (DDS) | head + both wrists |
amazing_hand (default) | AmazingHand | 8 servos (ZMQ bridge) | head |
HEAD_MOUNT: pan_tilt adds the 2-DoF pan/tilt head (ZMQ bridge) to any of them, carrying any
HEAD_SENSOR.
Mounting. The *_ee_mount bodies go inside *_wrist_yaw_link on the 29dof body, and inside
*_wrist_roll_link at the wrist pitch+yaw offsets (+0.084 m) on the 23dof body, i.e. where the hand
sits on a 29dof arm with its wrist pitch/yaw at zero. The bodies carry the bare wrist inertia: on
29dof the URDF's bare wrist_yaw_link; on 23dof Unitree's fused wrist-roll + rubber-hand link minus
the rubber hand's mass (Unitree lumps it into the 29dof wrist_yaw_link, which gives it). Dex hands
on the 23dof body and the AmazingHand on either body have no adapter CAD yet, so those mounts are
placeholders. The composed MJCF is written to a temp directory (<tmp>/unitree_g1_mujoco/) with
absolute mesh paths.
Direct users of this repository can also call:
from env import make_env
if __name__ == "__main__":
env = make_env(end_effector="dex3") # Omit the argument for dex1.
try:
env.reset()
while True:
env.step() # Body commands continue to arrive over DDS.
finally:
env.close()
HEAD_MOUNT: pan_tilt adds a 2-DoF Dynamixel pan/tilt head (assets/heads/pan_tilt.xml), here with
HEAD_SENSOR: d455, an Intel RealSense D455 (assets/sensors/d455.xml), and END_EFFECTOR: amazing_hand a
Pollen Robotics AmazingHand on each wrist
(assets/end_effectors/amazing_hand.xml). config.yaml defaults to BODY: 23dof,
END_EFFECTOR: amazing_hand, HEAD_MOUNT: pan_tilt, HEAD_SENSOR: d455, CAMERAS: ["head_camera"].
Head = mount + sensor: HEAD_MOUNT and HEAD_SENSOR are independent. The fixed mount is
the body's own head_sensor_mount (the stock head, at the pose of the stock camera); pan_tilt
replaces it with assets/heads/pan_tilt.xml. A sensor (assets/sensors/<name>.xml) is a
head_sensor body placed in the mount's head_sensor_mount, looking along its +x with +z up,
holding a head_camera and optionally its own geoms/inertia. A new camera is a new sensors file
and its name in sim/mjcf/compose.py's HEAD_SENSORS.
Camera intrinsics: each sensor's head_camera carries pinhole intrinsics for the recorded
640x480 color mode (resolution, focalpixel, principalpixel, sensorsize), so a 640x480
render has the real camera's geometry. They come from the Intel D400-series datasheet until each
unit's calibration is read (rs-enumerate-devices -c); the derivation is in each sensor file.
Lens distortion is not modelled.
HEAD_SENSOR | Datasheet RGB FOV (H x V) | 640x480 focal length | 640x480 FOV (H x V) |
|---|---|---|---|
d435i (default, stock G1) | 69 x 42 deg (1920x1080) | 623.01 px | 54.4 x 42.1 deg |
d455 | 90 x 65 deg (1280x800) | 380.36 px | 80.1 x 64.5 deg |
Mount chain: torso_link -> head_servo_link (fixed) -> xl330_link (head_yaw_joint,
pan, range -0.7..0.7 rad) -> head_tilt_link (head_pitch_joint, tilt, range -1.5708..0.8 rad)
-> head_sensor_mount (fixed, the front face of the tilt bracket's tip). Both joints are
position actuators with kp=5.
Head meshes: the links are primitive boxes (head_servo_link 20x20x20 mm, xl330_link
20x34x26 mm, head_tilt_link 90x25x25 mm); config.yaml's HEAD_MESHES: true (default) swaps in
the lab's assets/meshes/head/{head_servo_link,xl330_link,head_tilt_link}.STL when all three are
present (see THIRD_PARTY_NOTICES.md for their provenance).
Hands: each AmazingHand contributes 8 actuated hinge joints named by servo ID
(left_hand_motor11_joint .. left_hand_motor18_joint, right_hand_motor1_joint ..
right_hand_motor8_joint; finger 1 servo 1 = the lowest ID), ctrlrange snapped to exactly
+-pi/2.
Joint names vs lerobot: head/hand joints and actuators use the G1 MJCF snake_case style,
and sim/head_hand_sim.py's mjcf_joint_name maps lerobot's Unitree-style motor names onto
them: kHeadYaw -> head_yaw_joint, kHeadPitch -> head_pitch_joint,
kLeftHandMotor11 -> left_hand_motor11_joint. Link names (head_servo_link, xl330_link)
keep the lab URDF's part names; its d455_link is head_tilt_link here, as it can carry any sensor. Every body/joint/geom/mesh/material/actuator from the source
onshape-to-robot export is prefixed left_hand_/right_hand_, including the 24 passive
ball/hinge joints per hand that close the finger's parallel four-bar linkage (<equality connect> constraints, carried over renamed). All hand geoms are visual-only
(contype="0" conaffinity="0", no collision geoms in the source export). Meshes are
copied from AHSimulation/AH_Left|Right/mjcf under assets/amazing_hand/{left,right}/;
see THIRD_PARTY_NOTICES.md for the CC-BY-4.0 attribution.
Mount transform: the Onshape connector transform from the G1 wrist to the
AmazingHand mount is not available yet, so tools/build_amazing_hand.py uses a placeholder
(pos="0.13 0 0", a quaternion rotating the hand's local +z, its finger-reach axis,
onto the wrist's local +x, so the hand extends away from the elbow). Replace both the
offset and the rotation once the CAD is available.
DDS scope: NUM_MOTORS/motor_effort_limit_list stay body-only (23, matching
JOINT_SLOTS["23dof"]); the D455 pan/tilt head and the 16 hand actuators are never
wired to DDS. sim/base_sim.py's joint scan never adds joints to
left_hand_index/right_hand_index when END_EFFECTOR == "amazing_hand"
(is_dds_hand is False), so those two lists stay empty and dds_actuator_index
covers only the 23 body actuators; the 24 passive linkage joints per hand also carry a
left_hand_/right_hand_ prefix but are never actuated, so they would not match this
scan even if it ran. mj_data.ctrl for the 18 non-DDS actuators (head + both hands) is
left at whatever another writer sets, never overwritten by the body torque loop -- that
other writer is the ZMQ bridge described next.
Head/hand ZMQ bridge: whenever HEAD_MOUNT != "fixed" or END_EFFECTOR == "amazing_hand",
env.py's make_env() starts sim/head_hand_sim.py's SimHeadHandDevice (a
MuJoCo-backed stand-in for lerobot's real HeadHandDevice) behind
lerobot.robots.unitree_g1.headhand_zmq.HeadHandServer, in a daemon thread bound to
127.0.0.1. Ports default to HEADHAND_STATE_PORT: 6003 / HEADHAND_CMD_PORT: 6002
in config.yaml, overridable with UNITREE_G1_MUJOCO_HEADHAND_STATE_PORT /
UNITREE_G1_MUJOCO_HEADHAND_CMD_PORT. A lerobot.robots.unitree_g1.UnitreeG1 robot with
is_simulation=True, head_mount=pan_tilt and end_effector=amazing_hand talks to this
bridge exactly as it would to real Dynamixel/Feetech hardware, and passes its
body/end_effector/head_mount/head_sensor to make_env as the UNITREE_G1_MUJOCO_* variables below. The server thread is stopped from env.close().
LeRobot's make_env(repo_id) cannot forward keyword options, so every make_env option can
also be set through the environment as UNITREE_G1_MUJOCO_<OPTION>, e.g.
UNITREE_G1_MUJOCO_ONSCREEN=0 (no viewer window), UNITREE_G1_MUJOCO_PUBLISH_IMAGES=0,
UNITREE_G1_MUJOCO_CAMERA_PORT=5556, UNITREE_G1_MUJOCO_BODY=29dof,
UNITREE_G1_MUJOCO_END_EFFECTOR=dex1, UNITREE_G1_MUJOCO_JOYSTICK_TYPE=xbox. Keyword arguments still win when calling make_env
directly. Note that after disconnect() the Python process may not exit on its own because of
CycloneDDS finalizers; end the session with Ctrl-C.
Key presses in the MuJoCo viewer window are forwarded to LeRobot's unitree_g1_keyboard
teleoperator (sim/keyboard_forward.py), so it works on Wayland desktops where pynput
cannot capture keystrokes: focus the viewer window and use its key map (arrows = head,
q/e = hands, w a s d / i j k l = sticks, t/g = base height). Keys 7/8/9 still drive the elastic band.
The bridge reads a pygame joystick and publishes it as the robot's wireless_remote
(JOYSTICK_TYPE in config.yaml, default dualshock4 for a Sony DualShock 4 under SDL's
HIDAPI driver; xbox and switch as upstream). Plug the pad in before launching.
All three streams are enabled by default on tcp://127.0.0.1:5555, with
640 × 480 images and the existing approximately 30 Hz publishing setting.
| Stream name | Mount |
|---|---|
head_camera | Existing head camera |
left_wrist_cam | Left gripper base / wrist |
right_wrist_cam | Right gripper base / wrist |
Each stream is advertised through the same top-level JPEG key and nested
images / timestamps entries as head_camera. The existing
view_cameras_live.py discovers the names from those messages. They are also
listed in env.camera_configs, env.camera_names and env.metadata["cameras"].
The wrist cameras move with their respective wrists, with 95° vertical field of
view and the approximate extrinsics from the HIW-500 model builder.
LeRobot's ZMQ cameras require explicit client configuration, just as the head
camera does. Pass this dictionary as UnitreeG1Config(..., cameras=cameras):
from lerobot.cameras.zmq.configuration_zmq import ZMQCameraConfig
cameras = {
name: ZMQCameraConfig(
server_address="127.0.0.1", port=5555, camera_name=name,
width=640, height=480, fps=30,
)
for name in ("head_camera", "left_wrist_cam", "right_wrist_cam")
}
An equivalent camera configuration is in lerobot_cameras.json.
make_env(cameras=["head_camera"]) selects a subset; publish_images=False
disables publishing. onscreen=False disables the viewer for headless use.
Dex1 fingers use force actuators, driven by the existing bridge's external PD
controller. They start open at 0.0245 m. To preserve the simulator's hand
transport, the first two motor entries on rt/dex3/left/cmd and
rt/dex3/right/cmd command fingers 1 and 2, with matching state topics.
Gripper q is in metres and tau is in newtons; each finger is limited to 20 N.
Command both fingers to the same position for symmetric opening or closing.
Original hand mode retains all seven rotational command entries per side.
The simulation lower limit is -0.023 m, following HIW-500's mesh closure trim.
The source URDF retains the official -0.020 m lower limit.
Run python tools/build_dex1_model.py --official-limits to use that limit in MJCF
too; it leaves approximately 5.88 mm between the supplied finger pads.
This model update does not add finger actions to LeRobot's 29-body-motor
UnitreeG1 action schema.
Install the existing Unitree SDK2 / CycloneDDS prerequisites, then
python -m pip install -r requirements.txt. Regenerate the generated parts files with
python tools/build_dex1_model.py and python tools/build_amazing_hand.py.
MUJOCO_GL=egl python -m unittest discover -s tests -v
MUJOCO_GL=egl python tests/smoke_live.py
MUJOCO_GL=egl python tests/smoke_live.py --end-effector dex3
The regression suite uses real MuJoCo for both variants, motor/observation
mapping, force-controlled closure, wrist camera motion, rendering and camera
publisher shared-memory buffers. It isolates DDS with a test double.
smoke_live.py additionally requires the real Gymnasium, Unitree SDK2 and
ZMQ/OpenCV dependencies and checks the live environment and transports.
See VALIDATION.md for what was executed for this update.
The original Dex1 source URDF, Apache 2.0 license and attribution notice are in
reference/hiw500/. Unitree mesh assets retain their upstream terms.