人形机器人的研发离不开仿真:在真机上跑一次实验要调试硬件、担心摔机,而在仿真里可以并行跑几千个环境、一次训练几千万步。但"仿真器"之间的差异极大——有的追求物理精度,有的追求 GPU 并行训练速度,有的只为 ROS 整机联调。下表为 2025 年的最新格局(版本信息见文末参考来源):
| 仿真器 | 物理引擎特点 | 接触模型精度 | 渲染 | GPU 加速 | 强化学习支持 | 上手难度 | 适用场景 |
|---|---|---|---|---|---|---|---|
| MuJoCo (Google DeepMind,3.x) |
广义坐标 + 约束求解;软接触、隐式时间积分,数值稳定;CPU 多线程;C/Python 双 API | 高:凸体碰撞检测 + 软接触模型,默认参数即可用 | 内置 OpenGL(egl / osmesa / glfw),支持离屏渲染 | 物理本身无 GPU 加速;官方 mjx(JAX 重写)可在 GPU 上大规模并行 |
优秀:dm_control、gymnasium 包装器,Menagerie 提供 70+ 高质量机器人模型 | ★ 低 | 科研、RL 训练、生物力学、快速原型;人形机器人(Unitree H1/G1 官方模型) |
| Gazebo (Classic / Harmonic) |
多物理后端(ODE / Bullet / DART / Simbody);与 ROS 2 生态深度集成 | 中:ODE 接触需要手动调 spring/damping,易抖动 | OGRE | 无 GPU 物理 | 弱:单进程串行,速度慢,主要用于 ROS 仿真而非 RL | ★★ 中 | ROS 2 机器人开发、激光/相机等多传感器仿真、整机系统联调 |
| Isaac Lab (NVIDIA,基于 Isaac Sim,2.x) |
底层为 PhysX GPU 引擎;每帧可并行成百上千个环境,吞吐量远超 CPU 引擎 | 高:PhysX 支持软/硬接触、关节摩擦、材质参数 | RTX 实时光追(Omniverse 生态) | 原生 GPU 并行,这是其最大卖点 | 极佳:内置 rsl_rl / RLGames / skrl 等 PPO 实现,自带 H1/G1 等任务(如 Isaac-Velocity-Flat-H1-v0) | ★★★ 高(需 NVIDIA GPU + Omniverse 生态) | 大规模并行 RL 训练、腿式/人形机器人运动、抓取操作;Isaac Gym 的官方继任者 |
| Webots (Cyberbotics,R2025 系列) |
基于 ODE 的自研内核,可选多物理后端;自带图形界面,一键建场景 | 中:ODE 接触模型,适合教学演示 | OpenGL 3.3 | 无 GPU 物理 | 一般:社区 gym 包装器(如 gym-webots),单机速度慢 | ★ 低 | 教学、ROS 课程、机器人原型演示、移动机器人 |
| PyBullet (Bullet 引擎,3.x) |
Bullet 物理内核;Python 接口最简洁,支持软接触与约束;社区资料极多 | 中:接触参数可调,默认值即可跑通大部分任务 | OpenGL GUI / EGL 离屏 | 无 GPU 物理 | 好:大量 gym 环境(pybullet-gym、gym-pybullet-drones 等),适合快速验证算法 | ★ 低 | 快速原型、科研教学、抓取与四足机器人入门、算法对比实验 |
| RaiSim (ETH leggedrobotics,RaiSimLib 3.x) |
自研接触求解(互补性条件、严格无穿透),接触稳定性著称;物理在 CPU 上非常快 | 高:适合密集接触场景(四足、灵巧手),不会"陷进"地面 | 无内置渲染(需搭配第三方查看器) | RL 训练循环可用 GPU(PyTorch),物理求解为 CPU | 好:配套 raisimGymTorch,四足 RL 经典组合 | ★★ 中(注意许可证:科研免费、商用需授权) | 接触丰富的 RL 研究、四足/灵巧操作、需要高接触稳定性的场景 |
MuJoCo(Multi-Joint dynamics with Contact)由 Emanuel Todorov 团队开发,2021 年 10 月被 DeepMind 收购后以 Apache 2.0 协议开源,现在由 Google DeepMind 维护,2025 年已迭代到 3.x 系列(如 3.4.0)。它的核心优势:
mjx 是 JAX 版 MuJoCo,可在 GPU 上把数百个环境并行起来训练。MuJoCo 的 Python 绑定只需一个 pip 包(自 3.x 起内置预编译共享库,无需单独下载二进制):
pip install mujoco
# 查看版本
python -c "import mujoco; print(mujoco.__version__)"
mjpython 是随 pip 包一起安装的启动器,它会保证 Python 绑定能找到与包版本匹配的共享库,官方教程和示例都推荐用它运行:
mjpython my_script.py # 代替 python my_script.py
MuJoCo 的模型格式是 MJCF(XML),核心思路是"声明式建模":不写任何求解代码,只描述几何、惯性与关节,引擎自动完成动力学推导。以一个倒立摆(cartpole)为例:
<mujoco model="cartpole">
<compiler angle="degree"/> <!-- 编译选项:角度单位用度 -->
<option timestep="0.002"/> <!-- 全局仿真步长:2ms -->
<asset> <!-- 资源区:网格/纹理/材质 -->
<mesh file="pole.stl"/>
<material name="pole_mat" rgba="0.6 0.2 0.2 1"/>
</asset>
<worldbody> <!-- 世界根节点 -->
<body name="cart" pos="0 0 0.3"> <!-- 连杆:小车 -->
<joint name="slider" type="slide" axis="1 0 0" range="-2 2"/>
<geom type="box" size="0.4 0.1 0.05" mass="1"/> <!-- 几何:碰撞+视觉 -->
<body name="pole" pos="0 0 0.1"> <!-- 子连杆:摆杆 -->
<joint name="hinge" type="hinge" axis="0 1 0"/>
<geom type="capsule" fromto="0 0 0 0 0 0.5" size="0.03" mass="0.1"/>
</body>
</body>
</worldbody>
<actuator> <!-- 执行器:电机 -->
<motor joint="slider" ctrlrange="-20 20" gear="1"/>
</actuator>
</mujoco>
四个核心概念:asset 声明可复用的网格/纹理/材质;body 通过嵌套形成运动树(每个 body 是一根连杆);joint 定义连杆之间的自由度(slide 滑动 / hinge 转动 / free 六自由度);actuator 定义电机,motor 直接输出力矩,另有 position/velocity 内置伺服,可用 kp/kv 模拟 PD 控制——这与人形机器人关节模组的"位置环 + 力矩环"思路一致。
import mujoco
# 1) 加载并编译模型(从字符串或文件均可)
xml = open("cartpole.xml", encoding="utf-8").read()
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
# 2) 复位并推进仿真
mujoco.mj_resetData(model, data)
data.ctrl[0] = 5.0 # 给 0 号电机一个恒定力矩
for step in range(500):
mujoco.mj_step(model, data) # 每步推进一个 timestep(2ms)
# 3) 渲染一帧(离屏,返回 numpy 数组)
renderer = mujoco.Renderer(model, height=480, width=640)
frame = renderer.render(data) # shape = (480, 640, 3)
# 4) 或者直接打开交互窗口,边看边调
mujoco.viewer.launch_passive(model, data)
while data.time < 10.0:
mujoco.mj_step(model, data)
mujoco.mj_syncState(model, data) # 同步给渲染线程
理解两个关键数据对象:model(MjModel)是编译后的"静态图纸"(质量、惯量、关节约束、执行器参数),data(MjData)是"运行时状态"(关节位置 qpos、速度 qvel、控制量 ctrl、外力、接触力等)。RL 训练时读 data.qpos / data.qvel 组成观测,写 data.ctrl 作为动作,然后 mj_step 推进。
MuJoCo 从 2.3.3 起原生支持直接加载 URDF:解析器识别到 <robot> 根标签时自动完成"URDF → 内部模型"的转换,所以本站 00 部分的模型可以直接用:
import mujoco
# 本站 00 部分自带的 H1 / G1 模型
model = mujoco.MjModel.from_xml_path(
"../00_3D解剖/models/h1/h1.urdf") # 或 g1/g1_29dof.urdf
data = mujoco.MjData(model)
mujoco.viewer.launch_passive(model, data)
while data.time < 30.0:
mujoco.mj_step(model, data)
mujoco.mj_syncState(model, data)
visual/collision 网格路径是相对 URDF 文件解析的,移动文件后记得检查 mesh 是否能找到;inertial(质量/惯量)会导致模型不稳或编译报错,转换后先在空场景里看是否"原地塌掉";unitree_h1 / unitree_g1),或用 urdf2mjcf 类工具转换后手工校准。
单靠手写 PD 控制器很难让人形机器人稳定行走,业界主流做法是强化学习(RL):策略网络直接学习"从观测到关节指令"的映射。Isaac Lab 是 NVIDIA 官方框架(Isaac Gym 的继任者,Isaac Gym 已停止维护),底层用 PhysX 在 GPU 上并行数千个环境;legged-gym(ETH leggedrobotics)是腿式机器人 RL 的经典参考实现,其结构被 Unitree 官方项目 unitree_rl_gym(H1/G1/Go2)与 Isaac Lab 版 unitree_rl_lab 继承。
无论用哪个框架,一个 RL 环境的核心就是这三样。以人形机器人行走为例(具体数值参考 legged-gym 系默认配置):
| 组成 | 内容(示例维度) | 为什么这么设计 |
|---|---|---|
| 观察空间 | 基座线速度(3)+ 角速度(3)+ 姿态(四元数 4)+ 关节位置(每关节)+ 关节速度(每关节)+ 上一动作(每关节)+ 指令速度(2~3) | 策略需要"自身状态 + 任务指令"才能决策;关节角用 sin/cos 编码避免角度环绕;速度类观测做裁剪与归一化 |
| 动作空间 | 每个关节一个目标位置(输出后经 PD 位置伺服执行),连续、范围受限(如 ±0.5 rad) | 比直接输出力矩更稳、更接近真机关节模组的工作方式——模组本身有位置环 |
| 奖励函数 | 任务奖励(前进速度跟踪)+ 惩罚项(姿态倾斜、关节超限、能耗、动作变化率)+ 可选的步态/接触正则 | 任务奖励告诉策略"要做什么",惩罚项约束"不能乱来";每一项都要能解释、可调权重 |
实际命令行(以 Isaac Lab 自带 H1 平地行走任务为例):
# Isaac Lab(rsl_rl 入口)
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py \
--task Isaac-Velocity-Flat-H1-v0 --num_envs 4096 --headless
# legged-gym 系(unitree_rl_gym 的 H1 配置)
python legged_gym/scripts/train.py --task=unitree_h1 \
--num_envs=4096 --headless
TensorBoard / wandb 上主要盯四类曲线,看趋势而不是单点数值:
| 曲线 | 正常表现 | 异常信号 |
|---|---|---|
| mean reward | 先快速上升,后缓慢爬升趋于平台;曲线应单调变好(允许小波动) | 长期徘徊不涨 → 检查奖励尺度与观测;突然跳涨 → 警惕 reward hack |
| episode length | 随训练上升,直到接近最大步数(学会了"不摔"或完成任务) | 一直很短 → 初始条件太难或终止条件过严 |
| policy entropy | 缓慢下降,说明策略从"乱试"收敛到"确定动作" | 骤降到 0 → 过早收敛(可能陷入局部最优);居高不下 → 没学会,检查奖励是否稀疏 |
| critic loss / value | 价值估计与真实回报逐渐对齐 | 剧烈震荡 → 学习率过高或 GAE lambda 不匹配 |
训练完的 Actor 网络只是"一堆权重",要用起来先导出再回放:
# rsl_rl 内置导出(生成 policy.pt 与 policy.onnx)
from rsl_rl.utils import export_policy_as_onnx
export_policy_as_onnx(agent.actor_critic, path="exported", num_obs=obs_dim)
# 或者用 torch.jit 导出为 TorchScript
traced = torch.jit.trace(actor, example_obs)
traced.save("policy.pt")
回放验证三件事:① 确定性推理下能稳定完成任务(多跑几个随机种子);② 把策略换成"固定频率推理 + 模拟执行器延迟"再测一遍,提前暴露 sim-to-real 风险;③ 检查关节指令是否超限、是否频繁抖动(抖动策略在真机上会烧电机)。
第 4 步提到的 "clip 目标" 具体长这样:
L^CLIP(θ) = E[ min( r(θ)·A , clip( r(θ), 1−ε, 1+ε )·A ) ]
其中:
r(θ) = π_θ(a|s) / π_θ_old(a|s) ← 新旧策略对同一动作的概率比
A = GAE 优势估计 ← 这一拍动作比"平均水平"好多少
ε = clip 范围(典型 0.2) ← 概率比被夹在 [0.8, 1.2]
直觉上:概率比 r(θ) 衡量"这次更新把策略改动了多少",clip 把它强制夹在 1±ε 之间——好动作(A>0)最多把该动作概率放大 1+ε 倍,坏动作(A<0)最多压低到 1−ε 倍,min 项再取两者中较保守的那个。这样单次更新的步幅被硬性限制住,允许 PPO 在同一批数据上多跑几个 epoch 而不会把策略"改飞",这正是它比原始策略梯度稳定的根源。也因此,训练日志里如果看到概率比频繁贴着 0.8/1.2 的上下限,说明学习率偏大或数据已被更新得"太旧"。
以最简单的倒立摆为例,奖励 = 存活奖励 − 角度惩罚 − 控制量惩罚:
reward_t = w1·存活项 + w2·角度惩罚项 + w3·控制量惩罚项
= w1·1 − w2·|θ| − w3·|u|
取权重 w = [w1, w2, w3] = [1.0, 4.0, 0.01]
某一拍: 摆角 θ = 0.2 rad, 输出控制量 u = 2 N
代入:
存活项 = 1.0 × 1 = +1.00
角度惩罚 = 4.0 × |0.2| = −0.80
控制惩罚 = 0.01 × |2| = −0.02
─────────────────────────────────────
当拍 reward = 1.0 − 0.8 − 0.02 = 0.18
从这个算例能看出权重的杠杆效应:
调参经验:先让"当拍各项数量级"大致可比(像本例 1.0 / 0.8 / 0.02),再按训练表现微调;改一个权重就重跑一次对照实验,不要一次改三个。
学习 RL 控制人形机器人,建议按"复杂度递增"的顺序做三个实验,每个都自己写一遍观察/动作/奖励设计。下面给出设计要点与收敛提示。
| 实验 | 观察空间 | 动作空间 | 奖励设计要点 | 收敛提示 |
|---|---|---|---|---|
| ① 倒立摆 Swing-up (单关节,最简) |
摆角 θ(建议 sinθ、cosθ 编码)+ 角速度 θ̇ | 电机力矩(连续)或 bang-bang(离散) | 任务奖励用 cosθ(竖立时最大)+ 角速度惩罚 + 控制量惩罚;episode 在"竖立持续 N 步"或"倒下"时终止 |
几分钟内收敛;若只停留在"小角度附近"说明能量整形不足,可加"角速度符号"引导先甩起来 |
| ② 单腿弹跳 (Hopper 类,多关节+接触) |
关节角/角速度 + 基座高度与竖直速度 + 足底接触标志 | 髋、膝力矩(或目标位置+PD) | 高度保持奖励 + 竖直速度平滑 + 姿态(躯干水平)惩罚 + 接触时间正则;初始高度随机化 | 接触奖励(如落地缓冲惩罚)能让步态更自然;训练不稳先调接触摩擦的域随机化范围 |
| ③ H1 站立/行走 (19 自由度人形) |
基座线/角速度 + 姿态四元数 + 关节位置/速度 + 指令速度 + 上一动作 | 各关节目标位置(PD 位置伺服执行) | 前进速度跟踪(与指令速度的误差)+ 姿态惩罚 + 关节超限/能耗/动作平滑惩罚;可加分阶段课程 | 先学"站立不动"(指令速度=0),再学"慢走";reward 量级控制在 1e-2~1e1,权重冲突时逐项消融 |
以倒立摆为例的奖励伪代码(思路同样适用于 H1 行走):
function reward(state, action, next_state):
# 1) 任务项:竖立程度(θ=0 为竖立)
upright = cos(state.theta)
# 2) 平滑项:动作变化率惩罚,避免抖动
smooth = -w1 * (action - last_action)^2
# 3) 能耗项:控制量惩罚
effort = -w2 * action^2
# 4) 姿态项:角速度惩罚,避免"原地乱甩"拿奖励
sway = -w3 * state.theta_dot^2
return upright + smooth + effort + sway
设计奖励的三条铁律:可解释(每项都能说出物理含义)、可消融(关掉某一项看行为变化)、防 hack(所有"正奖励"都要想清楚策略会不会用怪异方式刷分)。
仿真训练出的策略直接上真机,十有八九会摔——因为仿真与真机之间有"现实差距"(sim-to-real gap)。业界总结出五大类技巧,每一项都是在训练时故意"加难",让策略学会在不确定中保持鲁棒:
| 技巧 | 一句话解释 | 示例做法 |
|---|---|---|
| 域随机化 (Domain Randomization) | 训练时随机改变物理参数,让策略见过"各种世界",部署时自然适应真机参数偏差 | 每个 episode 随机化:足底摩擦 μ∈[0.4, 1.2]、连杆质量 ±30%、关节阻尼 ±50%、地面不平度;Isaac Lab 用 randomization 模块声明 |
| 动作延迟注入 (Action Delay) | 真机的控制链路有通信与执行延迟,仿真里不模拟,策略就会"对过去的自己"下指令 | 训练时把动作缓冲 1~3 个控制周期再送入环境:action_buffer 队列,每步 pop 旧动作执行 |
| 观测噪声 (Observation Noise) | 真机传感器(编码器、IMU)有噪声,训练时给观测加噪,策略才不会"过度依赖"精确值 | 关节角加 N(0, 0.01 rad)、角速度加 N(0, 0.1 rad/s)、IMU 姿态加随机游走噪声 |
| 执行器模型 (Actuator Model) | 真机电机不是"指令即达",有带宽、力矩限幅、摩擦;把执行器特性建模进仿真 | 用一阶/二阶低通模拟电机响应:target 经 τ·dθ/dt + θ = cmd 平滑;同时加力矩饱和(如 ±扭矩)与死区 |
| 零力矩点约束 (ZMP Constraint) | 行走稳定性的物理本质:支撑反力合力作用点(ZMP)必须落在支撑多边形内;策略学坏会"边走边滑" | 训练中实时计算 ZMP(由足底接触力合成),奖励里加"ZMP 到支撑多边形边缘距离"的惩罚项,防止翻倒步态 |
训练完成只是开始,策略最终要跑在真机的控制回路里。典型链路:训练框架(Python)导出 → 部署端加载推理 → 生成关节指令 → 发给关节模组执行。人形机器人控制频率常见 50~200 Hz,推理延迟必须远小于控制周期。
C++ 端 ONNX Runtime 推理的核心代码(仅示意关键步骤):
#include <onnxruntime/core/session/onnxruntime_cxx_api.h>
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "rl-policy");
Ort::Session session(env, "policy.onnx",
Ort::SessionOptions{nullptr});
// 观测向量:基座姿态/速度 + 关节位置/速度 + 指令
std::vector<float> obs = build_observation(imu, encoders, cmd);
std::array<int64_t, 2> shape{1, OBS_DIM};
Ort::Value in = Ort::Value::CreateTensor<float>(
Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault),
obs.data(), obs.size(), shape.data(), shape.size());
// 推理得到目标关节角
auto out = session.Run(Ort::RunOptions{nullptr},
{"obs"}, &in, 1, {"action"}, 1);
float* action = out[0].GetTensorMutableData<float>();
// 打包为关节指令,经 CAN 总线下发(与 04-02 固件层衔接)
for (int j = 0; j < NUM_JOINTS; j++) {
send_joint_command(j, action[j], kp, kd, torque_limit);
}
MjModel.from_xml_path() 加载 URDF;它的原生模型格式是 MJCF(XML),主要运行在 CPU 上(GPU 并行需借助 mjx)。model(MjModel)与 data(MjData)两个对象的分工是?MjModel 是编译后的静态模型(质量、惯量、关节约束、执行器参数),MjData 是运行时状态(qpos 位置、qvel 速度、ctrl 控制量、接触力等);RL 训练时读 data.qpos/qvel 组成观测、写 data.ctrl 作为动作,再 mj_step 推进。