引言
机器人技术是一个专注于机器人设计与建造的科学领域,旨在通过复制或替代人类行为来执行任务并做出决策。它是一个多学科的综合体,融合了以下几个关键领域:
- 电气工程:主要负责传感器和执行器的设计与集成。传感器从环境中收集数据,将物理属性转换为电信号(类似于人类的五感);执行器则将这些信号转化为物理动作或运动(好比人类的肌肉)。
- 机械工程:关注物理结构的设计和运动实现。
- 计算机科学:负责人工智能软件和算法的开发。
目前,ROS (机器人操作系统)是机器人领域的主要框架,它以模块化的方式处理机器人所有不同的组成部分(传感器、电机、控制器、摄像头等),并协调它们之间的数据交换。ROS框架专为真实的机器人原型设计,可同时支持Python和C++。鉴于其广泛的流行度,许多库都在ROS之上构建,其中最先进的3D模拟器就是Gazebo。
图片由 Simon Kadula 在Unsplash上发布
由于Gazebo相对复杂,开发者仍然可以在ROS生态系统之外学习机器人技术的基础知识并构建用户友好的模拟。主要的Python库包括:
- PyBullet(适合初学者):非常适合用来探索URDF(统一机器人描述格式),这是一种用于描述机器人本体、部件和几何形状的XML模式。
- Webots(适合中级用户):其物理模拟更为精确,能够构建更复杂的仿真。
- MuJoCo(适合高级用户):作为一款真实世界模拟器,它常用于研究级别的实验。OpenAI的RoboGYM库就是基于MuJoCo构建的。

本文中的可视化效果使用PyBullet创建。
作为一份面向初学者的教程,本文将展示如何使用PyBullet构建简单的3D仿真。其中将包含一些实用的Python代码,这些代码可以轻松应用于其他类似场景(只需复制、粘贴、运行),并会逐行进行注释讲解,以便读者能够轻松复现示例。
环境设置
PyBullet是一款用户友好的物理模拟器,广泛应用于游戏、视觉特效、机器人技术和强化学习领域。通过以下任一命令,即可轻松安装(如果pip安装失败,conda通常可以解决):
pip install pybullet
conda install -c conda-forge pybullet
PyBullet主要有两种运行模式:
p.GUI→ 打开一个窗口并实时显示模拟过程。p.DIRECT→ 无图形界面模式,主要用于脚本运行。
import pybullet as p
p.connect(p.GUI) #或 p.connect(p.DIRECT)

作为一个物理模拟器,首要任务是设置重力:
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
此代码设置了模拟的全局重力向量。“-9.8”在Z轴上表示向下9.8米/秒²的加速度,这与地球上的重力加速度相同。若不设置重力,机器人和地面会像在太空中一样,漂浮在零重力环境中。
URDF文件
如果将机器人比作人体,那么URDF文件就是定义其物理结构的骨架。它以XML格式编写,是机器人的基本蓝图,规定了机器人的外观和运动方式。
接下来将展示如何创建一个简单的立方体,这相当于3D世界中的“Hello World”。
urdf_string = """
<robot name="cube_urdf">
<link name="cube_link">
<visual>
<geometry>
<box size="0.5 0.5 0.5"/> <!-- 50 cm cube -->
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.5 0.5 0.5"/>
</geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001"
ixy="0.0" ixz="0.0" iyz="0.0"/>
</inertial>
</link>
</robot>
"""
这些XML代码可以保存为URDF文件,也可以直接作为字符串使用。
import pybullet as p
import tempfile
## setup
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
## create a temporary URDF file in memory
with tempfile.NamedTemporaryFile(suffix=".urdf", mode="w", delete=False) as f:
f.write(urdf_string)
urdf_file = f.name
## load URDF
p.loadURDF(fileName=urdf_file, basePosition=[0,0,1])
## render simulation
for _ in range(240):
p.stepSimulation()

请注意,上述循环运行了240步。为何是240步?这是视频游戏开发中常用的固定时间步长,旨在提供流畅且响应迅速的体验,同时避免CPU过载。当以60 FPS(每秒帧数)渲染时,每1/60秒显示一帧,这意味着1/240秒的物理步长为每个渲染帧提供了4次物理更新。
在之前的代码中,立方体是通过p.stepSimulation()进行渲染的。这意味着模拟并非实时进行,而是由用户控制何时执行下一步物理计算。另一种方法是使用time.sleep将其绑定到实际时间。
import time
## render simulation
for _ in range(240):
p.stepSimulation() #添加一个物理步长(CPU速度 = 0.1秒)
time.sleep(1/240) #减慢到实时速度(240步 × 1/240秒休眠 = 1秒)

随着机器人XML代码变得越来越复杂,幸好PyBullet自带了一系列预设的URDF文件。无需手动创建XML,便可以直接加载默认的立方体模型。
import pybullet_data
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
p.loadURDF(fileName="cube.urdf", basePosition=[0,0,1])
URDF文件的核心在于定义两个主要元素:连杆(links)和关节(joints)。连杆是机器人的刚性部分(如同人体的骨骼),而关节则是连接两个刚性连杆的部位(如同人体的肌肉)。没有关节,机器人将只是一尊雕塑;有了关节,它才能成为一台拥有运动能力和特定功能的机器。
简而言之,每个机器人都是一个由关节连接起来的连杆树。每个关节可以是固定的(不可移动),也可以旋转(“旋转关节”)或滑动(“平移关节”)。因此,了解所使用的机器人结构至关重要。
接下来,以《星球大战》中著名的机器人R2D2为例进行说明。

