「Mujoco入门」
/~f0e33aW9pr~:/
MuJoCo 机械臂仿真快速入门指南
本文档基于本项目(
MuJoCo_project.py+PPO_DQN.py)编写,所有代码片段均出自项目源码或已在 Windows + conda 环境下实际验证通过。配套阅读:
提问与回答.txt(笛卡尔空间控制的核心概念问答,本文第 6 章做了提炼)。
目录
-
[XML 入门:读懂机械臂的"身体描述文件"](#XML 入门:读懂机械臂的"身体描述文件")
-
[简单控制 + 前置知识](#简单控制 + 前置知识)
-
[笛卡尔空间控制(MoveL / 画椭圆)](#笛卡尔空间控制(MoveL / 画椭圆))
-
[核心概念问答精华(提炼自 提问与回答.txt)](#核心概念问答精华(提炼自 提问与回答.txt))
1. 安装与环境配置
1.1 本项目需要什么
本项目不依赖 ROS,是纯 Python 技术栈,Windows 原生支持(无需 WSL/Ubuntu):
| 脚本 | 功能 | 依赖 |
|---|---|---|
MuJoCo_project.py |
UR5e 机械臂仿真、逆运动学、关节/笛卡尔轨迹 | mujoco ikpy transforms3d numpy scipy matplotlib |
PPO_DQN.py |
Panda 机械臂 PPO 强化学习抓取目标点 | 上面全部 + gymnasium stable-baselines3 torch |
1.2 创建 conda 环境并安装(已验证)
# 1. 创建环境(Python 3.10 与 mujoco/ikpy 兼容良好)
conda create -n mujoco python=3.10 -y
conda activate mujoco
# 2. 安装基础依赖(跑 MuJoCo_project.py 够用了)
pip install mujoco ikpy transforms3d numpy scipy matplotlib
# 3. (可选)跑 PPO_DQN.py 才需要装强化学习部分
pip install gymnasium stable-baselines3
pip install torch # 有 NVIDIA 显卡去 pytorch.org 装 CUDA 版
验证安装:
python -c "import mujoco, ikpy, transforms3d, numpy, scipy, matplotlib; print(mujoco.__version__)"
# 输出版本号即成功,例如 3.12.0
1.3 运行第一个 Demo
conda activate mujoco
cd C:\Awork\02-python-project\MuJoCo_Project-main # 必须在项目根目录!
python MuJoCo_project.py
⚠️ 必须在项目根目录运行 :脚本里全部是
./model/...相对路径,换目录会报FileNotFoundError。
MuJoCo_project.py 末尾的 __main__ 决定跑哪个 demo,取消对应注释即可切换:
if __name__ == "__main__":
# car() # 两轮小车(MJCF + tendon 耦合驱动)
ur5e() # UR5e 机械臂回到预设关节角
# ur5e_vision() # UR5e + matplotlib 实时末端轨迹 + viewer 画轨迹球
# point() # ikpy 逆运动学:走到指定位姿
# JointSpaceControl()# 关节空间轨迹控制
# MoveCurve() # 笛卡尔空间直线轨迹(MoveL)
# MoveEllipse() # 笛卡尔空间椭圆轨迹
1.4 MuJoCo viewer 鼠标/键盘操作
| 操作 | 效果 |
|---|---|
| 左键拖拽 | 旋转视角 |
| 滚轮 | 缩放 |
| 右键拖拽 / 中键拖拽 | 平移视角 |
| 空格 | 暂停/继续仿真 |
| 右键菜单 → Setting | 显示接触点、透明化、显示关节轴等 |
| 关闭窗口 | 退出 demo(viewer.is_running() 变 False) |
2. XML 入门:读懂机械臂的"身体描述文件"
MuJoCo 使用 MJCF (MuJoCo XML Format)描述模型。本项目 model/ 目录下有三类文件:
model/
├── hello.xml # 最简入门模型:两块悬浮的箱子
├── car.xml # 小车:freejoint + 关节 + tendon + motor
├── universal_robots_ur5e/ # UR5e 机械臂(MJCF,含 scene.xml 场景文件)
│ ├── ur5e.xml # 机械臂本体定义
│ └── scene.xml # 场景 = include 本体 + 灯光 + 地面 + 坐标轴
├── ur5e.urdf # URDF 格式的 UR5e(给 ikpy 做 IK 用)
└── franka_emika_panda/ # Panda 机械臂(PPO_DQN.py 用)
2.1 MJCF 的骨架
一个 MJCF 文件由几个固定区块组成(顺序可以记成:编译选项 → 资产 → 默认值 → 世界 → 执行器 → 传感器):
<mujoco model="hello_world">
<compiler autolimits="true"/> <!-- 自动根据 ctrlrange 等推断关节限位 -->
<asset> <!-- 纹理、材质、网格(mesh) 等资源 -->
<texture name="my_texture" type="2d" builtin="checker" ... />
<material name="my_material" texture="my_texture" ... />
</asset>
<default> <!-- 公共默认值,减少重复;支持嵌套 class -->
<joint damping=".03"/>
<default class="wheel">
<geom type="cylinder" size=".03 .01"/>
</default>
</default>
<worldbody> <!-- 世界本体:所有物体按树状结构挂在这里 -->
<light directional="true" pos="-1 0 1"/>
<geom type="plane" size="10 10 0.1"/> <!-- 地面 -->
<body pos="0 0 1"> <!-- 一个悬浮的箱子 -->
<geom type="box" size="0.1 5 1"/>
</body>
</worldbody>
<actuator> <!-- 执行器:电机,程序里通过 data.ctrl 驱动 -->
<motor name="forward" tendon="forward" ctrlrange="-1 1"/>
</actuator>
<sensor> <!-- 传感器:程序里通过 data.sensor 读取 -->
<jointactuatorfrc name="right" joint="right"/>
</sensor>
</mujoco>
2.2 三个最重要的标签:body / joint / geom
以 car.xml 的轮子为例(真实片段):
<body name="car" pos="0 0 .03"> <!-- body = 一个刚体,pos 是相对父级的坐标 -->
<freejoint/> <!-- 6 自由度浮动关节:整个小车可以在空间中自由运动 -->
<geom name="chasis" type="mesh" mesh="chasis"/> <!-- geom = 几何体(外观+碰撞体)-->
<body name="left wheel" pos="-.07 .06 0" zaxis="0 1 0">
<joint name="left"/> <!-- 铰链关节:这一级只有 1 个转动自由度 -->
<geom class="wheel"/> <!-- 继承 default 里 class="wheel" 的圆柱体 -->
</body>
</body>
关键认知:
-
body是树 :子body的pos是相对父级 的。世界坐标 = 自己的相对坐标 + 父级坐标 + 父级的父级坐标 + ......(项目hello.xml里的注释原话) -
joint决定自由度 :没有<joint>的 body 是焊死在父级上的(比如 UR5e 的基座);<freejoint/>给 6 个自由度(小车) -
geom负责"长什么样 + 撞了会怎样" :contype="0" conaffinity="0"表示不参与碰撞(纯装饰,如 UR5e 场景里的坐标轴)
2.3 看懂 UR5e 的定义(model/universal_robots_ur5e/ur5e.xml)
机械臂 = 一串 body 首尾相接,每级一个转动关节:
<body name="shoulder_link" pos="0 0 0.163">
<inertial mass="3.7" pos="0 0 0" diaginertia="..."/> <!-- 质量属性,动力学必需 -->
<joint name="shoulder_pan_joint" class="size3" axis="0 0 1"/> <!-- 绕 Z 轴旋转 -->
<geom mesh="shoulder_0" material="urblue" class="visual"/> <!-- 外观 mesh -->
<geom class="collision" size="0.06 0.06" .../> <!-- 简化的碰撞体 -->
<body name="upper_arm_link" pos="0 0.138 0" quat="1 0 1 0"> <!-- 下一段,quat 指定安装朝向 -->
<joint name="shoulder_lift_joint" class="size3"/>
...
执行器区:6 个关节对应 6 个 general 执行器 ,这就是代码里 data.ctrl[:6] 能控制 6 个关节的原因:
<actuator>
<general class="size3" name="shoulder_pan" joint="shoulder_pan_joint"/>
<general class="size3" name="shoulder_lift" joint="shoulder_lift_joint"/>
...
</actuator>
2.4 scene.xml 的组织方式(include 机制)
scene.xml 不重复定义机械臂,而是 include 本体,再补充环境(灯光/地面/参考物)------养成"本体文件 + 场景文件分开"的习惯:
<mujoco model="ur5e scene">
<include file="ur5e.xml"/> <!-- 引入机械臂本体 -->
<worldbody>
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
<geom name="floor" size="0 0 0.05" type="plane" .../>
<body name="target" pos="-0.13 0.5 0.1"> <!-- 绿色小球 = IK 的目标点参考物 -->
<geom size="0.02" rgba="0 1 0 0.5" contype="0" conaffinity="0"/>
</body>
</worldbody>
</mujoco>
2.5 MJCF vs URDF(本项目两种都用了!)
MJCF (ur5e.xml) |
URDF (ur5e.urdf) |
|
|---|---|---|
| 谁在用 | MuJoCo 仿真 | ikpy 逆运动学求解 |
| 特点 | 支持执行器/传感器/tendon 等仿真细节 | ROS 世界的通用格式,只描述结构 |
| 加载方式 | mujoco.MjModel.from_xml_path() |
ikpy.chain.Chain.from_urdf_file() |
本项目用同一台 UR5e 的两种格式:MJCF 负责"动起来",URDF 负责"算逆解"。这是常见做法,但要保证两边的连杆尺寸一致。
3. 简单控制 + 前置知识
3.1 核心对象:model 与 data
import mujoco
import numpy as np
# model:静态描述(结构、质量、关节限位)——加载后不变
model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
# data:动态状态(关节角、速度、力、末端位置)——每步仿真都在变
data = mujoco.MjData(model)
常用的 data 字段(读写都行):
| 字段 | 含义 | 本项目中的用法 |
|---|---|---|
data.qpos[:6] |
关节角度(位置) | 初始化机械臂摆到起始姿态 |
data.ctrl[:6] |
控制量(发给执行器的目标值) | 每个 demo 的核心控制入口 |
data.site_xpos[id] |
指定 site 的世界坐标 | 读取末端执行器位置 |
data.time |
仿真时间 | --- |
3.2 两个必懂的 API:mj_step vs mj_forward
这是 Q&A 里强调的重点,调试必用:
mujoco.mj_step(model, data)
# 作用:"往前走一步"。= mj_forward + 动力学求解 + 时间积分。
# 主循环里必须调用它,机械臂才会真正动。
mujoco.mj_forward(model, data)
# 作用:"刷新"。手动改了 qpos 之后,重算正运动学/碰撞等派生量,
# 但不推进时间。想让 site_xpos 立刻反映你刚设置的 qpos,就调它。
经典坑 :手动设置 data.qpos 后直接读 data.site_xpos,读到的是旧值------中间缺一次 mj_forward。(MoveEllipse() 里第 438 行就正确地调了它。)
3.3 最小可运行的控制程序
项目里的 ur5e() 就是最简控制示例------设初始关节角 → 设目标 ctrl → 循环步进:
def ur5e():
model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
data = mujoco.MjData(model)
start_joints = np.array([0, 0, 2.65, -1.3, 1.55, 0])
data.qpos[:6] = start_joints # 初始姿态:直接写关节角
data.ctrl[:6] = [-1.57, -1.34, 2.65, -1.3, 1.55, 0] # 目标关节角
# launch_passive:非阻塞 viewer,物理仿真跑在咱们自己的循环里
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
mujoco.mj_step(model, data) # 推进一个物理步
viewer.sync() # 把最新状态同步到渲染窗口
time.sleep(0.002) # 控制回放速度(不加会狂转)
理解了这 10 行,后面的所有 demo 都只是在这个骨架上"加料"。
3.4 前置知识:位姿 = 位置 + 姿态
-
位置 (position) :
[x, y, z],米。 -
姿态 (orientation):本项目用了三种表示,需要知道怎么互相转换:
from scipy.spatial.transform import Rotation as R
import transforms3d as tf
# 欧拉角(人类最好懂,3 个数,绕 xyz 轴的转角,弧度)
euler = [3.14, 0, 1.57]
rot_matrix = R.from_euler('xyz', euler).as_matrix() # 欧拉角 → 旋转矩阵
rot_matrix = tf.euler.euler2mat(*euler) # transforms3d 的等价写法
# 旋转矩阵(3x3,IK 库想要的格式)
# 四元数(MuJoCo 内部用的格式,data.xquat)
3.5 逆运动学(IK):第一次让机械臂"听懂人话"
point() 演示了"告诉机械臂末端去哪,它自己算每个关节转多少":
import ikpy.chain
import transforms3d as tf
# 1. 从 URDF 构建运动链
# active_links_mask: 首尾两个 link 固定不动(基座和工具端),中间 6 个活动
my_chain = ikpy.chain.Chain.from_urdf_file(
"model/ur5e.urdf",
active_links_mask=[False] + [True] * 6 + [False]
)
# 2. 定义目标位姿:末端位置 + 末端姿态(欧拉角转旋转矩阵)
ee_pos = [-0.13, 0.6, 0.1]
ee_euler = [3.14, 0, 1.57]
ee_orientation = tf.euler.euler2mat(*ee_euler)
# 3. IK 求解:输入位姿,输出 8 个数(首尾是固定的虚拟关节,真正用的是中间 6 个)
# initial_position 给一个"接近解"能大幅提高收敛速度和质量
ref_pos = [0, 0, -1.57, -1.34, 2.65, -1.3, 1.55, 0]
joint_angles = my_chain.inverse_kinematics(ee_pos, ee_orientation, "all",
initial_position=ref_pos)
# 4. 灌回 MuJoCo 执行
data.ctrl[:6] = joint_angles[1:-1] # [1:-1] 去掉首尾两个虚拟关节
📌 正运动学 vs 逆运动学(一句话记忆):
正运动学 (FK):给关节角 → 算末端在哪。确定解,简单。
逆运动学 (IK):给末端位姿 → 反算关节角。可能多解、无解,是"求解"过程。
4. 关节空间控制(MoveJ)
对应项目:
JointSpaceControl()+JointSpaceTrajectory类(MuJoCo_project.py)
4.1 什么是关节空间控制
直接告诉每个关节"从当前角度转到目标角度" ,中间过程在关节角度空间 做线性插值。末端在三维空间里走出什么曲线?------不知道,也不管(通常是弧线)。
4.2 轨迹生成器:逐点"喂"目标
项目实现了一个生成器(generator),按步长吐出中间路点:
class JointSpaceTrajectory:
def __init__(self, start_joints, end_joints, steps):
self.start_joints = np.array(start_joints)
self.end_joints = np.array(end_joints)
self.steps = steps
self.step = (self.end_joints - self.start_joints) / self.steps # 每步增量
self.trajectory = self._generate_trajectory()
self.waypoint = self.start_joints # 当前目标路点
def _generate_trajectory(self):
# 生成器:惰性地产生 start + step*i 序列,内存友好
for i in range(self.steps + 1):
yield self.start_joints + self.step * i
yield self.end_joints # 最后精确落在终点上
def get_next_waypoint(self, qpos):
# 核心逻辑:当前关节角足够接近当前路点(误差<0.02rad)才切换下一个
if np.allclose(qpos, self.waypoint, atol=0.02):
try:
self.waypoint = next(self.trajectory)
return self.waypoint
except StopIteration:
pass
return self.waypoint
4.3 主循环:IK 定终点 + 插值定过程
整体流程是"IK 算终点关节角 → 关节空间插值 → 仿真循环跟踪路点":
def JointSpaceControl():
# ... 加载模型 / 建 ikpy 链(同 point())...
# ① 用 IK 解出终点位姿对应的关节角
joint_angles = my_chain.inverse_kinematics(ee_pos, ee_orientation, "all",
initial_position=ref_pos)
end_joints = joint_angles[1:-1]
# ② 起点到终点之间撒 100 个路点
joint_trajectory = JointSpaceTrajectory(start_joints, end_joints, steps=100)
site_id = model.site("attachment_site").id # 末端 site 的 ID
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
# ③ 每一步:查询当前该追哪个路点,把它设为 ctrl 目标
waypoint = joint_trajectory.get_next_waypoint(data.qpos[:6])
data.ctrl[:6] = waypoint
mujoco.mj_step(model, data)
viewer.sync()
# (源码此处还附带 matplotlib 实时画末端轨迹 + viewer 里加红色轨迹球,
# 用 mjv_initGeom 往 viewer.user_scn 里塞小球,详见源码 223-244 行)
4.4 关节空间控制的特点(记住这个表)
| 特性 | 说明 |
|---|---|
| 计算量 | ✅ 极小(纯线性插值,全程只做一次 IK) |
| 末端路径 | ❌ 不可控,走的是弧线 |
| 关节运动 | ✅ 每个关节匀速、平滑 |
| 适用场景 | 自由空间移动、避障要求不高的点到点运动(对应工业机器人的 MoveJ 指令) |
5. 笛卡尔空间控制(MoveL / 画椭圆)
对应项目:
CartesianTrajectory类 +MoveCurve()(直线)+MoveEllipse()(椭圆)
5.1 什么是笛卡尔空间控制
告诉末端执行器在空间中的具体坐标和朝向 ,让末端走出你指定的直线/曲线。关节怎么转?IK 算去。对应工业机器人的 MoveL 指令,用于焊接、涂胶、写字等对路径有要求的任务。
5.2 为什么要"插值"
控制器不知道 A→B 中间怎么走,轨迹规划的核心任务就是在起终点之间填补足够多的中间点(本项目默认 100~200 个)。
5.3 位置用线性插值,姿态必须用 Slerp
位置 简单:np.linspace 均匀撒点。
姿态 有坑:直接对欧拉角做线性插值,遇到大角度旋转会走出"绕远路"的诡异轨迹。Slerp(球面线性插值) 强制旋转在球面上走最短路径,是工业级标准做法。
项目里的 CartesianTrajectory 把两者打包:
from scipy.spatial.transform import Rotation as R, Slerp
class CartesianTrajectory:
def __init__(self, p_start, euler_start, p_end, euler_end, num_points=100):
self.num_points = num_points
# 位置插值:线性。np.linspace 直接支持 (N,3) 数组按行插值
self.positions = np.linspace(p_start, p_end, num_points)
# 姿态插值:Slerp 球面插值
rot_start = R.from_euler('xyz', euler_start)
rot_end = R.from_euler('xyz', euler_end)
# Slerp 用法:给 [0,1] 两个关键帧,再喂归一化的时间轴取值
self.rotations = Slerp([0, 1], R.concatenate([rot_start, rot_end]))(
np.linspace(0, 1, num_points)
)
self.index = 0
def get_next_pose(self):
"""依次返回每个中间位姿 (位置, 旋转矩阵),取完就一直返回终点"""
if self.index < self.num_points:
pos = self.positions[self.index]
rot_mat = self.rotations[self.index].as_matrix()
self.index += 1
return pos, rot_mat
return self.positions[-1], self.rotations[-1].as_matrix()
5.4 主循环骨架:逐点 IK 跟踪(以 MoveEllipse 为例)
笛卡尔控制的标准套路:生成密集位姿点 → 到达一点切下一点 → 每步对当前目标点做 IK:
def MoveEllipse():
# ... 加载模型 / 建 ikpy 链 ...
# ① 生成椭圆轨迹点(200 个):中心(0,-0.2,0.5),半轴 0.3/0.15,水平面内
ellipse_points = generate_ellipse_points(
center=[0.0, -0.2, 0.5], a=0.3, b=0.15,
num_points=200, plane_normal=(0, 0, 1))
# ② 固定末端姿态(工具朝下),先对起点做一次 IK 得到初始关节角
fixed_rot = R.from_euler('xyz', [3.14, 0.0, 0.0]).as_matrix()
start_joints = my_chain.inverse_kinematics(
ellipse_points[0], fixed_rot, "all", initial_position=[0.0] * 8)
data.qpos[:6] = start_joints[1:-1]
mujoco.mj_forward(model, data) # 设置 qpos 后立即刷新派生量!
ik_guess = list(start_joints) # 动态初值:上一步的解
pos_tolerance = 0.01 # 距离目标 <1cm 视为"到达"
traj_idx = 0
target_pos = ellipse_points[traj_idx]
with mujoco.viewer.launch_passive(model, data) as viewer:
# 预先在 viewer 里画出整条椭圆轨迹(红色小球),肉眼先看到目标路径
viewer.user_scn.ngeom = 0
for i, pt in enumerate(ellipse_points):
if i % 2 == 1:
continue
mujoco.mjv_initGeom(
viewer.user_scn.geoms[viewer.user_scn.ngeom],
type=mujoco.mjtGeom.mjGEOM_SPHERE,
size=[0.005, 0, 0],
pos=np.array(pt, dtype=np.float64),
mat=np.eye(3).flatten(),
rgba=np.array([1.0, 0.0, 0.0, 1.0]))
viewer.user_scn.ngeom += 1
while viewer.is_running():
# ③ 距离当前目标点足够近 → 切换到下一个轨迹点
distance = np.linalg.norm(current_pos - target_pos)
if distance < pos_tolerance and traj_idx < num_points - 1:
traj_idx += 1
target_pos = ellipse_points[traj_idx]
# ④ 对当前目标点解 IK,用上一帧的解做初值(关键加速技巧)
joint_angles = my_chain.inverse_kinematics(
target_pos, fixed_rot, "all", initial_position=ik_guess)
data.ctrl[:6] = joint_angles[1:-1]
ik_guess = list(joint_angles) # 更新初值,下一步 IK 更快更稳
mujoco.mj_step(model, data)
viewer.sync()
generate_ellipse_points() 的思路也值得学:先在 2D 平面上参数化 u=a·cos(t), v=b·sin(t),再用两个正交基 (u,v,n) 构造旋转矩阵,把 2D 点变换到任意平面上的 3D 点 ------换成 a=b 就是画圆,改 plane_normal 就能让椭圆出现在竖直面里。自己写字母轨迹(比如"L"形折线)只需替换成折线点列。
5.5 笛卡尔空间控制的特点
| 特性 | 说明 |
|---|---|
| 计算量 | ❌ 较大(每个轨迹点都要做一次 IK) |
| 末端路径 | ✅ 精确可控,严格走直线/指定曲线 |
| 关节运动 | ⚠️ 可能变速、经过奇异区附近时关节会甩 |
| 适用场景 | 焊接、涂胶、装配、写字、擦桌子等路径敏感任务 |
6. 核心概念问答精华
提炼自
提问与回答.txt,这几组概念是本项目的"灵魂",搞懂它们比会调 API 更重要。
6.1 关节空间 vs 笛卡尔空间:一张表终结混淆
| 关节空间规划 | 笛卡尔空间规划 | |
|---|---|---|
| 你给什么 | 两组关节角 q_start, q_end |
两个末端位姿 Pose_A, Pose_B |
| 在哪插值 | 关节角度空间 | 笛卡尔空间(xyz + 旋转) |
| 要不要 IK | ❌ 不需要(只对终点做一次) | ✅ 每个中间点都要 |
| 末端走什么线 | 弧线(不可控) | 直线/指定曲线(可控) |
| 关节累不累 | 匀速平滑 | 可能变速甩动 |
| 计算量 | 小 | 大 |
| 工业指令名 | MoveJ | MoveL |
生活类比:关节空间 = 只关心肩膀手肘各转多少度,指尖画出弧线;笛卡尔空间 = 只要求指尖从纸的左下角画直线到右上角,肩膀手肘自己想办法。
6.2 最容易错的理解(Q&A 里专门纠正过)
❌ 错误:"给两个位姿 → 各做一次 IK → 关节空间插值"
这样末端走的还是弧线!
✅ 正确:"两个位姿之间先插出 100 个中间位姿 → 每个中间位姿分别做 IK → 依次执行这 100 组关节角"只有这样末端才走直线。插值必须发生在笛卡尔空间,IK 是逐点做的。
一句话总结:"关节差值只管电机爽不爽(平滑),不管末端画什么线;路径差值只管末端画什么线(直线),不管电机累不累。"
6.3 完整的工业级 Pipeline(你现在做的事在全局中的位置)
摄像头 → 物体 6D Pose(目标位姿)
↓
┌─────────────────────────────────────┐
│ 第1层:路径规划(全局,负责避障) │ ← RRT* 等采样规划器
│ 输出:一串避障的 waypoint │ (本项目尚未涉及,进阶内容)
├─────────────────────────────────────┤
│ 第2层:笛卡尔插值(局部平滑) │ ← 你的 CartesianTrajectory!
│ 相邻 waypoint 间 linspace + Slerp │
├─────────────────────────────────────┤
│ 第3层:逐点 IK 求解 │ ← 你的 ikpy.inverse_kinematics!
│ 每个密集位姿 → 一组关节角 q │
├─────────────────────────────────────┤
│ 第4层:仿真执行 │ ← 你的 mj_step + viewer!
└─────────────────────────────────────┘
6.4 重要认知:笛卡尔插值 ≠ 避障
笛卡尔规划只约束末端 ,完全不管"胳膊肘"撞不撞东西------为了让末端走直线,IK 解出的关节姿态可能让大臂直接穿进障碍物。避障是路径规划层(第 1 层)的职责。所以工业抓取的标准流程是混合使用:
1. 关节空间规划:home → 抓取点正上方(避障,末端走弧线无所谓)
2. 笛卡尔直线: 正上方 → 抓取点(MoveL,精确下探)
3. 闭爪
4. 笛卡尔直线: 抓取点 → 正上方(垂直抬起)
5. 关节空间规划: → 放置点正上方
6. 笛卡尔直线: 下探 → 放置 → 抬起
6.5 走这条路一定会遇到的 3 个坑(Q&A 原总结)
-
IK 无解导致轨迹断裂:目标点超出工作空间/奇异区。对策:规划时就做可达性检查;用上一帧解做初值。
-
关节跳变 (Joint Flip) :相邻两个目标位姿差很小,IK 却解出跳变 180° 的关节角(收敛到了另一个解)。对策:始终用上一个成功解做 IK 初值;检查
‖q_new − q_old‖是否突增。(本项目MoveEllipse的ik_guess更新就是在做这件事。) -
奇异点:关节某些构型下雅可比矩阵退化,微小的末端误差需要巨大的关节速度,表现为关节"狂甩"。对策:监测可操纵性指数、阻尼最小二乘(DLS)、规划时绕开低可操纵性区域。
6.6 这套 pipeline 和 MoveIt 的关系
MoveIt(ROS 生态的运动规划框架)把你手写的这套东西封装成了现成函数:
| 你手写的 | MoveIt 里的对应物 |
|---|---|
CartesianTrajectory(插值) |
compute_cartesian_path() 的内部第 1 步 |
ikpy.inverse_kinematics(逐点 IK) |
内置 KDL / TRAC-IK 求解器 |
| 手动检查关节跳变 | jump_threshold 参数 |
| (还没写的碰撞检测) | FCL 库自动处理 |
但 compute_cartesian_path 不是魔法 :它只是"直线插值 + 逐点 IK + 碰撞检查",不会自动绕障 (撞了就返回 fraction < 1.0 失败)。自动绕障要靠 OMPL(RRT*)在关节空间做全局规划。
学习建议:先在 MuJoCo 里手写(就是现在做的),理解每一层原理;将来做工程落地再切 MoveIt 2,才知道每个参数在调什么、什么时候封装不够用。 这是 Q&A 的原话结论,也是本项目的定位。
6.7 MuJoCo 双子星 API 再强调一遍
-
mj_forward= 刷新(改了 qpos 后重算派生量,不推进时间) -
mj_step= 往前走一步(含 forward + 动力学 + 积分)
7. 已知问题与避坑清单
实跑验证时发现的问题,跑之前改一下:
-
MoveCurve()有 bug (MuJoCo_project.py:370):IK 调用把target_pos同时当位置和姿态传了------# 原代码(错误): joint_angles = my_chain.inverse_kinematics( target_pos, target_pos, "all", initial_position=ik_guess) # 第二个参数应为姿态: # inverse_kinematics(target_pos, target_rot, "all", ...)想跑 MoveL 直线演示,建议先修这里;或者直接跑
MoveEllipse()(实现是正确的)。 -
PPO_DQN.py模型路径对不上 (第 379 行):代码里写的是./mujoco-learning-main/assets/.../panda_ppo_reach_target_v2,仓库里实际模型在./assets/model/rl_reach_target_checkpoint/panda_ppo_reach_target_v3,需要手改。 -
PPO_DQN.py用了 Linux 的/tmp:做可视化标志文件,Windows 上该机制会静默失效,但只影响"多进程训练时只开一个 viewer"的小逻辑,训练/测试本身不受影响。 -
IK 结果灌回仿真有静差 :
point()直接把 IK 关节角设为ctrl目标,由于关节是间接驱动(非位置伺服),末端最终位置与目标有厘米级偏差。这不是 bug,是这种"目标角当 ctrl"控制方式的固有特性,想精确可以减小步长/加 PD。 -
相对路径:一切 demo 必须在项目根目录下运行。
8. 学习路线建议
按本项目代码难度递增的顺序:
第 1 步 环境搭建(第 1 章) ✅ 已完成
第 2 步 XML 入门:改 hello.xml,加个关节让它倒下 ← 现在就可以玩
第 3 步 简单控制:跑通 ur5e(),改 ctrl 目标多试几组
第 4 步 感受 IK:跑 point(),改 ee_pos/ee_euler 观察解的变化
第 5 步 关节空间:跑 JointSpaceControl(),观察末端弧线
第 6 步 笛卡尔空间:跑 MoveEllipse(),观察末端贴着椭圆走
第 7 步 自己写轨迹:把椭圆换成"画圆/画字母L/写个Z"(改 generate_ellipse_points 的点列)
第 8 步 强化学习(可选):装好 RL 依赖,修好路径后跑 PPO_DQN.py
第 9 步 进阶方向(对应 Q&A 里的工业 pipeline):
- 用 scipy.optimize.least_squares 自己写雅可比 IK(替代 ikpy)
- 在 IK 残差里加关节限位/碰撞惩罚项
- 写一个简单 RRT 做第 1 层的全局避障规划
- 了解 MoveIt 2 / MJPC 等成熟框架
第 7 步练手模板(画折线,比如字母 "L"):
# 思路:把 MoveEllipse 的 ellipse_points 换成你自己的点列即可
waypoints = np.array([
[ 0.3, -0.2, 0.4], # 竖线起点
[ 0.3, -0.2, 0.2], # 竖线终点(往下)
[ 0.5, -0.2, 0.2], # 横线终点(往右)→ 连起来就是 "L"
])
# 想更平滑:用 np.linspace 在相邻 waypoint 之间撒点后拼接
文档版本:2026-08-29 | 环境已验证:Windows 10 + conda (Python 3.10.20) + mujoco 3.12.0 + ikpy 4.0.0