موضوعات این جلسه:
WorldGravityGroundObjectSimulation- هدف این جلسه این نیست که هنوز AGV بسازیم؛ میخواهیم اول یک دنیای فیزیکی ساده بسازیم که بعداً رباتمان داخل آن قرار بگیرد.
1. World یعنی چه؟
فعلاً این تصویر ذهنی را داشته باش:
PyBullet
│
└── World
│
├── Ground
├── Object
├── Object
└── ...
World یعنی محیط شبیهسازی که اجسام داخل آن قرار دارند و قوانین فیزیکی روی آنها اعمال میشود.
مثلاً در پروژه AGV در نهایت World میتواند شامل اینها باشد:
World
├── Ground
├── Walls
├── Obstacles
├── AGV
└── Other Objects
ولی امروز فقط با زمین و یک جسم کار میکنیم.
1.1 اولین World
فعلاً main.py را باز کن و کد جلسه قبل را با این جایگزین کن:
import pybullet as p
import time
print("Connecting to PyBullet...")
client = p.connect(p.GUI)
print("Connected!")
print("Connection ID:", client)
time.sleep(5)
p.disconnect(client)
print("Disconnected!")
حالا قرار است کمکم به آن اضافه کنیم.
2. Gravity چیست؟
در دنیای واقعی اگر یک جسم را رها کنی، به سمت زمین سقوط میکند.
علتش گرانش است.
در PyBullet باید گرانش را مشخص کنیم:
p.setGravity(0, 0, -9.81)
این سه عدد مربوط به محورهای:
X Y Z
هستند.
پس:
X = 0
Y = 0
Z = -9.81
یعنی گرانش در جهت منفی محور Z است.
فعلاً لازم نیست درباره Coordinate System زیاد جلو برویم؛ جلسه ۹ بهطور کامل درباره آن صحبت میکنیم.
2.1 حالا Gravity را اضافه کنیم
main.py را اینطور کن:
import pybullet as p
import time
print("Connecting to PyBullet...")
client = p.connect(p.GUI)
print("Connected!")
print("Connection ID:", client)
p.setGravity(0, 0, -9.81)
print("Gravity set!")
time.sleep(5)
p.disconnect(client)
print("Disconnected!")
اجرا کن.
خروجی باید چیزی شبیه:
Connecting to PyBullet...
Connected!
Connection ID: 0
Gravity set!
Disconnected!
اما هنوز هیچ چیزی نمیبینی.
چرا؟ چون هنوز هیچ جسمی در World ایجاد نکردهایم.
3. ساخت Ground
حالا میخواهیم زمین بسازیم.
PyBullet یک فایل آماده به نام:
plane.urdf
دارد.
فعلاً فقط از آن استفاده میکنیم.
این خط را بعد از Gravity اضافه کن:
p.loadURDF("plane.urdf")
پس:
import pybullet as p
import time
print("Connecting to PyBullet...")
client = p.connect(p.GUI)
print("Connected!")
print("Connection ID:", client)
p.setGravity(0, 0, -9.81)
print("Gravity set!")
p.loadURDF("plane.urdf")
print("Ground created!")
time.sleep(5)
p.disconnect(client)
print("Disconnected!")
اما این کد ارور (pybullet.error: Cannot load URDF file.) میده و دلیل اون p.loadURDF(“plane.urdf”) هست .
PyBullet فایل plane.urdf را در Current Working Directory پروژه پیدا نمیکند.
قبلاً فقط این را داشتیم:
p.connect(p.GUI)
time.sleep(5)
p.disconnect()
پس مشکلی برای پیدا کردن فایل وجود نداشت.
اما وقتی loadURDF() اضافه شد، PyBullet دنبال فایل plane.urdf میگردد و پیدایش نمیکند.
راه درست
PyBullet خودش مجموعهای از فایلهای آماده مثل plane.urdf دارد. باید مسیر دادههای خودش را به PyBullet معرفی کنیم.
این کار با:
p.setAdditionalSearchPath(pybullet_data.getDataPath())
انجام میشود.
پس باید pybullet_data را هم Import کنیم.
قدم اول— main.py را اصلاح کن
کل کد فعلی را با این جایگزین کن:
import pybullet as p
import pybullet_data
import time
print("Connecting to PyBullet...")
client = p.connect(p.GUI)
print("Connected!")
print("Connection ID:", client)
# مسیر فایلهای آماده PyBullet
p.setAdditionalSearchPath(pybullet_data.getDataPath())
# Gravity
p.setGravity(0, 0, -9.81)
print("Gravity set!")
# Ground
p.loadURDF("plane.urdf")
print("Ground created!")
time.sleep(5)
p.disconnect(client)
print("Disconnected!")
قدم دوم— اجرا کن
حالا باید این اتفاق بیفتد:
Connecting to PyBullet...
Connected!
Connection ID: 0
Gravity set!
Ground created!
و پنجره PyBullet حدود ۵ ثانیه باز بماند.
4. ساخت اولین Object
حالا یک جسم ساده میسازیم.
مثلاً یک مکعب.
اما اینجا یک مفهوم مهم PyBullet داریم:
برای یک جسم معمولاً باید دو چیز را مشخص کنیم:
4.1 Collision Shape
شکل برخورد فیزیکی جسم.
این:
collision_shape = p.createCollisionShape(
p.GEOM_BOX,
halfExtents=[0.5, 0.5, 0.5]
)
به PyBullet میگوید:
شکل هندسیای که موتور فیزیک برای برخورد این جسم استفاده کند، یک Box است.
مثلاً اگر بعداً یک AGV داشته باشیم:
AGV
┌───────────┐
│ │
│ AGV │
│ │
└───────────┘
وقتی AGV به دیوار برخورد کند، PyBullet باید بداند:
شکل فیزیکی AGV چیست؟
این همان Collision Shape است.
4.2 Visual Shape چیست؟
این:
visual_shape = p.createVisualShape(
p.GEOM_BOX,
halfExtents=[0.5, 0.5, 0.5]
)
به PyBullet میگوید:
این جسم را در پنجره شبیهسازی چگونه نمایش بده.
پس:
Collision Shape
↓
برای فیزیک و برخورد
Visual Shape
↓
برای نمایش
این دو میتوانند یک شکل باشند، ولی الزاماً مجبور نیستند یکسان باشند.
4.3 پس createMultiBody چیست؟
حالا این:
box_id = p.createMultiBody(
baseMass=1,
baseCollisionShapeIndex=collision_shape,
baseVisualShapeIndex=visual_shape,
basePosition=[0, 0, 2]
)
میآید این اجزا را کنار هم قرار میدهد و یک Body واقعی در Simulation میسازد.
یعنی:
Collision Shape ──┐
├──→ createMultiBody → Box
Visual Shape ─────┘
│
├── Mass = 1 kg
└── Position = [0,0,2]
بنابراین box_id شناسه این جسم در PyBullet است.
🔍 چرا به دو شکل جداگانه Collision Shape و Visual Shape نیاز داریم؟
🧠 دو تا مفهوم مهم رو از هم جدا کن:
| مفهوم | توضیح | مثال |
|---|---|---|
| شکل ظاهری (Visual Shape) | چیزی که میبینی | رنگ، بافت، شکل روی صفحهی نمایش |
| شکل برخوردی (Collision Shape) | چیزی که فیزیک باهاش کار میکنه | محاسبهی برخورد، اصطکاک، ضربه |
پس دلیلش
دلیل ۱: سرعت و کارایی
- شکل برخوردی باید ساده باشه تا محاسبات فیزیک سریع انجام بشه.
- شکل ظاهری میتونه پیچیده باشه چون فقط برای نمایش هست.
دلیل ۲: تشخیص برخورد
- وقتی مکعب به زمین میخوره، موتور فیزیک باید بدونه دقیقاً کجا برخورد اتفاق افتاده.
- شکل برخوردی دقیقاً مشخص میکنه که سطح برخورد کجاست.
📦 یک مثال عملی:
فرض کن یک مجسمهی بسیار پیچیده (مثل مجسمهی یک انسان) داری:
شکل ظاهری (چیزی که میبینی):
- همهی جزئیات: مو، لباس، انگشتان، چشمها
شکل برخوردی (چیزی که فیزیک باهاش کار میکنه):
- یک جعبهی ساده یا چندین کره که حجم کلی مجسمه رو پوشش میدن یا یک جعبهی شیشهای نامرئی که دور مجسمه رو گرفته
سوال: چرا مجسمه رو خودش نمیذاریم و یه جعبه دورش میذاریم؟
جواب:
- اگه مجسمه خودش برخورد کنه، محاسبهی برخورد با تمام جزئیاتش (انگشتان، بینی، لبههای لباس) بسیار سنگین و کند میشه.
- با جعبهی شیشهای، محاسبات سریعتر و سادهتر میشه.
baseMass=1
جرم جسم:
1 kg
basePosition=[0, 0, 2]
مکان اولیه جسم:
X = 0
Y = 0
Z = 2
یعنی مکعب را ۲ متر بالاتر از زمین قرار دادهایم.
boxHalfExtents = [0.5, 0.5, 0.5] چیه؟
جواب: این یک متغیر هست (نه تابع). یک لیست ساده در پایتون که سه عدد اعشاری توش ذخیره شده.
python
boxHalfExtents = [0.2, 0.4, 0.1]
# ^ ^
# مقدار (یک لیست) اسم متغیر
چرا اسمش رو “Half” (نصف) گذاشتن؟
دلیل: چون این اعداد نشاندهندهی نصف طول، نصف عرض و نصف ارتفاع جسم هستن، نه اندازهی کاملش!
| عدد | معنی | اندازهی کامل |
|---|---|---|
0.2 | نصف طول در محور X | طول کل = ۰.۴ متر |
0.4 | نصف عرض در محور Y | عرض کل = ۰.۸ متر |
0.1 | نصف ارتفاع در محور Z | ارتفاع کل = ۰.۲ متر |
چرا از نصف استفاده میکنن؟
چون در موتورهای فیزیک سهبعدی، مرکز جسم همیشه در نقطهی (۰, ۰, ۰) فرض میشه. پس برای تعیین ابعاد، باید بگیم از مرکز تا هر لبه چقدر فاصله هست (همون نصف ابعاد).
<---- 0.4 ----> (طول کامل) ┌──────────────┐ │ مرکز │ <- center = (0, 0, 0) │ (0,0,0) │ └──────────────┘ ↑ ↑ 0.2- 0.2+ (نصف طول از مرکز تا هر لبه)
5. کد کامل فعلی
حالا main.py باید این باشد:
import pybullet as p
import pybullet_data
import time
print("Connecting to PyBullet...")
client = p.connect(p.GUI)
print("Connected!")
print("Connection ID:", client)
# مسیر فایلهای آماده PyBullet
p.setAdditionalSearchPath(pybullet_data.getDataPath())
# Gravity
p.setGravity(0, 0, -9.81)
print("Gravity set!")
# Ground
p.loadURDF("plane.urdf")
print("Ground created!")
# Collision Shape
collision_shape = p.createCollisionShape(
p.GEOM_BOX,
halfExtents=[0.5, 0.5, 0.5]
)
# Visual Shape
visual_shape = p.createVisualShape(
p.GEOM_BOX,
halfExtents=[0.5, 0.5, 0.5]
)
# Object
box_id = p.createMultiBody(
baseMass=1,
baseCollisionShapeIndex=collision_shape,
baseVisualShapeIndex=visual_shape,
basePosition=[0, 0, 2]
)
print("Box created!")
time.sleep(5)
p.disconnect(client)
print("Disconnected!")
6. اجرا کن و چه چیزی باید ببینی؟
باید:
- پنجره PyBullet باز شود.
- زمین را ببینی.
- یک مکعب را ببینی که ابتدا در ارتفاع ۲ متر قرار دارد.
- اما یک نکته مهم وجود دارد…
ممکن است ببینی مکعب نمیافتد یا رفتار آن چیزی نباشد که انتظار داریم.
چرا؟
چون هنوز Simulation Loop نداریم.
و این دقیقاً موضوع جلسه ۸ است.
نکته کلیدی جلسه ۷
ما امروز این زنجیره را ساختیم:
PyBullet
│
├── World
│
├── Gravity
│
├── Ground
│
├── Box
├── Collision Shape
├── Visual Shape
└── Physical Body ==> Object واقعی
اما هنوز موتور فیزیک را در طول زمان جلو نبردهایم.
این کار در جلسه ۸ انجام میشود:
p.stepSimulation()
که به ما مفهوم Simulation Loop را یاد میدهد.
یک نکته مهم درباره کد
فعلاً این را در ذهنت نگه دار:
p.createCollisionShape(...)
یعنی:
«هندسهای که فیزیک و برخورد با آن کار میکند.»
و:
p.createVisualShape(...)
یعنی:
«هندسهای که ظاهر جسم را در GUI نشان میدهد.»
و:
p.createMultiBody(...)
یعنی:
«حالا این اجزای فیزیکی و بصری را به یک Body واقعی در Simulation تبدیل کن.»
این سه مفهوم بعداً برای ساخت AGV، چرخها، بدنه و موانع بسیار مهم میشوند.
فعلاً همین جلسه ۷ را اجرا کن
اگر پنجره PyBullet باز شد و زمین + مکعب را دیدی، خروجی یا نتیجه را بگو.
بعد میرویم سراغ جلسه ۸ — stepSimulation() و Simulation Loop و همانجا کاری میکنیم که مکعب واقعاً تحت تأثیر Gravity از ارتفاع ۲ متر سقوط کند و روی زمین قرار بگیرد.