上一节,你唤醒了一个灰白色的双足机器人,它在仿真窗口里静静地站着。你旋转视角、缩放画面,从各个角度打量它。但有一个问题始终悬而未决:它一动不动。
这一节,我们要终结这种静止。你要亲手写代码,向机器人的关节发送角度指令,看到它在屏幕上做出真实的动作。 这是你第一次扮演“大脑”的角色——你下达指令,虚拟身体执行指令。
在动手之前,我们需要建立一条最核心的认知:在仿真环境中,控制机器人就是控制关节的角度。
回想你自己的身体。你想弯肘,大脑不是直接命令“弯”,而是向下臂的屈肌发送一串神经信号,肌肉收缩,肘关节弯曲到一个特定角度。机器人的逻辑完全一样。你写的控制程序,本质上就是在不断回答同一个问题:每一个旋转关节,在下一个瞬间应该转多少度?
在MuJoCo中,关节的目标角度通过 data.ctrl 这个数组来设定。它是一个一维向量,长度等于机器人的驱动自由度数量。data.ctrl[0] 对应第一个驱动关节的目标角度,data.ctrl[1] 对应第二个,以此类推。你给这个数组赋值,仿真引擎就会在下一步尝试驱动电机去达到那些角度。
关节角度通常以弧度为单位。如果你更习惯角度,记住换算关系:
$$ \theta_{\text{rad}} = \theta_{\text{deg}} \times \frac{\pi}{180} $$
例如,$30^\circ$ 就是 $\frac{\pi}{6} \approx 0.5236$ 弧度。写代码时可以直接用 math.radians(30) 来转换。
在发送指令之前,我们需要先让前一小节的代码稍微进化一下。上一节的代码只是启动了仿真窗口并循环步进,但没有给任何控制信号——所以机器人只是被动地站在那里,任由重力把自己往下拉。
现在,我们要在仿真循环中主动设置 data.ctrl。先从一个最简单的实验开始:不发送任何指令,看看机器人会怎样。 把下面的代码跑起来:
import mujoco
import mujoco.viewer
import numpy as np
# 加载模型
model = mujoco.MjModel.from_xml_path('humanoid.xml')
data = mujoco.MjData(model)
# 打印驱动自由度数量
print(f"驱动关节数量: {model.nu}")
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
# 暂时不发送任何控制指令,保持默认值
# data.ctrl 的默认值通常是0
mujoco.mj_step(model, data)
viewer.sync()
你会看到,机器人并没有僵住。它的手臂可能会在重力作用下微微下垂,身体可能有轻微的晃动。这是因为仿真引擎在计算重力、惯性力和关节力矩的平衡。那个静止的站姿,本身就是一个动态平衡的结果。
现在,让我们干一件有仪式感的事情:让机器人对你挥手。
我们需要知道哪个关节控制手臂。对于 humanoid.xml 这个模型,肩部关节通常叫 arm_shoulder_y(控制前后摆动)和 arm_shoulder_x(控制左右展开)。你可以用上一节的关节遍历代码确认具体名称。
但更直接的方法,是通过关节名称找到它在 data.ctrl 中的索引。MuJoCo提供了这个功能:
import mujoco
import mujoco.viewer
import numpy as np
import math
model = mujoco.MjModel.from_xml_path('humanoid.xml')
data = mujoco.MjData(model)
# 通过关节名称找到控制索引
# 注意:关节名称和驱动器名称可能不同,需要查看 actuator 的名称
print("驱动器清单:")
for i in range(model.nu):
actuator_name = model.actuator(i).name
print(f" 索引 {i}: {actuator_name}")
运行这段代码。你会看到类似 shoulder_y_r、shoulder_x_r、elbow_y_r 这样的名字(以右臂为例)。记下 shoulder_y_r 的索引号——假设是某个数字,我们后面会用到。
现在,我们要让这个关节按照正弦波来回摆动。正弦波是最自然的往复运动,手臂的前后摆动轨迹天然就是正弦曲线。目标角度随时间变化的公式为:
$$ \theta(t) = A \cdot \sin(2\pi \cdot f \cdot t) $$
其中 $A$ 是振幅(摆动的最大角度),$f$ 是频率(每秒摆动几次),$t$ 是当前时间。
写成代码如下:
import mujoco
import mujoco.viewer
import numpy as np
import math
model = mujoco.MjModel.from_xml_path('humanoid.xml')
data = mujoco.MjData(model)
# 找到右肩前后摆动关节的控制索引
shoulder_idx = -1
for i in range(model.nu):
if 'shoulder_y_r' in model.actuator(i).name:
shoulder_idx = i
print(f"找到右肩关节驱动器: 索引 {i}, 名称 {model.actuator(i).name}")
break
if shoulder_idx == -1:
print("未找到 shoulder_y_r 驱动器,请检查驱动器清单中的实际名称")
exit()
# 获取仿真时间步长
timestep = model.opt.timestep
sim_time = 0.0
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
# 计算挥手动作的目标角度
# 振幅30度,频率1.5Hz
amplitude = math.radians(30)
frequency = 1.5
target_angle = amplitude * math.sin(2 * math.pi * frequency * sim_time)
# 将目标角度写入控制数组
data.ctrl[shoulder_idx] = target_angle
# 步进仿真
mujoco.mj_step(model, data)
viewer.sync()
# 更新时间
sim_time += timestep
运行这段代码。你会看到机器人开始对你挥手!它的右臂以前后30度的幅度,每秒1.5次的频率来回摆动。这个动作虽然简单,但它是你操控虚拟身体的第一步——从零到一的一步。
挥手是单关节运动。现在我们要做一个多关节协同的动作:深蹲。
深蹲需要同时弯曲髋关节、膝关节和踝关节。双足机器人的两条腿各有三到四个关节参与这个动作。我们需要找到所有相关的驱动器索引,然后让它们同时弯曲到一个特定角度。
import mujoco
import mujoco.viewer
import numpy as np
import math
model = mujoco.MjModel.from_xml_path('humanoid.xml')
data = mujoco.MjData(model)
# 找到双腿相关关节的驱动器索引
# 髋关节前后摆动 (hip_y),膝关节 (knee),踝关节 (ankle_y)
leg_joints = []
for i in range(model.nu):
name = model.actuator(i).name
if any(keyword in name for keyword in ['hip_y', 'knee', 'ankle_y']):
leg_joints.append(i)
print(f"找到腿部驱动器: 索引 {i}, 名称 {name}")
print(f"共找到 {len(leg_joints)} 个腿部关节驱动器")
# 仿真参数
timestep = model.opt.timestep
sim_time = 0.0
squat_duration = 2.0 # 下蹲用时2秒
hold_duration = 1.0 # 保持1秒
stand_duration = 2.0 # 起立用时2秒
cycle_duration = squat_duration + hold_duration + stand_duration
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
# 计算当前在运动周期中的位置
phase_time = sim_time % cycle_duration
if phase_time < squat_duration:
# 下蹲阶段:关节角度从0线性增加到目标角度
progress = phase_time / squat_duration
# 髋关节和膝关节弯曲45度,踝关节弯曲20度(保持脚掌贴地)
angle = progress * math.radians(45)
ankle_angle = progress * math.radians(20)
elif phase_time < squat_duration + hold_duration:
# 保持阶段:维持最大弯曲角度
angle = math.radians(45)
ankle_angle = math.radians(20)
else:
# 起立阶段:关节角度线性减小回0
progress = (phase_time - squat_duration - hold_duration) / stand_duration
angle = (1 - progress) * math.radians(45)
ankle_angle = (1 - progress) * math.radians(20)
# 将计算出的角度写入腿部关节
for idx in leg_joints:
name = model.actuator(idx).name
if 'ankle' in name:
data.ctrl[idx] = ankle_angle
else:
data.ctrl[idx] = angle
mujoco.mj_step(model, data)
viewer.sync()
sim_time += timestep
运行这段代码。机器人缓缓蹲下,保持一秒,再缓缓站起,然后循环往复。你刚刚实现了一个完整的深蹲周期——这是多关节协同控制的第一次实战。
有几个值得注意的细节。踝关节的角度和髋膝不同,因为它需要在身体下降时保持脚掌贴地。如果所有关节弯曲相同角度,脚后跟会翘起来,机器人的姿态就不自然了。这只是最粗糙的深蹲,真正的稳定行走还需要考虑重心投影、零力矩点等更精细的控制——那些我们会在后面的章节里学到。但此刻,看到你的机器人从静止到蹲下再站起,你已经感受到了控制的本质:给每一个关节,在每一个时刻,指定一个合适的角度。
挥手和深蹲都是脚本自动运行的。但真正好玩的是用键盘实时控制机器人,就像打游戏一样。
我们要用到 pygame 这个库来捕捉按键状态(需要先 pip install pygame)。逻辑很简单:按上键就下蹲,按下键就起立,按W键就双臂上举,按S键就双臂放下。
import mujoco
import mujoco.viewer
import numpy as np
import math
import pygame
import sys
# 初始化pygame用于键盘输入
pygame.init()
screen = pygame.display.set_mode((200, 200))
pygame.display.set_caption("键盘控制器")
model = mujoco.MjModel.from_xml_path('humanoid.xml')
data = mujoco.MjData(model)
# 找到关键关节的驱动器索引
arm_indices = {} # 手臂关节
leg_indices = {} # 腿部关节
for i in range(model.nu):
name = model.actuator(i).name
if 'shoulder' in name:
arm_indices[name] = i
if any(kw in name for kw in ['hip_y', 'knee', 'ankle']):
leg_indices[name] = i
print("手臂控制关节:", arm_indices)
print("腿部控制关节:", leg_indices)
# 状态变量
arm_angle = 0.0 # 当前手臂角度
leg_angle = 0.0 # 当前腿部角度
angle_step = 0.02 # 每次按键改变的角度增量(弧度)
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
# 处理pygame事件(必须调用,否则窗口会卡住)
for event in pygame.event.get():
if event.type == pygame.QUIT:
viewer.close()
pygame.quit()
sys.exit()
# 检测按键状态
keys = pygame.key.get_pressed()
# 控制腿部:上键下蹲,下键起立
if keys[pygame.K_UP]:
leg_angle = min(leg_angle + angle_step, math.radians(60))
print(f"下蹲: {math.degrees(leg_angle):.1f}度", end='\r')
if keys[pygame.K_DOWN]:
leg_angle = max(leg_angle - angle_step, 0)
print(f"起立: {math.degrees(leg_angle):.1f}度", end='\r')
# 控制手臂:W键上举,S键放下
if keys[pygame.K_w]:
arm_angle = min(arm_angle + angle_step, math.radians(90))
print(f"举手: {math.degrees(arm_angle):.1f}度", end='\r')
if keys[pygame.K_s]:
arm_angle = max(arm_angle - angle_step, math.radians(-45))
print(f"放手: {math.degrees(arm_angle):.1f}度", end='\r')
# 将角度写入对应关节
for name, idx in arm_indices.items():
# 根据关节的旋转轴决定角度正负
if 'y' in name:
data.ctrl[idx] = -arm_angle
else:
data.ctrl[idx] = arm_angle
for name, idx in leg_indices.items():
if 'ankle' in name:
data.ctrl[idx] = leg_angle * 0.4 # 踝关节弯曲较小
else:
data.ctrl[idx] = leg_angle
mujoco.mj_step(model, data)
viewer.sync()
pygame.quit()
运行这段代码。一个 pygame 小窗口会弹出(保持它在前台),同时仿真窗口也打开。现在按下方向键的上键——机器人开始下蹲;按下键——它缓缓站起。按W键——双臂上举;按S键——放下。
你在用键盘实时操控一具虚拟身体。 每一个按键按下,都会转化为特定关节的角度变化。这种实时反馈的操控感,是学习具身智能最上瘾的体验之一。尽情玩一会儿——让机器人做各种奇怪的动作,蹲到最低点再跳起来(虽然它不会真的跳),把一条手臂举到最高另一条放下。破坏性的实验在仿真里完全免费。
停下手指,回顾一下这一节你学会了什么。
你理解了关节角度控制是机器人运动控制的基本单元。你用正弦波让机器人挥手,用分段函数让它深蹲,用键盘实时操控它的手臂和腿部。这些看起来简单的操作,背后是全套的机器人控制流程:感知按键输入,决策目标角度,驱动执行器运动。
更重要的是,你建立了和这具虚拟身体的肌肉记忆。你知道了哪个关节管前后摆动,哪个管左右开合,知道了腿和手臂的运动范围大概在多少度以内。这些直觉,在后面学习强化学习和运动规划时,会成为你调试算法的第一手依据。
在下一节,我们将换一个视角。这一节你扮演的是遥控操作者,手动给出每一个关节的指令。但机器人未来需要自己做出决策。那么,机器人的“大脑”是如何从传感器数据中,自动计算出该执行什么动作的?我们将进入具身智能最核心的篇章:决策。