Adding a Custom Robot#
This guide walks through adding a new robot to ProtoMotions, using the H1_2 robot as a reference. We focus on the areas of change rather than comprehensive details.
Overview: What Needs to Change#
Adding a robot requires changes in these areas:
MJCF file - Robot physics definition
Robot config - ProtoMotions configuration
Registration - Automatic: the file name under
protomotions/robot_configs/is the robot nameRetargeting script (optional) - For motion transfer
Motion conversion (optional) - For retargeted motions
We’ll use H1_2 (Unitree H1 v2) and G1 as references throughout.
Step 1: MJCF File#
Create your robot’s MJCF file in protomotions/data/assets/mjcf/.
Key requirements:
Root body with
<freejoint/>for floating baseAll joints with proper limits and axes
Collision geometries for physics
Reference: Compare h1_2_no_mesh_box_feet.xml and g1_bm_no_mesh_box_feet.xml
to see naming conventions.
Note
If you have URDF, convert to MJCF first. The MJCF serves as the ground-truth robot specification.
Step 2: Robot Config#
Create protomotions/robot_configs/<your_robot>.py. Key fields to define:
Body Name Mappings:
Map common names to your robot’s body names for contact detection, observations, etc.
# From H1_2 config
common_naming_to_robot_body_names: Dict[str, List[str]] = field(
default_factory=lambda: {
"all_left_foot_bodies": ["left_ankle_roll_link"], # Your foot link
"all_right_foot_bodies": ["right_ankle_roll_link"],
"all_left_hand_bodies": ["left_wrist_yaw_link"], # Your hand link
"all_right_hand_bodies": ["right_wrist_yaw_link"],
"head_body_name": ["head_aux"], # Your head link
"torso_body_name": ["torso_link"], # Your torso link
}
)
Compare G1 vs H1_2:
G1 uses
left_rubber_handfor hands, H1_2 usesleft_wrist_yaw_linkG1 uses
headfor head, H1_2 useshead_auxDifferent joint names but same structure
Trackable Bodies:
Bodies used for MaskedMimic:
trackable_bodies_subset: List[str] = field(
default_factory=lambda: [
"torso_link", "head_aux",
"right_ankle_roll_link", "left_ankle_roll_link",
"left_wrist_yaw_link", "right_wrist_yaw_link",
]
)
Asset Configuration:
asset: RobotAssetConfig = field(
default_factory=lambda: RobotAssetConfig(
asset_file_name="mjcf/your_robot.xml", # MJCF is the source of truth; IsaacLab converts to USD
self_collisions=False, # Enable if needed
)
)
Default Root Height:
Standing height for reset/initialization:
default_root_height: float = 1.03 # H1_2 is taller than G1 (0.8)
PD Control Parameters:
Gains are not typed per joint. Each physical actuator is a MotorClass in
protomotions/robot_configs/motor_classes.py (its rotor armature plus
torque and speed limits), and its control_info() derives the PD gains
from that: a 10 Hz natural frequency with a damping ratio of 2, so the
joints are over-damped rather than critically damped. Add one class per
motor rating your robot uses, then map joint-name patterns to them:
# protomotions/robot_configs/motor_classes.py
H1_2_300 = MotorClass(armature=0.040, effort_limit=300, velocity_limit=50)
H1_2_200 = MotorClass(armature=0.030, effort_limit=200, velocity_limit=50)
H1_2_60 = MotorClass(armature=0.010, effort_limit=60, velocity_limit=50)
H1_2_40 = MotorClass(armature=0.005, effort_limit=40, velocity_limit=50)
# protomotions/robot_configs/h1_2.py
control: ControlConfig = field(
default_factory=lambda: ControlConfig(
control_type=ControlType.BUILT_IN_PD,
override_control_info={
# Hip joints: the 200 N·m motor
".*_hip_(yaw|pitch|roll)_joint": H1_2_200.control_info(),
# Knee joints: the 300 N·m motor
".*_knee_joint": H1_2_300.control_info(),
# Two motors in parallel on one joint: scale the gains
".*_ankle_pitch_joint": H1_2_60.control_info(scale=2),
# A joint whose actuator is weaker than its gain class
".*_elbow_joint": H1_2_40.control_info(effort_limit=18),
# ... more joints
},
)
)
ControlInfo(stiffness=..., damping=..., effort_limit=..., velocity_limit=...)
with literal numbers also works if you would rather not derive the gains.
Control Rate and Simulator-Specific Parameters:
The policy rate is control_hz, and each backend’s physics rate follows from
it. H1_2 and G1 run at 50 Hz with the default humanoid physics (IsaacGym 100,
IsaacLab and Newton 200, MuJoCo 1000 fps). There is no decimation field;
the simulator derives it as fps // control_hz. To tune one backend, pass
its block to SimulatorParams.humanoid. A passed block replaces that
backend’s template wholesale, so set every field you care about:
control_hz: int = 50
simulation_params: SimulatorParams = field(
default_factory=lambda: SimulatorParams.humanoid(
50,
isaacgym=IsaacGymSimParams(
fps=100,
substeps=2,
physx=IsaacGymPhysXParams(
num_position_iterations=8,
num_velocity_iterations=4,
),
),
newton=NewtonSimParams(fps=200),
)
)
Step 3: Registration#
There is nothing to edit in protomotions/robot_configs/factory.py. The
registry is the directory: the file you created in Step 2 is registered under
its file stem, so protomotions/robot_configs/your_robot.py is
--robot-name your_robot. Two rules apply:
The module must define exactly one class whose direct base is
RobotConfig; zero or several such classes makerobot_config()raiseTypeError.The stem must not be one of the infrastructure modules (
base,factory,motor_classes).
To confirm the robot is picked up:
python -c "from protomotions.robot_configs.factory import registered_robot_names; print(registered_robot_names())"
Step 4: Test with Random Pose Visualizer#
Before training, verify your robot loads correctly:
python examples/random_pose_visualizer.py \
--robot your_robot \
--simulator isaacgym
The viewer accepts every registered robot name. It draws a random joint pose with zero gravity and zero torque, holds it, and draws a new one every two seconds; there is no key to press. For a robot that is not in the viewer’s own table of marker bodies it falls back to neutral defaults, so a new robot needs no edit to the script. The robot should hold each pose without drifting or flying apart; if it does not, check the MJCF joint limits, the body order, the self-collision settings and the asset itself (meshes and inertias).
Step 5: Retargeting Script (Optional)#
To retarget motions to your robot, create a retargeting script.
Refer to Retargeting SMPL motions with PyRoki for details.
Multi-Simulator (IsaacLab) Considerations#
IsaacLab needs no USD asset from you. At scene construction the backend
converts the robot’s MJCF to USD itself and caches the result under
~/.cache/protomotions/isaaclab_mjcf_usd/. The cache key is a fingerprint of
the MJCF and every mesh it references, so an edited robot is converted again on
the next launch; there is nothing to re-export. RobotAssetConfig has no USD
field and accepts only an .xml path. See
IsaacLab 3 Migration Pin for the converter details.
After changing the MJCF, check that all simulators see the same robot:
Test on each simulator with
random_pose_visualizer.pyVerify joint order and limits match
Next Steps#
Retargeting SMPL motions with PyRoki - Retarget motions to your robot
PoseLib Toolkit - Understand FK/IK utilities
Core Abstractions - Robot config architecture