跳到主要内容

2. 正运动学

上一章我们只控一个关节。这一章把关节串起来——给出 n 个关节角,求末端执行器在世界坐标系下的 6-DoF 位姿。正运动学是逆运动学、步态规划、视觉-动作对齐的前提。

本章目标

  • 能写出齐次变换矩阵 T = [R p; 0 1]
  • 能从 URDF / MJCF 描述里读出每个关节的父坐标系与旋转轴
  • 能用 Python + NumPy 实现一个 3-DoF 机械臂的 FK,并在 MuJoCo 里验证末端位置误差 < 1e-6
  • 能画出雅可比矩阵(为第 3 章逆解做准备)

前置阅读

2.1 位置、姿态与位姿

先对照下图建立一个直觉:位置只回答"末端原点在哪",姿态只回答"末端坐标轴朝哪",位姿则把两者合在同一个变换里。

位置、姿态与位姿的关系:位置描述末端原点在哪,姿态描述末端坐标轴朝哪,位姿则把两者合在同一个变换里。
位置、姿态与位姿的关系:位置描述末端原点在哪,姿态描述末端坐标轴朝哪,位姿则把两者合在同一个变换里。

图里参考坐标系记为 {B},末端坐标系记为 {E},末端原点记为 。在继续推公式前,先把三个容易混用的词分开:

  • 位置(position):只回答"末端原点在哪"。图左侧蓝色虚线向量 表示的就是位置,通常写成
  • 姿态(orientation):只回答"末端坐标轴朝哪"。它描述的是 {E} 相对 {B} 的朝向,3D 里常用旋转矩阵 、四元数或 rpy 表示;平面机械臂里它通常退化成一个角度
  • 位姿(pose):位置 + 姿态。它既描述末端原点落在什么位置,也描述末端坐标系的朝向,因此是对刚体状态的完整描述。

这三个概念合在一起,就得到本章后面反复出现的齐次变换矩阵:

这里 负责"在哪", 负责"朝哪"。图下半部分画出的,就是这个矩阵的几何意义:它把末端坐标系 {E} 中表示的点,变换到参考坐标系 {B} 中。对 2D 平面机械臂,姿态进一步退化成一个角度,于是位姿矩阵也退化成常见的 3×3 形式。

本章后面的 3-DoF 平面例子会先把重点放在位置上,便于把链式乘法和坐标变换先吃透;到了第 3 章做 IK 时,平面臂的姿态/朝向约束会显式写成

2.2 坐标系、刚体变换、齐次矩阵

机器人学里所有"位姿"的问题,最后都落到一个问题:同一个点,在不同坐标系下的坐标怎么换算?

任意刚体变换都可以拆成"先旋转再平移":

写成齐次矩阵更紧凑,这也是后面所有代码里真正用到的形式:

两个关键性质:

  • 可以一路乘下去 个关节串起来的末端位姿就是 ,这一乘法正是 FK 的全部计算内容。
  • 可逆且正交,这在把世界坐标系下的目标搬到末端坐标系时会反复用到。

一个很常踩的坑:旋转矩阵来源五花八门——MuJoCo 默认给 (w,x,y,z) 四元数,URDF 默认 rpy(roll-pitch-yaw),文献里还经常是绕固定轴 vs 绕活动轴。先把"我这一步的 到底是从哪个约定来的"写清楚,比急着写矩阵乘法更重要。

2.3 DH 参数 vs URDF 父-子链式约定

经典教材(Craig 那本)用 DH 参数描述机械臂:每个连杆四个参数 ,对应一个固定形式的 4×4 矩阵。它的好处是"参数极少、推公式快";缺点是不可避免要让连杆坐标系和机械结构错位,初学者很容易把连杆长度和 混淆。

URDF / MJCF 走的是另一条路——父-子链式约定:

world → base → link1 → link2 → link3 → ... → end_effector
joint1 joint2 joint3

每条 <joint> 定义子连杆相对父连杆的固定偏置 + 轴向:

<joint name="joint2" type="revolute" axis="0 0 1">
<origin xyz="0.3 0 0" rpy="0 0 0"/>
<parent link="link1"/>
<child link="link2"/>
</joint>
  • 静止部分origin(平移+欧拉角)在 URDF 加载时就确定下来,本质是一个常量
  • 运动部分:绕 axis 旋转 的增量

于是第 个关节对应的齐次矩阵总是可以写成:

本章后面的代码都按这个更贴近 URDF 的写法走。好处是你写的 FK 代码可以直接从 URDF / MJCF 里读参数,不用再手动换算一遍 DH。

2.4 一个 3-DoF 平面臂的 FK 手推

作为热身,先考虑最干净的平面 3-DoF 臂——三个连杆长度 ,三个 z 轴旋转关节 。末端位置是:

如果把末端姿态也一起写出来,这个平面例子其实还有一个很干净的结果:

也就是说,这个 3-DoF 平面臂的完整 FK 输出可以理解成 。下一章解析 IK 时,这个 会作为"末端朝向"重新出现。

注意这里关节角是逐级累加的——这是下一章解析 IK 能那么干净收敛的关键。

把它写成齐次矩阵形式,方便后面推广到 3D:

把三个 展开一算,就会发现末端 正是 的第 4 列前两行;这也是所有 URDF 风格 FK 写法的通用套路——乘出来就直接读平移向量

2.5 用 NumPy 写 FK 并与 mj_forward 交叉校验

先定义两个最基础的齐次变换构造器,后面所有 FK 代码都靠它们拼积木:

