状态:草稿(2026-10-18),实测。画面和文中的数值均为仿真器内部的值,摄像头周期、延迟和噪声均为假设值。 录制与渲染于 2026-10-18 在家庭实验室(Home Lab)Kubernetes 的一个 Mini PC 节点上以临时 Pod 形式完成。Pod 资源上限为 CPU 6 核、内存 3 GiB(峰值使用 1.5 GiB),未使用 GPU。 环境版本为 MuJoCo 3.16.0、Python 3.11、numpy 2.3.4、Mesa 23.2.1(llvmpipe, OSMesa)、ffmpeg 4.4.2(libx264)。物理与控制部分与第 7 篇相同(时间步长 0.5 ms,控制频率 1 kHz,学习型混合策略种子 0)。 对评估用的第 201 号球进行了四种情况的测试(学习型混合策略延迟 30 ms·100 ms、传统基准线 30 ms、无学习的标称规划器 30 ms)。由于是确定性执行,各运行了一次,四个结果均与第 7 篇评估文件中同一球、同一条件的结果(结果类型及着陆点)一致。 原始数据(录制
out/rec/ball201.json、文中数值out/facts.json、录制·渲染·绘图脚本)位于工作目录measure/sim-tt-video/中。
目录
- 1. 直观理解:1.3 秒内发生的七项计算
- 2. 视频与图表的制作方法
- 3. 第 1 步:观测——捕获的点延迟到达
- 4. 第 2 步:预测——二次曲线拟合与 RK4·弹跳模型
- 5. 第 3 步:击球决策——从着陆点逆向推算
- 6. 第 4 步:逆运动学——四行闭式解
- 7. 第 5 步:轨迹与挥拍——在 39 ms 内重新规划的路径
- 8. 第 6 步:接触——发生在 1 ms 内的事
- 9. 第 7 步:着陆——距离目标 3.7 cm
- 10. 对比:相同的球,不同的方法与不同的延迟
- 11. 制作过程中的遭遇
- 12. 局限性与未测项
- 13. 下篇预告
- 参考资料
1. 直观理解:1.3 秒内发生的七项计算
第 5 篇和第 7 篇是通过 500 个球的成功率来讨论的。虽然数字很准确,但机器人接一个球的过程中, 计算了什么以及何时计算并不直观。这一次,我们从头到尾追踪一个球,将每一瞬间机器人脑海中的数值展示在屏幕上。
从公开数据的初始状态(对方底线上方、向我方飞来的球)到经过我方机器人并落在对方球场上,共计 1.307 s。这期间包含七个步骤:
- 观测。 摄像头拍摄球的位置,该数据点会有所延迟地到达。
- 预测。 根据到达的数据点拟合球的状态,并外推未来的轨迹。
- 击球决策。 从目标着陆点逆向推算,决定何时、何地、以何种拍面角度和多大力量击球。
- 逆运动学。 将该球拍姿态转换为四个关节的目标值。
- 轨迹与挥拍。 在当前姿态与目标姿态之间为每个关节连接五次多项式并进行跟踪。
- 接触。 球接触球拍,速度和旋转发生改变。
- 着陆。 球落在对方球场上。
机器人的配置与第 5 篇相同,为实际尺寸的龙门 2 轴(x 轴横向、z 轴纵向)+ 手腕 2 轴(偏航 yaw、俯仰 pitch),控制采用第 7 篇的学习型混合策略。该策略仅修正 6 个击球参数,其余均由下文所述的传统计算完成。
2. 视频与图表的制作方法
2.1 整体流程
| 步骤 | 仿真器视角 | 视频中的呈现内容 | 播放速度 |
|---|---|---|---|
| 1 观测 | 0–0.200 s | 拍摄的点(灰色)与到达的点(天蓝色) | 1/20 |
| 2 预测 | 在 0.200 s 暂停 | 二次式拟合、预测轨迹、估计旋转与真实旋转对比 | 静止 |
| 3 击球决策 | 在 0.200 s 暂停 | 击球位置、球拍目标、拍面法线、目标轨迹与落点 | 静止 |
| 4 逆运动学 | 在 0.200 s 暂停 | 当前姿态与目标姿态、四个关节值 | 静止 |
| 5 轨迹与挥拍 | 0.200–0.539 s | 计划球拍轨迹与实际轨迹对比、关节基准与实际图表、0.497 s 时的重新规划 | 1/10 |
| 6 接触 | 0.513–0.583 s | 入射速度与出射速度箭头、速率与旋转 | 1/10 |
| 7 着陆 | 0.543–1.307 s | 实际轨迹与计划轨迹对比、落点误差 | 1/4 |
| 对比 | 0–0.700 s | 同一球的基准线与混合策略对比、延迟 30 ms 与 100 ms 对比 | 1/4 |
视频分辨率为 1280×720,30 fps,时长 65.6 s(1,967 帧),H.264 编码,大小 2.6 MB。由于单篇文章视频时长限制在 30 s 以内,因此在各节中放置了分步骤剪辑片段(4.7–8.1 s),并在其下方附上了对应瞬间的静态图表。
2.2 录制:在不改变计算逻辑的前提下导出数据
第 7 篇的录制轨迹(rec_*.json)中仅包含 5 ms 间隔的球与球拍位置。为了展示分步计算过程,我们需要观测点、拟合的二次曲线、预测轨迹、各决策阶段的规划以及关节基准值,因此使用相同的环境和种子(每个球为 777000 + 球编号)重新运行了仿真。
环境代码保持未修改,仅在子类中进行读取。在每个 1 kHz 的控制周期复制状态,并在每个决策点复制估计值与规划结果。
class RecEnv(ttenv.TTEnv):
def _tick(self, qr, vr, ar):
done = super()._tick(qr, vr, ar) # 물리·제어는 원래 코드 그대로
d = self.d
v, w = ball_world(self) # 세계 좌표 속도·각속도
self.ticks.append([round(self.t, 6), *d.qpos[:11].tolist(), *d.qvel[6:10].tolist(),
*np.asarray(qr).tolist(), *v.tolist(), *w.tolist(), ...])
return done
来源:TT Lab 实验代码 sim-tt-video/src/vid_record.py(略去接触显示列)。
录制结果(结果类型、落点)与第 7 篇评估文件中相同球、相同条件的值进行了核对,四种情况下均完全一致。这证实了相同的计算流程被再次执行。
2.3 渲染:数值来自录制,图表使用专用展示模型
物理模型的机器人除了球拍外没有其他几何形状,若直接渲染则只有球拍悬浮在空中。因此,我们单独制作了一个关节配置(qpos 顺序、位置、轴、球拍大小)相同的 专用展示模型,为其添加了导轨、滑块、手腕和手柄,并将录制的 qpos 直接输入其中,通过 MuJoCo 的离屏渲染器进行渲染。该模型不计算接触。 观测点、预测轨迹、击球点等标记作为几何体放入 MuJoCo 场景中,字幕和数字则使用 NanumGothic 字体绘制在渲染帧之上。画面中的所有数字均读取自录制文件,仅进行速率(向量大小)、角度、cm 换算等直观展示所需的计算。 1 ms 记录间隔之间的帧(在 1/20 倍速下为 1.67 ms 间隔)通过对球和关节位置进行线性插值来渲染。
3. 第 1 步:观测——捕获的点延迟到达
- 摄像头以 120 Hz(约 8.3 ms 间隔)拍摄,位置带有标准差 σ = 3 mm 的噪声。在仿真器中,由于拍摄与 1 ms 控制周期对齐,实际间隔为 8–9 ms。
- 拍摄的点在 30 ms 后才到达机器人。在 0.2 s 进行第一次决策时,拍摄的点有 24 个,到达的点有 21 个。最后 30 ms 内的球对机器人而言尚不可见。
- 对手击球时的初始速率为 7.78 m/s,以运动方向为基准的上旋为 53.8 rad/s(引自 DeepMind 公开数据的初始状态)。
通过观测估计球状态的一般理论(如卡尔曼滤波等)可参见《乒乓球机器人手臂基础》第 6 篇“球飞行方程”。
4. 第 2 步:预测——二次曲线拟合与 RK4·弹跳模型
计算。 对到达的点(最多最近 36 个)进行关于时间的空间位置二次曲线最小二乘拟合,从而获取最后观测时刻的位置、速度和加速度。0.2 s 时的拟合使用了 21 个观测点,加速度为 (1.56, 6.90, −13.49) m/s²。 从加速度中减去重力和空气阻力,将剩余部分视为马格努斯力 (k_M/m) ω×v 并求解 ω。由于旋转仅表现为加速度的微小差异,因此设置了一个向 0 拉扯的先验项(标准差 300 rad/s)。 以此状态,用 4 ms 步长的 RK4 推进规划器的物理模型(阻力系数 0.471、马格努斯系数、球台反弹 0.975·摩擦 0.10),若触台则通过冲量模型进行反弹。
| 0.2 s 时的数值 | 机器人估计值 | 真值(仿真器) |
|---|---|---|
| 速度 (m/s) | (−1.23, −6.29, −0.39) | (−1.14, −6.20, −0.74) |
| 旋转 ω (rad/s) | (82, −16, 4) | (55, 3, 15) |
| 弹跳时刻 | 0.415 s | 0.429 s |
| 弹跳位置 (x, y) m | (−0.47, −1.16) | (−0.47, −1.26) |
预测的弹跳比实际早 14 ms,位置超前 10.3 cm。旋转估计的偏差最大(正如第 6 篇中所见,仅凭位置很难精准观测旋转)。
弹跳后的重新拟合。 当到达点的高度达到最低点并再次回升超过 1 cm 时,机器人将其判定为弹跳。该球在 0.425 s 被判定为弹跳,随后在收到 4 个观测点后的 0.497 s 做出了第二次决策。 若观测点少于 6 个,则将加速度设为重力并仅拟合直线,旋转则直接采用将初次估计通过弹跳模型计算后的值 (131, −25, 4) rad/s(真值为 (98, −7, 15))。该计算公式位于《乒乓球机器人手臂基础》第 6 篇的“重力·阻力·马格努斯与弹跳模型”一节中。
5. 第 3 步:击球决策——从着陆点逆向推算
计算。 依次求解三个要素:
- 出射速度。 目标着陆点为对方球场中心 (0, 0.75) m。设定水平速率为 4.5 m/s 并确定飞行时间 T = 距离 / 4.5,随后挑选使纯重力抛物线恰好到达球台高度的 v_z。
- 拍面法线与拍面速度。 将出射速度与入射速度之差的方向抓取为法线 n,并结合反弹系数 e 确定法线方向的拍面速度 v_pn = v_in·n + (Δv·n)/(1+e)。通过包含摩擦力的冲量模型重新计算预期结果并进行三次修正。
- 击球时刻。 龙门架没有深度 (y) 轴,因此球拍能够触及的 y 坐标仅由手腕角度决定。预测轨迹穿过该 y 坐标的时刻即为击球时刻。
def _v_des(self, b):
dx, dy = TARGET[0] - b[0], TARGET[1] - b[1]
dist = math.hypot(dx, dy)
vh = 4.5
Tf = dist / vh
vz = (T.BALL_R - b[2] + 0.5 * G * Tf * Tf) / Tf
return np.array([dx / dist * vh, dy / dist * vh, vz])
来源:TT Lab 实验代码 rl/src/ttenv.py(与第 7 篇相同的文件)。
学习到的策略会在此标称值上加上 6 个修正(范围为:面 yaw 和 pitch 各 ±20°,面速度 ±1.5 m/s,击球时刻 ±20 ms,球拍中心 x 和 z 各 ±6 cm)。
| 0.2 秒时的首次决策 | 标称(无学习) | 策略修正后 |
|---|---|---|
| 击球时刻 | 0.552 秒 | 0.556 秒(决策后 356 毫秒) |
| 击球位置 b (m) | (−0.58, −1.74, 0.32) | (−0.59, −1.75, 0.32) |
| 面 yaw / pitch | −12.3° / 4.7° | −7.7° / 0.4° |
| 面速度(法线方向) | 0.48 m/s | 1.10 m/s |
| 期望的出球速度 | (1.03, 4.38, 2.26) m/s | (1.02, 4.38, 2.27) m/s, 5.04 m/s |
将策略输出转换为单位后为:yaw +4.7°、pitch −4.0°、面速度 +0.63 m/s、击球时刻 +7.0 ms、中心 x −0.6 cm、z −2.7 cm。从期望落点反推球拍速度和法线的方程位于《乒乓球机器人手臂基础》第 7 篇打击方程中。
6. 四步逆运动学:四行闭式解
由于龙门架采用直角坐标,逆运动学无需迭代计算即可完成。手腕的两个角度可直接从所需的法线求出,龙门架的两个轴只需用球拍中心减去手腕偏移量即可。
def ik(c, n, rb: Robot):
n = np.asarray(n, float) / np.linalg.norm(n)
th = math.asin(max(-1.0, min(1.0, n[2])))
psi = math.atan2(-n[0], n[1])
x = c[0] - rb.L * math.sin(th) * math.sin(psi)
z = c[2] - rb.L * math.cos(th)
y_res = c[1] - (rb.y_g - rb.L * math.sin(th) * math.cos(psi))
q = np.array([x, z, psi, th])
ok = bool(np.all(q >= np.array(rb.lo)) and np.all(q <= np.array(rb.hi)))
return q, y_res, ok
来源:TT Lab 实验代码 sim/ttsim.py(已省略说明注释)。
| 关节 | 当前 (0.2 s) | 第一目标 | 极限 |
|---|---|---|---|
| 龙门架 x | 0.000 m | −0.595 m | ±0.9 m |
| 龙门架 z | 0.150 m | 0.144 m | −0.15–0.7 m |
| 手腕 yaw | 0.0° | −7.7° | ±80° |
| 手腕 pitch | 0.0° | 0.4° | ±80° |
球拍中心为 c = b − n·(球半径 + 球拍厚度/2) 加上策略的中心修正值。四个值均在极限范围内,因此没有被截断的值。 y_res 是龙门架无法对齐的深度方向差值,在标称规划中为 0,而在引入策略修正的第一目标中为 −2.9 cm。关于多解、奇点以及无解等常规手臂问题,请参考《乒乓球机器人手臂基础》第 4 篇逆运动学。
7. 第五步轨迹与挥拍:在 39 ms 内重新规划路径
计算。 每次做出决策时,都会根据当前的关节位置、速度和加速度,为每个关节连接一条五次多项式,一直延伸到打击瞬间的目标(位置 q、速度 (0, 0, 0, −v_pn/L)、加速度 0)。挥拍结束后,pitch 会在 0.12 秒内停下。1 kHz 的计算力矩控制(PD + 前馈,固有频率 50 rad/s)遵循此基准,加速度和速度会在轴极限处被截断。
- 第一次规划(0.200 s,356 ms)。 请求的最大加速度为:横轴 27 m/s²(极限 40),pitch 82 rad/s²(极限 600),留有余量。横轴最高速度为 3.13 m/s,略超 3 m/s 的极限。在 0.497 s 之前,实际值与基准值的差值在 x 方向不超过 0.27 cm,pitch 不超过 0.20°。
- 第二次规划(0.497 s,剩余 39 ms)。 弹地后重新调整的击球位置中,x 平移了 +4.2 cm,z 平移了 −5.2 cm,策略将 pitch 修正了 +6.1°,中心 x 修正了 +3.0 cm。结果,面 pitch 从 0.4° 变为 8.8°,面速度从 1.10 m/s 变为 0.33 m/s。 在剩余的 39 ms 内连接新的五次多项式时,横轴要求达到 323 m/s²(极限 40),高度轴要求达到 98 m/s²(极限 30)。实际值在极限处被截断,导致接触瞬间横轴与基准相差 6.6 cm 。
- 尽管如此,还是击中了。球与球拍中心的最短距离为 5.0 cm,由于球拍面板尺寸为 15×16 cm,因此落在了范围内。接触时间为 0.543 s,比计划的 0.536 s 迟了 7 ms。
五次多项式满足两端位置、速度和加速度的原理以及极限内的最短时间轨迹,可在《乒乓球机器人手臂基础》第 8 篇轨迹生成中找到,计算力矩控制可在第 9 篇控制中找到。
8. 第六步接触:1 ms 内发生的事
视频以 1/10 倍速播放此场景。记录显示球触及球拍的时长为 1 ms 的记录格(两次物理步进)。
| 速率 | 旋转大小 | 基于行进方向 | |
|---|---|---|---|
| 来球 (0.542 s) | 4.51 m/s | 98.9 rad/s | 上旋 97.6 rad/s |
| 出球 (0.544 s) | 4.89 m/s | 98.7 rad/s | 下旋 95.8 rad/s |
- 计划期望的出射速率为 5.06 m/s(仅考虑重力的计算值),实际为 4.89 m/s。
- 接触前夕的 pitch 关节速度为 −2.11 rad/s,推开球拍面的速度为 L·|θ̇| = 0.32 m/s。这与第二次规划的面速度 0.33 m/s 几乎一致。加上龙门架的移动,球拍面中心的速率为 0.69 m/s。
- 旋转矢量仅改变了 1.5 rad/s。由于球的方向反转,入射时的上旋在出射时变成了下旋。由于球拍橡胶的反弹和摩擦力采用的是设定值(摩擦力 0.5),因此实际球拍是否能完好保留这么大的旋转量尚不确定。
9. 第七步落点:距目标 3.7 cm
| 值 | 学习型混合 | 标称(无学习) |
|---|---|---|
| 落点 (x, y) m | (−0.004, 0.713) | (−0.007, 0.529) |
| 距离目标 (0, 0.75) | 3.7 cm | 22.1 cm |
| 最高点高度 | 0.68 m | 0.47 m |
| 结果 | 回击成功 | 回击成功 |
- 学习型混合策略击出的球从网上方 36.7 cm 处通过,并在接触后 0.674 秒落地。
- 规划原本通过仅考虑重力的抛物线预测最高点为 0.54 m,但实际球升到了 0.68 m。下旋产生的马格努斯力将球托高了。可以说,策略的修正弥补了规划公式的误差。在同一球的测试中,无学习的标称规划器落点短了 22.1 cm。
- 但这只是单个球。在 500 个评估球(120 Hz·30 ms)中,学习带来的净收益使成功率从标称的 80.6% 提升至本策略的 83.8%(第 7 篇)。
10. 比较:相同的球,不同的方法与不同的延迟
经典基线(延迟 30 ms):未击中,最近距离时相差 28.2 cm。 基线在 0.2 s 时利用 21 个观测值通过最小二乘法一次性拟合出位置、速度和旋转,读出的旋转为 (111, 279, −27) rad/s(真实值为 (55, 3, 15))。基于该预测,击球位置被设为 x −0.83 m,而当时实际球位于 x −0.54 m。 基线仅从预测弹地后 80 ms 到击球时刻前 80 ms 之间重新规划。由于预测弹地时间为 0.440 s,击球时刻为 0.545 s,该时间窗为空,因此规划仅有 0.2 s 时的一个。而混合策略则从观测中寻找弹地,并在收集到 4 个观测值后立即重新规划进行修正。
学习型混合(延迟 100 ms):未击中,最近距离时相差 32.4 cm。 这是相同的策略、相同的球。由于在 0.2 s 时仅到达了 13 个观测值(30 ms 延迟时为 21 个),读出的旋转为 (67, −5, 87) rad/s,且绕纵轴 (z) 的侧旋成分较大,因此击球位置被设为 x −0.19 m(当时实际球位置 x −0.55 m)。 弹地后的 4 个观测值直到 0.567 s 才到达,此时该规划算出的击球时刻 0.545 s 已经过去,因而无法使用。在 100 ms 延迟下,基于 500 个样本,本策略的回击成功率为 58.2%,基线为 26.6%(第 7 篇)。
11. 制作过程中遇到的问题
- GPU 当时处于占用状态。 GPU 节点的一张卡正被其他学习任务占用。为了不干扰该任务,我们在 CPU 节点上使用 Mesa 的软件渲染器(OSMesa、llvmpipe)进行绘制。1280×720 的简单场景耗时 32 ms,包含显示和字幕在内的最终渲染帧平均耗时 79 ms(1,967 帧耗时 2 分 36 秒)。
- 屏幕投影发生除以零错误。 为了将字幕的指示线贴到三维点上,我们读取了相机的视场角(frustum),但场景更新后不久,frustum 的宽度为 0。由于这是在渲染阶段由屏幕宽高比填充的值,因此我们改用相同的公式(高度 × 宽高比)进行计算。
- 圆柱体尺寸的顺序不匹配。 我们将落点标记放入了一个扁平的圆柱体中,结果却出现了一个高 4 cm 的圆柱桶。直接放入场景中的圆柱体尺寸顺序不是(半径、半径、高度),而是(半径、半高)。
- 观测数量多了一个。 0.2 s 的画面上显示“拍摄观测 25 个”,但录制的决策记录中只有 24 个。环境是在 1 ms 周期的 开始时 拍摄的,而决策是在该周期的 前方进行的,因此 0.200 s 时拍摄的点在决策瞬间尚不存在。屏幕显示也按照此规则进行了修正,并通过断言(assert)与决策记录保持一致性绑定。
- 字体中缺少减号。 数学减号(U+2212)在南哥体(NaNumGothic)中显示为方框。画面文字已替换为连字符。
- 龙门架框架遮挡了场景。 最初绘制的是带有上下导轨和柱子的框架,但在从机器人背面观察的每个场景中,顶部的导轨都挡住了球的路径。在仅用于展示的模型中移除了顶部导轨,仅保留底部导轨和移动柱。这不会对物理产生影响(物理模型的机器人除了球拍外本来就没有其他几何形状)。
- 视频元数据。 ffmpeg 默认会将编码器名称写入容器,而 x264 会将编码器设置字符串写入视频内部。由于博客的视频检查会拒绝这两者,因此我们用
-map_metadata -1 -map_chapters -1 -fflags +bitexact -flags:v +bitexact和-bsf:v filter_units=remove_types=6进行了清除,9 个剪辑均通过了检查函数。整个 65.6 秒的视频超出了单篇文章 30 秒的限制,因此在文章中将其拆分为按阶段的剪辑分别插入。
12. 局限性与未测量项
- 仅为一个球。 本文中的数字仅代表第 201 号评估球这一个数据。对于其他球,重新规划的时机以及触及轴极限的程度会有所不同。分布情况请参考第 5 篇和第 7 篇中的 500 个评估数据。
- sim-to-real 迁移、实际相机延迟、旋转估计准确度、球拍橡胶的反弹与摩擦、轴极限等,与第 7 篇一样,均采用设定值且未进行实际测量。
- 插值。 视频中的慢动作场景是通过对 1 ms 记录之间进行线性插值绘制的。短于 1 ms 的接触内变化(以 0.5 ms 物理步长为单位)在视频中不可见。
- 仅用于展示的几何体。 导轨、滑块、手腕和手柄的形状仅供示意,并非硬件设计(第 3 篇)的尺寸。只有球拍板(15×16 cm,厚度 14 mm)和关节布局与物理模型相同。
13. 转到下一篇
接下来是将低球和扩展工作空间的深度轴加入机器人模型中,并再次运行相同的评估(第 7 篇中预告)。我们将直接使用当前的录制与渲染工具,通过相同的七个步骤来展示当出现深度轴时,“击球时刻”的定义会发生怎样的变化。
TT 的一言。 过去只能通过成功率表格看到的机器人,跟着一个球看下来,发现在最后 39 ms 重新规划路径、即使被要求达到极限 8 倍的性能也能把球打进球拍的场景。一个数字背后竟然隐藏着如此惊心动魄的瞬间!
参考
外部链接与第 7 篇的地址相同(已确认在第 7 篇中于 2026-10-18 开放)。
- MuJoCo: https://github.com/google-deepmind/mujoco
- 公开初始小球状态: https://github.com/google-deepmind/competitive_robot_table_tennis(© 2025 Google LLC, CC BY 4.0. 数据未重新分发,仅刊载了仿真结果。)
- 字幕字体: Nanum Gothic(SIL Open Font License 1.1, Ubuntu fonts-nanum 软件包)
- 延伸阅读: 第 5 篇 经典控制基准, 第 6 篇 DeepMind 乒乓球机器人改进方向, 第 7 篇 乒乓球机械臂强化学习
- “乒乓球机械臂基础”: 第 4 篇 逆运动学, 第 6 篇 球飞行方程, 第 7 篇 击球方程, 第 8 篇 轨迹生成, 第 9 篇 控制
- 引用的代码以及图表和视频的轨迹均为 TT Lab 实验代码的输出。