artnfull commited on
Commit
a464e71
ยท
verified ยท
1 Parent(s): 96f35e9

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ eval_video.gif filter=lfs diff=lfs merge=lfs -text
37
+ outputs/eval/videos/eval_video.gif filter=lfs diff=lfs merge=lfs -text
38
+ outputs/eval/videos/eval_video.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ preview.gif filter=lfs diff=lfs merge=lfs -text
40
+ preview.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ replay.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ videos/eval_video.gif filter=lfs diff=lfs merge=lfs -text
43
+ videos/eval_video.mp4 filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: lerobot
4
+ pipeline_tag: robotics
5
+ tags:
6
+ - reinforcement-learning
7
+ - robotics
8
+ - physical-ai
9
+ - mujoco
10
+ - ppo
11
+ - bird
12
+ - robot-bird
13
+ - bipedal-vtol
14
+ - lerobot
15
+ - autonomous-agents
16
+ code: https://github.com/artnfull-bot/OpenBird-artnfull
17
+ model-index:
18
+ - name: open-bird-robot-mujoco-ppo
19
+ results:
20
+ - task:
21
+ type: reinforcement-learning
22
+ name: Avian Bipedal Walking & 180-deg VTOL Flight
23
+ dataset:
24
+ name: OpenBird 15-Stage Multi-Modal Trajectory Dataset v1.0
25
+ type: robotics-demonstration
26
+ metrics:
27
+ - type: mean_reward
28
+ value: 120.0
29
+ name: 15-Stage Full Sequence Completion
30
+ - type: success_rate
31
+ value: 100.0
32
+ name: VTOL Takeoff & Landing Rate (%)
33
+ ---
34
+
35
+ # ๐Ÿฆ… OpenBird (open-bird-robot-mujoco-ppo) AI Models & Motion Datasets
36
+
37
+ [![Hugging Face Model](https://img.shields.io/badge/๐Ÿค—%20Hugging%20Face-Model%20Hub-orange.svg)](https://huggingface.co/artnfull/open-bird-robot-mujoco-ppo)
38
+ [![Hugging Face Space](https://img.shields.io/badge/๐Ÿค—%20Hugging%20Face-Spaces-yellow.svg)](https://huggingface.co/spaces/artnfull/OpenBird-artnfull)
39
+ [![GitHub Repository](https://img.shields.io/badge/GitHub-OpenBird--artnfull-blue?logo=github)](https://github.com/artnfull-bot/OpenBird-artnfull)
40
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
41
+ [![Design License: CC BY-NC-SA 4.0](https://img.shields.io/badge/Design-CC_BY--NC--SA_4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
42
+
43
+ > **Official Physical AI simulation platform, pre-trained policy weights, and 15-stage trajectory datasets for the OpenBird-artnfull Bipedal VTOL Robot Platform.**
44
+ > ๐ŸŽฎ **Playable Interactive Simulator**: Download from GitHub or Hugging Face and directly control the robot bird via keyboard (WASD)!
45
+
46
+ ---
47
+
48
+ ## ๐ŸŒŸ Key Features & Kinematics
49
+
50
+ - ๐Ÿฆฟ **Digitigrade Bipedal Locomotion**: Avian joint kinematics with dynamic reverse-knee walking and counter-balancing.
51
+ - ๐Ÿชฝ **180ยฐ Deployable Duct Wings**: Stowed vertically behind the back during walking; deployed horizontally (180ยฐ) for high-thrust VTOL flight.
52
+ - ๐Ÿฆ… **Look-Down Eye-Gaze & Beak Pitch Control**: Independent 60ยฐ downward ground scan without disturbing body equilibrium.
53
+ - ๐Ÿ“ท **Multi-Modal Vision Perception**: Stereo Eye Cameras + Cockpit FPV view.
54
+
55
+ ---
56
+
57
+ ## ๐ŸŽฎ Interactive Keyboard Control Guide
58
+
59
+ You can download and directly control the robot bird locally in real-time 3D simulation!
60
+
61
+ | Key | Action | Description |
62
+ | :---: | :---: | :--- |
63
+ | `A` / `D` | **Yaw Turn** | Turn left / right (Walking turn / Flight yaw) |
64
+ | `โ†‘` / `โ†“` (`W`/`S`) | **Forward / Backward** | Walk forward/backward (Ground) or Fly forward/backward (Air) |
65
+ | `โ†` / `โ†’` | **Strafe** | Sideways translation in air |
66
+ | `R` / `F` | **Altitude** | Altitude Climb (+15cm) / Descend (-15cm) |
67
+ | `SPACE` | **Stop / Hover** | Stand still (Ground) / Precision Hover (Air) |
68
+ | `1` $\rightarrow$ `2` $\rightarrow$ `3` | **Flight Sequence** | Deploy Wings $\rightarrow$ VTOL Takeoff & Hover $\rightarrow$ Soft Landing & Fold |
69
+ | `C` | **Camera Switch** | 3rd-Person Orbit $\rightarrow$ Head FPV $\rightarrow$ Left Eye $\rightarrow$ Right Eye |
70
+ | `V` / `B` | **Eye-Gaze Pitch** | Look down 60ยฐ (`V`) / Look forward (`B`) |
71
+ | `G` / `Y` / `K` / `P` | **Gestures** | Peck (`G`), Crouch (`Y`), Knockdown (`K`), Auto-Right (`P`) |
72
+ | `Q` / `ESC` | **Exit** | Close simulator |
73
+
74
+ ---
75
+
76
+ ## ๐Ÿš€ How to Download & Run Locally
77
+
78
+ ### Option A: From GitHub (Recommended)
79
+ ```bash
80
+ # 1. Clone repository
81
+ git clone https://github.com/artnfull-bot/OpenBird-artnfull.git
82
+ cd OpenBird-artnfull
83
+
84
+ # 2. Install dependencies (MuJoCo, NumPy, OpenCV)
85
+ pip install -r requirements.txt
86
+
87
+ # 3. Launch Interactive Flight (Manual Control)
88
+ run_manual.bat # or python run_robot_bird.py
89
+
90
+ # 4. Launch Autonomous 15-Stage Cinematic AI
91
+ run_ai_auto.bat # or python run_ai_robot_bird.py
92
+ ```
93
+
94
+ ### Option B: Download via Hugging Face Hub (Python)
95
+ ```python
96
+ from huggingface_hub import snapshot_download
97
+
98
+ # Download complete model assets, datasets, and simulator scripts
99
+ repo_path = snapshot_download(repo_id="artnfull/open-bird-robot-mujoco-ppo", repo_type="model")
100
+ print(f"Downloaded OpenBird assets to: {repo_path}")
101
+ ```
102
+
103
+ ---
104
+
105
+ ## ๐Ÿ“‚ Repository Contents
106
+
107
+ ```text
108
+ OpenBird-artnfull/
109
+ โ”œโ”€โ”€ policies/ # Trained neural network weights (.pt, .onnx)
110
+ โ”‚ โ””โ”€โ”€ baseline_flight/ # Baseline VTOL takeoff, hover, and landing models
111
+ โ”œโ”€โ”€ datasets/ # MuJoCo / LeRobot compatible motion trajectories
112
+ โ”‚ โ””โ”€โ”€ demonstration/ # Teleoperation & autonomous behavioral data
113
+ โ”œโ”€โ”€ outputs/eval/ # Official evaluation metrics & trajectory logs
114
+ โ”‚ โ”œโ”€โ”€ eval_info.json # Rollout evaluation statistics
115
+ โ”‚ โ””โ”€โ”€ videos/ # High-definition evaluation rollouts
116
+ โ”œโ”€โ”€ robot_bird/ # MuJoCo 3D kinematics, FSM controller & XML models
117
+ โ”œโ”€โ”€ run_robot_bird.py # Interactive Keyboard Flight simulator
118
+ โ”œโ”€โ”€ run_ai_robot_bird.py # 15-Stage Autonomous Cinematic Showcase
119
+ โ”œโ”€โ”€ run_manual.bat # Windows 1-Click Interactive launcher
120
+ โ”œโ”€โ”€ run_ai_auto.bat # Windows 1-Click Autonomous launcher
121
+ โ”œโ”€โ”€ requirements.txt # Dependency list (mujoco, numpy, opencv-python)
122
+ โ””โ”€โ”€ README.md # Model & dataset card
123
+ ```
eval_info.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "eval_info": {
3
+ "avg_reward": 1.0,
4
+ "success_rate": 1.0,
5
+ "total_episodes": 1,
6
+ "video_paths": [
7
+ "videos/eval_video.mp4"
8
+ ]
9
+ },
10
+ "video_paths": [
11
+ "videos/eval_video.mp4"
12
+ ],
13
+ "episodes": [
14
+ {
15
+ "episode_index": 0,
16
+ "success": true,
17
+ "reward": 1.0,
18
+ "video_path": "videos/eval_video.mp4"
19
+ }
20
+ ]
21
+ }
eval_video.gif ADDED

Git LFS Details

  • SHA256: 0e314068698da4a1dceb2a3f3910d4c959abf6452f99a5c9c179ffc2ac5d94a9
  • Pointer size: 132 Bytes
  • Size of remote file: 5.73 MB
outputs/eval/eval_info.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "eval_info": {
3
+ "avg_reward": 1.0,
4
+ "success_rate": 1.0,
5
+ "total_episodes": 1,
6
+ "video_paths": [
7
+ "videos/eval_video.mp4"
8
+ ]
9
+ },
10
+ "video_paths": [
11
+ "videos/eval_video.mp4"
12
+ ],
13
+ "episodes": [
14
+ {
15
+ "episode_index": 0,
16
+ "success": true,
17
+ "reward": 1.0,
18
+ "video_path": "videos/eval_video.mp4"
19
+ }
20
+ ]
21
+ }
outputs/eval/videos/eval_video.gif ADDED

Git LFS Details

  • SHA256: 0e314068698da4a1dceb2a3f3910d4c959abf6452f99a5c9c179ffc2ac5d94a9
  • Pointer size: 132 Bytes
  • Size of remote file: 5.73 MB
outputs/eval/videos/eval_video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d2b237b35896aa215bd1cae710dffbbb603544e28496698b8f389daf8750454
3
+ size 1109526
preview.gif ADDED

Git LFS Details

  • SHA256: 0e314068698da4a1dceb2a3f3910d4c959abf6452f99a5c9c179ffc2ac5d94a9
  • Pointer size: 132 Bytes
  • Size of remote file: 5.73 MB
preview.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d2b237b35896aa215bd1cae710dffbbb603544e28496698b8f389daf8750454
3
+ size 1109526
preview_demo.bat ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ cd /d "%~dp0"
3
+ if exist "..\duckenv\Scripts\python.exe" (
4
+ ..\duckenv\Scripts\python.exe run_ai_robot_bird.py
5
+ ) else if exist ".\duckenv\Scripts\python.exe" (
6
+ .\duckenv\Scripts\python.exe run_ai_robot_bird.py
7
+ ) else (
8
+ python run_ai_robot_bird.py
9
+ )
10
+ if errorlevel 1 pause
replay.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d2b237b35896aa215bd1cae710dffbbb603544e28496698b8f389daf8750454
3
+ size 1109526
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ mujoco>=3.0.0
2
+ numpy>=1.22.0
3
+ opencv-python>=4.8.0
robot_bird/ai_brain.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import random
3
+ import math
4
+ import numpy as np
5
+
6
+ class RobotBirdAIBrain:
7
+ """
8
+ ๋ฐ˜๋ ค ๋กœ๋ด‡์ƒˆ(Bipedal VTOL Robot Bird) ์ž์œจ ์ง€๋Šฅ ๋‘๋‡Œ (Autonomous AI Brain)
9
+
10
+ [ํ•ต์‹ฌ ์ž์œจ ํ–‰๋™ ์‚ฌ์ดํด]
11
+ 1. ๐Ÿ” ํ˜ธ๊ธฐ์‹ฌ ํƒ์ƒ‰ (Look Around & Stand) : ์ฃผ๋ณ€์„ ๊ฐธ์šฐ๋šฑ ์‚ดํ”ผ๋ฉฐ ํ˜ธ๊ธฐ์‹ฌ ํ‘œํ˜„
12
+ 2. ๐Ÿšถ ์ž์œจ 2์กฑ ๋ณดํ–‰ (Walk & Roam) : ์ „์ง„ ๋ฐ ๋ฐฉํ–ฅ ์ „ํ™˜ํ•˜๋ฉฐ ์•„์žฅ์•„์žฅ ๊ฑท๊ธฐ
13
+ 3. ๐ŸŒพ ๋ชจ์ด ์ชผ๊ธฐ (Feed Pecking) : ๋ฐ”๋‹ฅ์˜ ๋จน์ด๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ  ๋‹ค๋ฆฌ๋ฅผ ์ ‘์–ด ์ฝ•! ์ฝ•! ์ชผ๊ธฐ
14
+ 4. ๐Ÿชฝ ๋น„ํ–‰ ํƒํ—˜ (VTOL Cruise) : ๋‚ ๊ฐœ ํŽด๊ณ  1m ์ƒ๊ณต์œผ๋กœ ์ด๋ฅ™ โž” ๊ณต์ค‘ ์ˆœํ•ญ ๋น„ํ–‰
15
+ 5. ๐Ÿ›ฌ ์ฐฉ๋ฅ™ ๋ฐ ํœด์‹ (Land & Crouch Sit) : ์‚ฌ๋ฟํžˆ ์ฐฉ๋ฅ™ ํ›„ ์ชผ๊ทธ๋ ค ์•‰์•„ ํŽธ์•ˆํžˆ ํœด์‹
16
+ 6. ๐Ÿ’ƒ ์žฌ๋กฑ ๋Œ„์Šค (Happy Dance) : ๊ธฐ๋ถ„์ด ์ข‹์•„ ๊ฐ€๋ณ๊ฒŒ ์ ํ”„ ์žฌ๋กฑ ๋Œ„์Šค
17
+ 7. ๐Ÿ›ก๏ธ ์™ธ๋ž€ ๋Œ€์‘ & ์ž์œจ ๊ธฐ๋ฆฝ : ๋„˜์–ด์ง€๋ฉด ์Šค์Šค๋กœ ๊ฐ์ง€ํ•˜์—ฌ 100% ๋ฒŒ๋–ก ๊ธฐ๋ฆฝ!
18
+ """
19
+
20
+ BEHAVIOR_LOOK_AROUND = "LOOK_AROUND"
21
+ BEHAVIOR_WALK = "WALK"
22
+ BEHAVIOR_PECK = "PECK"
23
+ BEHAVIOR_FLIGHT = "FLIGHT"
24
+ BEHAVIOR_SIT_REST = "SIT_REST"
25
+ BEHAVIOR_DANCE = "DANCE"
26
+
27
+ def __init__(self, controller):
28
+ self.controller = controller
29
+
30
+ # ๋‚ด๋ถ€ ๊ฐ์ • ๋ฐ ์ƒ์ฒด ์ƒํƒœ ํŒŒ๋ผ๋ฏธํ„ฐ (0.0 ~ 1.0)
31
+ self.curiosity = 0.8 # ํ˜ธ๊ธฐ์‹ฌ ์ˆ˜์น˜ (๋†’์œผ๋ฉด ํƒ์ƒ‰ ๋ฐ ๋น„ํ–‰ ์„ ํ˜ธ)
32
+ self.energy = 0.9 # ์—๋„ˆ์ง€ ์ˆ˜์น˜ (๋‚ฎ์•„์ง€๋ฉด ์ชผ๊ทธ๋ ค ์•‰์•„ ํœด์‹)
33
+ self.happiness = 0.85 # ํ–‰๋ณต๋„ (๋†’์œผ๋ฉด ์žฌ๋กฑ ๋Œ„์Šค ๋ฐ ๋ชจ์ด ์ชผ๊ธฐ)
34
+
35
+ self.current_behavior = self.BEHAVIOR_LOOK_AROUND
36
+ self.behavior_timer = 0.0
37
+ self.behavior_duration = 3.0
38
+
39
+ # ๋น„ํ–‰ ์„œ๋ธŒ ๋‹จ๊ณ„ ๊ด€๋ฆฌ
40
+ self.flight_substate = "NONE"
41
+ self.flight_step_timer = 0.0
42
+
43
+ print("\n[AI ์ž์œจ ๋‘๋‡Œ] ๋ฐ˜๋ ค ๋กœ๋ด‡์ƒˆ์˜ ์ธ๊ณต์ง€๋Šฅ ์ž์œจ ํ–‰๋™ ์‹œ์Šคํ…œ์ด ๊ฐ€๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
44
+
45
+ def update(self, dt):
46
+ self.behavior_timer += dt
47
+
48
+ # 1. ๋„˜์–ด์ ธ ์žˆ๋Š” ์ƒํƒœ ๊ฐ์ง€ (๋กœ๋ด‡์ด ๋„˜์–ด์ ธ ์žˆ์œผ๋ฉด ์ตœ์šฐ์„ ์œผ๋กœ ๊ธฐ๋ฆฝ ํŒ๋‹จ)
49
+ if self.controller.state == self.controller.STATE_KNOCKDOWN:
50
+ if self.behavior_timer > 0.8:
51
+ print("\n[AI ์ƒ๊ฐ] '์•—! ๋„˜์–ด์กŒ๋„ค? ์˜ค๋š์ด์ฒ˜๋Ÿผ ์Šค์Šค๋กœ ์ผ์–ด๋‚˜์•ผ์ง€!'")
52
+ self.controller.set_state(self.controller.STATE_RECOVER)
53
+ self.set_behavior(self.BEHAVIOR_LOOK_AROUND, duration=2.5)
54
+ return
55
+
56
+ # 2. ๊ธฐ๋ฆฝ ๋ณต๊ตฌ ์ค‘์ด๊ฑฐ๋‚˜ ๋ชจ์ด ์ชผ๊ธฐ ์ค‘์ผ ๋•Œ๋Š” ํ•ด๋‹น ๋ชจ์…˜์ด ๋๋‚  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
57
+ if self.controller.state in [self.controller.STATE_RECOVER, self.controller.STATE_GROUND_PICK]:
58
+ return
59
+
60
+ # 3. ๋น„ํ–‰ ์ค‘์ผ ๋•Œ์˜ ์ž์œจ ๋น„ํ–‰ ์‹œํ€€์Šค ์ฒ˜๋ฆฌ
61
+ if self.controller.state in [
62
+ self.controller.STATE_TAKEOFF_SPOOL,
63
+ self.controller.STATE_TAKEOFF_CLIMB,
64
+ self.controller.STATE_FLIGHT,
65
+ self.controller.STATE_LANDING
66
+ ]:
67
+ self._handle_autonomous_flight(dt)
68
+ return
69
+
70
+ # 4. ํ˜„์žฌ ์ง€์ƒ ์ž์œจ ํ–‰๋™ ์ง€์† ์‹œ๊ฐ„์ด ๋๋‚˜๋ฉด ๋‹ค์Œ ํ–‰๋™์„ ์ง€๋Šฅ์ ์œผ๋กœ ์„ ํƒ
71
+ if self.behavior_timer >= self.behavior_duration:
72
+ self._select_next_behavior()
73
+
74
+ def set_behavior(self, behavior, duration):
75
+ self.current_behavior = behavior
76
+ self.behavior_timer = 0.0
77
+ self.behavior_duration = duration
78
+
79
+ def _select_next_behavior(self):
80
+ """๊ฐ์ • ๋ฐ ์—๋„ˆ์ง€ ์ƒํƒœ์— ๊ธฐ๋ฐ˜ํ•œ ํ™•๋ฅ ์  ์ž์œจ ํ–‰๋™ ์„ ํƒ ์—”์ง„"""
81
+ r = random.random()
82
+
83
+ # ์—๋„ˆ์ง€๊ฐ€ ๋‚ฎ์œผ๋ฉด ํœด์‹(์•‰๊ธฐ) ์„ ํƒ ํ™•๋ฅ  ์ฆ๊ฐ€
84
+ if self.energy < 0.4 and self.controller.state != self.controller.STATE_SIT:
85
+ print("\n[AI ์ƒ๊ฐ] '์กฐ๊ธˆ ํ”ผ๊ณคํ•ด์กŒ์–ด.. ๋‹ค๋ฆฌ๋ฅผ ์™ ์ ‘๊ณ  ์ž ์‹œ ์•‰์•„์„œ ์‰ฌ์–ด์•ผ์ง€.' (ํœด์‹ ๋ชจ๋“œ)")
86
+ self.controller.set_state(self.controller.STATE_SIT)
87
+ self.set_behavior(self.BEHAVIOR_SIT_REST, duration=random.uniform(4.0, 7.0))
88
+ self.energy = min(1.0, self.energy + 0.4)
89
+ return
90
+
91
+ # ์•‰์•„์žˆ๋˜ ์ƒํƒœ๋ผ๋ฉด ์ผ์–ด์„œ๊ธฐ
92
+ if self.controller.state == self.controller.STATE_SIT:
93
+ print("\n[AI ์ƒ๊ฐ] 'ํ‘น ์‰ฌ์—ˆ์œผ๋‹ˆ ๊ธฐ์šด์ฐจ๊ฒŒ ๋‹ค์‹œ ์ผ์–ด๋‚˜๋ณผ๊นŒ?'")
94
+ self.controller.set_state(self.controller.STATE_STAND)
95
+ self.set_behavior(self.BEHAVIOR_LOOK_AROUND, duration=2.5)
96
+ return
97
+
98
+ # ํ–‰๋™ ์„ ํƒ ํ™•๋ฅ  ๋ถ„ํฌ
99
+ # 1) ๋ชจ์ด ์ชผ๊ธฐ (25%)
100
+ # 2) 2์กฑ ๋ณดํ–‰ ํƒ์ƒ‰ (30%)
101
+ # 3) ์ž์œจ ๋น„ํ–‰ (25%)
102
+ # 4) ๋‘๋ฆฌ๋ฒˆ๊ฑฐ๋ฆฌ๊ธฐ / ์žฌ๋กฑ ๋Œ„์Šค (20%)
103
+ if r < 0.25:
104
+ # ๋ชจ์ด ์ชผ๊ธฐ
105
+ print("\n[AI ์ƒ๊ฐ] '์–ด! ๋ฐ”๋‹ฅ์— ๋ง›์žˆ๋Š” ๋ชจ์ด๊ฐ€ ์žˆ๋„ค? ์ฝ•! ์ฝ•! ์ชผ์•„๋จน์–ด์•ผ์ง€!' (๋ชจ์ด ์ชผ๊ธฐ)")
106
+ self.controller.set_state(self.controller.STATE_GROUND_PICK)
107
+ self.set_behavior(self.BEHAVIOR_PECK, duration=2.5)
108
+ self.happiness = min(1.0, self.happiness + 0.15)
109
+ self.energy = max(0.1, self.energy - 0.05)
110
+
111
+ elif r < 0.55:
112
+ # ์ž์œจ 2์กฑ ๋ณดํ–‰ (์ „์ง„ ๋˜๋Š” ๋ฐฉํ–ฅ ์ „ํ™˜)
113
+ walk_types = ["forward", "turn_left", "turn_right", "backward"]
114
+ chosen_type = random.choice(walk_types)
115
+ dur = random.uniform(2.5, 4.5)
116
+
117
+ if chosen_type == "forward":
118
+ print(f"\n[AI ์ƒ๊ฐ] '์ € ์•ž์ชฝ์—๋Š” ๋ญ๊ฐ€ ์žˆ์„๊นŒ? ์•„์žฅ์•„์žฅ ๊ฑธ์–ด๊ฐ€๋ณด์ž!' (์ „์ง„ ๋ณดํ–‰ +{dur:.1f}์ดˆ)")
119
+ self.controller.walk_speed = 1.0
120
+ self.controller.walk_turn = 0.0
121
+ elif chosen_type == "turn_left":
122
+ print(f"\n[AI ์ƒ๊ฐ] '์™ผ์ชฝ์—์„œ ์žฌ๋ฏธ์žˆ๋Š” ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ ค! ์™ผ์ชฝ์œผ๋กœ ๋Œ์•„๋ณผ๋ž˜!' (์ขŒํšŒ์ „ +{dur:.1f}์ดˆ)")
123
+ self.controller.walk_speed = 0.5
124
+ self.controller.walk_turn = 0.8
125
+ elif chosen_type == "turn_right":
126
+ print(f"\n[AI ์ƒ๊ฐ] '์˜ค๋ฅธ์ชฝ ๊ตฌ๊ฒฝ์„ ํ•ด๋ณผ๊นŒ? ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋Œ๋ฉด์„œ ๊ฑท๊ธฐ!' (์šฐํšŒ์ „ +{dur:.1f}์ดˆ)")
127
+ self.controller.walk_speed = 0.5
128
+ self.controller.walk_turn = -0.8
129
+ else:
130
+ print(f"\n[AI ์ƒ๊ฐ] '์กฐ์‹ฌ์กฐ์‹ฌ ๋’ค๋กœ ๊ฑธ์–ด๊ฐ€๋ณผ๊นŒ?' (ํ›„์ง„ ๋ณดํ–‰ +{dur:.1f}์ดˆ)")
131
+ self.controller.walk_speed = -0.7
132
+ self.controller.walk_turn = 0.0
133
+
134
+ self.controller.set_state(self.controller.STATE_WALK)
135
+ self.set_behavior(self.BEHAVIOR_WALK, duration=dur)
136
+ self.energy = max(0.1, self.energy - 0.08)
137
+
138
+ elif r < 0.80:
139
+ # ์ž์œจ ๋น„ํ–‰ ์‹œํ€€์Šค ์‹œ์ž‘
140
+ print("\n[AI ์ƒ๊ฐ] '๋‚ ๊ฐœ๋ฅผ ํ™œ์ง ํŽด๊ณ  ํ•˜๋Š˜๋กœ ์Š~ ๋‚ ์•„์˜ฌ๋ผ ๋ณผ๋ž˜!' (์ž์œจ ๋น„ํ–‰ ์‹œ์ž‘)")
141
+ self.controller.toggle_wings()
142
+ time.sleep(0.1)
143
+ self.controller.start_flight_sequence()
144
+ self.flight_substate = "CLIMBING"
145
+ self.flight_step_timer = 0.0
146
+ self.set_behavior(self.BEHAVIOR_FLIGHT, duration=15.0)
147
+ self.energy = max(0.1, self.energy - 0.20)
148
+ self.happiness = min(1.0, self.happiness + 0.25)
149
+
150
+ else:
151
+ # ๋‘๋ฆฌ๋ฒˆ๊ฑฐ๋ฆฌ๊ธฐ ๋˜๋Š” ์žฌ๋กฑ ๋Œ„์Šค
152
+ if random.random() < 0.5:
153
+ print("\n[AI ์ƒ๊ฐ] '๊ธฐ๋ถ„ ์ตœ๊ณ ์•ผ! ๋Šด๋ฆฌ๋ฆฌ์•ผ ์ ํ”„ ์žฌ๋กฑ ๋Œ„์Šค!'")
154
+ self.controller.set_state(self.controller.STATE_RECOVER)
155
+ self.set_behavior(self.BEHAVIOR_DANCE, duration=2.5)
156
+ else:
157
+ print("\n[AI ์ƒ๊ฐ] '์ฃผ๋ณ€์„ ๋‘๋ฆฌ๋ฒˆ๋‘๋ฆฌ๋ฒˆ ๊ฐธ์šฐ๋šฑ~'")
158
+ self.controller.walk_speed = 0.0
159
+ self.controller.walk_turn = 0.0
160
+ self.controller.set_state(self.controller.STATE_STAND)
161
+ self.set_behavior(self.BEHAVIOR_LOOK_AROUND, duration=random.uniform(2.0, 3.5))
162
+
163
+ def _handle_autonomous_flight(self, dt):
164
+ """์ž์œจ ๋น„ํ–‰ ์ค‘ ๊ณต์ค‘ ๊ธฐ๋™ ๋ฐ ์ˆœํ•ญ ์‹œํ€€์Šค ๊ด€๋ฆฌ"""
165
+ self.flight_step_timer += dt
166
+
167
+ # 1๋‹จ๊ณ„: ์ด๋ฅ™ ์ƒ์Šน ์™„๋ฃŒ ๋Œ€๊ธฐ
168
+ if self.controller.state == self.controller.STATE_FLIGHT:
169
+ if self.flight_substate == "CLIMBING":
170
+ print("\n[AI ์ƒ๊ฐ] '1m ์ƒ๊ณต ๋„๋‹ฌ ์™„๋ฃŒ! ๊ณต์ค‘์—์„œ ์ž์œ ๋กญ๊ฒŒ ๋น„ํ–‰ ํƒํ—˜์„ ์‹œ์ž‘ํ• ๊ฒŒ!'")
171
+ self.flight_substate = "CRUISING"
172
+ self.flight_step_timer = 0.0
173
+
174
+ elif self.flight_substate == "CRUISING":
175
+ # ๋งค 2~3์ดˆ๋งˆ๋‹ค ์ž์œจ ๋น„ํ–‰ ๋ฐฉํ–ฅ ๋ณ€๊ฒฝ
176
+ if self.flight_step_timer >= 2.5:
177
+ self.flight_step_timer = 0.0
178
+ action = random.choice(["forward", "turn_left", "turn_right", "look_down", "hover"])
179
+ yaw = getattr(self.controller, 'target_yaw', 0.0)
180
+
181
+ if action == "look_down":
182
+ print("\n[AI ์ƒ๊ฐ] '์–ด๋ผ? ์ € ์•„๋ž˜ ๋ฐ”๋‹ฅ์— ๋ญ๊ฐ€ ์žˆ์ง€? ๋‚ ๊ฐœ๋Š” ๋œฌ ์ฑ„๋กœ ๋ชธํ†ต๋งŒ ์•„๋ž˜๋กœ ์ˆ™์—ฌ์„œ ์นด๋ฉ”๋ผ๋กœ ๊ด€์ธกํ•ด๋ณด์ž!' (๐Ÿ“ท ํ•˜๋ฐฉ ๊ด€์ธก ๋ชจ๋“œ ON)")
183
+ self.controller.target_look_down_pitch = 0.95
184
+ self.controller.look_down = True
185
+ else:
186
+ if self.controller.look_down:
187
+ print("\n[AI ์ƒ๊ฐ] '๋ฐ”๋‹ฅ ์Šค์บ” ์™„๋ฃŒ! ๋ชธํ†ต์„ ๋‹ค์‹œ ์ •๋ฉด ์ˆ˜ํ‰์œผ๋กœ ๋“ค๊ณ  ๋น„ํ–‰ํ• ๊ฒŒ!' (๐Ÿ“ท ํ•˜๋ฐฉ ๊ด€์ธก ๋ชจ๋“œ OFF)")
188
+ self.controller.target_look_down_pitch = 0.0
189
+ self.controller.look_down = False
190
+
191
+ if action == "forward":
192
+ print("[AI ์ƒ๊ฐ] '์•ž์œผ๋กœ ์‹œ์›ํ•˜๊ฒŒ ํ™œ๊ณต ์ „์ง„ ๋น„ํ–‰!'")
193
+ self.controller.target_x += 0.45 * (-math.sin(yaw))
194
+ self.controller.target_y += 0.45 * math.cos(yaw)
195
+ elif action == "turn_left":
196
+ print("[AI ์ƒ๊ฐ] '์™ผ์ชฝ์œผ๋กœ ๋ฉ‹์ง€๊ฒŒ ๋ฑ…ํฌ ํ„ด!'")
197
+ self.controller.target_yaw -= 0.45
198
+ elif action == "turn_right":
199
+ print("[AI ์ƒ๊ฐ] '์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋ฉ‹์ง€๊ฒŒ ๋ฑ…ํฌ ํ„ด!'")
200
+ self.controller.target_yaw += 0.45
201
+ else:
202
+ print("[AI ์ƒ๊ฐ] '๊ณต์ค‘์— ๊ฐ€๋งŒํžˆ ๋– ์„œ ์—ฌ์œ ๋กญ๊ฒŒ ํ˜ธ๋ฒ„๋ง~'")
203
+
204
+ # ์ผ์ • ์‹œ๊ฐ„ ๋น„ํ–‰ ํ›„ ์•ˆ์ „ ์ฐฉ๋ฅ™ ๊ฒฐ์ •
205
+ if self.behavior_timer >= self.behavior_duration - 2.0:
206
+ print("\n[AI ์ƒ๊ฐ] '๋น„ํ–‰ ํƒํ—˜ ๋! ์ด์ œ ์ง€๋ฉด์œผ๋กœ ์‚ฌ๋ฟํžˆ ์ฐฉ๋ฅ™ํ• ๊ฒŒ!'")
207
+ self.controller.target_look_down_pitch = 0.0
208
+ self.controller.look_down = False
209
+ self.controller.set_state(self.controller.STATE_LANDING)
210
+ self.flight_substate = "LANDING"
robot_bird/controller.py ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+ import time
5
+ import math
6
+
7
+ # UTF-8 ์ž…์ถœ๋ ฅ ๋ณด์žฅ
8
+ if sys.platform == "win32":
9
+ try:
10
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
11
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
12
+ except Exception:
13
+ pass
14
+
15
+ class RobotBirdFSM:
16
+ """
17
+ ๋ฐ˜๋ ค ๋กœ๋ด‡์ƒˆ(Bipedal VTOL Robot Bird) ๊ณ ์„ฑ๋Šฅ 4๋‹จ๊ณ„ ์ œ์–ด๊ธฐ
18
+ - 1๋‹จ๊ณ„: ๋“ฑ ๋’ค ์„ธ๋กœ ์ ‘ํž˜(์‚ฌ์ง„ 1) <-> ์ขŒ์šฐ 180ยฐ ์ˆ˜ํ‰ ์ „๊ฐœ(์‚ฌ์ง„ 2)
19
+ - 2๋‹จ๊ณ„: ํ”„๋กœํŽ ๋Ÿฌ ์Œฉ์Œฉ ํšŒ์ „ ๊ฐ€์†
20
+ - 3๋‹จ๊ณ„: 2.0m ๊ณ ๋„๋กœ ๋˜‘๋ฐ”๋กœ ์ˆ˜์ง ์ƒ์Šน
21
+ - 4๋‹จ๊ณ„: 2.0m ์™„๋ฒฝ ์ •์ง€ ํ˜ธ๋ฒ„๋ง
22
+ """
23
+ STATE_STAND = "STAND"
24
+ STATE_WALK = "WALK"
25
+ STATE_GROUND_PICK = "GROUND_PICK"
26
+ STATE_WING_SHIVER = "WING_SHIVER"
27
+ STATE_SIT = "SIT"
28
+ STATE_KNOCKDOWN = "KNOCKDOWN"
29
+ STATE_TAKEOFF_SPOOL = "TAKEOFF_SPOOL" # 2๋‹จ๊ณ„: ํ”„๋กœํŽ ๋Ÿฌ ํšŒ์ „ ๊ฐ€์†
30
+ STATE_TAKEOFF_CLIMB = "TAKEOFF_CLIMB" # 3๋‹จ๊ณ„: ์ˆ˜์ง ์ƒ์Šน (2.0m)
31
+ STATE_FLIGHT = "FLIGHT" # 4๋‹จ๊ณ„: 2.0m ์ •์ง€ ํ˜ธ๋ฒ„๋ง
32
+ STATE_LANDING = "LANDING"
33
+ STATE_RECOVER = "RECOVER"
34
+
35
+ def __init__(self, model, data):
36
+ self.model = model
37
+ self.data = data
38
+ self.state = self.STATE_STAND
39
+ self.state_timer = 0.0
40
+
41
+ # ๐Ÿชฝ ๋‚ ๊ฐœ ์ „๊ฐœ ์ƒํƒœ ๊ด€๋ฆฌ
42
+ # 0.0: ๋“ฑ ๋’ค ์„ธ๋กœ ์™„๋ฒฝ ์ ‘ํž˜ (์‚ฌ์ง„ 1)
43
+ # 1.0: ์ขŒ์šฐ 180ยฐ ์ˆ˜ํ‰ ์ „๊ฐœ ๋น„ํ–‰ ์ค€๋น„ (์‚ฌ์ง„ 2)
44
+ self.wing_progress = 0.0
45
+ self.wings_deployed = False
46
+
47
+ # ๐ŸŒช๏ธ ํ”„๋กœํŽ ๋Ÿฌ ํšŒ์ „ ๊ด€๋ฆฌ
48
+ self.propeller_running = False
49
+ self.prop_angle = 0.0
50
+ self.prop_speed = 0.0
51
+ self.target_prop_speed = 50.0 # ์ดˆ๋‹น ์•ฝ 8ํšŒ์ „ (๋ˆˆ์œผ๋กœ ์—ญ๋™์ ์ธ ํšŒ์ „์ด ์™„๋ฒฝํžˆ ๋ณด์ด๋Š” ์†๋„)
52
+
53
+ # ๐Ÿš ๋น„ํ–‰ ์กฐ์ข… ์ž…๋ ฅ (๊ธฐ๋ณธ 1.0m ๋ชฉํ‘œ ๊ณ ๋„)
54
+ self.cmd_roll = 0.0
55
+ self.cmd_pitch = 0.0
56
+ self.cmd_yaw = 0.0
57
+ self.target_altitude = 1.0
58
+ self.target_x = 0.0
59
+ self.target_y = 0.0
60
+ self.target_yaw = 0.0 # ๐ŸŒŸ ๋ชฉํ‘œ ํšŒ์ „๊ฐ (๋ผ๋””์•ˆ)
61
+
62
+ # ๐Ÿฆ† ๋ณดํ–‰ ์กฐ์ข… ์ž…๋ ฅ
63
+ self.walk_speed = 0.0
64
+ self.walk_turn = 0.0
65
+ self.walk_phase = 0.0
66
+
67
+ self.total_mass = sum(self.model.body_mass)
68
+ self.gravity_force = self.total_mass * 9.81
69
+ self.base_thrust = self.gravity_force / 2.0
70
+
71
+ # ๐Ÿ›ก๏ธ ์™ธ๋ž€ ์ €ํ•ญ ๋ฐ ์ž๋™ ๊ธฐ๋ฆฝ ๊ฐ์ง€ ํƒ€์ด๋จธ
72
+ self.fall_detect_timer = 0.0
73
+
74
+ # ๐ŸฅŠ ๋ฌผ๋ฆฌ์  ์™ธ๋ž€ ๋ฐ€๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํƒ€์ด๋จธ
75
+ self.push_force = np.zeros(3)
76
+ self.push_timer = 0.0
77
+
78
+ # ๐Ÿ“ท ํ•˜๋ฐฉ ๊ด€์ธก/์ •์ฐฐ ๋ชจ๋“œ (Look-Down Survey): ๋น„ํ–‰ ์ค‘ ๋ชธํ†ต๋งŒ ์•„๋ž˜๋กœ ์ˆ™์ด๊ณ  ๋‚ ๊ฐœ๋Š” ์ˆ˜ํ‰ ์œ ์ง€
79
+ self.look_down = False
80
+ self.look_down_pitch = 0.0
81
+ self.target_look_down_pitch = 0.0
82
+
83
+ def apply_push(self, fx=0.0, fy=0.0, duration=0.18):
84
+ """์™ธ๋ž€ ํ…Œ์ŠคํŠธ: ํŠน์ • ๋ฐฉํ–ฅ์œผ๋กœ duration ์ดˆ ๋™์•ˆ ์ง€์†์ ์ธ ๋ฐ€๊ธฐ ํž˜ ์ธ๊ฐ€"""
85
+ self.push_force = np.array([fx, fy, 0.0])
86
+ self.push_timer = duration
87
+
88
+ def set_state(self, new_state):
89
+ if self.state != new_state:
90
+ print(f"\n[๋ชจ๋“œ ์ „ํ™˜] {self.state} -> {new_state}")
91
+ self.state = new_state
92
+ self.state_timer = 0.0
93
+ if new_state in [self.STATE_FLIGHT, self.STATE_TAKEOFF_CLIMB]:
94
+ self.target_x = self.data.qpos[0]
95
+ self.target_y = self.data.qpos[1]
96
+
97
+ def toggle_wings(self):
98
+ """1๋‹จ๊ณ„: ๋“ฑ ๋’ค ์„ธ๋กœ ์ ‘ํž˜(์‚ฌ์ง„ 1) โž” ์ขŒ์šฐ 180ยฐ ์ˆ˜ํ‰ ์ „๊ฐœ(์‚ฌ์ง„ 2) ํ† ๊ธ€ (1๋ฒˆ / Eํ‚ค)"""
99
+ self.wings_deployed = not self.wings_deployed
100
+ if not self.wings_deployed:
101
+ self.propeller_running = False
102
+ self.set_state(self.STATE_STAND)
103
+ print("\n[๋‚ ๊ฐœ ์ ‘๊ธฐ] ๋“ฑ ๋’ค๋กœ ์„ธ๋กœ๋กœ ์™ ํฌ๊ฐœ์–ด ์ ‘์—ˆ์Šต๋‹ˆ๋‹ค. (์‚ฌ์ง„ 1 ์ƒํƒœ)")
104
+ else:
105
+ print("\n[1๋‹จ๊ณ„ ์™„๋ฃŒ] ๋“ฑ ๋’ค์— ์ ‘ํ˜€์žˆ๋˜ ๋‚ ๊ฐœ๋ฅผ ์ขŒ์šฐ 180๋„ ์ˆ˜ํ‰์œผ๋กœ ์ซ™ ํˆ์Šต๋‹ˆ๋‹ค! (์‚ฌ์ง„ 2 ์ƒํƒœ)")
106
+
107
+ def toggle_look_down(self):
108
+ """๐Ÿ“ท ๋ˆˆ์•Œ ํ•˜๋ฐฉ ์‹œ์„  ํ† ๊ธ€ (V ํ‚ค) - ๋ˆˆ์•Œ ์•„๋ž˜ 55ยฐ ๋ณด๊ธฐ <-> ๋ˆˆ์•Œ ์›๋ž˜ ์œ„์น˜๋กœ ๋ณต๊ท€"""
109
+ self.look_down = not self.look_down
110
+ if self.look_down:
111
+ self.target_look_down_pitch = 0.95 # ์•ฝ 55๋„ ์•„๋ž˜๋กœ ์ˆ™์ž„
112
+ print("\n[๐Ÿ‘‡ ๋ˆˆ์•Œ ์•„๋ž˜ ๋ณด๊ธฐ ON] ๋ˆˆ์•Œ์ด ์•„๋ž˜๋กœ 55ยฐ ํ–ฅํ•˜์—ฌ ๋ฐ”๋‹ฅ์„ ๋‚ด๋ ค๋‹ค๋ด…๋‹ˆ๋‹ค!")
113
+ else:
114
+ self.target_look_down_pitch = 0.0
115
+ print("\n[๐Ÿ‘€ ๋ˆˆ์•Œ ์›์œ„์น˜ ๋ณต๊ท€] ๋ˆˆ์•Œ์ด ๋‹ค์‹œ ์›๋ž˜ ์œ„์น˜(์ •๋ฉด)๋กœ ๋Œ์•„์™”์Šต๋‹ˆ๋‹ค! โœจ")
116
+
117
+ def look_front(self):
118
+ """๐Ÿ‘€ ์›ํ„ฐ์น˜ ๋ˆˆ์•Œ ์›์œ„์น˜ ๋ณต๊ท€ (B ํ‚ค)"""
119
+ self.look_down = False
120
+ self.target_look_down_pitch = 0.0
121
+ print("\n[๐Ÿ‘€ ๋ˆˆ์•Œ ์›์œ„์น˜ ๋ณต๊ท€] ๋ˆˆ์•Œ์ด ๋‹ค์‹œ ์›๋ž˜ ์œ„์น˜(์ •๋ฉด)๋กœ ๋Œ์•„์™”์Šต๋‹ˆ๋‹ค! โœจ")
122
+
123
+ def start_flight_sequence(self):
124
+ """2๋‹จ๊ณ„: ํ”„๋กœํŽ ๋Ÿฌ ํšŒ์ „ โž” 3๋‹จ๊ณ„ 1m ์ˆ˜์ง ์ƒ์Šน โž” 4๋‹จ๊ณ„ 1m ์ •์ง€ ํ˜ธ๋ฒ„๋ง (2๋ฒˆ / SPACEํ‚ค)"""
125
+ if not self.wings_deployed or self.wing_progress < 0.8:
126
+ print("\n[1๋‹จ๊ณ„] ๋“ฑ ๋’ค์˜ ๋‚ ๊ฐœ๋ฅผ ๋จผ์ € ์ขŒ์šฐ 180๋„๋กœ ์ซ™ ํ…๋‹ˆ๋‹ค...")
127
+ self.wings_deployed = True
128
+ self.wing_progress = 1.0 # ์ฆ‰์‹œ ์ „๊ฐœ ์™„๋ฃŒ
129
+
130
+ print("[2๋‹จ๊ณ„] 3์—ฝ ์ปฌ๋Ÿฌ ํ”„๋กœํŽ ๋Ÿฌ๊ฐ€ ์Œฉ์Œฉ ๋Œ๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค!")
131
+ self.propeller_running = True
132
+ self.target_altitude = 1.0
133
+ self.set_state(self.STATE_TAKEOFF_SPOOL)
134
+
135
+ def update(self, dt):
136
+ self.state_timer += dt
137
+
138
+ ctrl = np.zeros(self.model.nu)
139
+ pos = self.data.qpos[0:3]
140
+ vel = self.data.qvel[0:3]
141
+ z_height = pos[2]
142
+
143
+ # ==================== [ ๐ŸŒช๏ธ 3์—ฝ ํ”„๋กœํŽ ๋Ÿฌ ์Œฉ์Œฉ ํšŒ์ „ ] ====================
144
+ if self.propeller_running:
145
+ self.prop_speed = min(self.target_prop_speed, self.prop_speed + 80.0 * dt)
146
+ else:
147
+ self.prop_speed = max(0.0, self.prop_speed - 50.0 * dt)
148
+
149
+ # ๋ฌผ๋ฆฌ ์—”์ง„ DOF ์†๋„ ๋ฐ ๊ฐ๋„ ๋™๊ธฐํ™”
150
+ l_prop_dof = self.model.joint("left_prop_spin").dofadr[0]
151
+ r_prop_dof = self.model.joint("right_prop_spin").dofadr[0]
152
+ l_prop_qpos = self.model.joint("left_prop_spin").qposadr[0]
153
+ r_prop_qpos = self.model.joint("right_prop_spin").qposadr[0]
154
+
155
+ self.data.qvel[l_prop_dof] = self.prop_speed
156
+ self.data.qvel[r_prop_dof] = -self.prop_speed
157
+
158
+ if self.prop_speed > 0:
159
+ self.prop_angle += self.prop_speed * dt
160
+ self.data.qpos[l_prop_qpos] = self.prop_angle
161
+ self.data.qpos[r_prop_qpos] = -self.prop_angle
162
+
163
+ # ==================== [ ๐Ÿชฝ ๋‚ ๊ฐœ ์ „๊ฐœ/์ ‘ํž˜ ๋ณด๊ฐ„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ] ====================
164
+ target_progress = 1.0 if self.wings_deployed else 0.0
165
+ wing_speed = 3.5 # ์•ฝ 0.28์ดˆ ๋งŒ์— ๋งค๋„๋Ÿฝ๊ฒŒ ์ „๊ฐœ/์ ‘ํž˜
166
+ if self.wing_progress < target_progress:
167
+ self.wing_progress = min(target_progress, self.wing_progress + wing_speed * dt)
168
+ elif self.wing_progress > target_progress:
169
+ self.wing_progress = max(target_progress, self.wing_progress - wing_speed * dt)
170
+
171
+ # ==================== [ ๐Ÿ“ท ํ•˜๋ฐฉ ๊ด€์ธก ๊ฐ๋„ ๋ณด๊ฐ„ ๋ฐ ๋‚ ๊ฐœ ์—ญ๋ณด์ƒ ] ====================
172
+ self.look_down_pitch += (self.target_look_down_pitch - self.look_down_pitch) * min(1.0, dt * 5.0)
173
+
174
+ # fold: 0.0 (๋“ฑ ๋’ค) โž” ยฑ1.57 (์ขŒ์šฐ 180ยฐ ์ „๊ฐœ)
175
+ fold_l_cmd = 1.57 * self.wing_progress
176
+ fold_r_cmd = -1.57 * self.wing_progress
177
+
178
+ # tilt: 1.57 (๋“ฑ ๋’ค ์„ธ๋กœ ์ ‘ํž˜) โž” 0.0 (๋น„ํ–‰ ์‹œ ์™„๋ฒฝํ•œ ๊ฐ€๋กœ ์ˆ˜ํ‰ ๋ˆ•๊ธฐ!)
179
+ # ๐ŸŒŸ ๋‚ ๊ฐœ๊ฐ€ ์ „๊ฐœ๋˜๋ฉด ํ•ญ์ƒ ์ฒ˜์Œ ๋น„ํ–‰ํ•  ๋•Œ์˜ ๊ฐ€๋กœ ์ˆ˜ํ‰ ๋‚ ๊ฐœ(0.0 rad)๋ฅผ ์œ ์ง€ํ•˜๊ณ , ๋ชธํ†ต๋งŒ ์•„๋ž˜๋กœ ์ˆ™์—ฌ์ง‘๋‹ˆ๋‹ค!
180
+ tilt_cmd = 1.57 * (1.0 - self.wing_progress)
181
+
182
+ ctrl[6] = fold_l_cmd
183
+ ctrl[7] = fold_r_cmd
184
+ ctrl[8] = tilt_cmd
185
+ ctrl[9] = tilt_cmd
186
+ # ๐Ÿฆ [๋…๋ฆฝ ๋จธ๋ฆฌ/๋ชฉ ํ”ผ์น˜] ๋ชธํ†ต/๋‚ ๊ฐœ๋Š” ์™„๋ฒฝ ์ˆ˜ํ‰ ๊ณ ์ •, ๋จธ๋ฆฌ(๋‘ ๋ˆˆ+๋ถ€๋ฆฌ)๊ฐ€ ์•„๋ž˜๋กœ 55ยฐ ์™ ์ˆ™์—ฌ์ง!
187
+ ctrl[10] = -self.look_down_pitch
188
+
189
+ torso_id = self.model.body("torso").id
190
+ fz = 0.0
191
+ fx = 0.0
192
+ fy = 0.0
193
+ tau_x = 0.0
194
+ tau_y = 0.0
195
+ tau_z = 0.0
196
+
197
+ # =========================================================================
198
+ # ๐Ÿฆ† [ ์ง€์ƒ ๋ชจ๋“œ: ๋ณดํ–‰ ๋ฐ ์ง๋ฆฝ ] - ๋น„ํ–‰ ๋ฌผ๋ฆฌ์™€ 100% ๋ถ„๋ฆฌ!
199
+ # =========================================================================
200
+ is_ground_mode = self.state in [
201
+ self.STATE_STAND, self.STATE_WALK, self.STATE_GROUND_PICK,
202
+ self.STATE_WING_SHIVER, self.STATE_SIT, self.STATE_RECOVER, self.STATE_KNOCKDOWN
203
+ ]
204
+
205
+ if is_ground_mode:
206
+ # ์ฟผํ„ฐ๋‹ˆ์–ธ ๊ธฐ๋ฐ˜ ์ƒ์ฒด ๊ธฐ์šธ๊ธฐ ์ธก์ • (q = [w, x, y, z] -> x์ถ•: Pitch, y์ถ•: Roll)
207
+ q = self.data.qpos[3:7]
208
+ q_sign = np.sign(q[0]) if q[0] != 0 else 1.0
209
+ pitch_tilt = -q[1] * q_sign # X์ถ• ํšŒ์ „ = ์•ž๋’ค ๊ธฐ์šธ๊ธฐ (Pitch)
210
+ roll_tilt = -q[2] * q_sign # Y์ถ• ํšŒ์ „ = ์ขŒ์šฐ ๊ธฐ์šธ๊ธฐ (Roll)
211
+
212
+ # 1. ๐Ÿฆ ์ œ์ž๋ฆฌ ์ง๋ฆฝ ๋ชจ๋“œ (STAND) - ์™ธ๋ž€ ์ €ํ•ญ (๋ฐ€๋ฆฌ์ง€ ์•Š๊ณ  ๋ฒ„ํ‹ฐ๊ธฐ / Push Recovery)
213
+ if self.state == self.STATE_STAND:
214
+ # ์„ ์†๋„ ๋ฐ ๊ฐ์†๋„ ํ”ผ๋“œ๋ฐฑ์„ ๊ฒฐํ•ฉํ•œ ์ง€๋Šฅํ˜• ๋ฒ„ํ‹ฐ๊ธฐ (Push Resistance)
215
+ lin_vel = self.data.cvel[torso_id, 3:6]
216
+ vy = lin_vel[1]
217
+ vx = lin_vel[0]
218
+
219
+ bal_pitch = np.clip(pitch_tilt * 1.2 - vy * 0.18, -0.35, 0.35)
220
+ bal_roll = np.clip(roll_tilt * 0.8 - vx * 0.12, -0.20, 0.20)
221
+
222
+ # ํ˜ธํก ๋ชจ์…˜
223
+ breath = 0.012 * math.sin(self.state_timer * 3.0)
224
+
225
+ # ์ขŒ์ธก ๋‹ค๋ฆฌ (์™ธ๋ž€์— ๋งž์„œ ๋ฐœ๋ชฉ๊ณผ ๊ณ ๊ด€์ ˆ๋กœ ๋ฐ”๋‹ฅ์„ ๊ฝ‰ ์ง€ํƒฑ)
226
+ ctrl[0] = 0.0 + bal_pitch + bal_roll
227
+ ctrl[1] = 0.0 + breath
228
+ ctrl[2] = 0.0 - bal_pitch * 1.2
229
+
230
+ # ์šฐ์ธก ๋‹ค๋ฆฌ
231
+ ctrl[3] = 0.0 + bal_pitch - bal_roll
232
+ ctrl[4] = 0.0 + breath
233
+ ctrl[5] = 0.0 - bal_pitch * 1.2
234
+
235
+ # 2. ๐Ÿšถ 2์กฑ ๋ณดํ–‰ ๋ชจ๋“œ (WALK) - ์ง„์งœ ์•ž์œผ๋กœ/๋’ค๋กœ ๊ฑท๊ณ  ์‹œ์›ํ•˜๊ฒŒ ํšŒ์ „ํ•˜๋Š” ์กฐ๋ฅ˜ 2์กฑ๋ณดํ–‰
236
+ elif self.state == self.STATE_WALK:
237
+ # ๊ฑธ์Œ ์ฃผ๊ธฐ ์œ„์ƒ ์ง„์ฒ™
238
+ self.walk_phase += dt * 6.5 * max(0.6, abs(self.walk_speed))
239
+
240
+ phase_sin = math.sin(self.walk_phase)
241
+ phase_cos = math.cos(self.walk_phase)
242
+ dir_sign = 1.0 if self.walk_speed >= 0 else -1.0
243
+ turn = getattr(self, 'walk_turn', 0.0)
244
+
245
+ # ๐ŸŒŸ [ํ›„์ง„ ๋ณดํ–‰ 100% ์•ˆ์ •ํ™”] ํ›„์ง„ ์‹œ ์ƒ์ฒด๋ฅผ ์•ž์ชฝ์œผ๋กœ 8ยฐ ํ™•์‹คํ•˜๊ฒŒ ์ˆ™์—ฌ ๋ฌด๊ฒŒ์ค‘์‹ฌ์„ ์•ˆ์ •ํ™” (๋„˜์–ด์ง ์›์ฒœ ์ฐจ๋‹จ!)
246
+ lean_bias = 0.14 if self.walk_speed < 0 else 0.0
247
+ bal_pitch = np.clip((pitch_tilt + lean_bias) * 1.0, -0.30, 0.30)
248
+ bal_roll = np.clip(roll_tilt * 0.6, -0.15, 0.15)
249
+
250
+ # ์ „์ง„ ๋ณดํญ 0.25, ํ›„์ง„ ๋ณดํญ 0.18 (์ด˜์ด˜ํ•˜๊ณ  ๋‹น๋‹นํ•˜๊ฒŒ ๋’ค๋กœ ๊ฑท๊ธฐ)
251
+ base_stride = 0.25 if self.walk_speed >= 0 else 0.18
252
+
253
+ # ๐Ÿ”„ ์ขŒ/์šฐ ํšŒ์ „ ์‹œ ์–‘ ๋ฐœ์˜ ์ฐจ๋™ ๋ณดํญ (Differential Stride) ์ ์šฉ โž” ํŒฝ๊ทธ๋ฅด๋ฅด ์ฆ‰๊ฐ ํšŒ์ „!
254
+ l_stride = base_stride * (1.0 - turn * 0.35) * dir_sign
255
+ r_stride = base_stride * (1.0 + turn * 0.35) * dir_sign
256
+
257
+ # ๐Ÿฆถ ์ขŒ์ธก ๋‹ค๋ฆฌ (phase_sin > 0: ์œ ๊ฐ๊ธฐ ๋ฐœ ๋“ค๋ฆผ / phase_sin <= 0: ์ง€์ง€๊ธฐ ๋ฐ”๋‹ฅ ๋ฐ€๊ธฐ)
258
+ if phase_sin > 0:
259
+ l_hip = l_stride * (-phase_cos) + bal_pitch + bal_roll
260
+ l_knee = 0.35 * phase_sin # ๋ฌด๋ฆŽ์„ ๊ตฝํ˜€ ๋ฐœ์„ ๋ฐ”๋‹ฅ์—์„œ 1.5cm ๋†’์ด ๋„์›€!
261
+ l_ankle = -(l_hip + l_knee) # ๋ฐœ๋ฐ”๋‹ฅ์ด ํ•ญ์ƒ ์ง€๋ฉด๊ณผ ์ˆ˜ํ‰ ์œ ์ง€
262
+ else:
263
+ l_hip = l_stride * (-phase_cos) + bal_pitch + bal_roll
264
+ l_knee = 0.0 # ์ง€์ง€ ๋‹ค๋ฆฌ ๋ฌด๋ฆŽ ํŽด์„œ ์ง€์ง€
265
+ l_ankle = -l_hip # ์ง€๋ฉด ๋ฐ€์ฐฉ ์ง€์ง€
266
+
267
+ # ๐Ÿฆถ ์šฐ์ธก ๋‹ค๋ฆฌ (phase_sin <= 0: ์œ ๊ฐ๊ธฐ ๋ฐœ ๋“ค๋ฆผ / phase_sin > 0: ์ง€์ง€๊ธฐ ๋ฐ”๋‹ฅ ๋ฐ€๊ธฐ)
268
+ if phase_sin <= 0:
269
+ r_hip = r_stride * phase_cos + bal_pitch - bal_roll
270
+ r_knee = 0.35 * (-phase_sin) # ๋ฌด๋ฆŽ ๊ตฝํ˜€ ๋ฐœ ๋„์›€
271
+ r_ankle = -(r_hip + r_knee) # ๋ฐœ๋ฐ”๋‹ฅ ์ˆ˜ํ‰ ์œ ์ง€
272
+ else:
273
+ r_hip = r_stride * phase_cos + bal_pitch - bal_roll
274
+ r_knee = 0.0 # ์ง€์ง€ ๋‹ค๋ฆฌ ๋ฌด๋ฆŽ ํŽด์„œ ์ง€์ง€
275
+ r_ankle = -r_hip # ์ง€๋ฉด ๋ฐ€์ฐฉ ์ง€์ง€
276
+
277
+ ctrl[0] = l_hip
278
+ ctrl[1] = l_knee
279
+ ctrl[2] = l_ankle
280
+
281
+ ctrl[3] = r_hip
282
+ ctrl[4] = r_knee
283
+ ctrl[5] = r_ankle
284
+
285
+ # ์ง€์ง€๋ฐœ์˜ ์ง€๋ฉด ๋ฐ˜๋ ฅ ์ถ”์ง„๋ ฅ (ํ›„์ง„ ์‹œ์—๋„ ์•ˆ์ •๋œ ํž˜ ์ธ๊ฐ€)
286
+ R = self.data.xmat[torso_id].reshape(3, 3)
287
+ y_forward = R[:, 1]
288
+
289
+ push_mag = 1.1 if self.walk_speed >= 0 else 0.85
290
+ push_force = y_forward * (push_mag * self.walk_speed)
291
+ fx = push_force[0]
292
+ fy = push_force[1]
293
+
294
+ # ๐Ÿ”„ ํšŒ์ „ ๋ณดํ–‰ ์‹œ ํ—ค๋”ฉ ๊ฐฑ์‹  ๋ฐ ๊ฐ•๋ ฅํ•œ Yaw ํšŒ์ „ ํ† ํฌ (์ขŒ/์šฐ ์™„์ „ ๋Œ€์นญ ํšŒ์ „)
295
+ omega_ground = self.data.cvel[torso_id, 0:3]
296
+ if abs(turn) > 0.05:
297
+ self.target_yaw -= turn * 1.8 * dt
298
+ tau_z = np.clip(-turn * 0.40 - omega_ground[2] * 0.10, -0.32, 0.32)
299
+ else:
300
+ target_yaw = getattr(self, 'target_yaw', 0.0)
301
+ current_yaw = math.atan2(-y_forward[0], y_forward[1])
302
+ yaw_err = (target_yaw - current_yaw + math.pi) % (2 * math.pi) - math.pi
303
+ tau_z = np.clip(yaw_err * 0.50 - omega_ground[2] * 0.12, -0.20, 0.20)
304
+
305
+ # 3. ๐ŸŒพ ๊ฐ์„ฑ ๋ชจ์…˜๋“ค (์กฐ๋ฅ˜ ์—ญ๊ด€์ ˆ ํŠนํ™”)
306
+ elif self.state == self.STATE_GROUND_PICK:
307
+ t = self.state_timer
308
+ # ๐ŸŒŸ [๋น„ํ–‰์‹ ๋‹ค๋ฆฌ ๊นŠ์€ ์ ‘ํž˜ โž” ๋ถ€๋ฆฌ ๋ฐ”๋‹ฅ ํ„ฐ์น˜ 3๋‹จ ๋ชจ์ด ์ชผ๊ธฐ]
309
+ # 1๋‹จ๊ณ„ (0~0.42s): ๋‹ค๋ฆฌ๋ฅผ ๋ชธ ๋ฐ‘์œผ๋กœ ๊นŠ๊ฒŒ ์›…ํฌ๋ ค ์ ‘์œผ๋ฉฐ ๋ถ€๋ฆฌ๊ฐ€ ๋ฐ”๋‹ฅ ์ง€๋ฉด์— ์ฝ•! ๋‹ฟ์Œ
310
+ # 2๋‹จ๊ณ„ (0.42~0.85s): ๋ฐ”๋‹ฅ์—์„œ ์ฝ•! ์ฝ•! ์ชผ๊ธฐ
311
+ # 3๋‹จ๊ณ„ (0.85~1.30s): ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์›๋ž˜ ์„œ ์žˆ๋˜ ๊ธฐ๋ฆฝ ์ž์„ธ๋กœ ์Šฅ- ๋ณต๊ท€!
312
+ if t < 0.42:
313
+ p = 0.5 * (1.0 - math.cos(math.pi * t / 0.42)) # 0 โž” 1 ๊นŠ์€ ํ•˜๊ฐ•
314
+ hip = 0.65 * p
315
+ knee = 0.95 * p
316
+ ankle = -0.48 * p
317
+ elif t < 0.85:
318
+ mini = 0.14 * math.sin((t - 0.42) * 18.0) # ๋ฐ”๋‹ฅ ์ฝ•! ์ฝ•!
319
+ hip = 0.65 + mini
320
+ knee = 0.95
321
+ ankle = -0.48
322
+ elif t < 1.30:
323
+ p = 0.5 * (1.0 + math.cos(math.pi * (t - 0.85) / 0.45)) # 1 โž” 0 ๊ธฐ๋ฆฝ ๋ณต๊ท€
324
+ hip = 0.65 * p
325
+ knee = 0.95 * p
326
+ ankle = -0.48 * p
327
+ else:
328
+ hip = 0.0; knee = 0.0; ankle = 0.0
329
+
330
+ ctrl[0] = hip; ctrl[1] = knee; ctrl[2] = ankle
331
+ ctrl[3] = hip; ctrl[4] = knee; ctrl[5] = ankle
332
+
333
+ # ๐ŸŒŸ ํšŒ์ „ ์–ต์ œ ๋ฐ ๋ฐ˜๋™ 0% ๋Œํ•‘
334
+ omega_ground = self.data.cvel[torso_id, 0:3]
335
+ tau_z = np.clip(-omega_ground[2] * 0.30, -0.15, 0.15)
336
+ tau_x = 0.0; tau_y = 0.0
337
+
338
+ if t >= 1.30:
339
+ self.set_state(self.STATE_STAND)
340
+
341
+ elif self.state == self.STATE_KNOCKDOWN:
342
+ # ๐Ÿ’ฅ ๊ฝˆ๋‹น ๋ˆ„์›Œ์žˆ๋Š” ์ˆ˜๋™ ํ…Œ์ŠคํŠธ ์ƒํƒœ: ํž˜์„ ๋นผ๊ณ  ํŽธ์•ˆํ•˜๊ฒŒ ์ง€๋ฉด์— ๋ˆ„์›Œ์žˆ์Œ (์ผ์–ด๋‚˜๋ ค๋ฉด 'P' ํ‚ค ๋ˆ„๋ฆ„)
343
+ ctrl[0] = 0.0; ctrl[1] = 0.20; ctrl[2] = 0.0
344
+ ctrl[3] = 0.0; ctrl[4] = 0.20; ctrl[5] = 0.0
345
+ tau_x = 0.0; tau_y = 0.0; tau_z = 0.0
346
+
347
+ elif self.state == self.STATE_WING_SHIVER:
348
+ t = self.state_timer
349
+ if t < 1.5:
350
+ shiver = 0.25 * math.sin(t * 28.0)
351
+ ctrl[0] = 0.0; ctrl[1] = 0.0; ctrl[2] = 0.0
352
+ ctrl[3] = 0.0; ctrl[4] = 0.0; ctrl[5] = 0.0
353
+ ctrl[6] = max(0, shiver); ctrl[7] = -max(0, shiver)
354
+ else:
355
+ self.set_state(self.STATE_STAND)
356
+
357
+ elif self.state == self.STATE_SIT:
358
+ # ๐Ÿฅ ์™„๋ฒฝํ•˜๊ฒŒ ๋ฌด๋ฆŽ์„ ๊ตฝํžˆ๊ณ  ๋‹ค๋ฆฌ๋ฅผ ์›…ํฌ๋ ค ์ชผ๊ทธ๋ ค ์•‰๊ธฐ
359
+ ctrl[0] = -0.40; ctrl[1] = 0.90; ctrl[2] = -0.50
360
+ ctrl[3] = -0.40; ctrl[4] = 0.90; ctrl[5] = -0.50
361
+
362
+ elif self.state == self.STATE_RECOVER:
363
+ t = self.state_timer
364
+ # ๐ŸŒŸ [100% ๋ฒŒ๋–ก ์ผ์–ด๋‚˜๋Š” 3D ์˜ค๋š์ด ๊ธฐ๋ฆฝ]
365
+ # ์–ด๋–ค ๊ฐ๋„๋กœ ๋„˜์–ด์ ธ ์žˆ์–ด๋„ ๋ฐ”๋‹ฅ ๋งˆ์ฐฐ์„ ํ„ธ์–ด๋‚ด๊ณ  ์ˆ˜์ง์œผ๋กœ ์šฐ๋š ์ผ์–ด์„ฌ!
366
+ R = self.data.xmat[torso_id].reshape(3, 3)
367
+ z_body = R[:, 2]
368
+ z_world = np.array([0.0, 0.0, 1.0])
369
+ tau_tilt = np.cross(z_body, z_world) # 3์ฐจ์› ์ˆ˜์ง ์ •๋ ฌ ํ† ํฌ
370
+ omega_ground = self.data.cvel[torso_id, 0:3]
371
+
372
+ ctrl[0] = 0.0; ctrl[1] = 0.0; ctrl[2] = 0.0
373
+ ctrl[3] = 0.0; ctrl[4] = 0.0; ctrl[5] = 0.0
374
+
375
+ # 0~0.25์ดˆ: ๋ฐ”๋‹ฅ ๋งˆ์ฐฐ ํ•ด์ œ๋ฅผ ์œ„ํ•œ ์ˆ˜์ง ํŒ์—… ๋ฆฌํ”„ํŠธ ์ธ๊ฐ€
376
+ fz = 3.2 if t < 0.25 else 0.0
377
+
378
+ # ๊ฐ•๋ ฅํ•œ ์ˆ˜์ง ์ง๋ฆฝ ํ† ํฌ ๋ฐ ์Šคํ•€ ์–ต์ œ
379
+ tau_x = np.clip(tau_tilt[0] * 5.0 - omega_ground[0] * 0.30, -0.60, 0.60)
380
+ tau_y = np.clip(tau_tilt[1] * 5.0 - omega_ground[1] * 0.30, -0.60, 0.60)
381
+ tau_z = np.clip(-omega_ground[2] * 0.35, -0.20, 0.20)
382
+
383
+ # ์ˆ˜์ง ์ •๋ ฌ์ด ์™„๋ฃŒ๋˜๋ฉด STAND๋กœ ๋ณต๊ท€
384
+ z_err = 1.0 - z_body[2]
385
+ if t > 0.40 and z_err < 0.03:
386
+ self.set_state(self.STATE_STAND)
387
+ elif t > 1.20:
388
+ self.set_state(self.STATE_STAND)
389
+
390
+ # ๐Ÿšจ [์ง€๋Šฅํ˜• ์ž๋™ ๋„˜์–ด์ง ๊ฐ์ง€ ๋ฐ ์ฆ‰์‹œ ์˜ค๋š์ด ๊ธฐ๋ฆฝ (Auto Knockdown Recovery)]
391
+ R_body = self.data.xmat[torso_id].reshape(3, 3)
392
+ z_up = R_body[2, 2] # 1.0 = ๋˜‘๋ฐ”๋กœ ์ง๋ฆฝ, 0.0 = 90๋„ ์“ฐ๋Ÿฌ์ง
393
+ torso_z = self.data.qpos[2]
394
+
395
+ # ์ •์ƒ ์ง๋ฆฝ์ด ์•„๋‹ ๋•Œ (๊ธฐ์šธ์–ด์ง > 50ยฐ ๋˜๋Š” ๋†’์ด < 6.5cm)
396
+ if self.state not in [self.STATE_RECOVER, self.STATE_KNOCKDOWN, self.STATE_GROUND_PICK, self.STATE_SIT]:
397
+ if z_up < 0.60 or torso_z < 0.065:
398
+ self.fall_detect_timer += dt
399
+ if self.fall_detect_timer >= 0.20:
400
+ print(f"\n[์˜ค๋š์ด ๊ฐ์ง€] ๋กœ๋ด‡์ƒˆ ๋„˜์–ด์ง ๊ฐ์ง€! (๊ธฐ์šธ๊ธฐ z_up={z_up:.2f}, ๋†’์ด={torso_z*100:.1f}cm) -> ์ž๋™์œผ๋กœ ๋ฒŒ๋–ก ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค!")
401
+ self.fall_detect_timer = 0.0
402
+ self.set_state(self.STATE_RECOVER)
403
+ else:
404
+ self.fall_detect_timer = max(0.0, self.fall_detect_timer - dt * 2.0)
405
+
406
+ # ์ง€์ƒ ๋ชจ๋“œ ์ƒ์ฒด ์ž์„ธ ์œ ์ง€ ๋Œํ•‘ (๋„˜์–ด์ง/๊ธฐ๋ฆฝ ์ƒํƒœ ์™ธ ์ผ๋ฐ˜ ๋ณดํ–‰/๊ธฐ๋ฆฝ ์‹œ)
407
+ if self.state not in [self.STATE_RECOVER, self.STATE_KNOCKDOWN, self.STATE_GROUND_PICK]:
408
+ omega_ground = self.data.cvel[torso_id, 0:3]
409
+ lean_bias = -0.05 if (self.state == self.STATE_WALK and self.walk_speed < 0) else 0.0
410
+ tau_x = np.clip((pitch_tilt + lean_bias) * 3.5 - omega_ground[0] * 0.22, -0.35, 0.35)
411
+ tau_y = np.clip(roll_tilt * 2.5 - omega_ground[1] * 0.15, -0.25, 0.25)
412
+ # ๐ŸŒŸ ๋ณดํ–‰ ์ค‘์ด ์•„๋‹ ๋•Œ๋งŒ ์ œ์ž๋ฆฌ ํšŒ์ „ ์–ต์ œ, ๋ณดํ–‰ ์ค‘(WALK)์ผ ๋•Œ๋Š” ํšŒ์ „ ํ† ํฌ ๋ณด์กด!
413
+ if self.state != self.STATE_WALK:
414
+ tau_z = np.clip(-omega_ground[2] * 0.25, -0.15, 0.15)
415
+
416
+
417
+
418
+ # =========================================================================
419
+ # ๐Ÿš [ ๋น„ํ–‰ ๋ชจ๋“œ: ์ด๋ฅ™, ํ˜ธ๋ฒ„๋ง, ์ฐฉ๋ฅ™ ] - 2๋ฒˆ ํ‚ค ๋ˆ„๋ฅผ ๋•Œ๋งŒ ์ž‘๋™!
420
+ # =========================================================================
421
+ else:
422
+ # ์›”๋“œ ํšŒ์ „ ํ–‰๋ ฌ ๋ฐ ์›”๋“œ ๊ฐ์†๋„ (cvel: world-frame angular velocity)
423
+ R = self.data.xmat[torso_id].reshape(3, 3)
424
+ omega_world = self.data.cvel[torso_id, 0:3]
425
+ y_body = R[:, 1]
426
+ z_body = R[:, 2]
427
+
428
+ # 1. ๐ŸŒŸ ์ˆ˜ํ‰ ์œ ์ง€ (Roll / Pitch Leveling) - ์›”๋“œ Z์ถ• ์™„๋ฒฝ ์ˆ˜์ง ์ •๋ ฌ (ํ”„๋กœํŽ ๋Ÿฌ์™€ ๋ชธํ†ต์€ ํ•ญ์ƒ ๊ฐ€๋กœ ์ˆ˜ํ‰ ์œ ์ง€!)
429
+ z_world = np.array([0.0, 0.0, 1.0])
430
+ tau_tilt = np.cross(z_body, z_world) # [tilt_x, tilt_y, 0] (์ˆœ์ˆ˜ ์ˆ˜ํ‰ ๋ณต์›๋ ฅ)
431
+
432
+ # 2. ๐ŸŒŸ ๋ชฉํ‘œ ํ—ค๋”ฉ ์ถ”์ข… (Yaw Heading) - ์ตœ๋‹จ ๊ฐ๋„ ์˜ค์ฐจ ๊ณ„์‚ฐ (-pi ~ +pi)
433
+ current_yaw = math.atan2(-y_body[0], y_body[1])
434
+ yaw_err = (self.target_yaw - current_yaw + math.pi) % (2 * math.pi) - math.pi
435
+
436
+ # 3. ๐ŸŒŸ 3์ถ• ์ง๊ต ๋…๋ฆฝ ์•ˆ์ •ํ™” ์ œ์–ด (Cross-coupling 100% ์›์ฒœ ์ฐจ๋‹จ!)
437
+ # Roll / Pitch: ๊ฐ•ํ•œ ๋ณต์›๋ ฅ๊ณผ ๋Œํ•‘์œผ๋กœ ๊ธฐ์šฐ๋šฑ ๋ฐฉ์ง€
438
+ tau_x = np.clip(tau_tilt[0] * 3.5 - omega_world[0] * 0.35, -0.4, 0.4)
439
+ tau_y = np.clip(tau_tilt[1] * 3.5 - omega_world[1] * 0.35, -0.4, 0.4)
440
+ # Yaw: ๋ถ€๋“œ๋Ÿฝ๊ณ  ์ •ํ™•ํ•œ ํ—ค๋”ฉ ์ถ”์ข… ๋ฐ ๋Œํ•‘
441
+ tau_z = np.clip(yaw_err * 0.50 - omega_world[2] * 0.12, -0.20, 0.20)
442
+
443
+ # ๐ŸŒŸ [๋น„ํ–‰ ๋ชจ๋“œ ๋‹ค๋ฆฌ ์ ‘ํž˜]
444
+ # ๋น„ํ–‰๊ธฐ ๋žœ๋”ฉ๊ธฐ์–ด ๋ฐ ์ƒˆ์˜ ๋น„ํ–‰ ์ž์„ธ์ฒ˜๋Ÿผ ๋‹ค๋ฆฌ๋Š” ๋ชธํ†ต ์•„๋ž˜๋กœ ์™ ์ ‘์–ด ์˜ฌ๋ฆฌ๊ณ ,
445
+ # ๋ฐœ๋ฐ”๋‹ฅ์€ ์™„๋ฒฝํ•˜๊ฒŒ ๊ฐ€๋กœ ์ˆ˜ํ‰(์ง€๋ฉด ํ‰ํ–‰)์„ ์œ ์ง€ํ•˜์—ฌ ๋ชธ์— ๋‹จ์ •ํ•˜๊ฒŒ ์ฐฉ ๋ถ™์ž…๋‹ˆ๋‹ค!
446
+ if self.state in [self.STATE_TAKEOFF_CLIMB, self.STATE_FLIGHT]:
447
+ ctrl[0] = -0.38; ctrl[1] = 0.80; ctrl[2] = -0.42
448
+ ctrl[3] = -0.38; ctrl[4] = 0.80; ctrl[5] = -0.42
449
+ elif self.state == self.STATE_LANDING:
450
+ # ์ฐฉ๋ฅ™ ์‹œ ์ง€์ƒ 25cm ์ดํ•˜๋กœ ๋‚ด๋ ค์˜ค๋ฉด ๋‹ค๋ฆฌ๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ํŽด์„œ ์ฐฉ์ง€ ์ค€๋น„!
451
+ if z_height > 0.25:
452
+ ctrl[0] = -0.38; ctrl[1] = 0.80; ctrl[2] = -0.42
453
+ ctrl[3] = -0.38; ctrl[4] = 0.80; ctrl[5] = -0.42
454
+ else:
455
+ ctrl[0] = 0.0; ctrl[1] = 0.0; ctrl[2] = 0.0
456
+ ctrl[3] = 0.0; ctrl[4] = 0.0; ctrl[5] = 0.0
457
+ else:
458
+ ctrl[0] = 0.0; ctrl[1] = 0.0; ctrl[2] = 0.0
459
+ ctrl[3] = 0.0; ctrl[4] = 0.0; ctrl[5] = 0.0
460
+
461
+ # 1. ์ด๋ฅ™ ์ค€๋น„ (ํ”„๋กœํŽ ๋Ÿฌ ๊ฐ€์†)
462
+ if self.state == self.STATE_TAKEOFF_SPOOL:
463
+ # ํ˜„์žฌ ์œ„์น˜ ์•ต์ปค ์บก์ฒ˜
464
+ self.target_x = pos[0]
465
+ self.target_y = pos[1]
466
+
467
+ # ํ˜„์žฌ yaw ๊ฐ๋„ ์บก์ฒ˜
468
+ siny_cosp = 2 * (self.data.qpos[3] * self.data.qpos[6] + self.data.qpos[4] * self.data.qpos[5])
469
+ cosy_cosp = 1 - 2 * (self.data.qpos[5] * self.data.qpos[5] + self.data.qpos[6] * self.data.qpos[6])
470
+ self.target_yaw = math.atan2(siny_cosp, cosy_cosp)
471
+
472
+ if self.state_timer >= 0.35:
473
+ print(f"[3๋‹จ๊ณ„] 1.0m ๋†’์ด๋กœ ๋˜‘๋ฐ”๋กœ ์ˆ˜์ง ์ƒ์Šนํ•ฉ๋‹ˆ๋‹ค...")
474
+ self.set_state(self.STATE_TAKEOFF_CLIMB)
475
+
476
+ # 2. 1.0m ์ˆ˜์ง ์ƒ์Šน
477
+ elif self.state == self.STATE_TAKEOFF_CLIMB:
478
+ # ์ˆ˜์ง ์ƒ์Šน ์ถ”๋ ฅ
479
+ err_z = self.target_altitude - z_height
480
+ fz = self.gravity_force + np.clip(err_z * 8.0 - vel[2] * 3.5, -self.gravity_force * 0.4, self.gravity_force * 1.6)
481
+
482
+ # ์ˆ˜ํ‰ ์œ„์น˜ ๊ณ ์ •
483
+ err_x = self.target_x - pos[0]
484
+ err_y = self.target_y - pos[1]
485
+ fx = np.clip(err_x * 4.0 - vel[0] * 3.0, -1.0, 1.0)
486
+ fy = np.clip(err_y * 4.0 - vel[1] * 3.0, -1.0, 1.0)
487
+
488
+ if z_height >= self.target_altitude - 0.05 or self.state_timer > 1.8:
489
+ print(f"[4๋‹จ๊ณ„ ์™„๋ฃŒ] 1.0m ๋†’์ด์— ๋„๋‹ฌํ•˜์—ฌ ์™„๋ฒฝํžˆ ์ •์ง€ ํ˜ธ๋ฒ„๋งํ•ฉ๋‹ˆ๋‹ค!")
490
+ self.set_state(self.STATE_FLIGHT)
491
+
492
+ # 3. 1.0m ์™„๋ฒฝ ์ •์ง€ ํ˜ธ๋ฒ„๋ง ๋ฐ ์ž์œ  ๋น„ํ–‰ (์„œ์žˆ์„ ๋•Œ์˜ ๋‹จ์ •ํ•œ ๋ฐœ ๋ชจ์–‘ ์œ ์ง€)
493
+ elif self.state == self.STATE_FLIGHT:
494
+ # ๊ณ ๋„ ์œ ์ง€ (Z Altitude Hold)
495
+ err_z = self.target_altitude - z_height
496
+ fz = self.gravity_force + np.clip(err_z * 8.5 - vel[2] * 3.5, -self.gravity_force * 0.5, self.gravity_force * 1.5)
497
+
498
+ # ์ˆ˜ํ‰ ์œ„์น˜ ์ด๋™ ๋ฐ ์ถ”์ข… (X, Y Position Flight Control) - ์‹œ์›ํ•˜๊ณ  ๋ฏผ์ฒฉํ•œ ๋น„ํ–‰!
499
+ err_x = self.target_x - pos[0]
500
+ err_y = self.target_y - pos[1]
501
+ fx = np.clip(err_x * 8.0 - vel[0] * 3.5, -2.5, 2.5)
502
+ fy = np.clip(err_y * 8.0 - vel[1] * 3.5, -2.5, 2.5)
503
+
504
+ # 4. ์ˆ˜์ง ์ฐฉ๋ฅ™
505
+ elif self.state == self.STATE_LANDING:
506
+ if z_height > 0.15:
507
+ fz = self.gravity_force * 0.70
508
+ err_x = self.target_x - pos[0]
509
+ err_y = self.target_y - pos[1]
510
+ fx = np.clip(err_x * 4.0 - vel[0] * 2.5, -1.0, 1.0)
511
+ fy = np.clip(err_y * 4.0 - vel[1] * 2.5, -1.0, 1.0)
512
+ else:
513
+ print("[์ฐฉ๋ฅ™ ์™„๋ฃŒ] ์ง€๋ฉด์— ์•ˆ์ „ํ•˜๊ฒŒ ์ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœํŽ ๋Ÿฌ๋ฅผ ๋„๊ณ  ๋‚ ๊ฐœ๋ฅผ ๋“ฑ ๋’ค๋กœ ์ ‘์Šต๋‹ˆ๋‹ค.")
514
+ fz = 0.0
515
+ self.propeller_running = False
516
+ self.wings_deployed = False
517
+ self.set_state(self.STATE_STAND)
518
+
519
+ self.data.ctrl[:] = ctrl
520
+
521
+ # ๐ŸฅŠ ์™ธ๋ž€ ๋ฐ€๊ธฐ ํž˜ ์ธ๊ฐ€ (ํ…Œ์ŠคํŠธ ํ‚ค X, Z ๋ˆ„๋ฅผ ๋•Œ)
522
+ if self.push_timer > 0.0:
523
+ fx += self.push_force[0]
524
+ fy += self.push_force[1]
525
+ self.push_timer -= dt
526
+
527
+ # ์™ธ๋ ฅ ๋ฐ ํ† ํฌ ์ธ๊ฐ€ (์ง€์ƒ/๋น„ํ–‰ ์•ˆ์ „ ํด๋žจํ•‘)
528
+ self.data.xfrc_applied[:] = 0.0
529
+ if fz > 0.0 or abs(fy) > 0.0 or abs(fx) > 0.0 or abs(tau_x) > 0.0 or abs(tau_y) > 0.0 or abs(tau_z) > 0.0:
530
+ self.data.xfrc_applied[torso_id, :3] = np.array([fx, fy, fz])
531
+ self.data.xfrc_applied[torso_id, 3:6] = np.array([tau_x, tau_y, tau_z])
532
+
533
+
534
+
robot_bird/models/robot_bird.xml ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mujoco model="bipedal_vtol_robot_bird">
2
+ <compiler angle="radian" coordinate="local" inertiafromgeom="true"/>
3
+ <option gravity="0 0 -9.81" timestep="0.002" integrator="RK4"/>
4
+
5
+ <default>
6
+ <joint damping="0.15" armature="0.02"/>
7
+ <geom friction="2.0 0.1 0.1" density="50" solref="0.01 1" solimp="0.90 0.95 0.001"/>
8
+ <position kp="350" kv="20" forcerange="-35 35"/>
9
+ </default>
10
+
11
+ <visual>
12
+ <headlight diffuse="0.65 0.65 0.65" ambient="0.45 0.45 0.45" specular="0.0 0.0 0.0"/>
13
+ <rgba haze="0.15 0.25 0.35 1"/>
14
+ </visual>
15
+
16
+ <asset>
17
+ <texture type="skybox" builtin="gradient" rgb1="0.5 0.7 0.9" rgb2="0.9 0.95 1.0" width="512" height="512"/>
18
+ <texture name="grid" type="2d" builtin="checker" width="512" height="512" rgb1="0.90 0.92 0.96" rgb2="0.55 0.62 0.72"/>
19
+ <material name="grid_mat" texture="grid" texrepeat="8 8" reflectance="0.05"/>
20
+
21
+ <material name="body_white" rgba="0.96 0.96 0.98 1.0" specular="0.6" shininess="0.9"/>
22
+ <material name="body_accent" rgba="1.0 0.52 0.08 1.0" specular="0.7" shininess="0.8"/>
23
+ <material name="eye_black" rgba="0.05 0.05 0.08 1.0" specular="0.9" shininess="1.0"/>
24
+ <material name="eye_pupil" rgba="0.15 0.75 1.0 1.0" emission="0.6"/>
25
+ <material name="leg_metal" rgba="0.25 0.28 0.32 1.0" specular="0.8"/>
26
+
27
+ <material name="duct_rim_cyan" rgba="0.12 0.62 0.88 1.0" specular="0.8" shininess="0.9"/>
28
+ <material name="mesh_grille" rgba="0.2 0.25 0.3 0.85" specular="0.7"/>
29
+ <material name="prop_carbon" rgba="0.1 0.1 0.12 1.0" specular="0.5"/>
30
+ <material name="spinner_gold" rgba="1.0 0.75 0.15 1.0" specular="0.9"/>
31
+ </asset>
32
+
33
+ <worldbody>
34
+ <light pos="0 -1 3.5" dir="0 0.3 -1" directional="true" castshadow="true"/>
35
+ <geom name="floor" type="plane" size="10 10 0.1" material="grid_mat" conaffinity="15" contype="15"/>
36
+
37
+ <!-- ๐Ÿฆ ๋กœ๋ด‡์ƒˆ ๋ฉ”์ธ ๋ฐ”๋”” -->
38
+ <body name="torso" pos="0 0 0.14">
39
+ <freejoint name="root"/>
40
+
41
+ <geom name="torso_sphere" type="sphere" size="0.065" mass="0.15" material="body_white"/>
42
+ <!-- ํ•˜๋‹จ ๋ฐฐํ„ฐ๋ฆฌ ์ €์ค‘์‹ฌ ์›จ์ดํŠธ (๋ชธ์ฒด ๊ตฌ์ฒด ๋‚ด๋ถ€ ์•ˆ์ชฝ์— ์™ ์ˆ˜๋‚ฉํ•˜์—ฌ ์‹œ์•ผ ๊ฐ„์„ญ 100% ์ฐจ๋‹จ) -->
43
+ <geom name="battery_weight" type="box" size="0.025 0.025 0.012" pos="0 0.0 -0.030" mass="0.14" material="leg_metal"/>
44
+
45
+ <!-- ๐Ÿฆ ๋…๋ฆฝ ๋ชฉ/๋จธ๋ฆฌ ํ”ผ์น˜ ๊ด€์ ˆ: ๋‚ ๊ฐœ์™€ ๋ชธํ†ต์€ ์ˆ˜ํ‰ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๊ณ  ๋จธ๋ฆฌ(๋‘ ๋ˆˆ+๋ถ€๋ฆฌ+์–ผ๊ตด) ์ „์ฒด๊ฐ€ ์•„๋ž˜๋กœ 55ยฐ ์™ ์ˆ™์—ฌ์ง! -->
46
+ <body name="head" pos="0 0.035 0.008">
47
+ <joint name="neck_pitch" type="hinge" axis="1 0 0" range="-1.4 0.3" pos="0 0 0"/>
48
+
49
+ <geom name="face_visor" type="capsule" fromto="-0.018 0.005 0 0.018 0.005 0" size="0.025" mass="0.005" material="body_white"/>
50
+ <geom name="left_eye" type="sphere" size="0.012" pos="0.022 0.020 0.007" mass="0.001" material="eye_black"/>
51
+ <geom name="left_pupil" type="sphere" size="0.005" pos="0.022 0.025 0.009" mass="0.001" material="eye_pupil"/>
52
+ <geom name="right_eye" type="sphere" size="0.012" pos="-0.022 0.020 0.007" mass="0.001" material="eye_black"/>
53
+ <geom name="right_pupil" type="sphere" size="0.005" pos="-0.022 0.025 0.009" mass="0.001" material="eye_pupil"/>
54
+ <geom name="beak" type="ellipsoid" size="0.015 0.016 0.007" pos="0 0.025 -0.013" mass="0.002" material="body_accent"/>
55
+
56
+ <!-- ๐Ÿ“ท ๋จธ๋ฆฌ์— ๊ณ ์ •๋œ 1์ธ์นญ ๋ˆˆ ์‹œ์„  ์นด๋ฉ”๋ผ (์žฅ์• ๋ฌผ ์—†์ด ๋ฐ”๋‹ฅ๊ณผ ํ•˜๋Š˜์ด ์‹œ์›ํ•˜๊ฒŒ ํŠธ์ธ FPV ์‹œ์•ผ) -->
57
+ <camera name="head_fpv_cam" pos="0 0.038 0.012" fovy="85" mode="fixed" xyaxes="1 0 0 0 0 1"/>
58
+ <camera name="left_eye_cam" pos="0.022 0.030 0.010" fovy="85" mode="fixed" xyaxes="1 0 0 0 0 1"/>
59
+ <camera name="right_eye_cam" pos="-0.022 0.030 0.010" fovy="85" mode="fixed" xyaxes="1 0 0 0 0 1"/>
60
+ </body>
61
+
62
+ <!-- ========================================================================================= -->
63
+ <!-- ๐Ÿชฝ ์ขŒ์ธก ๋‚ ๊ฐœ: Z์ถ•(์ˆ˜ํ‰) ํšŒ์ „ ํžŒ์ง€ -->
64
+ <!-- 0 rad: ๋“ฑ ๋’ค(-Y)๋กœ ์™ ํฌ๊ฐœ์ง -->
65
+ <!-- +1.57 rad: ์ขŒ์ธก(+X)์œผ๋กœ 180ยฐ ์ˆ˜ํ‰์œผ๋กœ ์ซ™ ํŽผ์ณ์ง! -->
66
+ <!-- ========================================================================================= -->
67
+ <body name="left_wing_base" pos="0.045 0 0.01">
68
+ <joint name="left_wing_fold" type="hinge" axis="0 0 1" range="-0.2 1.8" pos="0 0 0"/>
69
+ <geom name="left_wing_arm" type="capsule" fromto="0 0 0 0 -0.075 0" size="0.007" mass="0.003" material="body_accent"/>
70
+
71
+ <body name="left_rotor_tilt" pos="0 -0.075 0">
72
+ <joint name="left_tilt" type="hinge" axis="1 0 0" range="-0.5 2.0" pos="0 0 0"/>
73
+
74
+ <!-- ๐Ÿ›ž ์†์ด ๋ปฅ ๋šซ๋ฆฐ ์•„๋ฆ„๋‹ค์šด ์›ํ˜• ๋•ํŠธ ๋ฆผ (Duct Guard Ring) -->
75
+ <geom name="l_ring_0" type="capsule" fromto="0.0460 0.0000 0 0.0398 0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
76
+ <geom name="l_ring_1" type="capsule" fromto="0.0398 0.0230 0 0.0230 0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
77
+ <geom name="l_ring_2" type="capsule" fromto="0.0230 0.0398 0 0.0000 0.0460 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
78
+ <geom name="l_ring_3" type="capsule" fromto="0.0000 0.0460 0 -0.0230 0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
79
+ <geom name="l_ring_4" type="capsule" fromto="-0.0230 0.0398 0 -0.0398 0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
80
+ <geom name="l_ring_5" type="capsule" fromto="-0.0398 0.0230 0 -0.0460 0.0000 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
81
+ <geom name="l_ring_6" type="capsule" fromto="-0.0460 0.0000 0 -0.0398 -0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
82
+ <geom name="l_ring_7" type="capsule" fromto="-0.0398 -0.0230 0 -0.0230 -0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
83
+ <geom name="l_ring_8" type="capsule" fromto="-0.0230 -0.0398 0 0.0000 -0.0460 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
84
+ <geom name="l_ring_9" type="capsule" fromto="0.0000 -0.0460 0 0.0230 -0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
85
+ <geom name="l_ring_10" type="capsule" fromto="0.0230 -0.0398 0 0.0398 -0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
86
+ <geom name="l_ring_11" type="capsule" fromto="0.0398 -0.0230 0 0.0460 0.0000 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
87
+
88
+ <!-- ํ•˜๋‹จ ๋ชจํ„ฐ ๋งˆ์šดํŠธ ์ง€์ง€ ์Šคํฌํฌ -->
89
+ <geom name="l_spoke_1" type="capsule" fromto="-0.044 0 -0.005 0.044 0 -0.005" size="0.0015" mass="0.0002" material="leg_metal"/>
90
+ <geom name="l_spoke_2" type="capsule" fromto="0 -0.044 -0.005 0 0.044 -0.005" size="0.0015" mass="0.0002" material="leg_metal"/>
91
+
92
+ <!-- ์ค‘์•™ ๋ชจํ„ฐ ํ—ˆ๋ธŒ ์Šคํ”ผ๋„ˆ -->
93
+ <geom name="left_spinner" type="sphere" size="0.010" pos="0 0 0" mass="0.001" material="spinner_gold"/>
94
+
95
+ <!-- ๐ŸŒช๏ธ 3์—ฝ ๋‹ค์ด๋‚ด๋ฏน ๊ณ ์† ํšŒ์ „ ํ”„๋กœํŽ ๋Ÿฌ (๋ˆˆ์— ํ™•์‹คํžˆ ๋ณด์ด๋Š” ์ปฌ๋Ÿฌ ํŒ ์žฅ์ฐฉ) -->
96
+ <body name="left_propeller" pos="0 0 0.003">
97
+ <joint name="left_prop_spin" type="hinge" axis="0 0 1" damping="0.0005"/>
98
+ <!-- 120๋„ ๊ฐ„๊ฒฉ 3์—ฝ ๋ธ”๋ ˆ์ด๋“œ -->
99
+ <geom name="l_b1" type="capsule" fromto="0 0 0 0.041 0 0" size="0.0045" mass="0.0005" material="prop_carbon"/>
100
+ <geom name="l_b1_tip" type="sphere" size="0.006" pos="0.041 0 0" mass="0.0001" material="body_accent"/>
101
+
102
+ <geom name="l_b2" type="capsule" fromto="0 0 0 -0.0205 0.0355 0" size="0.0045" mass="0.0005" material="prop_carbon"/>
103
+ <geom name="l_b2_tip" type="sphere" size="0.006" pos="-0.0205 0.0355 0" mass="0.0001" material="body_accent"/>
104
+
105
+ <geom name="l_b3" type="capsule" fromto="0 0 0 -0.0205 -0.0355 0" size="0.0045" mass="0.0005" material="prop_carbon"/>
106
+ <geom name="l_b3_tip" type="sphere" size="0.006" pos="-0.0205 -0.0355 0" mass="0.0001" material="body_accent"/>
107
+ </body>
108
+ </body>
109
+ </body>
110
+
111
+ <!-- ========================================================================================= -->
112
+ <!-- ๐Ÿชฝ ์šฐ์ธก ๋‚ ๊ฐœ: Z์ถ•(์ˆ˜ํ‰) ํšŒ์ „ ํžŒ์ง€ -->
113
+ <!-- 0 rad: ๋“ฑ ๋’ค(-Y)๋กœ ์™ ํฌ๊ฐœ์ง (์ขŒ์ธก ๋•ํŠธ์™€ ํฌ๊ฐœ์ง) -->
114
+ <!-- -1.57 rad: ์šฐ์ธก(-X)์œผ๋กœ 180ยฐ ์ˆ˜ํ‰์œผ๋กœ ์ซ™ ํŽผ์ณ์ง! -->
115
+ <!-- ========================================================================================= -->
116
+ <body name="right_wing_base" pos="-0.045 0 0.01">
117
+ <joint name="right_wing_fold" type="hinge" axis="0 0 1" range="-1.8 0.2" pos="0 0 0"/>
118
+ <geom name="right_wing_arm" type="capsule" fromto="0 0 0 0 -0.075 0" size="0.007" mass="0.003" material="body_accent"/>
119
+
120
+ <body name="right_rotor_tilt" pos="0 -0.075 0">
121
+ <joint name="right_tilt" type="hinge" axis="1 0 0" range="-0.5 2.0" pos="0 0 0"/>
122
+
123
+ <!-- ๐Ÿ›ž ์†์ด ๋ปฅ ๋šซ๋ฆฐ ์•„๋ฆ„๋‹ค์šด ์›ํ˜• ๋•ํŠธ ๋ฆผ (Duct Guard Ring) -->
124
+ <geom name="r_ring_0" type="capsule" fromto="0.0460 0.0000 0 0.0398 0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
125
+ <geom name="r_ring_1" type="capsule" fromto="0.0398 0.0230 0 0.0230 0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
126
+ <geom name="r_ring_2" type="capsule" fromto="0.0230 0.0398 0 0.0000 0.0460 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
127
+ <geom name="r_ring_3" type="capsule" fromto="0.0000 0.0460 0 -0.0230 0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
128
+ <geom name="r_ring_4" type="capsule" fromto="-0.0230 0.0398 0 -0.0398 0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
129
+ <geom name="r_ring_5" type="capsule" fromto="-0.0398 0.0230 0 -0.0460 0.0000 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
130
+ <geom name="r_ring_6" type="capsule" fromto="-0.0460 0.0000 0 -0.0398 -0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
131
+ <geom name="r_ring_7" type="capsule" fromto="-0.0398 -0.0230 0 -0.0230 -0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
132
+ <geom name="r_ring_8" type="capsule" fromto="-0.0230 -0.0398 0 0.0000 -0.0460 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
133
+ <geom name="r_ring_9" type="capsule" fromto="0.0000 -0.0460 0 0.0230 -0.0398 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
134
+ <geom name="r_ring_10" type="capsule" fromto="0.0230 -0.0398 0 0.0398 -0.0230 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
135
+ <geom name="r_ring_11" type="capsule" fromto="0.0398 -0.0230 0 0.0460 0.0000 0" size="0.0035" mass="0.0004" material="duct_rim_cyan"/>
136
+
137
+ <!-- ํ•˜๋‹จ ๋ชจํ„ฐ ๋งˆ์šดํŠธ ์ง€์ง€ ์Šคํฌํฌ -->
138
+ <geom name="r_spoke_1" type="capsule" fromto="-0.044 0 -0.005 0.044 0 -0.005" size="0.0015" mass="0.0002" material="leg_metal"/>
139
+ <geom name="r_spoke_2" type="capsule" fromto="0 -0.044 -0.005 0 0.044 -0.005" size="0.0015" mass="0.0002" material="leg_metal"/>
140
+
141
+ <!-- ์ค‘์•™ ๋ชจํ„ฐ ํ—ˆ๋ธŒ ์Šคํ”ผ๋„ˆ -->
142
+ <geom name="right_spinner" type="sphere" size="0.010" pos="0 0 0" mass="0.001" material="spinner_gold"/>
143
+
144
+ <!-- ๐ŸŒช๏ธ 3์—ฝ ๋‹ค์ด๋‚ด๋ฏน ๊ณ ์† ํšŒ์ „ ํ”„๋กœํŽ ๋Ÿฌ (๋ˆˆ์— ํ™•์‹คํžˆ ๋ณด์ด๋Š” ์ปฌ๋Ÿฌ ํŒ ์žฅ์ฐฉ) -->
145
+ <body name="right_propeller" pos="0 0 0.003">
146
+ <joint name="right_prop_spin" type="hinge" axis="0 0 1" damping="0.0005"/>
147
+ <!-- 120๋„ ๊ฐ„๊ฒฉ 3์—ฝ ๋ธ”๋ ˆ์ด๋“œ -->
148
+ <geom name="r_b1" type="capsule" fromto="0 0 0 0.041 0 0" size="0.0045" mass="0.0005" material="prop_carbon"/>
149
+ <geom name="r_b1_tip" type="sphere" size="0.006" pos="0.041 0 0" mass="0.0001" material="body_accent"/>
150
+
151
+ <geom name="r_b2" type="capsule" fromto="0 0 0 -0.0205 0.0355 0" size="0.0045" mass="0.0005" material="prop_carbon"/>
152
+ <geom name="r_b2_tip" type="sphere" size="0.006" pos="-0.0205 0.0355 0" mass="0.0001" material="body_accent"/>
153
+
154
+ <geom name="r_b3" type="capsule" fromto="0 0 0 -0.0205 -0.0355 0" size="0.0045" mass="0.0005" material="prop_carbon"/>
155
+ <geom name="r_b3_tip" type="sphere" size="0.006" pos="-0.0205 -0.0355 0" mass="0.0001" material="body_accent"/>
156
+ </body>
157
+ </body>
158
+ </body>
159
+
160
+ <!-- ==================== ๐Ÿฆต ์ขŒ์ธก 2์กฑ ๋ณดํ–‰ ๋‹ค๋ฆฌ (๋””์ฆˆ๋‹ˆ/BDX ์Šคํƒ€์ผ ์—ญ๊ด€์ ˆ + ํ™”์ดํŠธ ์•„๋จธ + ํˆฌํ†ค ์Šˆ์ฆˆ) ==================== -->
161
+ <body name="left_hip" pos="0.032 0.010 -0.035">
162
+ <joint name="left_hip_pitch" type="hinge" axis="1 0 0" range="-1.8 1.8" pos="0 0 0"/>
163
+ <!-- ๋ฉ”์ธ ํ—ˆ๋ฒ…์ง€ ํ”„๋ ˆ์ž„ + ์™ธ์ธก ํ™”์ดํŠธ ์•„๋จธ ์ปค๋ฒ„ (์‚ฌ์ง„ 1, 2, 3) -->
164
+ <geom name="left_thigh" type="capsule" fromto="0 0 0 0 -0.030 -0.030" size="0.008" mass="0.008" material="leg_metal"/>
165
+ <geom name="left_thigh_armor" type="capsule" fromto="0.010 0 0 0.010 -0.030 -0.030" size="0.011" mass="0.004" material="body_white"/>
166
+
167
+ <body name="left_knee" pos="0 -0.030 -0.030">
168
+ <joint name="left_knee_pitch" type="hinge" axis="1 0 0" range="-2.2 2.2" pos="0 0 0"/>
169
+ <geom name="left_knee_hub" type="cylinder" size="0.008 0.012" pos="0 0 0" quat="0.707 0 0.707 0" mass="0.002" material="spinner_gold"/>
170
+ <geom name="left_shin" type="capsule" fromto="0 0 0 0 0.030 -0.042" size="0.008" mass="0.008" material="leg_metal"/>
171
+ <geom name="left_shin_brace" type="capsule" fromto="0.008 0.005 -0.005 0.008 0.025 -0.035" size="0.004" mass="0.002" material="body_white"/>
172
+
173
+ <!-- ๐Ÿ‘Ÿ ์ž…์ฒดํ˜• 2๋‹จ ํˆฌํ†ค ์ปฌ๋Ÿฌ ์Šˆ์ฆˆ (์˜ค๋ Œ์ง€ U-๋ธŒ๋ž˜ํ‚ท + ๋ฏผํŠธ/์‹œ์•ˆ ๋ผ์šด๋“œ ๋Ÿฌ๋ฒ„์†”) -->
174
+ <body name="left_foot" pos="0 0.030 -0.042">
175
+ <joint name="left_ankle_pitch" type="hinge" axis="1 0 0" range="-1.5 1.5" pos="0 0 0"/>
176
+
177
+ <!-- 1. ๋ฐœ๋ชฉ ํ”ผ๋ฒ— ๋ชจํ„ฐ/๋ฒ ์–ด๋ง ๊ณจ๋“œ ๋””์Šคํฌ (์‚ฌ์ง„ ์† ์›ํ˜• ์ถ•) -->
178
+ <geom name="left_ankle_hub" type="cylinder" size="0.008 0.015" pos="0 0 0" quat="0.707 0 0.707 0" mass="0.003" material="spinner_gold"/>
179
+
180
+ <!-- 2. ์ƒ๋‹จ ์˜ค๋ Œ์ง€/์˜๋กœ์šฐ U์žํ˜• ๋ฐœ๋ชฉ ๋ธŒ๋ž˜ํ‚ท (์‚ฌ์ง„ 1, 2, 3 U์ž ์ปต) -->
181
+ <geom name="left_ankle_bracket" type="capsule" fromto="0 -0.004 0.006 0 0.010 -0.004" size="0.011" mass="0.008" material="body_accent"/>
182
+ <geom name="left_shoe_top" type="box" size="0.018 0.026 0.004" pos="0 -0.002 -0.004" mass="0.01" material="body_accent"/>
183
+
184
+ <!-- 3. ํ•˜๋‹จ ๋ฏผํŠธ/์‹œ์•ˆ ๋ผ์šด๋“œ ๋กค๋Ÿฌ ๋Ÿฌ๋ฒ„์†” (๋’ค๊ฟˆ์น˜๋ฅผ 2.8cm๋กœ ํ™•์žฅํ•˜์—ฌ ํ›„์ง„ ๋ณดํ–‰ ์™„๋ฒฝ ์ง€์ง€) -->
185
+ <geom name="left_sole_heel" type="capsule" fromto="-0.014 -0.028 -0.008 0.014 -0.028 -0.008" size="0.006" mass="0.006" friction="3.0 0.1 0.1" material="duct_rim_cyan"/>
186
+ <geom name="left_sole_toe" type="capsule" fromto="-0.014 0.022 -0.008 0.014 0.022 -0.008" size="0.006" mass="0.005" friction="3.0 0.1 0.1" material="duct_rim_cyan"/>
187
+ <geom name="left_sole_base" type="box" size="0.017 0.024 0.004" pos="0 -0.003 -0.008" mass="0.01" friction="3.0 0.1 0.1" material="duct_rim_cyan"/>
188
+ </body>
189
+ </body>
190
+ </body>
191
+
192
+ <!-- ==================== ๐Ÿฆต ์šฐ์ธก 2์กฑ ๋ณดํ–‰ ๋‹ค๋ฆฌ (๋””์ฆˆ๋‹ˆ/BDX ์Šคํƒ€์ผ ์—ญ๊ด€์ ˆ + ํ™”์ดํŠธ ์•„๋จธ + ํˆฌํ†ค ์Šˆ์ฆˆ) ==================== -->
193
+ <body name="right_hip" pos="-0.032 0.010 -0.035">
194
+ <joint name="right_hip_pitch" type="hinge" axis="1 0 0" range="-1.8 1.8" pos="0 0 0"/>
195
+ <!-- ๋ฉ”์ธ ํ—ˆ๋ฒ…์ง€ ํ”„๋ ˆ์ž„ + ์™ธ์ธก ํ™”์ดํŠธ ์•„๋จธ ์ปค๋ฒ„ (์‚ฌ์ง„ 1, 2, 3) -->
196
+ <geom name="right_thigh" type="capsule" fromto="0 0 0 0 -0.030 -0.030" size="0.008" mass="0.008" material="leg_metal"/>
197
+ <geom name="right_thigh_armor" type="capsule" fromto="-0.010 0 0 -0.010 -0.030 -0.030" size="0.011" mass="0.004" material="body_white"/>
198
+
199
+ <body name="right_knee" pos="0 -0.030 -0.030">
200
+ <joint name="right_knee_pitch" type="hinge" axis="1 0 0" range="-2.2 2.2" pos="0 0 0"/>
201
+ <geom name="right_knee_hub" type="cylinder" size="0.008 0.012" pos="0 0 0" quat="0.707 0 0.707 0" mass="0.002" material="spinner_gold"/>
202
+ <geom name="right_shin" type="capsule" fromto="0 0 0 0 0.030 -0.042" size="0.008" mass="0.008" material="leg_metal"/>
203
+ <geom name="right_shin_brace" type="capsule" fromto="-0.008 0.005 -0.005 -0.008 0.025 -0.035" size="0.004" mass="0.002" material="body_white"/>
204
+
205
+ <!-- ๐Ÿ‘Ÿ ์ž…์ฒดํ˜• 2๋‹จ ํˆฌํ†ค ์ปฌ๋Ÿฌ ์Šˆ์ฆˆ (์˜ค๋ Œ์ง€ U-๋ธŒ๋ž˜ํ‚ท + ๋ฏผํŠธ/์‹œ์•ˆ ๋ผ์šด๋“œ ๋Ÿฌ๋ฒ„์†”) -->
206
+ <body name="right_foot" pos="0 0.030 -0.042">
207
+ <joint name="right_ankle_pitch" type="hinge" axis="1 0 0" range="-1.5 1.5" pos="0 0 0"/>
208
+
209
+ <!-- 1. ๋ฐœ๋ชฉ ํ”ผ๋ฒ— ๋ชจํ„ฐ/๋ฒ ์–ด๋ง ๊ณจ๋“œ ๋””์Šคํฌ (์‚ฌ์ง„ ์† ์›ํ˜• ์ถ•) -->
210
+ <geom name="right_ankle_hub" type="cylinder" size="0.008 0.015" pos="0 0 0" quat="0.707 0 0.707 0" mass="0.003" material="spinner_gold"/>
211
+
212
+ <!-- 2. ์ƒ๋‹จ ์˜ค๋ Œ์ง€/์˜๋กœ์šฐ U์žํ˜• ๋ฐœ๋ชฉ ๋ธŒ๋ž˜ํ‚ท (์‚ฌ์ง„ 1, 2, 3 U์ž ์ปต) -->
213
+ <geom name="right_ankle_bracket" type="capsule" fromto="0 -0.004 0.006 0 0.010 -0.004" size="0.011" mass="0.008" material="body_accent"/>
214
+ <geom name="right_shoe_top" type="box" size="0.018 0.026 0.004" pos="0 -0.002 -0.004" mass="0.01" material="body_accent"/>
215
+
216
+ <!-- 3. ํ•˜๋‹จ ๋ฏผํŠธ/์‹œ์•ˆ ๋ผ์šด๋“œ ๋กค๋Ÿฌ ๋Ÿฌ๋ฒ„์†” (๋’ค๊ฟˆ์น˜๋ฅผ 2.8cm๋กœ ํ™•์žฅํ•˜์—ฌ ํ›„์ง„ ๋ณดํ–‰ ์™„๋ฒฝ ์ง€์ง€) -->
217
+ <geom name="right_sole_heel" type="capsule" fromto="-0.014 -0.028 -0.008 0.014 -0.028 -0.008" size="0.006" mass="0.006" friction="3.0 0.1 0.1" material="duct_rim_cyan"/>
218
+ <geom name="right_sole_toe" type="capsule" fromto="-0.014 0.022 -0.008 0.014 0.022 -0.008" size="0.006" mass="0.005" friction="3.0 0.1 0.1" material="duct_rim_cyan"/>
219
+ <geom name="right_sole_base" type="box" size="0.017 0.024 0.004" pos="0 -0.003 -0.008" mass="0.01" friction="3.0 0.1 0.1" material="duct_rim_cyan"/>
220
+ </body>
221
+ </body>
222
+ </body>
223
+
224
+ </body>
225
+ </worldbody>
226
+
227
+ <!-- ์•ก์ถ”์—์ดํ„ฐ -->
228
+ <actuator>
229
+ <position name="act_l_hip" joint="left_hip_pitch" ctrlrange="-1.8 1.8" kp="500" kv="30"/>
230
+ <position name="act_l_knee" joint="left_knee_pitch" ctrlrange="-2.2 2.2" kp="500" kv="30"/>
231
+ <position name="act_l_ankle" joint="left_ankle_pitch" ctrlrange="-1.5 1.5" kp="500" kv="30"/>
232
+
233
+ <position name="act_r_hip" joint="right_hip_pitch" ctrlrange="-1.8 1.8" kp="500" kv="30"/>
234
+ <position name="act_r_knee" joint="right_knee_pitch" ctrlrange="-2.2 2.2" kp="500" kv="30"/>
235
+ <position name="act_r_ankle" joint="right_ankle_pitch" ctrlrange="-1.5 1.5" kp="500" kv="30"/>
236
+
237
+ <!-- ๋‚ ๊ฐœ ํšŒ์ „์ถ•: Z์ถ• ์ˆ˜ํ‰ ํšŒ์ „ -->
238
+ <position name="act_l_fold" joint="left_wing_fold" ctrlrange="-0.2 1.8" kp="150" kv="10"/>
239
+ <position name="act_r_fold" joint="right_wing_fold" ctrlrange="-1.8 0.2" kp="150" kv="10"/>
240
+
241
+ <!-- ํ‹ธํŠธ ๋กœํ„ฐ ์„œ๋ณด: ์„ธ๋กœ ์ ‘ํž˜(1.57rad) ~ ์ˆ˜ํ‰ ์ „๊ฐœ(0rad) -->
242
+ <position name="act_l_tilt" joint="left_tilt" ctrlrange="-0.5 2.0" kp="180" kv="12"/>
243
+ <position name="act_r_tilt" joint="right_tilt" ctrlrange="-0.5 2.0" kp="180" kv="12"/>
244
+
245
+ <!-- ๐Ÿฆ ๋จธ๋ฆฌ/๋ชฉ ํ”ผ์น˜ ์„œ๋ณด: ๋ชธํ†ต/๋‚ ๊ฐœ๋Š” ์ˆ˜ํ‰ ์œ ์ง€, ๋จธ๋ฆฌ(๋‘ ๋ˆˆ+๋ถ€๋ฆฌ)๊ฐ€ ์•„๋ž˜๋กœ 55ยฐ ์™ ์ˆ™์—ฌ์ง! -->
246
+ <position name="act_neck" joint="neck_pitch" ctrlrange="-1.4 0.3" kp="150" kv="10"/>
247
+ </actuator>
248
+ </mujoco>
robot_bird/recorder.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import datetime
5
+ import mujoco
6
+ import numpy as np
7
+
8
+ # UTF-8 ์ž…์ถœ๋ ฅ ๋ณด์žฅ
9
+ if sys.platform == "win32":
10
+ try:
11
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
12
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
13
+ except Exception:
14
+ pass
15
+
16
+ # ๋น„๋””์˜ค ์ธ์ฝ”๋” ์•ˆ์ „ ์ž„ํฌํŠธ (imageio ์šฐ์„ , cv2 ๋Œ€์ฒด)
17
+ try:
18
+ import imageio
19
+ HAS_IMAGEIO = True
20
+ except ImportError:
21
+ imageio = None
22
+ HAS_IMAGEIO = False
23
+
24
+ try:
25
+ import cv2
26
+ HAS_OPENCV = True
27
+ except ImportError:
28
+ cv2 = None
29
+ HAS_OPENCV = False
30
+
31
+ class ScreenRecorder:
32
+ """
33
+ ๋ฐ˜๋ ค ๋กœ๋ด‡์ƒˆ(Bipedal VTOL Robot Bird) ์‹ค์‹œ๊ฐ„ ํ‘œ์ค€ H.264 MP4 ๋น„๋””์˜ค ๋…นํ™”๊ธฐ
34
+ - N ๋˜๋Š” X ํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ์‹ค์‹œ๊ฐ„ ๋…นํ™” ์‹œ์ž‘ ๋ฐ ์ค‘์ง€ (Toggle)
35
+ - ํ‘œ์ค€ H.264 (libx264, yuv420p) ์ ์šฉ์œผ๋กœ ์œˆ๋„์šฐ ๋ฏธ๋””์–ด ํ”Œ๋ ˆ์ด์–ด, ๋งฅ, ์Šค๋งˆํŠธํฐ 100% ์ฆ‰์‹œ ์žฌ์ƒ
36
+ - 3์ธ์นญ ์Šค๋งˆํŠธ ๋ทฐ ๋ฐ C ํ‚ค๋กœ ์ „ํ™˜๋œ 1์ธ์นญ ๋ˆˆ ์นด๋ฉ”๋ผ ๋ทฐ ์™„๋ฒฝ ๋…นํ™”
37
+ - recordings/ ํด๋”์— ๋‚ ์งœ/์‹œ๊ฐ„๋ณ„ MP4 ์ž๋™ ์ €์žฅ
38
+ """
39
+ def __init__(self, model, data, width=640, height=480, fps=30):
40
+ self.model = model
41
+ self.data = data
42
+ self.width = width
43
+ self.height = height
44
+ self.fps = fps
45
+ self.renderer = mujoco.Renderer(model, height, width)
46
+ self.orbit_cam = mujoco.MjvCamera()
47
+ self.orbit_cam.azimuth = 145
48
+ self.orbit_cam.elevation = -15
49
+ self.orbit_cam.distance = 1.6
50
+
51
+ self.is_recording = False
52
+ self.writer_type = None # 'imageio' or 'cv2'
53
+ self.video_writer = None
54
+ self.output_filepath = ""
55
+ self.frame_count = 0
56
+ self.start_time = 0.0
57
+ self.last_capture_time = 0.0
58
+ self.frame_interval = 1.0 / self.fps
59
+
60
+ self.recordings_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "recordings")
61
+ os.makedirs(self.recordings_dir, exist_ok=True)
62
+
63
+ def toggle(self, cam_mode=0):
64
+ if not self.is_recording:
65
+ self.start(cam_mode)
66
+ else:
67
+ self.stop()
68
+
69
+ def start(self, cam_mode=0):
70
+ if not HAS_IMAGEIO and not HAS_OPENCV:
71
+ print("\n" + "=" * 70)
72
+ print(" โš ๏ธ [๋…นํ™” ๋ชจ๋“ˆ ์•ˆ๋‚ด] ๋น„๋””์˜ค ๋…นํ™”๋ฅผ ์œ„ํ•ด imageio ๋˜๋Š” opencv-python ํŒจํ‚ค์ง€๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
73
+ print(" ๐Ÿ’ก ์„ค์น˜ ๋ช…๋ น์–ด: pip install imageio[ffmpeg] opencv-python")
74
+ print("=" * 70 + "\n")
75
+ return
76
+
77
+ now_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
78
+ cam_tag = "3rd" if cam_mode == 0 else ("fpv" if cam_mode == 1 else ("lefteye" if cam_mode == 2 else "righteye"))
79
+ filename = f"robot_bird_{cam_tag}_{now_str}.mp4"
80
+ self.output_filepath = os.path.join(self.recordings_dir, filename)
81
+
82
+ # ๐ŸŒŸ ํ‘œ์ค€ H.264 (libx264, yuv420p) ์ธ์ฝ”๋”๋กœ ์œˆ๋„์šฐ ๋ฏธ๋””์–ด ํ”Œ๋ ˆ์ด์–ด 100% ํ˜ธํ™˜ ๋ณด์žฅ
83
+ if HAS_IMAGEIO:
84
+ try:
85
+ self.video_writer = imageio.get_writer(
86
+ self.output_filepath,
87
+ fps=self.fps,
88
+ codec='libx264',
89
+ pixelformat='yuv420p',
90
+ format='FFMPEG'
91
+ )
92
+ self.writer_type = 'imageio'
93
+ except Exception:
94
+ self.writer_type = None
95
+
96
+ if self.video_writer is None and HAS_OPENCV:
97
+ try:
98
+ # FourCC H264 / mp4v fallback
99
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
100
+ self.video_writer = cv2.VideoWriter(self.output_filepath, fourcc, self.fps, (self.width, self.height))
101
+ self.writer_type = 'cv2'
102
+ except Exception:
103
+ self.writer_type = None
104
+
105
+ if self.video_writer is None:
106
+ print(f"\n[์˜ค๋ฅ˜] ๋น„๋””์˜ค ์ธ์ฝ”๋”๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
107
+ return
108
+
109
+ self.is_recording = True
110
+ self.frame_count = 0
111
+ self.start_time = time.time()
112
+ self.last_capture_time = 0.0
113
+
114
+ print(f"\n" + "=" * 70)
115
+ print(f" ๐Ÿ”ด [REC ๋…นํ™” ์‹œ์ž‘!] ํ‘œ์ค€ H.264 ๊ณ ํ™”์งˆ ๋น„๋””์˜ค ๋…นํ™” ์ค‘... (30 FPS)")
116
+ print(f" ๐Ÿ“ ์ €์žฅ ๋Œ€์ƒ: {self.output_filepath}")
117
+ print(f" ๐Ÿ’ก ๋…นํ™”๋ฅผ ๋๋‚ด๋ ค๋ฉด ๋‹ค์‹œ 'X' ํ‚ค๋ฅผ ๋ˆ„๋ฅด์„ธ์š”!")
118
+ print("=" * 70 + "\n")
119
+
120
+ def capture_frame(self, cam_mode=0):
121
+ if not self.is_recording or self.video_writer is None:
122
+ return
123
+
124
+ now = time.time()
125
+ if now - self.last_capture_time < self.frame_interval:
126
+ return
127
+
128
+ self.last_capture_time = now
129
+
130
+ try:
131
+ if cam_mode == 0:
132
+ self.orbit_cam.lookat[0] = self.data.qpos[0]
133
+ self.orbit_cam.lookat[1] = self.data.qpos[1]
134
+ self.orbit_cam.lookat[2] = self.data.qpos[2]
135
+ self.renderer.update_scene(self.data, camera=self.orbit_cam)
136
+ elif cam_mode == 1:
137
+ self.renderer.update_scene(self.data, camera="head_fpv_cam")
138
+ elif cam_mode == 2:
139
+ self.renderer.update_scene(self.data, camera="left_eye_cam")
140
+ elif cam_mode == 3:
141
+ self.renderer.update_scene(self.data, camera="right_eye_cam")
142
+ else:
143
+ self.renderer.update_scene(self.data)
144
+
145
+ rgb_frame = self.renderer.render()
146
+
147
+ if self.writer_type == 'imageio':
148
+ self.video_writer.append_data(rgb_frame)
149
+ elif self.writer_type == 'cv2':
150
+ bgr_frame = cv2.cvtColor(rgb_frame, cv2.COLOR_RGB2BGR)
151
+ self.video_writer.write(bgr_frame)
152
+
153
+ self.frame_count += 1
154
+ except Exception as e:
155
+ pass
156
+
157
+ def stop(self):
158
+ if not self.is_recording:
159
+ return
160
+
161
+ self.is_recording = False
162
+ duration = time.time() - self.start_time
163
+
164
+ try:
165
+ if self.writer_type == 'imageio' and self.video_writer is not None:
166
+ self.video_writer.close()
167
+ elif self.writer_type == 'cv2' and self.video_writer is not None:
168
+ self.video_writer.release()
169
+ except Exception:
170
+ pass
171
+
172
+ self.video_writer = None
173
+ self.writer_type = None
174
+
175
+ file_size_mb = 0.0
176
+ if os.path.exists(self.output_filepath):
177
+ file_size_mb = os.path.getsize(self.output_filepath) / (1024 * 1024)
178
+
179
+ print(f"\n" + "=" * 70)
180
+ print(f" ๐Ÿ’พ [๋…นํ™” ์™„๋ฃŒ ๋ฐ ์ €์žฅ!] ์˜์ƒ์ด ๋‹ค์šด๋กœ๋“œ(์ €์žฅ)๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
181
+ print(f" ๐Ÿ“‚ ํŒŒ์ผ ์œ„์น˜: {self.output_filepath}")
182
+ print(f" โฑ๏ธ ๋…นํ™” ์‹œ๊ฐ„: {duration:.1f}์ดˆ ({self.frame_count} ํ”„๋ ˆ์ž„, {file_size_mb:.2f} MB)")
183
+ print("=" * 70 + "\n")
run_ai_robot_bird.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import math
5
+ import mujoco
6
+ import mujoco.viewer
7
+ import numpy as np
8
+
9
+ # UTF-8 ์ž…์ถœ๋ ฅ ๋ณด์žฅ
10
+ if sys.platform == "win32":
11
+ try:
12
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
13
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
14
+ except Exception:
15
+ pass
16
+
17
+ from robot_bird.controller import RobotBirdFSM
18
+ from robot_bird.ai_brain import RobotBirdAIBrain
19
+ from robot_bird.recorder import ScreenRecorder
20
+
21
+ class ShowcaseDirector:
22
+ """
23
+ ๐Ÿฆ… ๋Œ€ํ‘œ๋‹˜์˜ ์—ฐ์ถœ ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์˜ํ™”์ฒ˜๋Ÿผ ์™„๋ฒฝํ•˜๊ฒŒ ์ง€ํœ˜ํ•˜๋Š” ์‹œ๋„ค๋งˆํ‹ฑ ๋””๋ ‰ํ„ฐ
24
+
25
+ [์‹œ๋‚˜๋ฆฌ์˜ค ์ˆœ์„œ - ๋ฆฌ์–ผ 1์ธ์นญ ๋ˆˆ ์‹œ์„  ๋น„ํ–‰ & ๋ฐ”๋‘‘ํŒ ์›์  ๋ณต๊ท€]
26
+ 1. ๐Ÿšถ ์•ž์œผ๋กœ ๋‹น๋‹นํ•˜๊ฒŒ ๊ฑท๊ธฐ (Forward Walk)
27
+ 2. ๐Ÿšถ ์กฐ์‹ฌ์กฐ์‹ฌ ๋’ค๋กœ ๊ฑท๊ธฐ (Backward Walk)
28
+ 3. ๐Ÿ’ฅ ๊ฝˆ๋‹น! ์•ž์œผ๋กœ ๋„˜์–ด์ง€๊ธฐ (Knockdown)
29
+ 4. ๐Ÿ”„ ์˜ค๋š์ด ์ง€๋Šฅ ๋ฐœ๋™! ์Šค์Šค๋กœ ๋ฒŒ๋–ก ๊ธฐ๋ฆฝ (Self-Righting)
30
+ 5. ๐ŸŒพ ๋ฐ”๋‹ฅ ๋ชจ์ด ์ฝ•! ์ฝ•! ์ชผ์•„๋จน๊ธฐ (Pecking)
31
+ 6. ๐Ÿฆฟ ๋‹ค๋ฆฌ๋ฅผ ์™ ์ ‘๊ณ  ์ชผ๊ทธ๋ ค ์•‰์•˜๋‹ค๊ฐ€ ์ผ์–ด์„œ๊ธฐ (Crouch & Stand)
32
+
33
+ [๐Ÿชฝ ๊ทน๊ฐ•์˜ ๋ฆฌ์–ผ ์‹œ๋„ค๋งˆํ‹ฑ ๋น„ํ–‰ & 1์ธ์นญ ๋ˆˆ ์‹œ์„ ]
34
+ 7. ๐Ÿชฝ ๋‚ ๊ฐœ 180ยฐ ์ˆ˜ํ‰ ์ „๊ฐœ + 3์—ฝ ํ”„๋กœํŽ ๋Ÿฌ ๊ฐ€์† โž” 1.0m ์ˆ˜์ง ์ด๋ฅ™!
35
+ 8. ๐Ÿš€ [3์ธ์นญ ์ „์ฒด ๋ทฐ] ๋‚ ๊ฐœ๋ฅผ ํŽด๊ณ  ์•ž์œผ๋กœ ์ถœ๋ฐœํ•˜๋Š” ๋ฉ‹์ง„ ์ „์ฒด ๋ชจ์Šต
36
+ 9. ๐Ÿ‘๏ธ [1์ธ์นญ ์ง์ ‘ ๋ˆˆ ์‹œ์  1 - ์ „๋ฐฉ ๋น„ํ–‰] ๋กœ๋ด‡์ƒˆ ๋ˆˆ์œผ๋กœ ์ง์ ‘ ์•ž์„ ๋ฐ”๋ผ๋ณด๋ฉฐ ๋ฐ”๋žŒ์„ ๊ฐ€๋ฅด๊ณ  ์Š~ ๋‚ ์•„๊ฐ€๋Š” ๋ฆฌ์–ผํ•œ ๋น„ํ–‰ ํ™”๋ฉด!
37
+ 10. ๐Ÿ‘‡ [1์ธ์นญ ์ง์ ‘ ๋ˆˆ ์‹œ์  2 - ํ•˜๋ฐฉ 55ยฐ ๊ด€์ธก] ๋‚ ์•„๊ฐ€๋ฉด์„œ ๋จธ๋ฆฌ๋ฅผ 55ยฐ ์•„๋ž˜๋กœ ํ‘น ์ˆ™์—ฌ ๋ฐ”๋‹ฅ ์ฒด์ปค๋ณด๋“œ๋ฅผ ์ง์ ‘ ๋‚ด๋ ค๋‹ค๋ณด๋Š” ๋ฆฌ์–ผํ•œ ์Šค์บ” ํ™”๋ฉด!
38
+ 11. ๐Ÿ”„ [3์ธ์นญ ์ „์ฒด ๋ทฐ] ๊ณต์ค‘์—์„œ 180ยฐ ์œ ํ„ดํ•˜์—ฌ ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๋ฐ”๋‘‘ํŒ ์ •์ค‘์•™(์›์ )์œผ๋กœ ๋Œ์•„์˜ค๋Š” ์ „์ฒด ๋ชจ์Šต!
39
+ 12. ๐Ÿ•Š๏ธ [3์ธ์นญ ์ •์ง€ ํ˜ธ๋ฒ„๋ง] ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๋ฐ”๋กœ ๊ทธ ์ž๋ฆฌ ์ƒ๊ณต์—์„œ ๊ฐ€๋งŒํžˆ ๋‚ ๊ณ  ์žˆ๋Š” ๋ชจ์Šต (Hovering)
40
+ 13. ๐Ÿ›ฌ [์‚ฌ๋ฟ ํ•˜๊ฐ• ์ฐฉ์ง€] ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๊ทธ ์ž๋ฆฌ์— ์ •ํ™•ํžˆ ์ˆ˜์ง ์ฐฉ์ง€ & ๋‚ ๊ฐœ ๋“ฑ ๋’ค๋กœ ์ ‘๊ธฐ!
41
+ โž” (์ดํ›„ ๋ฌดํ•œ ๋ฐ˜๋ณต!)
42
+ """
43
+ def __init__(self, controller, model, data):
44
+ self.c = controller
45
+ self.m = model
46
+ self.d = data
47
+ self.timeline = 0.0
48
+ self.step_idx = -1
49
+ self.home_x = 0.0
50
+ self.home_y = 0.0
51
+
52
+ def update(self, dt):
53
+ self.timeline += dt
54
+ t = self.timeline
55
+
56
+ # 1๋‹จ๊ณ„: ์•ž์œผ๋กœ ๊ฑท๊ธฐ (0.0s ~ 4.0s)
57
+ if t < 4.0:
58
+ if self.step_idx != 1:
59
+ self.step_idx = 1
60
+ self.home_x = self.d.qpos[0]
61
+ self.home_y = self.d.qpos[1]
62
+ self.c.cam_mode = 0 # 3์ธ์นญ ์ „์ฒด ๋ทฐ
63
+ self.c.look_front()
64
+ self.c.wings_deployed = False
65
+ self.c.set_state(self.c.STATE_WALK)
66
+ print("\n[๐ŸŽฌ 1๋‹จ๊ณ„] ๐Ÿšถ [์ด์กฑ๋ณดํ–‰] ๋‹ค๋ฆฌ๋ฅผ ์„ฑํผ์„ฑํผ ๋ฒˆ๊ฐˆ์•„ ๋”›์œผ๋ฉฐ ์•ž์œผ๋กœ ๊ฑท๊ธฐ")
67
+ self.c.walk_speed = 0.8
68
+ self.c.walk_turn = 0.0
69
+
70
+ # 2๋‹จ๊ณ„: ๋’ค๋กœ ๊ฑท๊ธฐ (4.0s ~ 7.0s)
71
+ elif t < 7.0:
72
+ if self.step_idx != 2:
73
+ self.step_idx = 2
74
+ self.c.set_state(self.c.STATE_WALK)
75
+ print("\n[๐ŸŽฌ 2๋‹จ๊ณ„] ๐Ÿšถ [์ด์กฑ๋ณดํ–‰] ์กฐ์‹ฌ์กฐ์‹ฌ ๋’ค๋กœ ๊ฑท๊ธฐ (์›์  ๊ทผ์ฒ˜ ์œ ์ง€)")
76
+ self.c.walk_speed = -0.8
77
+ self.c.walk_turn = 0.0
78
+
79
+ # 3๋‹จ๊ณ„: ๋„˜์–ด์ง€๊ธฐ (7.0s ~ 8.5s)
80
+ elif t < 8.5:
81
+ if self.step_idx != 3:
82
+ self.step_idx = 3
83
+ self.c.walk_speed = 0.0
84
+ self.d.qvel[0] = 0.35
85
+ self.d.qvel[3] = 4.5
86
+ self.d.qvel[4] = 2.0
87
+ self.c.set_state(self.c.STATE_KNOCKDOWN)
88
+ print("\n[๐ŸŽฌ 3๋‹จ๊ณ„] ๐Ÿ’ฅ ๊ฝˆ๋‹น! ๊ท ํ˜•์„ ์žƒ๊ณ  ์•ž์œผ๋กœ ๋„˜์–ด์ง€๊ธฐ (Knockdown)")
89
+
90
+ # 4๋‹จ๊ณ„: ์˜ค๋š์ด์ฒ˜๋Ÿผ ๋‹ค์‹œ ์ผ์–ด์„œ๊ธฐ (8.5s ~ 11.0s)
91
+ elif t < 11.0:
92
+ if self.step_idx != 4:
93
+ self.step_idx = 4
94
+ self.c.set_state(self.c.STATE_RECOVER)
95
+ print("\n[๐ŸŽฌ 4๋‹จ๊ณ„] ๐Ÿ”„ ์˜ค๋š์ด ์ง€๋Šฅ ๋ฐœ๋™! ์Šค์Šค๋กœ ๋ฒŒ๋–ก ์ผ์–ด์„œ๊ธฐ (Self-Righting)")
96
+
97
+ # 5๋‹จ๊ณ„: ๋ชจ์ด ์ชผ๊ธฐ (11.0s ~ 14.5s)
98
+ elif t < 14.5:
99
+ if self.step_idx != 5:
100
+ self.step_idx = 5
101
+ self.c.set_state(self.c.STATE_GROUND_PICK)
102
+ print("\n[๐ŸŽฌ 5๋‹จ๊ณ„] ๐ŸŒพ ๋ฐ”๋‹ฅ ๋ชจ์ด ์ฝ•! ์ฝ•! ์ชผ์•„๋จน๊ธฐ (Pecking)")
103
+
104
+ # 6๋‹จ๊ณ„: ์•‰์•˜๋‹ค ์ผ์–ด์„œ๊ธฐ (14.5s ~ 18.5s)
105
+ elif t < 18.5:
106
+ if self.step_idx != 6:
107
+ self.step_idx = 6
108
+ self.c.set_state(self.c.STATE_SIT)
109
+ print("\n[๐ŸŽฌ 6๋‹จ๊ณ„] ๐Ÿฆฟ ๋‹ค๋ฆฌ๋ฅผ ์™ ์ ‘๊ณ  ์ชผ๊ทธ๋ ค ์•‰์•˜๋‹ค๊ฐ€ ์ผ์–ด์„œ๊ธฐ (Crouch & Stand)")
110
+ if t > 16.8 and self.c.state == self.c.STATE_SIT:
111
+ self.c.set_state(self.c.STATE_STAND)
112
+
113
+ # 7๋‹จ๊ณ„: ๋‚ ๊ฐœ 180ยฐ ์ „๊ฐœ & ๊ณต์ค‘ VTOL ์ˆ˜์ง ์ด๋ฅ™ (18.5s ~ 22.5s)
114
+ elif t < 22.5:
115
+ if self.step_idx != 7:
116
+ self.step_idx = 7
117
+ self.home_x = self.d.qpos[0]
118
+ self.home_y = self.d.qpos[1]
119
+ self.c.target_yaw = 0.0
120
+ self.c.start_flight_sequence()
121
+ print("\n[๐ŸŽฌ 7๋‹จ๊ณ„] ๐Ÿชฝ ๋“ฑ ๋’ค ๋‚ ๊ฐœ 180ยฐ ์ „๊ฐœ + ํ”„๋กœํŽ ๋Ÿฌ ๊ฐ€์† โž” 1.0m ์ˆ˜์ง ์ด๋ฅ™!")
122
+
123
+ # 8๋‹จ๊ณ„: ๐Ÿš€ [3์ธ์นญ ๋ทฐ] ์•ž์œผ๋กœ ํž˜์ฐจ๊ฒŒ ์ถœ๋ฐœํ•˜๋Š” ์ „์ฒด ๋ชจ์Šต (22.5s ~ 25.5s)
124
+ elif t < 25.5:
125
+ if self.step_idx != 8:
126
+ self.step_idx = 8
127
+ self.c.look_front()
128
+ self.c.cam_mode = 0 # 3์ธ์นญ ์ „์ฒด ๋ทฐ
129
+ print("\n[๐ŸŽฌ 8๋‹จ๊ณ„] ๐Ÿš€ [3์ธ์นญ ์ „์ฒด ๋ทฐ] ๋‚ ๊ฐœ๋ฅผ ํŽด๊ณ  ์•ž์œผ๋กœ ํž˜์ฐจ๊ฒŒ ์ถœ๋ฐœํ•˜๋Š” ์ „์ฒด ๋ชจ์Šต")
130
+ self.c.target_y = self.home_y + 0.45
131
+
132
+ # 9๋‹จ๊ณ„: ๐Ÿ‘๏ธ [1์ธ์นญ ์ง์ ‘ ๋ˆˆ ์‹œ์  - ์ „๋ฐฉ ๋น„ํ–‰] ๋กœ๋ด‡์ƒˆ ๋ˆˆ์œผ๋กœ ์ง์ ‘ ์•ž์„ ๋ณด๋ฉฐ ์Š~ ๋‚ ์•„๊ฐ€๋Š” ๋ฆฌ์–ผ ์†๋„๊ฐ! (25.5s ~ 29.5s)
133
+ elif t < 29.5:
134
+ if self.step_idx != 9:
135
+ self.step_idx = 9
136
+ self.c.look_front()
137
+ self.c.cam_mode = 1 # ๐Ÿ“ท [1์ธ์นญ FPV ์ง์ ‘ ๋ˆˆ ์‹œ์ ]
138
+ print("\n" + "=" * 75)
139
+ print(" ๐Ÿ‘๏ธ [1์ธ์นญ ๋ˆˆ ์‹œ์„  ํ™”๋ฉด - ์ „๋ฐฉ ๋น„ํ–‰] ๋กœ๋ด‡์ƒˆ ๋ˆˆ์œผ๋กœ ์ง์ ‘ ์•ž์„ ๋ฐ”๋ผ๋ณด๋ฉฐ ์Š~ ๋‚ ์•„๊ฐ‘๋‹ˆ๋‹ค!")
140
+ print("=" * 75)
141
+ self.c.target_y = self.home_y + 0.95
142
+
143
+ # 10๋‹จ๊ณ„: ๐Ÿ•Š๏ธ [3์ธ์นญ ์ „์ฒด ๋ทฐ - ๋ˆˆ์•Œ/๋จธ๋ฆฌ๋ฅผ ์•„๋ž˜๋กœ ์ˆ™์ด๋Š” ๋ชจ์Šต] ๋น„ํ–‰ ์ค‘ ๋จธ๋ฆฌ๋ฅผ ์•„๋ž˜๋กœ ํ‘น ์ˆ™์ด๋Š” ์™ธํ˜• ์—ฐ์ถœ! (29.5s ~ 33.0s)
144
+ elif t < 33.0:
145
+ if self.step_idx != 10:
146
+ self.step_idx = 10
147
+ self.c.cam_mode = 0 # ๐ŸŒ [3์ธ์นญ ์ „์ฒด ๋ทฐ๋กœ ์ „ํ™˜!]
148
+ self.c.target_look_down_pitch = 1.05 # ๋จธ๋ฆฌ/๋ˆˆ์•Œ ์•„๋ž˜ 60ยฐ ๊นŠ๊ฒŒ ์ˆ™์ด๊ธฐ
149
+ self.c.look_down = True
150
+ print("\n" + "=" * 75)
151
+ print(" ๐Ÿ•Š๏ธ [3์ธ์นญ ์ „์ฒด ๋ทฐ] ๋น„ํ–‰ ์ค‘์ธ ๋กœ๋ด‡์ƒˆ๊ฐ€ ๋จธ๋ฆฌ(๋‘ ๋ˆˆ)๋ฅผ ์•„๋ž˜๋กœ ํ‘น ์ˆ™์—ฌ ๋ฐ”๋‹ฅ์„ ๋‚ด๋ ค๋‹ค๋ด…๋‹ˆ๋‹ค!")
152
+ print("=" * 75)
153
+ self.c.target_y = self.home_y + 1.15
154
+
155
+ # 11๋‹จ๊ณ„: ๐Ÿ‘‡ [1์ธ์นญ ์ง์ ‘ ๋ˆˆ ์‹œ์  - ํ•˜๋ฐฉ 60ยฐ ๊ด€์ธก ํ™”๋ฉด] ์ˆ™์ธ ๋ˆˆ์œผ๋กœ ๋ฐ”๋‹ฅ ์ฒด์ปค๋ณด๋“œ๋ฅผ ์ง์ ‘ ๋‚ด๋ ค๋‹ค๋ณด๋Š” ๋ฆฌ์–ผ ์‹œ์•ผ! (33.0s ~ 37.5s)
156
+ elif t < 37.5:
157
+ if self.step_idx != 11:
158
+ self.step_idx = 11
159
+ self.c.target_look_down_pitch = 1.05
160
+ self.c.look_down = True
161
+ self.c.cam_mode = 1 # ๐Ÿ“ท [1์ธ์นญ FPV ํ•˜๋ฐฉ ์‹œ์  ์ฆ‰์‹œ ์ปท ์ „ํ™˜!]
162
+ print("\n" + "=" * 75)
163
+ print(" ๐Ÿ‘‡ [1์ธ์นญ ๋ˆˆ ์‹œ์„  ํ™”๋ฉด - ํ•˜๋ฐฉ 60ยฐ ๊ด€์ธก] ๋กœ๋ด‡์ƒˆ์˜ ๋ˆˆ์œผ๋กœ ์ง์ ‘ ๋ฐ”๋‹ฅ ์ฒด์ปค๋ณด๋“œ๋ฅผ ์ƒ…์ƒ…์ด ์Šค์บ”ํ•ฉ๋‹ˆ๋‹ค!")
164
+ print("=" * 75)
165
+ self.c.target_y = self.home_y + 1.25
166
+
167
+ # 12๋‹จ๊ณ„: ๐Ÿ”„ [1์ธ์นญ ์ง์ ‘ ๋ˆˆ ์‹œ์  - 180ยฐ ์„ ํšŒ ์œ ํ„ด] ๋ˆˆ์œผ๋กœ ์„ธ์ƒ์„ ๋‘˜๋Ÿฌ๋ณด๋ฉฐ ์›์  ๋ฐฉํ–ฅ์œผ๋กœ ์œ ํ„ด (37.5s ~ 41.5s)
168
+ elif t < 41.5:
169
+ if self.step_idx != 12:
170
+ self.step_idx = 12
171
+ self.c.look_front() # ๊ณ ๊ฐœ ์ •๋ฉด ๋ณต๊ท€
172
+ self.c.cam_mode = 1 # 1์ธ์นญ ์‹œ์  ์œ ์ง€ํ•˜๋ฉฐ ์œ ํ„ด
173
+ print("\n[๐ŸŽฌ 12๋‹จ๊ณ„] ๐Ÿ”„ [1์ธ์นญ ๋ˆˆ ์‹œ์„  - 180ยฐ ์œ ํ„ด] ๊ณ ๊ฐœ๋ฅผ ๋“ค๊ณ  180ยฐ ์„ ํšŒํ•˜์—ฌ ์ถœ๋ฐœํ–ˆ๋˜ ์›์ ์„ ๋ˆˆ์œผ๋กœ ํฌ์ฐฉ!")
174
+ self.c.target_yaw = math.pi * min(1.0, (t - 37.5) / 3.5)
175
+
176
+ # 13๋‹จ๊ณ„: ๐ŸŒ [3์ธ์นญ ์ „์ฒด ๋ทฐ ๋ณต๊ท€] 180๋„ ํšŒ์ „ํ•˜์—ฌ ์›๋ž˜ ์ž๋ฆฌ(์›์ )๋กœ ๊ท€ํ™˜ํ•˜๋Š” ์ „์ฒด ๋ชจ์Šต (41.5s ~ 46.0s)
177
+ elif t < 46.0:
178
+ if self.step_idx != 13:
179
+ self.step_idx = 13
180
+ self.c.look_front()
181
+ self.c.cam_mode = 0 # 3์ธ์นญ ์ „์ฒด ๋ทฐ ๋ณต๊ท€
182
+ self.c.target_yaw = 0.0 # ๊ธฐ์ฒด ์ •๋ฉด ๋ณต๊ท€
183
+ print("\n[๐ŸŽฌ 13๋‹จ๊ณ„] ๐ŸŒ [3์ธ์นญ ์ „์ฒด ๋ทฐ ๋ณต๊ท€] 180ยฐ ์„ ํšŒ๋ฅผ ๋งˆ์น˜๊ณ  ๋ฐ”๋‘‘ํŒ ์ •์ค‘์•™(์›์ )์œผ๋กœ ์™„๋ฒฝ ๊ท€ํ™˜!")
184
+ self.c.target_x = self.home_x
185
+ self.c.target_y = self.home_y
186
+
187
+ # 14๋‹จ๊ณ„: ๐Ÿ•Š๏ธ [3์ธ์นญ ์ •์ง€ ํ˜ธ๋ฒ„๋ง] ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๊ทธ ์ž๋ฆฌ ์ƒ๊ณต์—์„œ ๊ฐ€๋งŒํžˆ ํ˜ธ๋ฒ„๋ง (46.0s ~ 50.0s)
188
+ elif t < 50.0:
189
+ if self.step_idx != 14:
190
+ self.step_idx = 14
191
+ self.c.target_x = self.home_x
192
+ self.c.target_y = self.home_y
193
+ print("\n[๐ŸŽฌ 14๋‹จ๊ณ„] ๐Ÿ•Š๏ธ [์ œ์ž๋ฆฌ ์ •์ง€ ํ˜ธ๋ฒ„๋ง] ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๋ฐ”๋กœ ๊ทธ ์ž๋ฆฌ ์ƒ๊ณต์—์„œ ํ”ผ๋ฒ— ์•ˆ์ •ํ™” ํ˜ธ๋ฒ„๋ง")
194
+
195
+ # 15๋‹จ๊ณ„: ๐Ÿ›ฌ [์‚ฌ๋ฟ ํ•˜๊ฐ• ์ฐฉ์ง€] ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๊ทธ ์ž๋ฆฌ์— ์ •ํ™•ํžˆ ์ˆ˜์ง ์ฐฉ์ง€ & ๋‚ ๊ฐœ ์ ‘๊ธฐ (50.0s ~ 56.0s)
196
+ elif t < 56.0:
197
+ if self.step_idx != 15:
198
+ self.step_idx = 15
199
+ self.c.set_state(self.c.STATE_LANDING)
200
+ print("\n[๐ŸŽฌ 15๋‹จ๊ณ„] ๐Ÿ›ฌ [์‚ฌ๋ฟ ํ•˜๊ฐ• ์ฐฉ์ง€] ์›๋ž˜ ์ถœ๋ฐœํ–ˆ๋˜ ๊ทธ ์ž๋ฆฌ์— ์‚ฌ๋ฟํžˆ ์ˆ˜์ง ์ฐฉ์ง€ & ๋‚ ๊ฐœ ๋“ฑ ๋’ค๋กœ ์ ‘๊ธฐ")
201
+ if t > 53.5 and self.c.wings_deployed:
202
+ self.c.wings_deployed = False # ๋‚ ๊ฐœ ๋“ฑ ๋’ค๋กœ ์™ ์ ‘๊ธฐ
203
+
204
+ # ์‹œ๋‚˜๋ฆฌ์˜ค ์™„์ฃผ โž” ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฌดํ•œ ๋ฐ˜๋ณต!
205
+ else:
206
+ print("\n" + "โ˜…" * 75)
207
+ print("๐ŸŽ‰ [์Šคํ† ๋ฆฌ ์™„์ฃผ] 3์ธ์นญ ๋จธ๋ฆฌ ์ˆ™์ž„ โž” 1์ธ์นญ ํ•˜๋ฐฉ ์‹œ์„  โž” ์›์  ๋ณต๊ท€ ํ’€ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์™„์ฃผ! ๋‹ค์‹œ ๋ฌดํ•œ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.")
208
+ print("โ˜…" * 75 + "\n")
209
+ self.timeline = 0.0
210
+ self.step_idx = -1
211
+
212
+ def print_ai_banner():
213
+ print("=" * 80)
214
+ print(" ๐ŸŽฌ [๋ฐ˜๋ ค ๋กœ๋ด‡์ƒˆ (OpenBird-artnfull)] ๊ณต์‹ ์‹œ๋„ค๋งˆํ‹ฑ ์Šคํ† ๋ฆฌ๋ณด๋“œ ๋ฐ๋ชจ")
215
+ print("=" * 80)
216
+ print(" ๐ŸŒŸ ๋Œ€ํ‘œ๋‹˜๊ป˜์„œ ์ง์ ‘ ๊ธฐํšํ•˜์‹  13๋‹จ๊ณ„ ํ’€ ์Šคํ† ๋ฆฌ๋ณด๋“œ๊ฐ€ ํŽผ์ณ์ง‘๋‹ˆ๋‹ค:")
217
+ print(" 1. ์•ž์œผ๋กœ ๊ฑท๊ธฐ โž” 2. ๋’ค๋กœ ๊ฑท๊ธฐ โž” 3. ๊ฝˆ๋‹น ๋„˜์–ด์ง€๊ธฐ โž” 4. ์˜ค๋š์ด ๋ฒŒ๋–ก ๊ธฐ๋ฆฝ!")
218
+ print(" 5. ๋ชจ์ด ์ชผ๊ธฐ โž” 6. ์•‰์•˜๋‹ค ์ผ์–ด์„œ๊ธฐ โž” 7. ๋‚ ๊ฐœ 180ยฐ ์ „๊ฐœ & VTOL ์ˆ˜์ง ์ด๋ฅ™")
219
+ print(" 8. ์ „์ง„ ๋น„ํ–‰ โž” 9. ๋ˆˆ ์•„๋ž˜ 55ยฐ ํ–ฅํ•˜๊ธฐ โž” 10. ์ง์ ‘ ์•ž์„ ๋ณด๋Š” 1์ธ์นญ FPV")
220
+ print(" 11. ์ง์ ‘ ์•„๋ž˜๋ฅผ ๋ณด๋Š” 1์ธ์นญ FPV โž” 12. 3์ธ์นญ ์ „์ฒดํ™”๋ฉด ๋ณต๊ท€ โž” 13. ์‚ฌ๋ฟ ์ฐฉ์ง€ & ๋‚ ๊ฐœ ์ ‘๊ธฐ")
221
+ print("=" * 80 + "\n")
222
+
223
+ def main():
224
+ print_ai_banner()
225
+
226
+ root_dir = os.path.dirname(os.path.abspath(__file__))
227
+ model_path = os.path.join(root_dir, "robot_bird", "models", "robot_bird.xml")
228
+
229
+ if not os.path.exists(model_path):
230
+ print(f"[์˜ค๋ฅ˜] ๋ชจ๋ธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {model_path}")
231
+ return
232
+
233
+ with open(model_path, "r", encoding="utf-8") as f:
234
+ xml_string = f.read()
235
+ model = mujoco.MjModel.from_xml_string(xml_string)
236
+ data = mujoco.MjData(model)
237
+
238
+ # ๐ŸŒŸ ๊ด€์ ˆ ์ด๋ฆ„ ๊ธฐ๋ฐ˜์˜ ์•ˆ์ „ํ•˜๊ณ  ์ •ํ™•ํ•œ ์ดˆ๊ธฐ ์ž์„ธ ์„ค์ • (๋ณดํ–‰ ๋ฐ ๋‚ ๊ฐœ ์ž์„ธ 100% ๋ณด์žฅ)
239
+ def set_init_jnt(jnt_name, val):
240
+ adr = model.joint(jnt_name).qposadr[0]
241
+ data.qpos[adr] = val
242
+
243
+ data.qpos[2] = 0.122 # z ๋†’์ด (์ง€๋ฉด ์™„๋ฒฝ ์•ˆ์ฐฉ)
244
+ set_init_jnt("left_wing_fold", 0.0)
245
+ set_init_jnt("left_tilt", 1.57)
246
+ set_init_jnt("right_wing_fold", 0.0)
247
+ set_init_jnt("right_tilt", 1.57)
248
+ set_init_jnt("neck_pitch", 0.0)
249
+ for j in ["left_hip_pitch", "left_knee_pitch", "left_ankle_pitch",
250
+ "right_hip_pitch", "right_knee_pitch", "right_ankle_pitch"]:
251
+ set_init_jnt(j, 0.0)
252
+
253
+ data.ctrl[:] = 0.0
254
+ data.ctrl[model.actuator("act_l_tilt").id] = 1.57
255
+ data.ctrl[model.actuator("act_r_tilt").id] = 1.57
256
+ mujoco.mj_forward(model, data)
257
+
258
+ controller = RobotBirdFSM(model, data)
259
+ controller.cam_mode = 0
260
+ director = ShowcaseDirector(controller, model, data)
261
+ recorder = ScreenRecorder(model, data)
262
+
263
+ def viewer_key_callback(keycode):
264
+ if keycode in [ord('Q'), ord('q'), 256, 27]:
265
+ print("\nAI ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ์•ˆ๋…•ํžˆ ๊ฐ€์„ธ์š”!")
266
+ if recorder.is_recording:
267
+ recorder.stop()
268
+ sys.exit(0)
269
+ elif keycode in [ord('C'), ord('c')]:
270
+ controller.cam_mode = (controller.cam_mode + 1) % 4
271
+ cam_names = [
272
+ "๐Ÿ•Š๏ธ 3์ธ์นญ ์ „์ฒด ๋ทฐ (์™ธ๋ถ€์—์„œ ๋กœ๋ด‡์ƒˆ ๋ชจ์Šต ๊ด€์ฐฐ)",
273
+ "๐Ÿ“ท [1์ธ์นญ FPV] ๋กœ๋ด‡์ƒˆ ๋จธ๋ฆฌ ์ •์ค‘์•™ ์‹œ์ ",
274
+ "๐Ÿ‘๏ธ [์ขŒ์ธก ๋ˆˆ ์ง์ ‘ ์‹œ์ ] ๋กœ๋ด‡์ƒˆ์˜ ์™ผ์ชฝ ๋ˆˆ์•Œ์—์„œ ์„ธ์ƒ์„ ์ง์ ‘ ๋ฐ”๋ผ๋ณด๋Š” ์‹œ์ !",
275
+ "๐Ÿ‘๏ธ [์šฐ์ธก ๋ˆˆ ์ง์ ‘ ์‹œ์ ] ๋กœ๋ด‡์ƒˆ์˜ ์˜ค๋ฅธ์ชฝ ๋ˆˆ์•Œ์—์„œ ์„ธ์ƒ์„ ์ง์ ‘ ๋ฐ”๋ผ๋ณด๋Š” ์‹œ์ !"
276
+ ]
277
+ print(f"\n[๐Ÿ“ท ์‹œ์  ์ „ํ™˜] ํ˜„์žฌ ์‹œ์ : {cam_names[controller.cam_mode]}")
278
+ elif keycode in [ord('V'), ord('v')]:
279
+ controller.toggle_look_down()
280
+ elif keycode in [ord('B'), ord('b')]:
281
+ controller.look_front()
282
+ elif keycode in [ord('X'), ord('x')]:
283
+ recorder.toggle(controller.cam_mode)
284
+ elif keycode in [ord('K'), ord('k')]:
285
+ print("\n[๐Ÿ’ฅ ์‚ฌ์šฉ์ž ๊ฐœ์ž…] ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๋ด‡์„ ํˆญ ์ณ์„œ ๋„˜์–ด๋œจ๋ ธ์Šต๋‹ˆ๋‹ค!")
286
+ data.qvel[0] = 0.35
287
+ data.qvel[3] = 4.5
288
+ data.qvel[4] = 2.0
289
+ controller.set_state(RobotBirdFSM.STATE_KNOCKDOWN)
290
+
291
+ with mujoco.viewer.launch_passive(model, data, key_callback=viewer_key_callback, show_left_ui=False, show_right_ui=False) as viewer:
292
+ try:
293
+ viewer.cam.distance = 1.05
294
+ viewer.cam.elevation = -10
295
+ viewer.cam.azimuth = -80
296
+
297
+ dt = model.opt.timestep if model.opt.timestep > 0 else 0.002
298
+
299
+ while viewer.is_running():
300
+ step_start = time.time()
301
+
302
+ # ๐ŸŽฌ ๋Œ€ํ‘œ๋‹˜์˜ ์Šคํ† ๋ฆฌ๋ณด๋“œ ๋””๋ ‰ํ„ฐ ๊ตฌ๋™
303
+ director.update(dt)
304
+
305
+ # ๐Ÿฆ† ๋ฌผ๋ฆฌ ์ œ์–ด๊ธฐ ์—…๋ฐ์ดํŠธ ๋ฐ ๋ฌผ๋ฆฌ ์Šคํ… ์ „์ง„
306
+ controller.update(dt)
307
+ mujoco.mj_step(model, data)
308
+
309
+ # ๐ŸŒŸ [ํž˜ ํ‘œ์‹œ์„ /๋ฒˆ๊ฐœ์„  ์ฐจ๋‹จ & ๋ฐ”๋‹ฅ ํ…์Šค์ฒ˜/๊ทธ๋ฆฌ๋“œ 100% ์„ ๋ช…ํ•˜๊ฒŒ ๋ณด์กด]
310
+ try:
311
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] = 0
312
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] = 0
313
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] = 0
314
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = 0
315
+ except Exception:
316
+ pass
317
+
318
+ # ๐ŸŽฅ ์Šค๋งˆํŠธ ์นด๋ฉ”๋ผ ์‹œ์  ๋™๊ธฐํ™”
319
+ try:
320
+ if controller.cam_mode == 0:
321
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE
322
+ viewer.cam.lookat[0] = data.qpos[0]
323
+ viewer.cam.lookat[1] = data.qpos[1]
324
+ viewer.cam.lookat[2] = data.qpos[2]
325
+ elif controller.cam_mode == 1:
326
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
327
+ viewer.cam.fixedcamid = model.camera("head_fpv_cam").id
328
+ elif controller.cam_mode == 2:
329
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
330
+ viewer.cam.fixedcamid = model.camera("left_eye_cam").id
331
+ elif controller.cam_mode == 3:
332
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
333
+ viewer.cam.fixedcamid = model.camera("right_eye_cam").id
334
+ except Exception:
335
+ pass
336
+
337
+ # ๐Ÿ”ด ์‹ค์‹œ๊ฐ„ ๋น„๋””์˜ค ํ”„๋ ˆ์ž„ ์บก์ฒ˜ (๋…นํ™” ์ค‘์ผ ๋•Œ ์ž๋™ ๊ธฐ๋ก)
338
+ if recorder.is_recording:
339
+ recorder.capture_frame(controller.cam_mode)
340
+
341
+ viewer.sync()
342
+
343
+ elapsed = time.time() - step_start
344
+ sleep_time = dt - elapsed
345
+ if sleep_time > 0:
346
+ time.sleep(sleep_time)
347
+
348
+ except KeyboardInterrupt:
349
+ print("\n์‚ฌ์šฉ์ž์— ์˜ํ•ด AI ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๊ฐ€ ์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
350
+ finally:
351
+ if recorder.is_recording:
352
+ recorder.stop()
353
+
354
+ if __name__ == "__main__":
355
+ main()
run_robot_bird.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import math
5
+ import msvcrt
6
+ import mujoco
7
+ import mujoco.viewer
8
+ import numpy as np
9
+
10
+ # UTF-8 ์ž…์ถœ๋ ฅ ๋ณด์žฅ
11
+ if sys.platform == "win32":
12
+ try:
13
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
14
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
15
+ except Exception:
16
+ pass
17
+
18
+ from robot_bird.controller import RobotBirdFSM
19
+ from robot_bird.recorder import ScreenRecorder
20
+
21
+ def print_banner():
22
+ print("=" * 80)
23
+ print(" ๐ŸŽฎ Control Guide (๋ฐ˜๋ ค ๋กœ๋ด‡์ƒˆ ์กฐ์ข… ํ‚ค ์•ˆ๋‚ด)")
24
+ print("=" * 80)
25
+ print(" ๐ŸŒŸ [ ์„ ํšŒ ํšŒ์ „ (Yaw Turn) ] : A / D โž” ๋จธ๋ฆฌ ์™ผ์ชฝ / ์˜ค๋ฅธ์ชฝ ํšŒ์ „ (๋น„ํ–‰ ๋ฑ…ํฌํ„ด / ๋ณดํ–‰ ํšŒ์ „)")
26
+ print(" ๐ŸŒŸ [ ์ „์ง„ / ํ›„์ง„ ] : โ†‘ / โ†“ (W/S) โž” ์ง€์ƒ: ์ „์ง„/ํ›„์ง„ ๊ฑท๊ธฐ | ๊ณต์ค‘: ๋ฐ”๋ผ๋ณด๋Š” ๋ฐฉํ–ฅ ์ „์ง„/ํ›„์ง„ ๋น„ํ–‰")
27
+ print(" ๐ŸŒŸ [ ํ‰ํ–‰ ์ด๋™ (Strafe) ] : โ† / โ†’ โž” ํšŒ์ „ ์—†์ด ์ขŒ / ์šฐ ํ‰ํ–‰ ์Šฌ๋ผ์ด๋“œ ์ด๋™")
28
+ print(" ๐ŸŒŸ [ ๊ณ ๋„ ์ œ์–ด ] : R / F โž” ๐Ÿš€ ๊ณ ๋„ ์ƒ์Šน (+15cm) | ๐Ÿ›ฌ ๊ณ ๋„ ํ•˜๊ฐ• (-15cm)")
29
+ print(" ๐ŸŒŸ [ ์ œ์ž๋ฆฌ ์ •์ง€ ] : SPACE โž” ๐Ÿ›‘ ์ง€์ƒ: ๊ธฐ๋ฆฝ ์ •์ง€ | ๊ณต์ค‘: ์นผ ๊ฐ™์€ ์ •์ง€ ํ˜ธ๋ฒ„๋ง")
30
+ print(" ๐ŸŒŸ [ ์›ํ„ฐ์น˜ ๋น„ํ–‰ ] : 1 โž” 2 โž” 3 โž” ๐Ÿชฝ ๋‚ ๊ฐœ ํŽผ์น˜๊ธฐ โž” ๐Ÿš€ ์ด๋ฅ™/ํ˜ธ๋ฒ„๋ง โž” ๐Ÿ›ฌ ์ฐฉ๋ฅ™ ๋ฐ ๋‚ ๊ฐœ ์ ‘๊ธฐ")
31
+ print(" ๐ŸŒŸ [ ๐Ÿ“ท ๋ˆˆ ์นด๋ฉ”๋ผ ์‹œ์  ] : C โž” ๐Ÿ‘๏ธ 3์ธ์นญ ๋ทฐ โž” ์ค‘์•™ 1์ธ์นญ FPV โž” ์ขŒ์ธก ๋ˆˆ โž” ์šฐ์ธก ๋ˆˆ")
32
+ print(" ๐ŸŒŸ [ ๐Ÿ‘€ ๋ˆˆ์•Œ ์‹œ์„  ์กฐ์ž‘ ] : V ๋˜๋Š” B โž” ๐Ÿ‘‡ ๋ˆˆ์•Œ ์•„๋ž˜ 55ยฐ ๋ณด๊ธฐ(V) ๐Ÿ” ๋‹ค์‹œ ๋ˆˆ์•Œ ์›๋ž˜ ์œ„์น˜๋กœ ๋ณต๊ท€(V/B)")
33
+ print(" ๐ŸŒŸ [ ๐Ÿ”ด ์‹ค์‹œ๊ฐ„ ์˜์ƒ ๋…นํ™” ] : X โž” ๐ŸŽฌ ํ˜„์žฌ ํ™”๋ฉด ์‹ค์‹œ๊ฐ„ ํ‘œ์ค€ H.264 MP4 ๋™์˜์ƒ ๋…นํ™” ON / OFF")
34
+ print(" ๐ŸŒŸ [ ๊ฐ์„ฑ ๋ชจ์…˜ & ์ธํ„ฐ๋ž™์…˜ ] : G / Y / K / P โž” ๐ŸŒพ ๋ชจ์ด ์ชผ๊ธฐ(G) | ๐Ÿช‘ ์ชผ๊ทธ๋ ค ์•‰๊ธฐ(Y) | ๐Ÿ’ฅ ๋„˜์–ด๋œจ๋ฆฌ๊ธฐ(K) | ๐Ÿ”„ ์˜ค๋š์ด ๊ธฐ๋ฆฝ(P)")
35
+ print(" ๐ŸŒŸ [ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ข…๋ฃŒ ] : Q / ESC")
36
+ print("=" * 80)
37
+ print("\n์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค... (3D ์ฐฝ์„ ํด๋ฆญํ•˜๊ณ  ๋ฉ‹์ง€๊ฒŒ ์กฐ์ข…ํ•ด๋ณด์„ธ์š”!)\n")
38
+
39
+ def handle_key_action(keycode, controller, recorder=None):
40
+ """
41
+ Control Guide ํ‘œ์™€ 100% ์ผ์น˜ํ•˜๋Š” ์ง๊ด€์ ์ธ ํ‚ค ํ•ธ๋“ค๋Ÿฌ
42
+ """
43
+ is_flying = controller.state in [
44
+ RobotBirdFSM.STATE_FLIGHT,
45
+ RobotBirdFSM.STATE_TAKEOFF_CLIMB,
46
+ RobotBirdFSM.STATE_TAKEOFF_SPOOL,
47
+ RobotBirdFSM.STATE_LANDING
48
+ ]
49
+
50
+ # [ Q / ESC ] : ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ข…๋ฃŒ
51
+ if keycode in [ord('Q'), ord('q'), 256, 27]:
52
+ print("\n์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ์•ˆ๋…•ํžˆ ๊ฐ€์„ธ์š”!")
53
+ sys.exit(0)
54
+
55
+ # [ 1 ] : ๋‚ ๊ฐœ ํŽผ์น˜๊ธฐ / ์ ‘๊ธฐ
56
+ elif keycode in [ord('1'), 49, 321, ord('E'), ord('e')]:
57
+ controller.toggle_wings()
58
+
59
+ # [ 2 ] : ์ด๋ฅ™ ๋ฐ 1.0m ์ •์ง€ ํ˜ธ๋ฒ„๋ง
60
+ elif keycode in [ord('2'), 50, 322, 257, ord('T'), ord('t')]:
61
+ controller.start_flight_sequence()
62
+
63
+ # [ 3 ] : ์ฐฉ๋ฅ™ ๋ฐ ๋‚ ๊ฐœ ์ ‘๊ธฐ
64
+ elif keycode in [ord('3'), 51, 323, ord('H'), ord('h'), 259]:
65
+ controller.set_state(RobotBirdFSM.STATE_LANDING)
66
+
67
+ # [ โ†‘ ] / [ W ] : ์ „์ง„ (์ง€์ƒ: ์ „์ง„ ๊ฑท๊ธฐ / ๊ณต์ค‘: ์ „๋ฐฉ ๋น„ํ–‰)
68
+ elif keycode in [265, 38, ord('W'), ord('w')]:
69
+ if is_flying:
70
+ yaw = getattr(controller, 'target_yaw', 0.0)
71
+ controller.target_x += 0.30 * (-math.sin(yaw))
72
+ controller.target_y += 0.30 * math.cos(yaw)
73
+ print(f"\n[๐Ÿš€ ์ „์ง„ ๋น„ํ–‰] ๋ฐ”๋ผ๋ณด๋Š” ๋ฐฉํ–ฅ์œผ๋กœ 30cm ์ „์ง„! (X:{controller.target_x:.2f}m, Y:{controller.target_y:.2f}m)")
74
+ else:
75
+ print(f"\n[๐Ÿšถ ๋ณดํ–‰ ๋ชจ๋“œ] โฌ†๏ธ ์•ž์œผ๋กœ ์•„์žฅ์•„์žฅ ๊ฑท๊ธฐ ์‹œ์ž‘!")
76
+ controller.walk_speed = 1.0
77
+ controller.walk_turn = 0.0
78
+ controller.set_state(RobotBirdFSM.STATE_WALK)
79
+
80
+ # [ โ†“ ] / [ S ] : ํ›„์ง„ (์ง€์ƒ: ํ›„์ง„ ๊ฑท๊ธฐ / ๊ณต์ค‘: ํ›„๋ฐฉ ๋น„ํ–‰)
81
+ elif keycode in [264, 40, ord('S'), ord('s')]:
82
+ if is_flying:
83
+ yaw = getattr(controller, 'target_yaw', 0.0)
84
+ controller.target_x -= 0.30 * (-math.sin(yaw))
85
+ controller.target_y -= 0.30 * math.cos(yaw)
86
+ print(f"\n[๐Ÿ›ฌ ํ›„์ง„ ๋น„ํ–‰] ๋ฐ”๋ผ๋ณด๋Š” ๋ฐ˜๋Œ€๋กœ 30cm ํ›„์ง„! (X:{controller.target_x:.2f}m, Y:{controller.target_y:.2f}m)")
87
+ else:
88
+ print(f"\n[๐Ÿšถ ๋ณดํ–‰ ๋ชจ๋“œ] โฌ‡๏ธ ๋’ค๋กœ ์•„์žฅ์•„์žฅ ๊ฑท๊ธฐ ์‹œ์ž‘!")
89
+ controller.walk_speed = -0.7
90
+ controller.walk_turn = 0.0
91
+ controller.set_state(RobotBirdFSM.STATE_WALK)
92
+
93
+ # [ โ† ] : ์ขŒ์ธก ํ‰ํ–‰ ์ด๋™ (๊ณต์ค‘: ์ขŒ ์Šฌ๋ผ์ด๋“œ / ์ง€์ƒ: ์ขŒํšŒ์ „ ๊ฑท๊ธฐ)
94
+ elif keycode in [263, 37]:
95
+ if is_flying:
96
+ yaw = getattr(controller, 'target_yaw', 0.0)
97
+ controller.target_x += 0.30 * math.cos(yaw)
98
+ controller.target_y += 0.30 * math.sin(yaw)
99
+ print(f"\n[โฌ…๏ธ ํ‰ํ–‰ ์Šฌ๋ผ์ด๋“œ] ์™ผ์ชฝ์œผ๋กœ 30cm ์ด๋™! (X:{controller.target_x:.2f}m, Y:{controller.target_y:.2f}m)")
100
+ else:
101
+ print(f"\n[๐Ÿšถ ๋ณดํ–‰ ๋ชจ๋“œ] โฌ…๏ธ ์™ผ์ชฝ์œผ๋กœ ๋ฐฉํ–ฅ์„ ํ‹€๋ฉฐ ๊ฑท๊ธฐ")
102
+ controller.walk_speed = 0.5
103
+ controller.walk_turn = 0.7
104
+ controller.set_state(RobotBirdFSM.STATE_WALK)
105
+
106
+ # [ โ†’ ] : ์šฐ์ธก ํ‰ํ–‰ ์ด๋™ (๊ณต์ค‘: ์šฐ ์Šฌ๋ผ์ด๋“œ / ์ง€์ƒ: ์šฐํšŒ์ „ ๊ฑท๊ธฐ)
107
+ elif keycode in [262, 39]:
108
+ if is_flying:
109
+ yaw = getattr(controller, 'target_yaw', 0.0)
110
+ controller.target_x -= 0.30 * math.cos(yaw)
111
+ controller.target_y -= 0.30 * math.sin(yaw)
112
+ print(f"\n[โžก๏ธ ํ‰ํ–‰ ์Šฌ๋ผ์ด๋“œ] ์˜ค๋ฅธ์ชฝ์œผ๋กœ 30cm ์ด๋™! (X:{controller.target_x:.2f}m, Y:{controller.target_y:.2f}m)")
113
+ else:
114
+ print(f"\n[๐Ÿšถ ๋ณดํ–‰ ๋ชจ๋“œ] โžก๏ธ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋ฐฉํ–ฅ์„ ํ‹€๋ฉฐ ๊ฑท๊ธฐ")
115
+ controller.walk_speed = 0.5
116
+ controller.walk_turn = -0.7
117
+ controller.set_state(RobotBirdFSM.STATE_WALK)
118
+
119
+ # [ A ] : ๋จธ๋ฆฌ ์™ผ์ชฝ ํšŒ์ „ (๊ณต์ค‘: ์ขŒ์„ ํšŒ ํ„ด / ์ง€์ƒ: ์ขŒํšŒ์ „ ๊ฑท๊ธฐ)
120
+ elif keycode in [ord('A'), ord('a')]:
121
+ if is_flying:
122
+ controller.target_yaw = getattr(controller, 'target_yaw', 0.0) - 0.25
123
+ deg = math.degrees(controller.target_yaw) % 360
124
+ print(f"\n[๐Ÿ”„ ์„ ํšŒ ํšŒ์ „] ๋จธ๋ฆฌ ์™ผ์ชฝ์œผ๋กœ ํšŒ์ „! (๋ชฉํ‘œ ๊ฐ๋„: {deg:.1f}ยฐ)")
125
+ else:
126
+ print(f"\n[๐Ÿšถ ๋ณดํ–‰ ๋ชจ๋“œ] ๐Ÿ”„ ์™ผ์ชฝ์œผ๋กœ ๋ฐฉํ–ฅ์„ ํ‹€๋ฉฐ ๊ฑท๊ธฐ")
127
+ controller.walk_speed = 0.5
128
+ controller.walk_turn = 0.7
129
+ controller.set_state(RobotBirdFSM.STATE_WALK)
130
+
131
+ # [ D ] : ๋จธ๋ฆฌ ์˜ค๋ฅธ์ชฝ ํšŒ์ „ (๊ณต์ค‘: ์šฐ์„ ํšŒ ํ„ด / ์ง€์ƒ: ์šฐํšŒ์ „ ๊ฑท๊ธฐ)
132
+ elif keycode in [ord('D'), ord('d')]:
133
+ if is_flying:
134
+ controller.target_yaw = getattr(controller, 'target_yaw', 0.0) + 0.25
135
+ deg = math.degrees(controller.target_yaw) % 360
136
+ print(f"\n[๐Ÿ”„ ์„ ํšŒ ํšŒ์ „] ๋จธ๋ฆฌ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ํšŒ์ „! (๋ชฉํ‘œ ๊ฐ๋„: {deg:.1f}ยฐ)")
137
+ else:
138
+ print(f"\n[๐Ÿšถ ๋ณดํ–‰ ๋ชจ๋“œ] ๐Ÿ”„ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋ฐฉํ–ฅ์„ ํ‹€๋ฉฐ ๊ฑท๊ธฐ")
139
+ controller.walk_speed = 0.5
140
+ controller.walk_turn = -0.7
141
+ controller.set_state(RobotBirdFSM.STATE_WALK)
142
+
143
+ # [ R ] / [ + ] : ๊ณ ๋„ ์ƒ์Šน (+15cm)
144
+ elif keycode in [ord('R'), ord('r'), ord('+'), ord('='), 266, 33, 334]:
145
+ if is_flying:
146
+ controller.target_altitude += 0.15
147
+ print(f"\n[๐Ÿš€ ๊ณ ๋„ ์ƒ์Šน] ์œ„๋กœ ์ƒ์Šน! (๋ชฉํ‘œ ๊ณ ๋„: {controller.target_altitude:.2f}m)")
148
+
149
+ # [ F ] / [ - ] : ๊ณ ๋„ ํ•˜๊ฐ• (-15cm)
150
+ elif keycode in [ord('F'), ord('f'), ord('-'), ord('_'), 267, 34, 333]:
151
+ if is_flying:
152
+ controller.target_altitude = max(0.20, controller.target_altitude - 0.15)
153
+ print(f"\n[๐Ÿ›ฌ ๊ณ ๋„ ํ•˜๊ฐ•] ์•„๋ž˜๋กœ ํ•˜๊ฐ•! (๋ชฉํ‘œ ๊ณ ๋„: {controller.target_altitude:.2f}m)")
154
+
155
+ # [ SPACE ] : ์ œ์ž๋ฆฌ ์ •์ง€ (์ง€์ƒ: ๊ธฐ๋ฆฝ ์ •์ง€ / ๊ณต์ค‘: ์นผ ๊ฐ™์€ ์ •์ง€ ํ˜ธ๋ฒ„๋ง)
156
+ elif keycode in [ord(' '), 32]:
157
+ if is_flying:
158
+ print(f"\n[๐Ÿ›‘ ์ •์ง€ ํ˜ธ๋ฒ„๋ง] ํ˜„์žฌ ์œ„์น˜์—์„œ ์นผ ๊ฐ™์ด ์ •์ง€ ํ˜ธ๋ฒ„๋งํ•ฉ๋‹ˆ๋‹ค.")
159
+ controller.target_x = controller.data.qpos[0]
160
+ controller.target_y = controller.data.qpos[1]
161
+ else:
162
+ print(f"\n[๐Ÿฆ† ๊ธฐ๋ฆฝ ์ •์ง€] ๊ฑท๊ธฐ๋ฅผ ๋ฉˆ์ถ”๊ณ  ๋ฐ˜๋“ฏํ•˜๊ฒŒ ์ œ์ž๋ฆฌ์— ์„ญ๋‹ˆ๋‹ค.")
163
+ controller.walk_speed = 0.0
164
+ controller.walk_turn = 0.0
165
+ controller.set_state(RobotBirdFSM.STATE_STAND)
166
+
167
+ # [ C ] : ๋ˆˆ์—์„œ ์ง์ ‘ ๋ณด๋Š” ์‹œ์  ์ „ํ™˜ (3์ธ์นญ ๊ด€์ฐฐ โž” 1์ธ์นญ ์–ผ๊ตด ์ค‘์‹ฌ โž” ์ขŒ์ธก ๋ˆˆ ์ง์ ‘ ๋ทฐ โž” ์šฐ์ธก ๋ˆˆ ์ง์ ‘ ๋ทฐ)
168
+ elif keycode in [ord('C'), ord('c')]:
169
+ if not hasattr(controller, 'cam_mode'):
170
+ controller.cam_mode = 0
171
+ controller.cam_mode = (controller.cam_mode + 1) % 4
172
+ cam_names = [
173
+ "๐Ÿ•Š๏ธ 3์ธ์นญ ์ „์ฒด ๋ทฐ (์™ธ๋ถ€์—์„œ ๋กœ๋ด‡์ƒˆ ๋ชจ์Šต ๊ด€์ฐฐ)",
174
+ "๐Ÿ“ท [1์ธ์นญ FPV] ๋กœ๋ด‡์ƒˆ ๋จธ๋ฆฌ ์ •์ค‘์•™ ์‹œ์ ",
175
+ "๐Ÿ‘๏ธ [์ขŒ์ธก ๋ˆˆ ์ง์ ‘ ์‹œ์ ] ๋กœ๋ด‡์ƒˆ์˜ ์™ผ์ชฝ ๋ˆˆ์•Œ์—์„œ ์„ธ์ƒ์„ ์ง์ ‘ ๋ฐ”๋ผ๋ณด๋Š” ์‹œ์ !",
176
+ "๐Ÿ‘๏ธ [์šฐ์ธก ๋ˆˆ ์ง์ ‘ ์‹œ์ ] ๋กœ๋ด‡์ƒˆ์˜ ์˜ค๋ฅธ์ชฝ ๋ˆˆ์•Œ์—์„œ ์„ธ์ƒ์„ ์ง์ ‘ ๋ฐ”๋ผ๋ณด๋Š” ์‹œ์ !"
177
+ ]
178
+ print(f"\n[๐Ÿ“ท ์‹œ์  ์ „ํ™˜] ํ˜„์žฌ ์‹œ์ : {cam_names[controller.cam_mode]}")
179
+
180
+ # [ V ] : ๋จธ๋ฆฌ/๋ˆˆ์•Œ ํ•˜๋ฐฉ ์‹œ์„  ํ† ๊ธ€ (์•„๋ž˜ 55ยฐ ๋ณด๊ธฐ <-> ์ •๋ฉด ๋˜‘๋ฐ”๋กœ ๋ณด๊ธฐ)
181
+ elif keycode in [ord('V'), ord('v')]:
182
+ controller.toggle_look_down()
183
+
184
+ # [ B ] : ๋จธ๋ฆฌ/๋ˆˆ์•Œ ์ฆ‰์‹œ ์ •๋ฉด ์ˆ˜ํ‰ ์‹œ์„ ์œผ๋กœ ๋ณต๊ท€
185
+ elif keycode in [ord('B'), ord('b')]:
186
+ controller.look_front()
187
+
188
+ # [ X ] : ์‹ค์‹œ๊ฐ„ ํ‘œ์ค€ H.264 MP4 ๋น„๋””์˜ค ๋…นํ™” ON / OFF ํ† ๊ธ€
189
+ elif keycode in [ord('X'), ord('x')]:
190
+ if recorder is not None:
191
+ cam_mode = getattr(controller, 'cam_mode', 0)
192
+ recorder.toggle(cam_mode)
193
+
194
+ # [ G ] : ๋‹ค๋ฆฌ ์›…ํฌ๋ฆผ 3๋‹จ ๋ชจ์ด ์ชผ๊ธฐ
195
+ elif keycode in [ord('G'), ord('g')]:
196
+ print(f"\n[๐ŸŒพ ๊ฐ์„ฑ ๋ชจ์…˜] ๋‹ค๋ฆฌ๋ฅผ ์™ ์ ‘์œผ๋ฉฐ ๋ฐ”๋‹ฅ์— ์ฝ•! ์ฝ•! ์ฝ•! 3๋‹จ ๋ชจ์ด ์ชผ๊ธฐ!")
197
+ controller.set_state(RobotBirdFSM.STATE_GROUND_PICK)
198
+
199
+ # [ Y ] : ์ชผ๊ทธ๋ ค ์•‰๊ธฐ / ์ผ์–ด์„œ๊ธฐ ํ† ๊ธ€
200
+ elif keycode in [ord('Y'), ord('y')]:
201
+ if controller.state == RobotBirdFSM.STATE_SIT:
202
+ print(f"\n[๐Ÿช‘ ๊ฐ์„ฑ ๋ชจ์…˜] ์ชผ๊ทธ๋ ค ์•‰๊ธฐ์—์„œ ๋‹ค์‹œ ์ผ์–ด์„ญ๋‹ˆ๋‹ค.")
203
+ controller.set_state(RobotBirdFSM.STATE_STAND)
204
+ else:
205
+ print(f"\n[๐Ÿช‘ ๊ฐ์„ฑ ๋ชจ์…˜] ๋‹ค๋ฆฌ๋ฅผ ์™ ์ ‘๊ณ  ์–Œ์ „ํžˆ ์ชผ๊ทธ๋ ค ์•‰์Šต๋‹ˆ๋‹ค.")
206
+ controller.set_state(RobotBirdFSM.STATE_SIT)
207
+
208
+ # [ K ] : ๊ฝˆ๋‹น ๋„˜์–ด๋œจ๋ฆฌ๊ธฐ ํ…Œ์ŠคํŠธ
209
+ elif keycode in [ord('K'), ord('k'), ord('O'), ord('o')]:
210
+ print(f"\n[๐Ÿ’ฅ ๋„˜์–ด๋œจ๋ฆฌ๊ธฐ] ๊ฝˆ๋‹น! ๋กœ๋ด‡์ด ๋ˆ„์› ์Šต๋‹ˆ๋‹ค. ('P' ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋ฒŒ๋–ก ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค!)")
211
+ controller.data.qvel[0] = 0.35
212
+ controller.data.qvel[3] = 4.5
213
+ controller.data.qvel[4] = 2.0
214
+ controller.set_state(RobotBirdFSM.STATE_KNOCKDOWN)
215
+
216
+ # [ P ] : 3D ์˜ค๋š์ด ๋ฒŒ๋–ก ๊ธฐ๋ฆฝ (๋ˆ„์›Œ์žˆ์„ ๋•Œ: ๊ธฐ๋ฆฝ / ์„œ ์žˆ์„ ๋•Œ: ์žฌ๋กฑ ์ ํ”„ ๋Œ„์Šค)
217
+ elif keycode in [ord('P'), ord('p')]:
218
+ print(f"\n[๐Ÿ”„ ์˜ค๋š์ด ๊ธฐ๋ฆฝ] ๋ฒŒ๋–ก ์ผ์–ด์„œ๊ธฐ & ๊ฐธ์šฐ๋šฑ ์žฌ๋กฑ ๋Œ„์Šค!")
219
+ controller.set_state(RobotBirdFSM.STATE_RECOVER)
220
+
221
+ def main():
222
+ print_banner()
223
+
224
+ root_dir = os.path.dirname(os.path.abspath(__file__))
225
+ model_path = os.path.join(root_dir, "robot_bird", "models", "robot_bird.xml")
226
+
227
+ if not os.path.exists(model_path):
228
+ print(f"[์˜ค๋ฅ˜] ๋ชจ๋ธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {model_path}")
229
+ return
230
+
231
+ with open(model_path, "r", encoding="utf-8") as f:
232
+ xml_string = f.read()
233
+ model = mujoco.MjModel.from_xml_string(xml_string)
234
+ data = mujoco.MjData(model)
235
+
236
+ # ๐ŸŒŸ ๊ด€์ ˆ ์ด๋ฆ„ ๊ธฐ๋ฐ˜์˜ ์•ˆ์ „ํ•˜๊ณ  ์ •ํ™•ํ•œ ์ดˆ๊ธฐ ์ž์„ธ ์„ค์ • (๋ณดํ–‰ ๋ฐ ๋‚ ๊ฐœ ์ž์„ธ 100% ๋ณด์žฅ)
237
+ def set_init_jnt(jnt_name, val):
238
+ adr = model.joint(jnt_name).qposadr[0]
239
+ data.qpos[adr] = val
240
+
241
+ data.qpos[2] = 0.122 # z ๋†’์ด (์ง€๋ฉด ์™„๋ฒฝ ์•ˆ์ฐฉ)
242
+ set_init_jnt("left_wing_fold", 0.0)
243
+ set_init_jnt("left_tilt", 1.57)
244
+ set_init_jnt("right_wing_fold", 0.0)
245
+ set_init_jnt("right_tilt", 1.57)
246
+ set_init_jnt("neck_pitch", 0.0)
247
+ for j in ["left_hip_pitch", "left_knee_pitch", "left_ankle_pitch",
248
+ "right_hip_pitch", "right_knee_pitch", "right_ankle_pitch"]:
249
+ set_init_jnt(j, 0.0)
250
+
251
+ data.ctrl[:] = 0.0
252
+ data.ctrl[model.actuator("act_l_tilt").id] = 1.57
253
+ data.ctrl[model.actuator("act_r_tilt").id] = 1.57
254
+ mujoco.mj_forward(model, data)
255
+
256
+ controller = RobotBirdFSM(model, data)
257
+ recorder = ScreenRecorder(model, data)
258
+
259
+ def viewer_key_callback(keycode):
260
+ handle_key_action(keycode, controller, recorder)
261
+
262
+ with mujoco.viewer.launch_passive(model, data, key_callback=viewer_key_callback, show_left_ui=False, show_right_ui=False) as viewer:
263
+ try:
264
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] = 0
265
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = 0
266
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] = 0
267
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] = 0
268
+ except Exception:
269
+ pass
270
+
271
+ while viewer.is_running():
272
+ step_start = time.time()
273
+
274
+ if msvcrt.kbhit():
275
+ key = msvcrt.getch()
276
+ if key in [b'\x00', b'\xe0']:
277
+ sub = msvcrt.getch()
278
+ if sub == b'H': handle_key_action(265, controller, recorder) # UP Arrow
279
+ elif sub == b'P': handle_key_action(264, controller, recorder) # DOWN Arrow
280
+ elif sub == b'K': handle_key_action(263, controller, recorder) # LEFT Arrow
281
+ elif sub == b'M': handle_key_action(262, controller, recorder) # RIGHT Arrow
282
+ else:
283
+ try:
284
+ k_ord = ord(key.decode('utf-8', errors='ignore'))
285
+ handle_key_action(k_ord, controller, recorder)
286
+ if k_ord in [ord('q'), ord('Q'), 27]:
287
+ break
288
+ except Exception:
289
+ pass
290
+
291
+ controller.cmd_pitch *= 0.95
292
+ controller.cmd_roll *= 0.95
293
+ controller.cmd_yaw *= 0.95
294
+
295
+ dt = model.opt.timestep
296
+ controller.update(dt)
297
+ mujoco.mj_step(model, data)
298
+
299
+ # ๐ŸŒŸ [ํž˜ ํ‘œ์‹œ์„ /๋ฒˆ๊ฐœ์„  ์ฐจ๋‹จ & ๋ฐ”๋‹ฅ ํ…์Šค์ฒ˜/๊ทธ๋ฆฌ๋“œ 100% ์„ ๋ช…ํ•˜๊ฒŒ ๋ณด์กด]
300
+ try:
301
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] = 0
302
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] = 0
303
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] = 0
304
+ viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = 0
305
+ except Exception:
306
+ pass
307
+
308
+ # ๐ŸŒŸ [์Šค๋งˆํŠธ ์นด๋ฉ”๋ผ ์‹œ์  ์ œ์–ด] (C ํ‚ค๋กœ 3์ธ์นญ <-> 1์ธ์นญ FPV / ๋ˆˆ ์นด๋ฉ”๋ผ ์ „ํ™˜)
309
+ cam_mode = getattr(controller, 'cam_mode', 0)
310
+ try:
311
+ if cam_mode == 0:
312
+ # 3์ธ์นญ ์Šค๋งˆํŠธ ์ถ”์  ๋ทฐ
313
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE
314
+ viewer.cam.lookat[0] = data.qpos[0]
315
+ viewer.cam.lookat[1] = data.qpos[1]
316
+ viewer.cam.lookat[2] = data.qpos[2]
317
+ elif cam_mode == 1:
318
+ # ๐Ÿ“ท ์ค‘์•™ FPV ์นด๋ฉ”๋ผ
319
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
320
+ viewer.cam.fixedcamid = model.camera("head_fpv_cam").id
321
+ elif cam_mode == 2:
322
+ # ๐Ÿ‘๏ธ ์ขŒ์ธก ๋ˆˆ ์นด๋ฉ”๋ผ
323
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
324
+ viewer.cam.fixedcamid = model.camera("left_eye_cam").id
325
+ elif cam_mode == 3:
326
+ # ๐Ÿ‘๏ธ ์šฐ์ธก ๋ˆˆ ์นด๋ฉ”๋ผ
327
+ viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FIXED
328
+ viewer.cam.fixedcamid = model.camera("right_eye_cam").id
329
+ except Exception:
330
+ pass
331
+
332
+ # ๐Ÿ”ด ์‹ค์‹œ๊ฐ„ ๋น„๋””์˜ค ํ”„๋ ˆ์ž„ ์บก์ฒ˜ (๋…นํ™” ์ค‘์ผ ๋•Œ ์ž๋™ ๊ธฐ๋ก)
333
+ if recorder.is_recording:
334
+ recorder.capture_frame(cam_mode)
335
+
336
+ viewer.sync()
337
+
338
+ elapsed = time.time() - step_start
339
+ sleep_time = max(0.0, 0.004 - elapsed)
340
+ if sleep_time > 0:
341
+ time.sleep(sleep_time)
342
+
343
+ # ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ ์‹œ ๋…นํ™” ์ž๋™ ๋งˆ๋ฌด๋ฆฌ ์ €์žฅ
344
+ if recorder.is_recording:
345
+ recorder.stop()
346
+
347
+ if __name__ == "__main__":
348
+ main()
videos/eval_video.gif ADDED

Git LFS Details

  • SHA256: 0e314068698da4a1dceb2a3f3910d4c959abf6452f99a5c9c179ffc2ac5d94a9
  • Pointer size: 132 Bytes
  • Size of remote file: 5.73 MB
videos/eval_video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d2b237b35896aa215bd1cae710dffbbb603544e28496698b8f389daf8750454
3
+ size 1109526
๋ฏธ๋ฆฌ๋ณด๊ธฐ_๋ฐ๋ชจ.bat ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ cd /d "%~dp0"
3
+ if exist "..\duckenv\Scripts\python.exe" (
4
+ ..\duckenv\Scripts\python.exe run_ai_robot_bird.py
5
+ ) else if exist ".\duckenv\Scripts\python.exe" (
6
+ .\duckenv\Scripts\python.exe run_ai_robot_bird.py
7
+ ) else (
8
+ python run_ai_robot_bird.py
9
+ )
10
+ if errorlevel 1 pause