用Python和PyBullet开启机器人仿真之旅

机器人是能够执行任务并复制或替代人类动作的机器。机器人学是专注于机器人设计和构建的科学领域。它是一个多学科的组合,包括用于传感器和执行器的电气工程、用于物理结构设计的机械工程,以及用于人工智能软件和算法的计算机科学[citation:1]。

目前,ROS(机器人操作系统)是处理机器人所有不同部分(传感器、电机、控制器、摄像头……)的主要框架,其中所有组件以模块化方式交换数据。ROS框架适用于真实的机器人原型,可以与Python和C++一起使用。由于其普及性,有许多建立在ROS之上的库,例如最先进的3D模拟器Gazebo[citation:1]。

由于Gazebo相当复杂,我们仍然可以在ROS生态系统之外学习机器人学的基础知识并构建用户友好的模拟。主要的Python库有:

  • PyBullet(初学者) — 非常适合尝试URDF(统一机器人描述格式),这是一种描述机器人身体、零件和几何形状的XML模式。
  • Webots(中级) — 物理更准确,因此可以构建更复杂的模拟。
  • MuJoCo(高级) — 现实世界模拟器,用于研究级实验。一个研究机构的RoboGYM库就是建立在MuJoCo之上的[citation:4]。

由于这是一个面向初学者的教程,将展示如何使用PyBullet构建简单的3D模拟。将提供一些有用的Python代码,这些代码可以轻松应用于其他类似情况(只需复制、粘贴、运行),并通过注释逐步讲解每一行代码,以便你能复现这个例子。

准备工作

PyBullet是一个适用于游戏、视觉效果、机器人学和强化学习的最易用的物理模拟器[citation:3]。你可以使用以下命令之一轻松安装它(如果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)

设置模拟的全局重力矢量。Z轴上的“-9.8”意味着向下9.8 m/s²的加速度,就像在地球上一样。没有这个设置,你的机器人和平面将在零重力下漂浮,就像在太空中一样。

URDF文件

如果机器人是人体,那么URDF文件就是定义其物理结构的骨骼。它是用XML编写的,基本上是机器的蓝图,定义了机器人的外观和运动方式[citation:3]。

我将展示如何创建一个简单的立方体,这是相当于print("hello world")的3D版本。

urdf_string = """"
<robot name="cube_urdf">
  <link name="cube_link">
    <visual>
      <geometry>
        <box size="0.5 0.5 0.5"/> <!-- 50 cm 立方体 -->
      </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>
"""

你可以将XLM代码保存为URDF文件,或将其作为字符串使用。

import pybullet as p
import tempfile

## 设置
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)

## 在内存中创建一个临时的URDF文件
with tempfile.NamedTemporaryFile(suffix=".urdf", mode="w", delete=False) as f:
    f.write(urdf_string)
    urdf_file = f.name

## 加载URDF
p.loadURDF(fileName=urdf_file, basePosition=[0,0,1])

## 渲染模拟
for _ in range(240):
    p.stepSimulation()

请注意,循环运行了240步。为什么是240?这是视频游戏开发中常用的固定时间步长,旨在提供流畅和响应迅速的体验,而不会过度消耗CPU。60 FPS(每秒帧数)意味着每1/60秒显示一帧,而1/240秒的物理步长则提供每渲染帧4次物理更新。

在之前的代码中,我使用p.stepSimulation()渲染了立方体。这意味着模拟不是实时的,由你控制下一个物理步长何时发生。或者,你可以使用time.sleep将其绑定到现实世界的时钟。

import time

## 渲染模拟
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文件的核心定义了两样东西:连杆关节。连杆是机器人的固体部分(就像我们的骨骼),而关节是两个刚性连杆之间的连接(就像我们的肌肉)。没有关节,你的机器人只是一个雕像;有了关节,它就变成了具有运动和目的的机器[citation:6]。

简而言之,每个机器人都是一个由关节连接的连杆树。每个关节可以是固定的(不能移动)或旋转的(“旋转关节”)和滑动的(“棱柱关节”)。因此,你需要了解你正在使用的机器人。

让我们以《星球大战》中著名的机器人R2D2为例。

关节

这次,我们还必须导入一个平面,以便为机器人创建地面。如果没有平面,物体将没有表面可碰撞,它们只会无限期地下落。

## 设置
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())

## 加载URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])

## 渲染模拟
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=旋转,1=棱柱(滑动),4=固定
    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 = 基座
}
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,意味着它连接到基座,即机器人的根部分(就像我们的脊柱)。对于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)连接到右前轮:base_link → right_leg → right_front_wheel。关节轴是(0,0,1),所以我们知道轮子绕Z轴旋转。限制(lower=0, upper=-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

## 设置
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())

## 加载URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])

## 稳定下来
for _ in range(240):
    p.stepSimulation()

right_wheels = [2,3]
left_wheels = [6,7]

### 先转弯
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)

### 然后向前移动
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上找到。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

公众号二维码

公众号二维码

posted @ 2025-12-16 19:14  CodeShare  阅读(19)  评论(0)    收藏  举报