import numpy as np

def rot_z(theta):
c, s = np.cos(theta), np.sin(theta)
T = np.eye(4)
T[:3, :3] = [[c, -s, 0],
[s, c, 0],
[0, 0, 1]]
return T

def trans(x, y, z):
T = np.eye(4)
T[:3, 3] = [x, y, z]
return T

3-DoF 平面臂的 FK:

def fk_planar(thetas, L=(0.3, 0.25, 0.15)):
t1, t2, t3 = thetas
T = rot_z(t1) @ trans(L[0], 0, 0) \
@ rot_z(t2) @ trans(L[1], 0, 0) \
@ rot_z(t3) @ trans(L[2], 0, 0)
return T # 末端位置直接读 T[:3, 3]

然后用 MuJoCo 的 mj_forward 作为"标准答案"交叉校验——给 100 组随机关节角,比较末端位置的最大误差:

import mujoco

model = mujoco.MjModel.from_xml_path('planar_3dof.xml')
data = mujoco.MjData(model)
end_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, 'end_site')

max_err = 0.0
for _ in range(100):
q = np.random.uniform(-np.pi, np.pi, size=3)
data.qpos[:3] = q
mujoco.mj_forward(model, data) # 让 MuJoCo 用 kinematic tree 算
p_mj = data.site_xpos[end_id] # 它算的末端位置

p_ours = fk_planar(q)[:3, 3] # 我们自己算的末端位置

max_err = max(max_err, np.linalg.norm(p_mj - p_ours))

print(f'max |p_ours - p_mj| = {max_err:.2e}')
# 期望量级在 1e-14 ~ 1e-12;超过 1e-6 基本是参数/轴向写错了

为什么要花这一步校验? 因为 FK 是后面所有章节的基石——IK 靠它算残差,雅可比靠它的导数,控制器靠它估末端位置。差 0.5 mm 在第 3 章 末端画圆时就是一圈肉眼可见的毛刺。把"和物理引擎完全一致"作为第一次运行的硬指标,后面所有实验才有意义。

2.6 雅可比矩阵:从 FK 到速度映射

FK 把关节角映射到末端位姿。若只关心位置部分,可以写成 ;对时间求导就得到雅可比矩阵,它描述"关节角怎么动,末端怎么动":

对平面 3-DoF 臂,(平面位置对 3 个关节求偏导);3D 一般机械臂是 (前 3 行位置、后 3 行旋转,分别叫线速度雅可比和角速度雅可比)。

有两种常用算法:

  1. 解析求导:把 FK 关于每个 求偏导,适合符号化推一次、运行时直接算——速度快,但和具体机器人强绑定。
  2. 几何雅可比:对第 列,线速度部分是 ,角速度部分就是 (关节轴在世界坐标系下的方向)。代码写起来通用,也是下一章 IK 用的那一种。

雅可比是运动学和动力学之间的桥:它一出现,速度、力、能量就都能用同一套代数来写——所以第 3 章数值 IK、第 5 章脚端接触力、第 6 章 RL 奖励里都会反复见到它。

2.7 实验 1:画出工作空间散点图

工作空间(reachable workspace)是末端能到达的所有点的集合。用 FK 做一次 Monte Carlo 就能把它画出来:

import matplotlib.pyplot as plt

pts = []
for _ in range(20000):
q = np.random.uniform(-np.pi, np.pi, size=3)
pts.append(fk_planar(q)[:2, 3])
pts = np.array(pts)

plt.figure(figsize=(5, 5))
plt.scatter(pts[:, 0], pts[:, 1], s=1, alpha=0.3)
plt.axis('equal'); plt.title('3-DoF planar arm workspace')
plt.show()

你会看到一个甜甜圈形状的区域——外圈是三节完全伸直能到的最远距离 ,内圈是连杆互相折叠能贴到的最近距离。这张图在第 3 章选 IK 目标时会派上用场:目标点落在甜甜圈外就是典型的"无解",这时再好的 IK 算法也救不了你。

2.8 实验 2:把末端位置叠加到 MuJoCo 可视化上

MuJoCo 支持在 viewer 里叠加"调试几何",非常适合把 FK 的中间结果可视化出来——比如末端位置、每一节连杆坐标轴、目标点。

import mujoco.viewer

with mujoco.viewer.launch_passive(model, data) as v:
while v.is_running():
# 假设我们在 Python 里用自己的 FK 算末端位置
p = fk_planar(data.qpos[:3])[:3, 3]

# 清空旧的调试几何再画
v.user_scn.ngeom = 0
mujoco.mjv_initGeom(
v.user_scn.geoms[0],
type=mujoco.mjtGeom.mjGEOM_SPHERE,
size=[0.02, 0, 0], pos=p, mat=np.eye(3).flatten(),
rgba=[1, 0.3, 0.3, 1],
)
v.user_scn.ngeom = 1

mujoco.mj_step(model, data)
v.sync()

这一步看似"只是画个红球",但它实际上是把 Python 世界里的数学模型和 MuJoCo 世界里的物理模型对上。如果红球不在机械臂末端上,不是 viewer 的锅——要么坐标系没对齐,要么链式乘法少了一环、多了一环。到第 3 章做 IK 时,目标点当前末端都是用这种方式实时叠到场景里的。

组队学习任务

  • 实现 fk.py,输入关节角列表,输出末端 4×4 齐次矩阵
  • 对 100 个随机关节组合,比较自写 FK 和 mj_forward 的最大误差
  • 画工作空间散点图(matplotlib 3D)

参考资料