图片由 Alexandr Popadin 在Unsplash上发布
关节分析
这次,除了机器人,还需要导入一个平面作为机器人的地面。如果没有平面,物体将缺乏碰撞表面,会无限期地向下坠落。
## setup
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
## load URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])
## render simulation
for _ in range(240):
p.stepSimulation()
time.sleep(1/240)
p.disconnect()

在使用机器人之前,必须了解其组成部分。PyBullet对信息结构进行了标准化,因此导入的每个机器人都将始终由相同的17个通用属性定义。为了运行脚本,这里选择不带图形界面的物理服务器连接模式(p.DIRECT)。分析关节的主要函数是p.getJointInfo()。
p.connect(p.DIRECT)
dic_info = {
0:"joint Index", #从0开始
1:"joint Name",
2:"joint Type", #0=旋转关节 (revolute),1=平移关节 (prismatic),4=固定关节 (fixed)
3:"state vectorIndex",
4:"velocity vectorIndex",
5:"flags", #忽略,通常为0
6:"joint Damping",
7:"joint Friction", #摩擦系数
8:"joint lowerLimit", #最小角度
9:"joint upperLimit", #最大角度
10:"joint maxForce", #允许的最大力
11:"joint maxVelocity", #最大速度
12:"link Name", #连接到此关节的子连杆
13:"joint Axis",
14:"parent FramePos", #位置
15:"parent FrameOrn", #方向
16:"parent Index" #−1 = 基础连杆 (base)
}
for i in range(p.getNumJoints(bodyUniqueId=robo)):
print(f"--- JOINT {i} ---")
joint_info = p.getJointInfo(bodyUniqueId=robo, jointIndex=i)
print({dic_info[k]:joint_info[k] for k in dic_info.keys()})

无论是轮子、肘部还是夹持器,每个关节都必须具备这些相同的特性。上述代码显示R2D2有15个关节。我们来分析第一个关节,名为“base to right-leg”:
- 关节类型为4,表示它不可移动。父连杆为-1,意味着它连接到机器人的基础连杆(base),即机器人的根部(如同人类的脊柱)。对于R2D2来说,基础连杆就是那个巨大的蓝白色圆筒形主体。
- 连杆名称是“right leg”,因此可以理解这个关节连接了机器人的基础连杆和右腿,但它并非电动关节。这一点由关节轴、关节阻尼和关节摩擦力都为零所证实。
- 父框架位置和方向定义了右腿连接到基础连杆的位置。
连杆分析
另一方面,为了分析连杆(物理身体段),必须遍历关节以提取连杆名称。然后,可以使用两个主要函数:p.getDynamicsInfo() 来了解连杆的物理属性,以及 p.getLinkState() 来获取其空间状态。
p.connect(p.DIRECT)
for i in range(p.getNumJoints(robo)):
link_name = p.getJointInfo(robo, i)[12].decode('utf-8') #字段 12="link Name"
dyn = p.getDynamicsInfo(robo, i)
pos, orn, *_ = p.getLinkState(robo, i)
dic_info = {"Mass":dyn[0], "Friction":dyn[1], "Position":pos, "Orientation":orn}
print(f"--- LINK {i}: {link_name} ---")
print(dic_info)

让我们分析第一个连杆,“right-leg”。其质量为10千克,这会影响重力和惯性;而摩擦力则影响其与地面接触时的滑动程度。方向(0.707=sin(45°)/cos(45°))和位置表明右腿连杆是一个实心部件,相对于基础连杆略微向前(5厘米),并倾斜了90度。
机器人运动
现在,我们来看看一个可以实际移动的关节。
例如,关节2是右前轮。它是一个类型为0的旋转关节。这个关节连接着右腿(连杆索引1)和右前轮:即基础连杆 → 右腿 → 右前轮。关节轴为(0,0,1),这表示车轮绕Z轴旋转。其限制(下限=0,上限=-1)表明车轮可以无限旋转,这对于转子来说是正常的。
我们可以轻松地驱动这个关节。控制机器人执行器的主要命令是p.setJointMotorControl2(),它允许控制关节的力、速度和位置。在这种情况下,若要使车轮旋转,需要施加力使其速度从零逐渐达到目标速度,然后通过平衡施加的力与阻力(如摩擦力、阻尼、重力)来维持该速度。
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=2,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=10, force=5)
现在,如果对所有4个轮子施加相同的力,就能让这个小机器人向前移动。根据前面进行的分析,我们知道车轮的关节编号分别是2和3(右侧),以及6和7(左侧)。

考虑到这一点,首先可以通过仅旋转一侧的轮子让R2D2转身,然后再对所有轮子同时施加力使其向前移动。
import pybullet as p
import pybullet_data
import time
## setup
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
## load URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])
## settle down
for _ in range(240):
p.stepSimulation()
right_wheels = [2,3]
left_wheels = [6,7]
### first turn
for _ in range(240):
for j in right_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=-100, force=50)
for j in left_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=100, force=50)
p.stepSimulation()
time.sleep(1/240)
### then move forward
for _ in range(500):
for j in right_wheels + left_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=-100, force=10)
p.stepSimulation()
time.sleep(1/240)
p.disconnect()

请注意,每个机器人具有不同的质量(重量),因此其运动表现可能会因模拟物理特性(例如重力)而异。读者可以尝试不同的力与速度值,直到找到理想的运动效果。

结论
本文作为一份教程,旨在介绍PyBullet以及如何为机器人技术创建非常基础的3D仿真。读者学习了如何导入URDF对象,并分析关节和连杆。同时,也使用著名的R2D2机器人进行了实践。未来将推出更多关于高级机器人的教程。
本文的完整代码请访问:GitHub
