Введение
Робототехника представляет собой область науки, которая занимается проектированием и созданием механизмов, способных выполнять операции и принимать решения, имитируя или заменяя действия человека. Эта дисциплина объединяет несколько направлений:
- Электротехника отвечает за датчики и исполнительные механизмы. Датчики собирают сведения из окружающей среды, преобразовывая физические характеристики в электрические сигналы, подобно нашим пяти органам чувств. Исполнительные механизмы превращают эти сигналы в механические движения или действия, аналогично мышцам.
- Машиностроение занимается конструированием и кинематикой физической конструкции.
- Информатика обеспечивает разработку программного обеспечения на базе искусственного интеллекта и алгоритмов.
На сегодняшний день ведущей платформой для робототехники служит ROS (Операционная система для роботов), которая координирует взаимодействие различных элементов робота, таких как датчики, двигатели, контроллеры и камеры. Все компоненты обмениваются данными модульным образом. Фреймворк ROS предназначен для работы с реальными прототипами роботов и поддерживает как Python, так и C++. Благодаря своей популярности, на базе ROS создано множество библиотек, включая Gazebo — наиболее продвинутый симулятор в 3D.
Поскольку Gazebo отличается высокой сложностью, для освоения фундаментальных принципов робототехники и создания доступных симуляций можно обойтись вне экосистемы ROS. Ключевые библиотеки на Python включают:
- PyBullet (для новичков) — идеальна для экспериментов с URDF (Unified Robot Description Format), XML-схемой, описывающей структуру робота, его компоненты и геометрию.
- Webots (средний уровень) — предлагает более точную физику, позволяя моделировать сложные сценарии.
- MuJoCo (продвинутый уровень) — симулятор реального мира, применяемый в научных исследованиях. Библиотека RoboGYM от OpenAI построена на базе MuJoCo.
В рамках этого руководства для начинающих мы разберем, как создавать простые 3D-симуляции с использованием PyBullet. Будут представлены практические фрагменты кода на Python, которые легко адаптировать для других ситуаций (скопировать, вставить, запустить), с подробными комментариями к каждой строке для удобства воспроизведения примера.
Настройка
PyBullet — это наиболее удобный симулятор физики для игр, визуальных эффектов, робототехники и обучения с подкреплением. Установка выполняется одной из команд (если pip не сработает, используйте conda):
pip install pybullet
conda install -c conda-forge pybulletPyBullet поддерживает два основных режима работы:
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-аналогом классического print(“hello world”).
urdf_string = '''
<robot name="cube_urdf">
<link name="cube_link">
<visual>
<geometry>
<box size="0.5 0.5 0.5"/><!-- куб 50 см -->
</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
## настройка
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? Это стандартный фиксированный шаг времени в разработке видеоигр, обеспечивающий плавность и отзывчивость без чрезмерной нагрузки на процессор. 60 FPS подразумевает отображение кадра каждые 1/60 секунды, а шаг физики в 1/240 секунды дает четыре обновления физики на каждый рендеринг.
В приведенном коде куб отображается с помощью 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 лежит определение двух ключевых элементов: звеньев и суставов. Звенья — это твердые части роботов (аналогичные костям), а сустав — соединение между двумя жесткими звеньями (подобное мышцам). Без суставов робот был бы просто статуей, но с ними он превращается в подвижный механизм с функциональностью.
Вкратце, каждый робот — это дерево звеньев, связанных суставами. Сустав может быть неподвижным (фиксированным) или вращательным («революционный сустав») либо скользящим («призматический сустав»). Поэтому важно понимать конструкцию конкретного робота.
Рассмотрим пример с известным роботом R2-D2 из «Звездных войн».
Суставы
На этот раз необходимо импортировать плоскость, чтобы обеспечить поверхность для робота. Без нее объекты не смогут столкнуться с землей и будут падать бесконечно.
## настройка
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()})Каждый сустав, будь то колесо, локоть или захват, должен предоставлять эти характеристики. Вышеуказанный код демонстрирует, что у R2-D2 15 суставов. Проанализируем первый, «base to right-leg»:
- Тип сустава равен 4, что указывает на неподвижность. Родительское звено — -1, то есть соединено с базой, корневой частью робота (как позвоночник в скелете). Для R2-D2 база — это основной цилиндрический корпус, большая синяя и белая бочка.
- Название звена — «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-оси. Пределы (нижний=0, верхний=-1) означают бесконечное вращение, типичное для роторов.
Управление таким суставом просто. Основная команда для акселераторов — p.setJointMotorControl2(), позволяющая регулировать силу, скорость и положение. Здесь мы заставим колесо вращаться, применяя силу для постепенного разгона от нуля до целевой скорости с последующим поддержанием баланса сил (против трения, демпфирования, гравитации).
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=2, controlMode=p.VELOCITY_CONTROL, targetVelocity=10, force=5)Если задействовать все четыре колеса аналогичной силой, робот двинется вперед. Из предыдущего анализа известно, что суставы колес — 2 и 3 (правая сторона), 6 и 7 (левая).
С учетом этого сначала заставим R2-D2 повернуться, вращая только одну сторону, а затем применим силу ко всем колесам для прямолинейного движения.
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-объектов, анализ суставов и звеньев, а также взаимодействие с роботом R2-D2. В будущем появятся уроки с более сложными моделями.