Commit ·
cee5182
1
Parent(s): 1078429
Added v0
Browse files- README.md +262 -10
- app.py +497 -0
- backend/__init__.py +4 -0
- backend/game_models.py +239 -0
- backend/main.py +105 -0
- backend/mcp_server.py +1119 -0
- backend/storage.py +101 -0
- backend/tools/__init__.py +89 -0
- backend/tools/player_tools.py +347 -0
- backend/tools/rendering_tools.py +848 -0
- backend/tools/scene_tools.py +362 -0
- chat_client.py +918 -0
- frontend/game_viewer.html +1515 -0
- requirements.txt +14 -0
README.md
CHANGED
|
@@ -1,13 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
| 1 |
+
# GCP - Game Context Protocol
|
| 2 |
+
|
| 3 |
+
**Build 3D scenes and games with natural language**
|
| 4 |
+
|
| 5 |
+
## What is it?
|
| 6 |
+
|
| 7 |
+
GCP (Game Context Protocol) is an AI-powered scene builder that lets you create interactive 3D game environments using simple commands. Built with Three.js and designed for LLM integration via MCP, it also works as a standalone HTTP service.
|
| 8 |
+
|
| 9 |
+
Simply describe what you want:
|
| 10 |
+
|
| 11 |
+
> "Add a red cube at 0,2,0"
|
| 12 |
+
> "Create a level 50 units wide"
|
| 13 |
+
> "Set lighting to night"
|
| 14 |
+
|
| 15 |
+
…and it instantly builds your 3D scene with:
|
| 16 |
+
- Real-time 3D rendering
|
| 17 |
+
- Interactive camera controls
|
| 18 |
+
- Dynamic object placement
|
| 19 |
+
- Lighting presets
|
| 20 |
+
- Primitive shapes (cubes, spheres, cylinders, etc.)
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## Features
|
| 25 |
+
|
| 26 |
+
### 🎨 Scene Building
|
| 27 |
+
- **6 primitive types**: cube, sphere, cylinder, plane, cone, torus
|
| 28 |
+
- **Flexible positioning**: Place objects anywhere in 3D space
|
| 29 |
+
- **Material system**: Colors, metalness, roughness, opacity
|
| 30 |
+
- **Dynamic scaling**: Custom size for each object
|
| 31 |
+
|
| 32 |
+
### 💡 Lighting System
|
| 33 |
+
- **4 presets**: day, night, sunset, studio
|
| 34 |
+
- **Multiple light types**: ambient, directional, point, spot
|
| 35 |
+
- **Automatic shadows**: Realistic lighting effects
|
| 36 |
+
|
| 37 |
+
### 🎮 FPS Controller
|
| 38 |
+
- **Physics-based movement**: Cannon.js integration with gravity, jumping, collisions
|
| 39 |
+
- **WASD controls**: Smooth keyboard-based movement
|
| 40 |
+
- **Mouse look**: Full 360° camera control with configurable sensitivity
|
| 41 |
+
- **Configurable feel**: Adjustable speed, jump force, FOV, air control
|
| 42 |
+
- **10x10 bounded world**: White floor and walls with collision detection
|
| 43 |
+
|
| 44 |
+
### 👀 Interactive Viewer
|
| 45 |
+
- **Dual camera modes**: FPS (first-person) and Orbit (overview)
|
| 46 |
+
- **Real-time updates**: See changes instantly via postMessage API
|
| 47 |
+
- **Grid helper**: Optional floor grid for spatial reference
|
| 48 |
+
- **Auto-centering**: Camera automatically frames your scene in Orbit mode
|
| 49 |
+
|
| 50 |
+
### 🤖 AI Integration
|
| 51 |
+
- **MCP protocol**: Works with Claude, GPT, and other AI assistants
|
| 52 |
+
- **Natural language**: Simple commands like "add a blue sphere" or "set speed to 10"
|
| 53 |
+
- **Context aware**: Builds on existing scenes
|
| 54 |
+
- **16 MCP tools**: Scene building (5) + Player controller (11)
|
| 55 |
+
- **No coding required**: Pure natural language scene building
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## Quick Start
|
| 60 |
+
|
| 61 |
+
### Installation
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
# Clone the repository
|
| 65 |
+
git clone https://github.com/ArturoNereu/3DViz-MCP.git
|
| 66 |
+
cd 3DViz-MCP
|
| 67 |
+
|
| 68 |
+
# Install dependencies
|
| 69 |
+
pip install -r requirements.txt
|
| 70 |
+
|
| 71 |
+
# Run the application
|
| 72 |
+
python app.py
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
The app will start two servers:
|
| 76 |
+
- **FastAPI** (port 8000): MCP server and scene API
|
| 77 |
+
- **Gradio** (port 7860): Chat interface
|
| 78 |
+
|
| 79 |
+
Open `http://localhost:7860` in your browser.
|
| 80 |
+
|
| 81 |
+
### Example Commands
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
Add a red cube at 0,2,0
|
| 85 |
+
Add a blue sphere at 5,1,5
|
| 86 |
+
Add a green cylinder at -3,1,0
|
| 87 |
+
Set lighting to night
|
| 88 |
+
Create a level 100 units wide
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## How It Works
|
| 94 |
+
|
| 95 |
+
### For AI Assistants (MCP)
|
| 96 |
+
|
| 97 |
+
The MCP server exposes 16 tools that AI assistants can call:
|
| 98 |
+
|
| 99 |
+
**Scene Building (5 tools):**
|
| 100 |
+
- `create_scene_tool` - Create a new 3D scene/level
|
| 101 |
+
- `add_object_tool` - Add objects to the scene
|
| 102 |
+
- `remove_object_tool` - Remove objects from scene
|
| 103 |
+
- `set_lighting_tool` - Change lighting preset
|
| 104 |
+
- `get_scene_info_tool` - Get scene details
|
| 105 |
+
|
| 106 |
+
**Player Controller Phase 1 (5 tools):**
|
| 107 |
+
- `set_player_speed` - Movement speed
|
| 108 |
+
- `set_jump_force` - Jump height
|
| 109 |
+
- `set_mouse_sensitivity` - Mouse look sensitivity + Y-invert
|
| 110 |
+
- `set_gravity` - World gravity
|
| 111 |
+
- `set_player_dimensions` - Player size
|
| 112 |
+
|
| 113 |
+
**Player Controller Phase 2 (4 tools):**
|
| 114 |
+
- `set_movement_acceleration` - Movement feel
|
| 115 |
+
- `set_air_control` - Airborne control
|
| 116 |
+
- `set_camera_fov` - Field of view
|
| 117 |
+
- `set_vertical_look_limits` - Look angle limits
|
| 118 |
+
|
| 119 |
+
**Configuration (1 tool):**
|
| 120 |
+
- `get_player_config` - Get all player settings
|
| 121 |
+
|
| 122 |
+
### For Developers (HTTP API)
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
import requests
|
| 126 |
+
|
| 127 |
+
# Create a new scene
|
| 128 |
+
response = requests.post("http://localhost:8000/api/scenes", json={
|
| 129 |
+
"name": "My Scene",
|
| 130 |
+
"world_width": 50.0,
|
| 131 |
+
"lighting_preset": "day"
|
| 132 |
+
})
|
| 133 |
+
|
| 134 |
+
scene_id = response.json()["scene_id"]
|
| 135 |
+
|
| 136 |
+
# Add an object
|
| 137 |
+
requests.post(f"http://localhost:8000/api/scenes/{scene_id}/objects", json={
|
| 138 |
+
"object_type": "cube",
|
| 139 |
+
"position": {"x": 0, "y": 1, "z": 0},
|
| 140 |
+
"material": {"color": "#ff0000"}
|
| 141 |
+
})
|
| 142 |
+
|
| 143 |
+
# View your scene
|
| 144 |
+
viewer_url = f"http://localhost:8000/view/scene/{scene_id}"
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## Architecture
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
GCP - Game Context Protocol
|
| 153 |
+
├── app.py # Gradio chat interface
|
| 154 |
+
├── backend/
|
| 155 |
+
│ ├── main.py # FastAPI + GCP server
|
| 156 |
+
│ ├── game_models.py # Scene, GameObject, Light models
|
| 157 |
+
│ ├── game_tools.py # GCP tool implementations
|
| 158 |
+
│ └── storage.py # In-memory scene storage
|
| 159 |
+
└── frontend/
|
| 160 |
+
└── game_viewer.html # Three.js 3D renderer
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### Tech Stack
|
| 164 |
+
- **Backend**: FastAPI, FastMCP, Pydantic
|
| 165 |
+
- **Frontend**: Three.js, Gradio
|
| 166 |
+
- **3D Rendering**: Three.js with orbit controls
|
| 167 |
+
- **AI Integration**: MCP (Model Context Protocol)
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## API Endpoints
|
| 172 |
+
|
| 173 |
+
### Scenes
|
| 174 |
+
- `POST /api/scenes` - Create a new scene
|
| 175 |
+
- `GET /api/scenes/{scene_id}` - Get scene data
|
| 176 |
+
- `GET /view/scene/{scene_id}` - View scene in browser
|
| 177 |
+
|
| 178 |
+
### MCP
|
| 179 |
+
- `GET /mcp` - MCP protocol endpoint
|
| 180 |
+
- `GET /docs` - API documentation
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
## Supported Objects
|
| 185 |
+
|
| 186 |
+
| Type | Description |
|
| 187 |
+
|------|-------------|
|
| 188 |
+
| `cube` | Box geometry |
|
| 189 |
+
| `sphere` | Spherical geometry |
|
| 190 |
+
| `cylinder` | Cylindrical geometry |
|
| 191 |
+
| `plane` | Flat surface (floor/wall) |
|
| 192 |
+
| `cone` | Conical geometry |
|
| 193 |
+
| `torus` | Donut shape |
|
| 194 |
+
|
| 195 |
+
## Supported Colors
|
| 196 |
+
|
| 197 |
+
red, blue, green, yellow, purple, orange, pink, brown, black, white, or any hex code (#ff0000)
|
| 198 |
+
|
| 199 |
+
## Lighting Presets
|
| 200 |
+
|
| 201 |
+
- **day**: Bright white directional light
|
| 202 |
+
- **night**: Dark blue moonlight
|
| 203 |
+
- **sunset**: Warm orange light
|
| 204 |
+
- **studio**: Neutral balanced lighting
|
| 205 |
+
|
| 206 |
---
|
| 207 |
+
|
| 208 |
+
## Development
|
| 209 |
+
|
| 210 |
+
### Project Structure
|
| 211 |
+
|
| 212 |
+
```
|
| 213 |
+
backend/game_models.py # Data models (Scene, GameObject, etc.)
|
| 214 |
+
backend/game_tools.py # Tool implementations
|
| 215 |
+
backend/main.py # FastAPI routes + MCP tools
|
| 216 |
+
frontend/game_viewer.html # Three.js viewer
|
| 217 |
+
app.py # Gradio chat interface
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
### Adding New Object Types
|
| 221 |
+
|
| 222 |
+
1. Add to `ObjectType` enum in `game_models.py`
|
| 223 |
+
2. Add geometry case in `game_viewer.html` `renderGameObjects()`
|
| 224 |
+
3. Update command parsing in `app.py` `chat_response()`
|
| 225 |
+
|
| 226 |
+
---
|
| 227 |
+
|
| 228 |
+
## Roadmap
|
| 229 |
+
|
| 230 |
+
### ✅ Completed
|
| 231 |
+
- **Phase 1**: Player Controller - Core Controls (5 tools)
|
| 232 |
+
- **Phase 2**: Player Controller - Enhanced Feel (4 tools)
|
| 233 |
+
- Physics engine integration (Cannon.js)
|
| 234 |
+
- FPS controls (WASD + mouse look)
|
| 235 |
+
|
| 236 |
+
### 🚧 Next Phase: Rendering & Lighting Tools
|
| 237 |
+
- Add/remove individual lights
|
| 238 |
+
- Update light properties (color, intensity, position)
|
| 239 |
+
- Change object materials (color, metalness, roughness)
|
| 240 |
+
- Set background color
|
| 241 |
+
|
| 242 |
+
### 🔮 Phase 3: World Building
|
| 243 |
+
- glTF model loading (Kenney assets)
|
| 244 |
+
- Prefab system (props, buildings, terrain)
|
| 245 |
+
- Scene templates
|
| 246 |
+
- Export to Unity/Unreal
|
| 247 |
+
|
| 248 |
+
### 💭 Future Ideas
|
| 249 |
+
- NPC system with behaviors
|
| 250 |
+
- Multiplayer support
|
| 251 |
+
- Procedural generation
|
| 252 |
+
|
| 253 |
+
---
|
| 254 |
+
|
| 255 |
+
## License
|
| 256 |
+
|
| 257 |
+
MIT License - feel free to use in your projects!
|
| 258 |
+
|
| 259 |
+
## Contributing
|
| 260 |
+
|
| 261 |
+
Contributions welcome! Please open an issue or PR.
|
| 262 |
+
|
| 263 |
---
|
| 264 |
|
| 265 |
+
**Built with ❤️ for AI-powered game development**
|
app.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GCP - Game Context Protocol
|
| 3 |
+
Build 3D game scenes with natural language
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import threading
|
| 9 |
+
import uvicorn
|
| 10 |
+
import time
|
| 11 |
+
import requests
|
| 12 |
+
from backend.main import app as fastapi_app
|
| 13 |
+
|
| 14 |
+
# Get base URLs from environment
|
| 15 |
+
# SPACE_URL is the public-facing URL (for Gradio)
|
| 16 |
+
# FastAPI runs on port 8000, Gradio on 7860
|
| 17 |
+
SPACE_URL = os.getenv("SPACE_URL", "http://localhost:7860")
|
| 18 |
+
# For local dev, FastAPI is on a different port; in HF Spaces, use same domain
|
| 19 |
+
FASTAPI_URL = os.getenv("FASTAPI_URL", "http://localhost:8000")
|
| 20 |
+
BASE_URL = SPACE_URL # For display in UI
|
| 21 |
+
|
| 22 |
+
# Global state for current scene
|
| 23 |
+
current_scene_id = None
|
| 24 |
+
current_scene_url = None
|
| 25 |
+
selected_object_id = None # Track currently looked-at object (FPS mode)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def add_cache_buster(url):
|
| 29 |
+
"""Add timestamp to URL to force iframe reload"""
|
| 30 |
+
import time
|
| 31 |
+
timestamp = int(time.time() * 1000)
|
| 32 |
+
separator = "&" if "?" in url else "?"
|
| 33 |
+
return f"{url}{separator}t={timestamp}"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def wait_for_fastapi(max_retries=30, retry_interval=1):
|
| 37 |
+
"""
|
| 38 |
+
Wait for FastAPI to be ready with health check.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
max_retries: Maximum number of health check attempts
|
| 42 |
+
retry_interval: Seconds to wait between retries
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
True if FastAPI is ready, False otherwise
|
| 46 |
+
"""
|
| 47 |
+
print("\n" + "="*60)
|
| 48 |
+
print("⏳ Waiting for FastAPI/MCP server to be ready...")
|
| 49 |
+
print("="*60)
|
| 50 |
+
|
| 51 |
+
for i in range(max_retries):
|
| 52 |
+
try:
|
| 53 |
+
response = requests.get(f"{FASTAPI_URL}/health", timeout=2)
|
| 54 |
+
if response.status_code == 200:
|
| 55 |
+
data = response.json()
|
| 56 |
+
print(f"\n✅ FastAPI is ready! Service: {data.get('service', 'Unknown')}")
|
| 57 |
+
print(f" Version: {data.get('version', 'Unknown')}")
|
| 58 |
+
print(f" Status: {data.get('status', 'Unknown')}")
|
| 59 |
+
print("="*60 + "\n")
|
| 60 |
+
return True
|
| 61 |
+
except (requests.ConnectionError, requests.Timeout):
|
| 62 |
+
if i < max_retries - 1:
|
| 63 |
+
print(f" Attempt {i+1}/{max_retries}: FastAPI not ready yet, retrying in {retry_interval}s...")
|
| 64 |
+
time.sleep(retry_interval)
|
| 65 |
+
else:
|
| 66 |
+
print(f"\n⚠️ FastAPI health check failed after {max_retries} attempts")
|
| 67 |
+
print(" The server might still start, but there could be issues.")
|
| 68 |
+
print("="*60 + "\n")
|
| 69 |
+
return False
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f" Unexpected error during health check: {e}")
|
| 72 |
+
time.sleep(retry_interval)
|
| 73 |
+
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# Start FastAPI/MCP server in background
|
| 78 |
+
def start_fastapi():
|
| 79 |
+
print("\n" + "="*60)
|
| 80 |
+
print("Starting FastAPI/MCP server on port 8000...")
|
| 81 |
+
print("="*60 + "\n")
|
| 82 |
+
uvicorn.run(fastapi_app, host="0.0.0.0", port=8000, log_level="info")
|
| 83 |
+
|
| 84 |
+
fastapi_thread = threading.Thread(target=start_fastapi, daemon=True)
|
| 85 |
+
fastapi_thread.start()
|
| 86 |
+
|
| 87 |
+
# Wait for FastAPI to be ready
|
| 88 |
+
wait_for_fastapi()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def create_default_scene():
|
| 92 |
+
"""Use the clean default Welcome Scene created on server startup"""
|
| 93 |
+
global current_scene_id, current_scene_url
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
# Use the pre-initialized "welcome" scene from storage
|
| 97 |
+
# (created in backend/storage.py on module load)
|
| 98 |
+
current_scene_id = "welcome"
|
| 99 |
+
current_scene_url = f"{FASTAPI_URL}/view/scene/welcome"
|
| 100 |
+
|
| 101 |
+
print(f"✅ Using default Welcome Scene")
|
| 102 |
+
print(f" Scene ID: {current_scene_id}")
|
| 103 |
+
print(f" Viewer URL: {current_scene_url}")
|
| 104 |
+
print(f" - Clean 10x10 FPS world with physics")
|
| 105 |
+
print(f" - Ground plane + walls (created by viewer)")
|
| 106 |
+
print(f" - Player starts at (0, 1, 0)")
|
| 107 |
+
|
| 108 |
+
return current_scene_url
|
| 109 |
+
|
| 110 |
+
except Exception as e:
|
| 111 |
+
import traceback
|
| 112 |
+
print(f"❌ Error loading default scene: {e}")
|
| 113 |
+
print(f" Full traceback:")
|
| 114 |
+
traceback.print_exc()
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# Initialize the GPT chat client
|
| 119 |
+
gpt_client = None
|
| 120 |
+
|
| 121 |
+
def get_gpt_client():
|
| 122 |
+
"""Get or create the GPT chat client"""
|
| 123 |
+
global gpt_client, current_scene_id
|
| 124 |
+
if gpt_client is None or gpt_client.scene_id != current_scene_id:
|
| 125 |
+
from chat_client import GCPChatClient
|
| 126 |
+
gpt_client = GCPChatClient(scene_id=current_scene_id, base_url=FASTAPI_URL)
|
| 127 |
+
return gpt_client
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def chat_response(message, history):
|
| 131 |
+
"""Handle chat messages using GPT with tool calling"""
|
| 132 |
+
global current_scene_id, current_scene_url
|
| 133 |
+
|
| 134 |
+
# Handle help command locally (no need for LLM)
|
| 135 |
+
if message.lower().strip() == "help":
|
| 136 |
+
return """**GCP - Game Context Protocol**
|
| 137 |
+
|
| 138 |
+
I'm an AI assistant that can help you build 3D scenes using natural language.
|
| 139 |
+
|
| 140 |
+
**What I can do:**
|
| 141 |
+
- Add objects: "add a red cube at 2, 1, 0"
|
| 142 |
+
- Change lighting: "set lighting to night"
|
| 143 |
+
- Configure player: "set speed to 10" or "make the player move half as fast"
|
| 144 |
+
- Add lights: "add a point light above the cube"
|
| 145 |
+
- Update materials: "make it shiny and metallic"
|
| 146 |
+
- Set backgrounds: "gradient background from blue to orange"
|
| 147 |
+
- Add fog: "add some fog to the scene"
|
| 148 |
+
- Query state: "what's the current player speed?" or "show me the scene info"
|
| 149 |
+
|
| 150 |
+
**I understand context**, so you can say things like:
|
| 151 |
+
- "double the jump force"
|
| 152 |
+
- "make it twice as bright"
|
| 153 |
+
- "reduce gravity by half"
|
| 154 |
+
|
| 155 |
+
**Tips:**
|
| 156 |
+
- Press C in viewer to toggle FPS/Orbit camera
|
| 157 |
+
- WASD to move, Space to jump in FPS mode
|
| 158 |
+
- Click in viewer to enable mouse-look
|
| 159 |
+
""", None
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
client = get_gpt_client()
|
| 163 |
+
response, action_data = client.chat(message)
|
| 164 |
+
return response, action_data
|
| 165 |
+
except Exception as e:
|
| 166 |
+
import traceback
|
| 167 |
+
traceback.print_exc()
|
| 168 |
+
return f"Error: {str(e)}", None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# Create default scene on startup
|
| 172 |
+
print("Creating default scene...")
|
| 173 |
+
default_viewer_url = create_default_scene()
|
| 174 |
+
print(f"Default viewer URL: {default_viewer_url}")
|
| 175 |
+
if not default_viewer_url:
|
| 176 |
+
print("⚠️ WARNING: Default scene creation failed! No viewer URL generated.")
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# Minimal CSS - only essential styling, let Gradio handle layout
|
| 180 |
+
APP_CSS = """
|
| 181 |
+
/* Viewer iframe needs explicit sizing */
|
| 182 |
+
#viewer-container {
|
| 183 |
+
width: 100%;
|
| 184 |
+
height: 600px;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
#viewer-container iframe {
|
| 188 |
+
width: 100%;
|
| 189 |
+
height: 100%;
|
| 190 |
+
border: none;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* PostMessage Container - must be invisible */
|
| 194 |
+
#postmessage-container {
|
| 195 |
+
position: absolute;
|
| 196 |
+
width: 0;
|
| 197 |
+
height: 0;
|
| 198 |
+
overflow: hidden;
|
| 199 |
+
pointer-events: none;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* Toast Notifications */
|
| 203 |
+
#toast-container {
|
| 204 |
+
position: fixed;
|
| 205 |
+
top: 20px;
|
| 206 |
+
right: 20px;
|
| 207 |
+
z-index: 200;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.toast {
|
| 211 |
+
background: rgba(0, 0, 0, 0.9);
|
| 212 |
+
color: white;
|
| 213 |
+
padding: 15px 20px;
|
| 214 |
+
border-radius: 8px;
|
| 215 |
+
margin-bottom: 10px;
|
| 216 |
+
border-left: 4px solid #2196f3;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.toast.success { border-left-color: #4caf50; }
|
| 220 |
+
.toast.error { border-left-color: #f44336; }
|
| 221 |
+
|
| 222 |
+
/* Hide the action data textbox but keep it in DOM for events to fire */
|
| 223 |
+
.hidden-action {
|
| 224 |
+
position: absolute !important;
|
| 225 |
+
width: 1px !important;
|
| 226 |
+
height: 1px !important;
|
| 227 |
+
padding: 0 !important;
|
| 228 |
+
margin: -1px !important;
|
| 229 |
+
overflow: hidden !important;
|
| 230 |
+
clip: rect(0, 0, 0, 0) !important;
|
| 231 |
+
white-space: nowrap !important;
|
| 232 |
+
border: 0 !important;
|
| 233 |
+
}
|
| 234 |
+
"""
|
| 235 |
+
|
| 236 |
+
# Build immersive chat interface with overlay
|
| 237 |
+
with gr.Blocks(
|
| 238 |
+
title="GCP - Game Context Protocol",
|
| 239 |
+
) as demo:
|
| 240 |
+
# Initialize JavaScript functionality (minimal essentials only)
|
| 241 |
+
gr.HTML("""
|
| 242 |
+
<script>
|
| 243 |
+
(function() {
|
| 244 |
+
// PostMessage API Helper - sends commands to the 3D viewer iframe
|
| 245 |
+
window.sendMessageToViewer = function(action, data) {
|
| 246 |
+
const iframe = document.querySelector('#viewer-container iframe');
|
| 247 |
+
if (iframe && iframe.contentWindow) {
|
| 248 |
+
iframe.contentWindow.postMessage({ action, data }, '*');
|
| 249 |
+
}
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
// Toast Notification Function
|
| 253 |
+
window.showToast = function(message, type = 'info') {
|
| 254 |
+
let toastContainer = document.getElementById('toast-container');
|
| 255 |
+
if (!toastContainer) {
|
| 256 |
+
toastContainer = document.createElement('div');
|
| 257 |
+
toastContainer.id = 'toast-container';
|
| 258 |
+
document.body.appendChild(toastContainer);
|
| 259 |
+
}
|
| 260 |
+
const toast = document.createElement('div');
|
| 261 |
+
toast.className = `toast ${type}`;
|
| 262 |
+
toast.textContent = message;
|
| 263 |
+
toastContainer.appendChild(toast);
|
| 264 |
+
setTimeout(() => toast.remove(), 3000);
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
// Loading Indicator Functions
|
| 268 |
+
window.showLoading = function() {
|
| 269 |
+
let loadingIndicator = document.getElementById('loading-indicator');
|
| 270 |
+
if (!loadingIndicator) {
|
| 271 |
+
loadingIndicator = document.createElement('div');
|
| 272 |
+
loadingIndicator.id = 'loading-indicator';
|
| 273 |
+
loadingIndicator.innerHTML = '<div class="spinner"></div>';
|
| 274 |
+
document.body.appendChild(loadingIndicator);
|
| 275 |
+
}
|
| 276 |
+
loadingIndicator.style.display = 'block';
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
window.hideLoading = function() {
|
| 280 |
+
const loadingIndicator = document.getElementById('loading-indicator');
|
| 281 |
+
if (loadingIndicator) loadingIndicator.style.display = 'none';
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
// Handle messages from iframe (screenshot, object selection)
|
| 285 |
+
window.addEventListener('message', function(event) {
|
| 286 |
+
if (event.data && event.data.action === 'screenshot') {
|
| 287 |
+
const { dataURL, sceneName, timestamp } = event.data.data;
|
| 288 |
+
const link = document.createElement('a');
|
| 289 |
+
link.href = dataURL;
|
| 290 |
+
link.download = `${sceneName}_${timestamp}.png`;
|
| 291 |
+
link.click();
|
| 292 |
+
if (window.showToast) {
|
| 293 |
+
window.showToast('Screenshot saved!', 'success');
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
if (event.data && event.data.action === 'objectInspect') {
|
| 298 |
+
const objInfo = event.data.data;
|
| 299 |
+
if (window.showToast) {
|
| 300 |
+
window.showToast(`Selected: ${objInfo.name}`, 'info');
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
if (event.data && event.data.action === 'objectSelected') {
|
| 305 |
+
const objData = event.data.data;
|
| 306 |
+
window.selectedObjectId = objData.object_id;
|
| 307 |
+
if (window.showToast) {
|
| 308 |
+
window.showToast(`Looking at: ${objData.object_type} (${objData.distance}m)`, 'info');
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
if (event.data && event.data.action === 'objectDeselected') {
|
| 313 |
+
window.selectedObjectId = null;
|
| 314 |
+
}
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
// Initialize toast container on load
|
| 318 |
+
setTimeout(function() {
|
| 319 |
+
if (!document.getElementById('toast-container')) {
|
| 320 |
+
const toastContainer = document.createElement('div');
|
| 321 |
+
toastContainer.id = 'toast-container';
|
| 322 |
+
document.body.appendChild(toastContainer);
|
| 323 |
+
}
|
| 324 |
+
}, 1000);
|
| 325 |
+
|
| 326 |
+
})();
|
| 327 |
+
</script>
|
| 328 |
+
""")
|
| 329 |
+
|
| 330 |
+
# State for file handling
|
| 331 |
+
file_state = gr.State([])
|
| 332 |
+
|
| 333 |
+
# Component for passing action data to JavaScript via .change() event
|
| 334 |
+
#
|
| 335 |
+
# IMPORTANT: DO NOT set visible=False!
|
| 336 |
+
# Gradio's visible=False removes the component from the DOM or renders it in a way
|
| 337 |
+
# that prevents .change() events from firing when the value is updated programmatically.
|
| 338 |
+
# Instead, we use visible=True and hide it with CSS (see .hidden-action in APP_CSS).
|
| 339 |
+
# This keeps the element in the DOM so events fire properly.
|
| 340 |
+
#
|
| 341 |
+
# The flow: bot() returns JSON → action_data updates → .change() fires → JS sends postMessage to iframe
|
| 342 |
+
action_data = gr.Textbox(value="", elem_id="action-data", visible=True, elem_classes=["hidden-action"])
|
| 343 |
+
|
| 344 |
+
# Main container - side by side layout: Chat (left) | Viewer (right)
|
| 345 |
+
with gr.Row(elem_id="main-container", equal_height=True):
|
| 346 |
+
# Left column: Chat interface (scale=1 = ~25% width)
|
| 347 |
+
with gr.Column(elem_id="chat-column", scale=1, min_width=350):
|
| 348 |
+
gr.Markdown("### 🎮 GCP - Game Context Protocol")
|
| 349 |
+
chatbot = gr.Chatbot(
|
| 350 |
+
height=500, # Taller to fill vertical space
|
| 351 |
+
show_label=False,
|
| 352 |
+
elem_id="chatbot",
|
| 353 |
+
# Gradio 6: type="messages" is now the default, removed
|
| 354 |
+
)
|
| 355 |
+
msg = gr.Textbox(
|
| 356 |
+
placeholder="'add a red cube' • 'set lighting to night' • 'help'",
|
| 357 |
+
show_label=False,
|
| 358 |
+
container=False,
|
| 359 |
+
elem_id="chat-input"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
# Right column: 3D Viewer (scale=3 = ~75% width)
|
| 363 |
+
with gr.Column(elem_id="viewer-column", scale=3):
|
| 364 |
+
if default_viewer_url:
|
| 365 |
+
initial_viewer_html = f'<div id="viewer-container"><iframe src="{default_viewer_url}"></iframe></div>'
|
| 366 |
+
print(f"📊 Setting up viewer iframe with src: {default_viewer_url}")
|
| 367 |
+
else:
|
| 368 |
+
initial_viewer_html = '<div id="viewer-container" style="display: flex; align-items: center; justify-content: center; color: #666;"><p>⚠️ Scene failed to load. Check console logs.</p></div>'
|
| 369 |
+
print("⚠️ No viewer URL available - showing error message")
|
| 370 |
+
|
| 371 |
+
viewer = gr.HTML(
|
| 372 |
+
value=initial_viewer_html,
|
| 373 |
+
elem_id="viewer-fullscreen"
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
def user(user_message, history):
|
| 377 |
+
"""Handle user input"""
|
| 378 |
+
history = history or []
|
| 379 |
+
history.append({"role": "user", "content": user_message})
|
| 380 |
+
return "", history
|
| 381 |
+
|
| 382 |
+
def bot(history):
|
| 383 |
+
"""Generate bot response"""
|
| 384 |
+
# Gradio 6: content can be a string or list of content blocks
|
| 385 |
+
content = history[-1]["content"]
|
| 386 |
+
if isinstance(content, list):
|
| 387 |
+
# Extract text from content blocks
|
| 388 |
+
user_message = " ".join(
|
| 389 |
+
block.get("text", "") if isinstance(block, dict) else str(block)
|
| 390 |
+
for block in content
|
| 391 |
+
)
|
| 392 |
+
else:
|
| 393 |
+
user_message = content
|
| 394 |
+
|
| 395 |
+
# Process command (history not used by chat_response)
|
| 396 |
+
bot_message, action_result = chat_response(user_message, [])
|
| 397 |
+
history.append({"role": "assistant", "content": bot_message})
|
| 398 |
+
|
| 399 |
+
# Handle action_result
|
| 400 |
+
viewer_html = viewer.value # Default: keep current viewer
|
| 401 |
+
action_json = "" # Default: no action (empty string)
|
| 402 |
+
|
| 403 |
+
if action_result:
|
| 404 |
+
action_type = action_result.get("action")
|
| 405 |
+
|
| 406 |
+
if action_type == "reload":
|
| 407 |
+
# Full reload: update iframe src
|
| 408 |
+
viewer_html = f'<div id="viewer-container" style="width:100%; min-height:500px; height:70vh;"><iframe src="{action_result["url"]}" style="width:100%; height:100%; border:none;"></iframe></div>'
|
| 409 |
+
|
| 410 |
+
elif action_type in ["addObject", "setLighting", "setControlMode", "updateMaterial", "addLight", "removeLight", "updateLight", "setBackground", "setFog"]:
|
| 411 |
+
# Build action JSON for the JavaScript watcher
|
| 412 |
+
import json
|
| 413 |
+
import time
|
| 414 |
+
|
| 415 |
+
# Determine toast message based on action type
|
| 416 |
+
toast_message = ""
|
| 417 |
+
if action_type == "addObject":
|
| 418 |
+
obj_type = action_result["data"].get("type", "object")
|
| 419 |
+
toast_message = f"Added {obj_type} to scene"
|
| 420 |
+
elif action_type == "setLighting":
|
| 421 |
+
toast_message = "Lighting updated"
|
| 422 |
+
elif action_type == "setControlMode":
|
| 423 |
+
mode = action_result["data"].get("mode", "")
|
| 424 |
+
toast_message = f"Switched to {mode.upper()} mode"
|
| 425 |
+
elif action_type == "updateMaterial":
|
| 426 |
+
toast_message = "Material updated"
|
| 427 |
+
elif action_type == "addLight":
|
| 428 |
+
light_name = action_result["data"].get("name", "Light")
|
| 429 |
+
toast_message = f"Added light: {light_name}"
|
| 430 |
+
elif action_type == "removeLight":
|
| 431 |
+
toast_message = "Light removed"
|
| 432 |
+
elif action_type == "updateLight":
|
| 433 |
+
toast_message = "Light updated"
|
| 434 |
+
elif action_type == "setBackground":
|
| 435 |
+
toast_message = "Background updated"
|
| 436 |
+
elif action_type == "setFog":
|
| 437 |
+
toast_message = "Fog updated"
|
| 438 |
+
|
| 439 |
+
# Create JSON payload for the .then() JavaScript handler
|
| 440 |
+
action_json = json.dumps({
|
| 441 |
+
"action": action_result["action"],
|
| 442 |
+
"data": action_result["data"],
|
| 443 |
+
"toast": toast_message,
|
| 444 |
+
"toastType": "success"
|
| 445 |
+
})
|
| 446 |
+
|
| 447 |
+
return history, viewer_html, action_json
|
| 448 |
+
|
| 449 |
+
# When action_data changes, this handler sends postMessage to the iframe
|
| 450 |
+
def handle_action_change(action_json):
|
| 451 |
+
return action_json # Pass through for JS
|
| 452 |
+
|
| 453 |
+
action_data.change(
|
| 454 |
+
fn=handle_action_change,
|
| 455 |
+
inputs=[action_data],
|
| 456 |
+
outputs=[action_data],
|
| 457 |
+
js="""
|
| 458 |
+
(actionJson) => {
|
| 459 |
+
if (actionJson && actionJson.length > 2) {
|
| 460 |
+
try {
|
| 461 |
+
const actionData = JSON.parse(actionJson);
|
| 462 |
+
const iframe = document.querySelector('#viewer-container iframe');
|
| 463 |
+
if (iframe && iframe.contentWindow) {
|
| 464 |
+
iframe.contentWindow.postMessage({
|
| 465 |
+
action: actionData.action,
|
| 466 |
+
data: actionData.data
|
| 467 |
+
}, '*');
|
| 468 |
+
if (actionData.toast && window.showToast) {
|
| 469 |
+
window.showToast(actionData.toast, actionData.toastType || 'success');
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
} catch (e) {
|
| 473 |
+
// Silently ignore parse errors
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
return actionJson;
|
| 477 |
+
}
|
| 478 |
+
"""
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
msg.submit(
|
| 482 |
+
user,
|
| 483 |
+
[msg, chatbot],
|
| 484 |
+
[msg, chatbot],
|
| 485 |
+
queue=False
|
| 486 |
+
).then(
|
| 487 |
+
bot,
|
| 488 |
+
[chatbot],
|
| 489 |
+
[chatbot, viewer, action_data]
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
if __name__ == "__main__":
|
| 494 |
+
# Enable queue for handling multiple concurrent users (important for HF Spaces)
|
| 495 |
+
demo.queue()
|
| 496 |
+
# Gradio 6: theme and css moved from gr.Blocks() to launch()
|
| 497 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft(), css=APP_CSS)
|
backend/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
3DViz MCP Server Backend Package
|
| 3 |
+
"""
|
| 4 |
+
__version__ = "1.0.0"
|
backend/game_models.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data models for GCP - Game Context Protocol
|
| 3 |
+
Using plain dictionaries for simplicity and clarity.
|
| 4 |
+
"""
|
| 5 |
+
from typing import Optional, Literal, Dict, Any, List
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Game Types
|
| 11 |
+
ObjectType = Literal["cube", "sphere", "cylinder", "plane", "cone", "torus", "model"]
|
| 12 |
+
LightType = Literal["ambient", "directional", "point", "spot"]
|
| 13 |
+
LightingPreset = Literal["day", "night", "sunset", "studio"]
|
| 14 |
+
CameraMode = Literal["fps", "orbit", "top_down", "free"]
|
| 15 |
+
MaterialType = Literal["standard", "basic", "phong", "toon"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Factory functions for creating default data structures
|
| 19 |
+
|
| 20 |
+
def create_vector3(x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Dict[str, float]:
|
| 21 |
+
"""Create a 3D vector for position, rotation, or scale."""
|
| 22 |
+
return {"x": x, "y": y, "z": z}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def create_material(
|
| 26 |
+
type: MaterialType = "standard",
|
| 27 |
+
color: str = "#ffffff",
|
| 28 |
+
metalness: float = 0.5,
|
| 29 |
+
roughness: float = 0.5,
|
| 30 |
+
opacity: float = 1.0,
|
| 31 |
+
wireframe: bool = False
|
| 32 |
+
) -> Dict[str, Any]:
|
| 33 |
+
"""Create material properties."""
|
| 34 |
+
return {
|
| 35 |
+
"type": type,
|
| 36 |
+
"color": color,
|
| 37 |
+
"metalness": metalness,
|
| 38 |
+
"roughness": roughness,
|
| 39 |
+
"opacity": opacity,
|
| 40 |
+
"wireframe": wireframe
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def create_game_object(
|
| 45 |
+
object_type: ObjectType = "cube",
|
| 46 |
+
name: Optional[str] = None,
|
| 47 |
+
position: Optional[Dict[str, float]] = None,
|
| 48 |
+
rotation: Optional[Dict[str, float]] = None,
|
| 49 |
+
scale: Optional[Dict[str, float]] = None,
|
| 50 |
+
material: Optional[Dict[str, Any]] = None,
|
| 51 |
+
model_path: Optional[str] = None,
|
| 52 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 53 |
+
) -> Dict[str, Any]:
|
| 54 |
+
"""Create a 3D game object."""
|
| 55 |
+
return {
|
| 56 |
+
"id": str(uuid.uuid4()),
|
| 57 |
+
"name": name,
|
| 58 |
+
"type": object_type,
|
| 59 |
+
"position": position or create_vector3(),
|
| 60 |
+
"rotation": rotation or create_vector3(),
|
| 61 |
+
"scale": scale or create_vector3(1, 1, 1),
|
| 62 |
+
"material": material or create_material(),
|
| 63 |
+
"model_path": model_path,
|
| 64 |
+
"metadata": metadata or {},
|
| 65 |
+
"created_at": datetime.utcnow().isoformat()
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def create_light(
|
| 70 |
+
light_type: LightType = "directional",
|
| 71 |
+
name: Optional[str] = None,
|
| 72 |
+
color: str = "#ffffff",
|
| 73 |
+
intensity: float = 1.0,
|
| 74 |
+
position: Optional[Dict[str, float]] = None,
|
| 75 |
+
target: Optional[Dict[str, float]] = None,
|
| 76 |
+
cast_shadow: bool = True
|
| 77 |
+
) -> Dict[str, Any]:
|
| 78 |
+
"""Create a light source."""
|
| 79 |
+
return {
|
| 80 |
+
"id": str(uuid.uuid4()),
|
| 81 |
+
"name": name,
|
| 82 |
+
"type": light_type,
|
| 83 |
+
"color": color,
|
| 84 |
+
"intensity": intensity,
|
| 85 |
+
"position": position or create_vector3(10, 10, 10),
|
| 86 |
+
"target": target,
|
| 87 |
+
"cast_shadow": cast_shadow
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def create_environment(
|
| 92 |
+
background_color: str = "#87CEEB",
|
| 93 |
+
fog_enabled: bool = False,
|
| 94 |
+
fog_color: str = "#ffffff",
|
| 95 |
+
fog_near: float = 10.0,
|
| 96 |
+
fog_far: float = 100.0,
|
| 97 |
+
ambient_light_intensity: float = 0.5,
|
| 98 |
+
lighting_preset: LightingPreset = "day"
|
| 99 |
+
) -> Dict[str, Any]:
|
| 100 |
+
"""Create environment settings."""
|
| 101 |
+
return {
|
| 102 |
+
"background_color": background_color,
|
| 103 |
+
"fog_enabled": fog_enabled,
|
| 104 |
+
"fog_color": fog_color,
|
| 105 |
+
"fog_near": fog_near,
|
| 106 |
+
"fog_far": fog_far,
|
| 107 |
+
"ambient_light_intensity": ambient_light_intensity,
|
| 108 |
+
"lighting_preset": lighting_preset
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def create_player(
|
| 113 |
+
position: Optional[Dict[str, float]] = None,
|
| 114 |
+
rotation: Optional[Dict[str, float]] = None,
|
| 115 |
+
camera_mode: CameraMode = "orbit",
|
| 116 |
+
movement_speed: float = 5.0,
|
| 117 |
+
look_sensitivity: float = 0.002
|
| 118 |
+
) -> Dict[str, Any]:
|
| 119 |
+
"""Create player/camera configuration."""
|
| 120 |
+
return {
|
| 121 |
+
"position": position or create_vector3(0, 5, 10),
|
| 122 |
+
"rotation": rotation or create_vector3(),
|
| 123 |
+
"camera_mode": camera_mode,
|
| 124 |
+
"movement_speed": movement_speed,
|
| 125 |
+
"look_sensitivity": look_sensitivity
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def create_player_config(
|
| 130 |
+
move_speed: float = 5.0,
|
| 131 |
+
jump_force: float = 5.0,
|
| 132 |
+
mouse_sensitivity: float = 0.002,
|
| 133 |
+
invert_y: bool = False,
|
| 134 |
+
gravity: float = -9.82,
|
| 135 |
+
player_height: float = 1.7,
|
| 136 |
+
player_radius: float = 0.3,
|
| 137 |
+
eye_height: float = 1.6,
|
| 138 |
+
player_mass: float = 80.0,
|
| 139 |
+
linear_damping: float = 0.9,
|
| 140 |
+
movement_acceleration: float = 0.0,
|
| 141 |
+
air_control: float = 1.0,
|
| 142 |
+
camera_fov: float = 75.0,
|
| 143 |
+
min_pitch: float = -89.0,
|
| 144 |
+
max_pitch: float = 89.0
|
| 145 |
+
) -> Dict[str, Any]:
|
| 146 |
+
"""
|
| 147 |
+
Create player controller configuration for FPS mode.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
move_speed: Horizontal movement speed in units/second
|
| 151 |
+
jump_force: Initial upward velocity for jumps in m/s
|
| 152 |
+
mouse_sensitivity: Mouse look sensitivity multiplier
|
| 153 |
+
invert_y: Whether to invert vertical mouse look
|
| 154 |
+
gravity: World gravity in m/s² (negative = downward)
|
| 155 |
+
player_height: Player collision capsule height in meters
|
| 156 |
+
player_radius: Player collision capsule radius in meters
|
| 157 |
+
eye_height: Camera height from player feet in meters
|
| 158 |
+
player_mass: Player body mass in kg
|
| 159 |
+
linear_damping: Air resistance (0.0-1.0, higher = more friction)
|
| 160 |
+
movement_acceleration: Acceleration time (0.0=instant, higher=slower)
|
| 161 |
+
air_control: Movement control while airborne (0.0-1.0)
|
| 162 |
+
camera_fov: Field of view in degrees (typical: 60-90)
|
| 163 |
+
min_pitch: Minimum vertical look angle in degrees (looking down)
|
| 164 |
+
max_pitch: Maximum vertical look angle in degrees (looking up)
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Dictionary with player controller configuration
|
| 168 |
+
"""
|
| 169 |
+
return {
|
| 170 |
+
"move_speed": move_speed,
|
| 171 |
+
"jump_force": jump_force,
|
| 172 |
+
"mouse_sensitivity": mouse_sensitivity,
|
| 173 |
+
"invert_y": invert_y,
|
| 174 |
+
"gravity": gravity,
|
| 175 |
+
"player_height": player_height,
|
| 176 |
+
"player_radius": player_radius,
|
| 177 |
+
"eye_height": eye_height,
|
| 178 |
+
"player_mass": player_mass,
|
| 179 |
+
"linear_damping": linear_damping,
|
| 180 |
+
"movement_acceleration": movement_acceleration,
|
| 181 |
+
"air_control": air_control,
|
| 182 |
+
"camera_fov": camera_fov,
|
| 183 |
+
"min_pitch": min_pitch,
|
| 184 |
+
"max_pitch": max_pitch
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def create_scene(
|
| 189 |
+
name: str = "Untitled Scene",
|
| 190 |
+
description: Optional[str] = None,
|
| 191 |
+
world_width: float = 100.0,
|
| 192 |
+
world_height: float = 100.0,
|
| 193 |
+
world_depth: float = 100.0,
|
| 194 |
+
objects: Optional[List[Dict[str, Any]]] = None,
|
| 195 |
+
lights: Optional[List[Dict[str, Any]]] = None,
|
| 196 |
+
environment: Optional[Dict[str, Any]] = None,
|
| 197 |
+
player: Optional[Dict[str, Any]] = None,
|
| 198 |
+
show_grid: bool = True,
|
| 199 |
+
grid_size: float = 100.0,
|
| 200 |
+
grid_divisions: int = 20,
|
| 201 |
+
tags: Optional[List[str]] = None
|
| 202 |
+
) -> Dict[str, Any]:
|
| 203 |
+
"""Create a complete 3D scene."""
|
| 204 |
+
now = datetime.utcnow().isoformat()
|
| 205 |
+
return {
|
| 206 |
+
"scene_id": str(uuid.uuid4()),
|
| 207 |
+
"name": name,
|
| 208 |
+
"description": description,
|
| 209 |
+
"world_width": world_width,
|
| 210 |
+
"world_height": world_height,
|
| 211 |
+
"world_depth": world_depth,
|
| 212 |
+
"objects": objects or [],
|
| 213 |
+
"lights": lights or [],
|
| 214 |
+
"environment": environment or create_environment(),
|
| 215 |
+
"player": player or create_player(),
|
| 216 |
+
"show_grid": show_grid,
|
| 217 |
+
"grid_size": grid_size,
|
| 218 |
+
"grid_divisions": grid_divisions,
|
| 219 |
+
"tags": tags or [],
|
| 220 |
+
"created_at": now,
|
| 221 |
+
"updated_at": now
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# Validation helpers (optional, for type checking)
|
| 226 |
+
|
| 227 |
+
def validate_vector3(v: Dict[str, float]) -> bool:
|
| 228 |
+
"""Check if a dict is a valid Vector3."""
|
| 229 |
+
return isinstance(v, dict) and all(k in v for k in ["x", "y", "z"])
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def validate_lighting_preset(preset: str) -> bool:
|
| 233 |
+
"""Check if a string is a valid lighting preset."""
|
| 234 |
+
return preset in ["day", "night", "sunset", "studio"]
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def validate_object_type(obj_type: str) -> bool:
|
| 238 |
+
"""Check if a string is a valid object type."""
|
| 239 |
+
return obj_type in ["cube", "sphere", "cylinder", "plane", "cone", "torus", "model"]
|
backend/main.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GCP - Game Context Protocol Server
|
| 3 |
+
FastAPI server for the 3D scene viewer HTTP endpoints.
|
| 4 |
+
MCP tools are defined in mcp_server.py
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
from fastapi import FastAPI, HTTPException
|
| 8 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
|
| 11 |
+
from backend.storage import storage
|
| 12 |
+
|
| 13 |
+
# Create FastAPI app
|
| 14 |
+
app = FastAPI(
|
| 15 |
+
title="GCP - Game Context Protocol",
|
| 16 |
+
description="3D scene building server. Use MCP tools via Claude/AI assistants, or view scenes via HTTP.",
|
| 17 |
+
version="2.0.0",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Add CORS middleware
|
| 21 |
+
app.add_middleware(
|
| 22 |
+
CORSMiddleware,
|
| 23 |
+
allow_origins=["*"],
|
| 24 |
+
allow_credentials=True,
|
| 25 |
+
allow_methods=["*"],
|
| 26 |
+
allow_headers=["*"],
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# =============================================================================
|
| 31 |
+
# HTTP Endpoints (for viewer and health checks)
|
| 32 |
+
# =============================================================================
|
| 33 |
+
|
| 34 |
+
@app.get("/health")
|
| 35 |
+
async def health_check():
|
| 36 |
+
"""Health check endpoint for startup verification."""
|
| 37 |
+
return {
|
| 38 |
+
"status": "healthy",
|
| 39 |
+
"service": "GCP - Game Context Protocol",
|
| 40 |
+
"version": "2.0.0",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@app.get("/")
|
| 45 |
+
async def root():
|
| 46 |
+
"""Root endpoint with API information."""
|
| 47 |
+
return {
|
| 48 |
+
"name": "GCP - Game Context Protocol",
|
| 49 |
+
"version": "2.0.0",
|
| 50 |
+
"description": "MCP server for building 3D scenes with AI assistants",
|
| 51 |
+
"endpoints": {
|
| 52 |
+
"viewer": "/view/scene/{scene_id}",
|
| 53 |
+
"scene_data": "/api/scenes/{scene_id}",
|
| 54 |
+
"health": "/health",
|
| 55 |
+
},
|
| 56 |
+
"mcp": "Connect via stdio transport using: python -m backend.mcp_server",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@app.get("/api/scenes/{scene_id}")
|
| 61 |
+
async def get_scene_api(scene_id: str):
|
| 62 |
+
"""Get scene configuration and data as JSON."""
|
| 63 |
+
scene = storage.get(scene_id)
|
| 64 |
+
if not scene:
|
| 65 |
+
raise HTTPException(status_code=404, detail=f"Scene '{scene_id}' not found")
|
| 66 |
+
return scene
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@app.get("/view/scene/{scene_id}")
|
| 70 |
+
async def view_scene(scene_id: str):
|
| 71 |
+
"""Serve the Three.js viewer for a scene."""
|
| 72 |
+
scene = storage.get(scene_id)
|
| 73 |
+
if not scene:
|
| 74 |
+
raise HTTPException(status_code=404, detail=f"Scene '{scene_id}' not found")
|
| 75 |
+
|
| 76 |
+
# Serve the game viewer HTML
|
| 77 |
+
try:
|
| 78 |
+
viewer_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "game_viewer.html")
|
| 79 |
+
with open(viewer_path, 'r') as f:
|
| 80 |
+
html_content = f.read()
|
| 81 |
+
return HTMLResponse(content=html_content)
|
| 82 |
+
except FileNotFoundError:
|
| 83 |
+
raise HTTPException(status_code=500, detail="Game viewer HTML not found")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@app.get("/manifest.json")
|
| 87 |
+
async def get_manifest():
|
| 88 |
+
"""Serve PWA manifest to avoid 404 errors."""
|
| 89 |
+
try:
|
| 90 |
+
manifest_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "manifest.json")
|
| 91 |
+
with open(manifest_path, 'r') as f:
|
| 92 |
+
import json
|
| 93 |
+
manifest_content = json.load(f)
|
| 94 |
+
return JSONResponse(content=manifest_content)
|
| 95 |
+
except FileNotFoundError:
|
| 96 |
+
return JSONResponse(content={"name": "GCP", "short_name": "GCP"})
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# =============================================================================
|
| 100 |
+
# Run server
|
| 101 |
+
# =============================================================================
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
import uvicorn
|
| 105 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
backend/mcp_server.py
ADDED
|
@@ -0,0 +1,1119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GCP - Game Context Protocol
|
| 3 |
+
MCP Server using the official Anthropic MCP SDK
|
| 4 |
+
|
| 5 |
+
This module defines all MCP tools for building 3D scenes with AI assistants.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
from typing import Any
|
| 9 |
+
from mcp.server import Server
|
| 10 |
+
from mcp.types import Tool, TextContent
|
| 11 |
+
|
| 12 |
+
# Import tool implementations
|
| 13 |
+
from backend.tools.scene_tools import (
|
| 14 |
+
create_game_scene,
|
| 15 |
+
add_game_object,
|
| 16 |
+
remove_game_object,
|
| 17 |
+
set_scene_lighting,
|
| 18 |
+
get_scene_info,
|
| 19 |
+
)
|
| 20 |
+
from backend.tools.player_tools import (
|
| 21 |
+
set_player_speed,
|
| 22 |
+
set_jump_force,
|
| 23 |
+
set_mouse_sensitivity,
|
| 24 |
+
set_gravity,
|
| 25 |
+
set_player_dimensions,
|
| 26 |
+
set_movement_acceleration,
|
| 27 |
+
set_air_control,
|
| 28 |
+
set_camera_fov,
|
| 29 |
+
set_vertical_look_limits,
|
| 30 |
+
get_player_config,
|
| 31 |
+
)
|
| 32 |
+
from backend.tools.rendering_tools import (
|
| 33 |
+
add_light,
|
| 34 |
+
remove_light,
|
| 35 |
+
update_light,
|
| 36 |
+
get_lights,
|
| 37 |
+
update_object_material,
|
| 38 |
+
set_background_color,
|
| 39 |
+
set_fog,
|
| 40 |
+
# Post-processing
|
| 41 |
+
set_bloom,
|
| 42 |
+
set_ssao,
|
| 43 |
+
set_color_grading,
|
| 44 |
+
set_vignette,
|
| 45 |
+
get_post_processing,
|
| 46 |
+
# Camera effects
|
| 47 |
+
set_depth_of_field,
|
| 48 |
+
set_motion_blur,
|
| 49 |
+
set_chromatic_aberration,
|
| 50 |
+
get_camera_effects,
|
| 51 |
+
)
|
| 52 |
+
from backend.game_models import create_vector3, create_material
|
| 53 |
+
|
| 54 |
+
# Base URL for viewer links
|
| 55 |
+
BASE_URL = os.getenv("SPACE_URL", "http://localhost:8000")
|
| 56 |
+
|
| 57 |
+
# Create MCP server instance
|
| 58 |
+
server = Server("gcp-server")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# =============================================================================
|
| 62 |
+
# Tool Definitions
|
| 63 |
+
# =============================================================================
|
| 64 |
+
|
| 65 |
+
SCENE_TOOLS = [
|
| 66 |
+
Tool(
|
| 67 |
+
name="create_scene",
|
| 68 |
+
description="""Create a new 3D scene/level.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
name: Name of the scene (default: "New Scene")
|
| 72 |
+
description: Optional description
|
| 73 |
+
world_width: Width of the world in units (default: 100.0)
|
| 74 |
+
world_height: Height of the world in units (default: 100.0)
|
| 75 |
+
world_depth: Depth of the world in units (default: 100.0)
|
| 76 |
+
lighting_preset: "day", "night", "sunset", or "studio" (default: "day")
|
| 77 |
+
|
| 78 |
+
Returns: scene_id, viewer_url, and confirmation message""",
|
| 79 |
+
inputSchema={
|
| 80 |
+
"type": "object",
|
| 81 |
+
"properties": {
|
| 82 |
+
"name": {"type": "string", "default": "New Scene"},
|
| 83 |
+
"description": {"type": "string"},
|
| 84 |
+
"world_width": {"type": "number", "default": 100.0},
|
| 85 |
+
"world_height": {"type": "number", "default": 100.0},
|
| 86 |
+
"world_depth": {"type": "number", "default": 100.0},
|
| 87 |
+
"lighting_preset": {
|
| 88 |
+
"type": "string",
|
| 89 |
+
"enum": ["day", "night", "sunset", "studio"],
|
| 90 |
+
"default": "day"
|
| 91 |
+
},
|
| 92 |
+
},
|
| 93 |
+
},
|
| 94 |
+
),
|
| 95 |
+
Tool(
|
| 96 |
+
name="add_object",
|
| 97 |
+
description="""Add a 3D object to the scene.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
scene_id: ID of the scene to add the object to (required)
|
| 101 |
+
object_type: "cube", "sphere", "cylinder", "plane", "cone", or "torus" (default: "cube")
|
| 102 |
+
name: Optional name for the object
|
| 103 |
+
x, y, z: Position coordinates (default: 0, 0, 0)
|
| 104 |
+
rotation_x, rotation_y, rotation_z: Rotation in degrees (default: 0, 0, 0)
|
| 105 |
+
scale_x, scale_y, scale_z: Scale factors (default: 1, 1, 1)
|
| 106 |
+
color: Hex color code like "#ff0000" for red (default: "#ffffff")
|
| 107 |
+
|
| 108 |
+
Returns: object_id, scene_id, viewer_url, and confirmation message""",
|
| 109 |
+
inputSchema={
|
| 110 |
+
"type": "object",
|
| 111 |
+
"properties": {
|
| 112 |
+
"scene_id": {"type": "string"},
|
| 113 |
+
"object_type": {
|
| 114 |
+
"type": "string",
|
| 115 |
+
"enum": ["cube", "sphere", "cylinder", "plane", "cone", "torus"],
|
| 116 |
+
"default": "cube"
|
| 117 |
+
},
|
| 118 |
+
"name": {"type": "string"},
|
| 119 |
+
"x": {"type": "number", "default": 0.0},
|
| 120 |
+
"y": {"type": "number", "default": 0.0},
|
| 121 |
+
"z": {"type": "number", "default": 0.0},
|
| 122 |
+
"rotation_x": {"type": "number", "default": 0.0},
|
| 123 |
+
"rotation_y": {"type": "number", "default": 0.0},
|
| 124 |
+
"rotation_z": {"type": "number", "default": 0.0},
|
| 125 |
+
"scale_x": {"type": "number", "default": 1.0},
|
| 126 |
+
"scale_y": {"type": "number", "default": 1.0},
|
| 127 |
+
"scale_z": {"type": "number", "default": 1.0},
|
| 128 |
+
"color": {"type": "string", "default": "#ffffff"},
|
| 129 |
+
},
|
| 130 |
+
"required": ["scene_id"],
|
| 131 |
+
},
|
| 132 |
+
),
|
| 133 |
+
Tool(
|
| 134 |
+
name="remove_object",
|
| 135 |
+
description="""Remove an object from the scene.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
scene_id: ID of the scene (required)
|
| 139 |
+
object_id: ID of the object to remove (required)
|
| 140 |
+
|
| 141 |
+
Returns: scene_id, viewer_url, and confirmation message""",
|
| 142 |
+
inputSchema={
|
| 143 |
+
"type": "object",
|
| 144 |
+
"properties": {
|
| 145 |
+
"scene_id": {"type": "string"},
|
| 146 |
+
"object_id": {"type": "string"},
|
| 147 |
+
},
|
| 148 |
+
"required": ["scene_id", "object_id"],
|
| 149 |
+
},
|
| 150 |
+
),
|
| 151 |
+
Tool(
|
| 152 |
+
name="set_lighting",
|
| 153 |
+
description="""Set the lighting preset for the scene.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
scene_id: ID of the scene (required)
|
| 157 |
+
preset: "day", "night", "sunset", or "studio" (default: "day")
|
| 158 |
+
|
| 159 |
+
Returns: scene_id, viewer_url, and confirmation message""",
|
| 160 |
+
inputSchema={
|
| 161 |
+
"type": "object",
|
| 162 |
+
"properties": {
|
| 163 |
+
"scene_id": {"type": "string"},
|
| 164 |
+
"preset": {
|
| 165 |
+
"type": "string",
|
| 166 |
+
"enum": ["day", "night", "sunset", "studio"],
|
| 167 |
+
"default": "day"
|
| 168 |
+
},
|
| 169 |
+
},
|
| 170 |
+
"required": ["scene_id"],
|
| 171 |
+
},
|
| 172 |
+
),
|
| 173 |
+
Tool(
|
| 174 |
+
name="get_scene_info",
|
| 175 |
+
description="""Get detailed information about a scene.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
scene_id: ID of the scene to retrieve (required)
|
| 179 |
+
|
| 180 |
+
Returns: scene details including name, objects, lights, and viewer_url""",
|
| 181 |
+
inputSchema={
|
| 182 |
+
"type": "object",
|
| 183 |
+
"properties": {
|
| 184 |
+
"scene_id": {"type": "string"},
|
| 185 |
+
},
|
| 186 |
+
"required": ["scene_id"],
|
| 187 |
+
},
|
| 188 |
+
),
|
| 189 |
+
]
|
| 190 |
+
|
| 191 |
+
PLAYER_TOOLS = [
|
| 192 |
+
Tool(
|
| 193 |
+
name="set_player_speed",
|
| 194 |
+
description="""Set the player's movement speed.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
scene_id: ID of the scene (required)
|
| 198 |
+
walk_speed: Movement speed in units/second (default: 5.0)
|
| 199 |
+
|
| 200 |
+
Example: set_player_speed(scene_id, walk_speed=8.0) for faster movement""",
|
| 201 |
+
inputSchema={
|
| 202 |
+
"type": "object",
|
| 203 |
+
"properties": {
|
| 204 |
+
"scene_id": {"type": "string"},
|
| 205 |
+
"walk_speed": {"type": "number", "default": 5.0},
|
| 206 |
+
},
|
| 207 |
+
"required": ["scene_id"],
|
| 208 |
+
},
|
| 209 |
+
),
|
| 210 |
+
Tool(
|
| 211 |
+
name="set_jump_force",
|
| 212 |
+
description="""Configure jump height by setting initial upward velocity.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
scene_id: ID of the scene (required)
|
| 216 |
+
jump_force: Initial jump velocity in m/s (default: 5.0, higher = higher jumps)
|
| 217 |
+
|
| 218 |
+
Example: set_jump_force(scene_id, jump_force=7.0) for higher jumps""",
|
| 219 |
+
inputSchema={
|
| 220 |
+
"type": "object",
|
| 221 |
+
"properties": {
|
| 222 |
+
"scene_id": {"type": "string"},
|
| 223 |
+
"jump_force": {"type": "number", "default": 5.0},
|
| 224 |
+
},
|
| 225 |
+
"required": ["scene_id"],
|
| 226 |
+
},
|
| 227 |
+
),
|
| 228 |
+
Tool(
|
| 229 |
+
name="set_mouse_sensitivity",
|
| 230 |
+
description="""Configure mouse look sensitivity and Y-axis inversion.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
scene_id: ID of the scene (required)
|
| 234 |
+
sensitivity: Mouse sensitivity multiplier (default: 0.002, lower = more precise)
|
| 235 |
+
invert_y: Invert vertical look, flight-sim style (default: false)
|
| 236 |
+
|
| 237 |
+
Example: set_mouse_sensitivity(scene_id, sensitivity=0.001) for precise aiming""",
|
| 238 |
+
inputSchema={
|
| 239 |
+
"type": "object",
|
| 240 |
+
"properties": {
|
| 241 |
+
"scene_id": {"type": "string"},
|
| 242 |
+
"sensitivity": {"type": "number", "default": 0.002},
|
| 243 |
+
"invert_y": {"type": "boolean", "default": False},
|
| 244 |
+
},
|
| 245 |
+
"required": ["scene_id"],
|
| 246 |
+
},
|
| 247 |
+
),
|
| 248 |
+
Tool(
|
| 249 |
+
name="set_gravity",
|
| 250 |
+
description="""Set the world's gravity strength.
|
| 251 |
+
|
| 252 |
+
Args:
|
| 253 |
+
scene_id: ID of the scene (required)
|
| 254 |
+
gravity: Gravity in m/s² (default: -9.82 = Earth, negative = downward)
|
| 255 |
+
|
| 256 |
+
Examples:
|
| 257 |
+
-9.82 = Earth gravity (default)
|
| 258 |
+
-1.62 = Moon gravity (floaty)
|
| 259 |
+
-3.7 = Mars gravity
|
| 260 |
+
-20.0 = Heavy gravity""",
|
| 261 |
+
inputSchema={
|
| 262 |
+
"type": "object",
|
| 263 |
+
"properties": {
|
| 264 |
+
"scene_id": {"type": "string"},
|
| 265 |
+
"gravity": {"type": "number", "default": -9.82},
|
| 266 |
+
},
|
| 267 |
+
"required": ["scene_id"],
|
| 268 |
+
},
|
| 269 |
+
),
|
| 270 |
+
Tool(
|
| 271 |
+
name="set_player_dimensions",
|
| 272 |
+
description="""Configure player collision capsule dimensions.
|
| 273 |
+
|
| 274 |
+
Args:
|
| 275 |
+
scene_id: ID of the scene (required)
|
| 276 |
+
height: Player height in meters (default: 1.7)
|
| 277 |
+
radius: Player radius in meters (default: 0.3)
|
| 278 |
+
eye_height: Camera height from feet (default: height - 0.1)
|
| 279 |
+
|
| 280 |
+
Example: set_player_dimensions(scene_id, height=1.2, radius=0.25) for child-sized""",
|
| 281 |
+
inputSchema={
|
| 282 |
+
"type": "object",
|
| 283 |
+
"properties": {
|
| 284 |
+
"scene_id": {"type": "string"},
|
| 285 |
+
"height": {"type": "number", "default": 1.7},
|
| 286 |
+
"radius": {"type": "number", "default": 0.3},
|
| 287 |
+
"eye_height": {"type": "number"},
|
| 288 |
+
},
|
| 289 |
+
"required": ["scene_id"],
|
| 290 |
+
},
|
| 291 |
+
),
|
| 292 |
+
Tool(
|
| 293 |
+
name="set_movement_acceleration",
|
| 294 |
+
description="""Configure how quickly player reaches max speed and movement friction.
|
| 295 |
+
|
| 296 |
+
Args:
|
| 297 |
+
scene_id: ID of the scene (required)
|
| 298 |
+
acceleration: Time to reach max speed (0.0=instant, 0.5=snappy, 1.0=sliding)
|
| 299 |
+
damping: Linear damping/friction (0.0-1.0, higher=more friction, default: 0.9)
|
| 300 |
+
|
| 301 |
+
Example: set_movement_acceleration(scene_id, acceleration=0.3, damping=0.5) for sliding feel""",
|
| 302 |
+
inputSchema={
|
| 303 |
+
"type": "object",
|
| 304 |
+
"properties": {
|
| 305 |
+
"scene_id": {"type": "string"},
|
| 306 |
+
"acceleration": {"type": "number", "default": 0.0},
|
| 307 |
+
"damping": {"type": "number", "default": 0.9},
|
| 308 |
+
},
|
| 309 |
+
"required": ["scene_id"],
|
| 310 |
+
},
|
| 311 |
+
),
|
| 312 |
+
Tool(
|
| 313 |
+
name="set_air_control",
|
| 314 |
+
description="""Configure movement control while airborne (jumping/falling).
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
scene_id: ID of the scene (required)
|
| 318 |
+
air_control_factor: Control while airborne 0.0-1.0 (default: 1.0)
|
| 319 |
+
1.0 = full control (typical FPS)
|
| 320 |
+
0.0 = no air steering (realistic)
|
| 321 |
+
|
| 322 |
+
Example: set_air_control(scene_id, air_control_factor=0.3) for limited air control""",
|
| 323 |
+
inputSchema={
|
| 324 |
+
"type": "object",
|
| 325 |
+
"properties": {
|
| 326 |
+
"scene_id": {"type": "string"},
|
| 327 |
+
"air_control_factor": {"type": "number", "default": 1.0},
|
| 328 |
+
},
|
| 329 |
+
"required": ["scene_id"],
|
| 330 |
+
},
|
| 331 |
+
),
|
| 332 |
+
Tool(
|
| 333 |
+
name="set_camera_fov",
|
| 334 |
+
description="""Set the camera field of view.
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
scene_id: ID of the scene (required)
|
| 338 |
+
fov: Field of view in degrees (default: 75)
|
| 339 |
+
60-70 = narrow/zoomed (competitive shooters)
|
| 340 |
+
75-85 = normal (most games)
|
| 341 |
+
90-120 = wide/"quake pro" style
|
| 342 |
+
|
| 343 |
+
Example: set_camera_fov(scene_id, fov=90.0) for wide angle""",
|
| 344 |
+
inputSchema={
|
| 345 |
+
"type": "object",
|
| 346 |
+
"properties": {
|
| 347 |
+
"scene_id": {"type": "string"},
|
| 348 |
+
"fov": {"type": "number", "default": 75.0},
|
| 349 |
+
},
|
| 350 |
+
"required": ["scene_id"],
|
| 351 |
+
},
|
| 352 |
+
),
|
| 353 |
+
Tool(
|
| 354 |
+
name="set_vertical_look_limits",
|
| 355 |
+
description="""Configure how far up and down the player can look.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
scene_id: ID of the scene (required)
|
| 359 |
+
min_pitch: Min angle in degrees, looking down (default: -89)
|
| 360 |
+
max_pitch: Max angle in degrees, looking up (default: 89)
|
| 361 |
+
|
| 362 |
+
Example: set_vertical_look_limits(scene_id, min_pitch=-45, max_pitch=45) for restricted""",
|
| 363 |
+
inputSchema={
|
| 364 |
+
"type": "object",
|
| 365 |
+
"properties": {
|
| 366 |
+
"scene_id": {"type": "string"},
|
| 367 |
+
"min_pitch": {"type": "number", "default": -89.0},
|
| 368 |
+
"max_pitch": {"type": "number", "default": 89.0},
|
| 369 |
+
},
|
| 370 |
+
"required": ["scene_id"],
|
| 371 |
+
},
|
| 372 |
+
),
|
| 373 |
+
Tool(
|
| 374 |
+
name="get_player_config",
|
| 375 |
+
description="""Get the current player controller configuration.
|
| 376 |
+
|
| 377 |
+
Args:
|
| 378 |
+
scene_id: ID of the scene (required)
|
| 379 |
+
|
| 380 |
+
Returns: All player settings (speed, jump, gravity, dimensions, etc.)""",
|
| 381 |
+
inputSchema={
|
| 382 |
+
"type": "object",
|
| 383 |
+
"properties": {
|
| 384 |
+
"scene_id": {"type": "string"},
|
| 385 |
+
},
|
| 386 |
+
"required": ["scene_id"],
|
| 387 |
+
},
|
| 388 |
+
),
|
| 389 |
+
]
|
| 390 |
+
|
| 391 |
+
RENDERING_TOOLS = [
|
| 392 |
+
Tool(
|
| 393 |
+
name="add_light",
|
| 394 |
+
description="""Add a new light source to the scene.
|
| 395 |
+
|
| 396 |
+
Args:
|
| 397 |
+
scene_id: Scene to modify (required)
|
| 398 |
+
light_type: "ambient", "directional", "point", or "spot" (required)
|
| 399 |
+
name: Unique identifier like "Torch1", "MainLight" (required)
|
| 400 |
+
color: Hex color code (default: "#ffffff")
|
| 401 |
+
intensity: Brightness 0.0-2.0 (default: 1.0)
|
| 402 |
+
position: {x, y, z} for directional/point/spot lights
|
| 403 |
+
target: {x, y, z} where directional/spot lights point
|
| 404 |
+
cast_shadow: Enable shadows (default: false)
|
| 405 |
+
spot_angle: Cone angle in degrees for spot lights (default: 45)
|
| 406 |
+
|
| 407 |
+
Examples:
|
| 408 |
+
add_light(scene_id, "point", "Torch", "#ff6600", 1.5, {x:2, y:3, z:0})
|
| 409 |
+
add_light(scene_id, "ambient", "Fill", "#aaaaaa", 0.3)""",
|
| 410 |
+
inputSchema={
|
| 411 |
+
"type": "object",
|
| 412 |
+
"properties": {
|
| 413 |
+
"scene_id": {"type": "string"},
|
| 414 |
+
"light_type": {
|
| 415 |
+
"type": "string",
|
| 416 |
+
"enum": ["ambient", "directional", "point", "spot"]
|
| 417 |
+
},
|
| 418 |
+
"name": {"type": "string"},
|
| 419 |
+
"color": {"type": "string", "default": "#ffffff"},
|
| 420 |
+
"intensity": {"type": "number", "default": 1.0},
|
| 421 |
+
"position": {
|
| 422 |
+
"type": "object",
|
| 423 |
+
"properties": {
|
| 424 |
+
"x": {"type": "number"},
|
| 425 |
+
"y": {"type": "number"},
|
| 426 |
+
"z": {"type": "number"},
|
| 427 |
+
}
|
| 428 |
+
},
|
| 429 |
+
"target": {
|
| 430 |
+
"type": "object",
|
| 431 |
+
"properties": {
|
| 432 |
+
"x": {"type": "number"},
|
| 433 |
+
"y": {"type": "number"},
|
| 434 |
+
"z": {"type": "number"},
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"cast_shadow": {"type": "boolean", "default": False},
|
| 438 |
+
"spot_angle": {"type": "number", "default": 45.0},
|
| 439 |
+
},
|
| 440 |
+
"required": ["scene_id", "light_type", "name"],
|
| 441 |
+
},
|
| 442 |
+
),
|
| 443 |
+
Tool(
|
| 444 |
+
name="remove_light",
|
| 445 |
+
description="""Remove a light from the scene.
|
| 446 |
+
|
| 447 |
+
Args:
|
| 448 |
+
scene_id: Scene to modify (required)
|
| 449 |
+
light_name: Name of the light to remove (required)
|
| 450 |
+
|
| 451 |
+
Example: remove_light(scene_id, "Torch1")""",
|
| 452 |
+
inputSchema={
|
| 453 |
+
"type": "object",
|
| 454 |
+
"properties": {
|
| 455 |
+
"scene_id": {"type": "string"},
|
| 456 |
+
"light_name": {"type": "string"},
|
| 457 |
+
},
|
| 458 |
+
"required": ["scene_id", "light_name"],
|
| 459 |
+
},
|
| 460 |
+
),
|
| 461 |
+
Tool(
|
| 462 |
+
name="update_light",
|
| 463 |
+
description="""Update existing light properties.
|
| 464 |
+
|
| 465 |
+
Args:
|
| 466 |
+
scene_id: Scene to modify (required)
|
| 467 |
+
light_name: Name of light to update (required)
|
| 468 |
+
color: New hex color (optional)
|
| 469 |
+
intensity: New brightness 0.0-2.0 (optional)
|
| 470 |
+
position: New {x, y, z} position (optional)
|
| 471 |
+
cast_shadow: Enable/disable shadows (optional)
|
| 472 |
+
|
| 473 |
+
Example: update_light(scene_id, "Sun", color="#ffaa00", intensity=0.8)""",
|
| 474 |
+
inputSchema={
|
| 475 |
+
"type": "object",
|
| 476 |
+
"properties": {
|
| 477 |
+
"scene_id": {"type": "string"},
|
| 478 |
+
"light_name": {"type": "string"},
|
| 479 |
+
"color": {"type": "string"},
|
| 480 |
+
"intensity": {"type": "number"},
|
| 481 |
+
"position": {
|
| 482 |
+
"type": "object",
|
| 483 |
+
"properties": {
|
| 484 |
+
"x": {"type": "number"},
|
| 485 |
+
"y": {"type": "number"},
|
| 486 |
+
"z": {"type": "number"},
|
| 487 |
+
}
|
| 488 |
+
},
|
| 489 |
+
"cast_shadow": {"type": "boolean"},
|
| 490 |
+
},
|
| 491 |
+
"required": ["scene_id", "light_name"],
|
| 492 |
+
},
|
| 493 |
+
),
|
| 494 |
+
Tool(
|
| 495 |
+
name="get_lights",
|
| 496 |
+
description="""Get all lights in the scene.
|
| 497 |
+
|
| 498 |
+
Args:
|
| 499 |
+
scene_id: Scene to query (required)
|
| 500 |
+
|
| 501 |
+
Returns: List of all lights with their properties""",
|
| 502 |
+
inputSchema={
|
| 503 |
+
"type": "object",
|
| 504 |
+
"properties": {
|
| 505 |
+
"scene_id": {"type": "string"},
|
| 506 |
+
},
|
| 507 |
+
"required": ["scene_id"],
|
| 508 |
+
},
|
| 509 |
+
),
|
| 510 |
+
Tool(
|
| 511 |
+
name="update_object_material",
|
| 512 |
+
description="""Update an object's material properties.
|
| 513 |
+
|
| 514 |
+
Args:
|
| 515 |
+
scene_id: Scene to modify (required)
|
| 516 |
+
object_id: ID of object to update (required)
|
| 517 |
+
color: Hex color code (optional)
|
| 518 |
+
metalness: 0.0 (matte) to 1.0 (metal) (optional)
|
| 519 |
+
roughness: 0.0 (shiny) to 1.0 (rough) (optional)
|
| 520 |
+
opacity: 0.0 (invisible) to 1.0 (solid) (optional)
|
| 521 |
+
emissive: Hex color for self-glow (optional)
|
| 522 |
+
emissive_intensity: Glow brightness 0.0-1.0 (optional)
|
| 523 |
+
|
| 524 |
+
Examples:
|
| 525 |
+
update_object_material(scene_id, object_id, color="#ff0000")
|
| 526 |
+
update_object_material(scene_id, object_id, metalness=0.9, roughness=0.1)
|
| 527 |
+
update_object_material(scene_id, object_id, emissive="#00ffff", emissive_intensity=0.8)""",
|
| 528 |
+
inputSchema={
|
| 529 |
+
"type": "object",
|
| 530 |
+
"properties": {
|
| 531 |
+
"scene_id": {"type": "string"},
|
| 532 |
+
"object_id": {"type": "string"},
|
| 533 |
+
"color": {"type": "string"},
|
| 534 |
+
"metalness": {"type": "number"},
|
| 535 |
+
"roughness": {"type": "number"},
|
| 536 |
+
"opacity": {"type": "number"},
|
| 537 |
+
"emissive": {"type": "string"},
|
| 538 |
+
"emissive_intensity": {"type": "number"},
|
| 539 |
+
},
|
| 540 |
+
"required": ["scene_id", "object_id"],
|
| 541 |
+
},
|
| 542 |
+
),
|
| 543 |
+
Tool(
|
| 544 |
+
name="set_background_color",
|
| 545 |
+
description="""Set scene background color or gradient.
|
| 546 |
+
|
| 547 |
+
Args:
|
| 548 |
+
scene_id: Scene to modify (required)
|
| 549 |
+
color: Hex color for solid background
|
| 550 |
+
bg_type: "solid" or "gradient" (default: "solid")
|
| 551 |
+
gradient_top: Top hex color for gradient
|
| 552 |
+
gradient_bottom: Bottom hex color for gradient
|
| 553 |
+
|
| 554 |
+
Examples:
|
| 555 |
+
set_background_color(scene_id, color="#000000") # Black
|
| 556 |
+
set_background_color(scene_id, bg_type="gradient", gradient_top="#87CEEB", gradient_bottom="#FFE4B5")""",
|
| 557 |
+
inputSchema={
|
| 558 |
+
"type": "object",
|
| 559 |
+
"properties": {
|
| 560 |
+
"scene_id": {"type": "string"},
|
| 561 |
+
"color": {"type": "string"},
|
| 562 |
+
"bg_type": {
|
| 563 |
+
"type": "string",
|
| 564 |
+
"enum": ["solid", "gradient"],
|
| 565 |
+
"default": "solid"
|
| 566 |
+
},
|
| 567 |
+
"gradient_top": {"type": "string"},
|
| 568 |
+
"gradient_bottom": {"type": "string"},
|
| 569 |
+
},
|
| 570 |
+
"required": ["scene_id"],
|
| 571 |
+
},
|
| 572 |
+
),
|
| 573 |
+
Tool(
|
| 574 |
+
name="set_fog",
|
| 575 |
+
description="""Add atmospheric fog to the scene.
|
| 576 |
+
|
| 577 |
+
Args:
|
| 578 |
+
scene_id: Scene to modify (required)
|
| 579 |
+
enabled: Enable or disable fog (required)
|
| 580 |
+
color: Hex color of fog (default: "#aaaaaa")
|
| 581 |
+
near: Start distance for linear fog
|
| 582 |
+
far: End distance for linear fog
|
| 583 |
+
density: Density for exponential fog (overrides near/far)
|
| 584 |
+
|
| 585 |
+
Examples:
|
| 586 |
+
set_fog(scene_id, enabled=True, color="#aaaaaa", near=10, far=50)
|
| 587 |
+
set_fog(scene_id, enabled=True, density=0.05) # Exponential
|
| 588 |
+
set_fog(scene_id, enabled=False) # Disable""",
|
| 589 |
+
inputSchema={
|
| 590 |
+
"type": "object",
|
| 591 |
+
"properties": {
|
| 592 |
+
"scene_id": {"type": "string"},
|
| 593 |
+
"enabled": {"type": "boolean"},
|
| 594 |
+
"color": {"type": "string", "default": "#aaaaaa"},
|
| 595 |
+
"near": {"type": "number"},
|
| 596 |
+
"far": {"type": "number"},
|
| 597 |
+
"density": {"type": "number"},
|
| 598 |
+
},
|
| 599 |
+
"required": ["scene_id", "enabled"],
|
| 600 |
+
},
|
| 601 |
+
),
|
| 602 |
+
]
|
| 603 |
+
|
| 604 |
+
POST_PROCESSING_TOOLS = [
|
| 605 |
+
Tool(
|
| 606 |
+
name="set_bloom",
|
| 607 |
+
description="""Configure bloom (glow) post-processing effect.
|
| 608 |
+
|
| 609 |
+
Args:
|
| 610 |
+
scene_id: Scene to modify (required)
|
| 611 |
+
enabled: Enable/disable bloom (required)
|
| 612 |
+
strength: Bloom intensity 0.0-3.0 (default: 1.0)
|
| 613 |
+
radius: Bloom spread/blur radius 0.0-1.0 (default: 0.4)
|
| 614 |
+
threshold: Brightness threshold to trigger bloom 0.0-1.0 (default: 0.8)
|
| 615 |
+
|
| 616 |
+
Example: set_bloom(scene_id, enabled=True, strength=1.5, threshold=0.6)""",
|
| 617 |
+
inputSchema={
|
| 618 |
+
"type": "object",
|
| 619 |
+
"properties": {
|
| 620 |
+
"scene_id": {"type": "string"},
|
| 621 |
+
"enabled": {"type": "boolean"},
|
| 622 |
+
"strength": {"type": "number", "default": 1.0},
|
| 623 |
+
"radius": {"type": "number", "default": 0.4},
|
| 624 |
+
"threshold": {"type": "number", "default": 0.8},
|
| 625 |
+
},
|
| 626 |
+
"required": ["scene_id", "enabled"],
|
| 627 |
+
},
|
| 628 |
+
),
|
| 629 |
+
Tool(
|
| 630 |
+
name="set_ssao",
|
| 631 |
+
description="""Configure Screen Space Ambient Occlusion (SSAO).
|
| 632 |
+
|
| 633 |
+
Adds soft shadows in corners and crevices for depth and realism.
|
| 634 |
+
|
| 635 |
+
Args:
|
| 636 |
+
scene_id: Scene to modify (required)
|
| 637 |
+
enabled: Enable/disable SSAO (required)
|
| 638 |
+
radius: Sample radius in world units 0.1-2.0 (default: 0.5)
|
| 639 |
+
intensity: Shadow intensity 0.0-2.0 (default: 1.0)
|
| 640 |
+
bias: Depth bias to prevent self-occlusion 0.001-0.1 (default: 0.025)
|
| 641 |
+
|
| 642 |
+
Example: set_ssao(scene_id, enabled=True, intensity=1.5)""",
|
| 643 |
+
inputSchema={
|
| 644 |
+
"type": "object",
|
| 645 |
+
"properties": {
|
| 646 |
+
"scene_id": {"type": "string"},
|
| 647 |
+
"enabled": {"type": "boolean"},
|
| 648 |
+
"radius": {"type": "number", "default": 0.5},
|
| 649 |
+
"intensity": {"type": "number", "default": 1.0},
|
| 650 |
+
"bias": {"type": "number", "default": 0.025},
|
| 651 |
+
},
|
| 652 |
+
"required": ["scene_id", "enabled"],
|
| 653 |
+
},
|
| 654 |
+
),
|
| 655 |
+
Tool(
|
| 656 |
+
name="set_color_grading",
|
| 657 |
+
description="""Configure color grading post-processing.
|
| 658 |
+
|
| 659 |
+
Adjust overall image colors for cinematic looks or stylized effects.
|
| 660 |
+
|
| 661 |
+
Args:
|
| 662 |
+
scene_id: Scene to modify (required)
|
| 663 |
+
enabled: Enable/disable color grading (required)
|
| 664 |
+
brightness: Brightness adjustment -1.0 to 1.0 (default: 0.0)
|
| 665 |
+
contrast: Contrast multiplier 0.0-2.0 (default: 1.0)
|
| 666 |
+
saturation: Color saturation 0.0-2.0 (default: 1.0)
|
| 667 |
+
hue: Hue shift in degrees -180 to 180 (default: 0)
|
| 668 |
+
exposure: Exposure adjustment 0.0-3.0 (default: 1.0)
|
| 669 |
+
gamma: Gamma correction 0.5-2.5 (default: 1.0)
|
| 670 |
+
|
| 671 |
+
Example: set_color_grading(scene_id, enabled=True, saturation=0.5) for desaturated look""",
|
| 672 |
+
inputSchema={
|
| 673 |
+
"type": "object",
|
| 674 |
+
"properties": {
|
| 675 |
+
"scene_id": {"type": "string"},
|
| 676 |
+
"enabled": {"type": "boolean"},
|
| 677 |
+
"brightness": {"type": "number", "default": 0.0},
|
| 678 |
+
"contrast": {"type": "number", "default": 1.0},
|
| 679 |
+
"saturation": {"type": "number", "default": 1.0},
|
| 680 |
+
"hue": {"type": "number", "default": 0.0},
|
| 681 |
+
"exposure": {"type": "number", "default": 1.0},
|
| 682 |
+
"gamma": {"type": "number", "default": 1.0},
|
| 683 |
+
},
|
| 684 |
+
"required": ["scene_id", "enabled"],
|
| 685 |
+
},
|
| 686 |
+
),
|
| 687 |
+
Tool(
|
| 688 |
+
name="set_vignette",
|
| 689 |
+
description="""Configure vignette effect (darkened edges).
|
| 690 |
+
|
| 691 |
+
Darkens corners and edges of the screen, drawing focus to the center.
|
| 692 |
+
|
| 693 |
+
Args:
|
| 694 |
+
scene_id: Scene to modify (required)
|
| 695 |
+
enabled: Enable/disable vignette (required)
|
| 696 |
+
intensity: Darkness of the vignette 0.0-1.0 (default: 0.5)
|
| 697 |
+
smoothness: Softness of the vignette edge 0.0-1.0 (default: 0.5)
|
| 698 |
+
|
| 699 |
+
Example: set_vignette(scene_id, enabled=True, intensity=0.7)""",
|
| 700 |
+
inputSchema={
|
| 701 |
+
"type": "object",
|
| 702 |
+
"properties": {
|
| 703 |
+
"scene_id": {"type": "string"},
|
| 704 |
+
"enabled": {"type": "boolean"},
|
| 705 |
+
"intensity": {"type": "number", "default": 0.5},
|
| 706 |
+
"smoothness": {"type": "number", "default": 0.5},
|
| 707 |
+
},
|
| 708 |
+
"required": ["scene_id", "enabled"],
|
| 709 |
+
},
|
| 710 |
+
),
|
| 711 |
+
Tool(
|
| 712 |
+
name="get_post_processing",
|
| 713 |
+
description="""Get all post-processing settings for the scene.
|
| 714 |
+
|
| 715 |
+
Args:
|
| 716 |
+
scene_id: Scene to query (required)
|
| 717 |
+
|
| 718 |
+
Returns: All post-processing settings (bloom, SSAO, color grading, vignette)""",
|
| 719 |
+
inputSchema={
|
| 720 |
+
"type": "object",
|
| 721 |
+
"properties": {
|
| 722 |
+
"scene_id": {"type": "string"},
|
| 723 |
+
},
|
| 724 |
+
"required": ["scene_id"],
|
| 725 |
+
},
|
| 726 |
+
),
|
| 727 |
+
]
|
| 728 |
+
|
| 729 |
+
CAMERA_EFFECTS_TOOLS = [
|
| 730 |
+
Tool(
|
| 731 |
+
name="set_depth_of_field",
|
| 732 |
+
description="""Configure depth of field (DoF) camera effect.
|
| 733 |
+
|
| 734 |
+
Blurs objects that are not at the focus distance, simulating real camera lenses.
|
| 735 |
+
|
| 736 |
+
Args:
|
| 737 |
+
scene_id: Scene to modify (required)
|
| 738 |
+
enabled: Enable/disable depth of field (required)
|
| 739 |
+
focus_distance: Distance to the focal plane in units (default: 10.0)
|
| 740 |
+
aperture: Aperture size, affects blur amount 0.001-0.1 (default: 0.025)
|
| 741 |
+
max_blur: Maximum blur strength 0.0-0.05 (default: 0.01)
|
| 742 |
+
|
| 743 |
+
Example: set_depth_of_field(scene_id, enabled=True, focus_distance=5.0)""",
|
| 744 |
+
inputSchema={
|
| 745 |
+
"type": "object",
|
| 746 |
+
"properties": {
|
| 747 |
+
"scene_id": {"type": "string"},
|
| 748 |
+
"enabled": {"type": "boolean"},
|
| 749 |
+
"focus_distance": {"type": "number", "default": 10.0},
|
| 750 |
+
"aperture": {"type": "number", "default": 0.025},
|
| 751 |
+
"max_blur": {"type": "number", "default": 0.01},
|
| 752 |
+
},
|
| 753 |
+
"required": ["scene_id", "enabled"],
|
| 754 |
+
},
|
| 755 |
+
),
|
| 756 |
+
Tool(
|
| 757 |
+
name="set_motion_blur",
|
| 758 |
+
description="""Configure motion blur camera effect.
|
| 759 |
+
|
| 760 |
+
Adds blur in the direction of camera or object movement.
|
| 761 |
+
|
| 762 |
+
Args:
|
| 763 |
+
scene_id: Scene to modify (required)
|
| 764 |
+
enabled: Enable/disable motion blur (required)
|
| 765 |
+
intensity: Blur intensity 0.0-2.0 (default: 0.5)
|
| 766 |
+
samples: Quality samples for blur 4-32 (default: 8)
|
| 767 |
+
|
| 768 |
+
Example: set_motion_blur(scene_id, enabled=True, intensity=0.8)""",
|
| 769 |
+
inputSchema={
|
| 770 |
+
"type": "object",
|
| 771 |
+
"properties": {
|
| 772 |
+
"scene_id": {"type": "string"},
|
| 773 |
+
"enabled": {"type": "boolean"},
|
| 774 |
+
"intensity": {"type": "number", "default": 0.5},
|
| 775 |
+
"samples": {"type": "integer", "default": 8},
|
| 776 |
+
},
|
| 777 |
+
"required": ["scene_id", "enabled"],
|
| 778 |
+
},
|
| 779 |
+
),
|
| 780 |
+
Tool(
|
| 781 |
+
name="set_chromatic_aberration",
|
| 782 |
+
description="""Configure chromatic aberration effect.
|
| 783 |
+
|
| 784 |
+
Simulates lens imperfection by separating color channels at the edges.
|
| 785 |
+
|
| 786 |
+
Args:
|
| 787 |
+
scene_id: Scene to modify (required)
|
| 788 |
+
enabled: Enable/disable chromatic aberration (required)
|
| 789 |
+
intensity: Effect strength 0.0-0.05 (default: 0.005)
|
| 790 |
+
|
| 791 |
+
Example: set_chromatic_aberration(scene_id, enabled=True, intensity=0.01)""",
|
| 792 |
+
inputSchema={
|
| 793 |
+
"type": "object",
|
| 794 |
+
"properties": {
|
| 795 |
+
"scene_id": {"type": "string"},
|
| 796 |
+
"enabled": {"type": "boolean"},
|
| 797 |
+
"intensity": {"type": "number", "default": 0.005},
|
| 798 |
+
},
|
| 799 |
+
"required": ["scene_id", "enabled"],
|
| 800 |
+
},
|
| 801 |
+
),
|
| 802 |
+
Tool(
|
| 803 |
+
name="get_camera_effects",
|
| 804 |
+
description="""Get all camera effects settings for the scene.
|
| 805 |
+
|
| 806 |
+
Args:
|
| 807 |
+
scene_id: Scene to query (required)
|
| 808 |
+
|
| 809 |
+
Returns: All camera effects settings (depth of field, motion blur, chromatic aberration)""",
|
| 810 |
+
inputSchema={
|
| 811 |
+
"type": "object",
|
| 812 |
+
"properties": {
|
| 813 |
+
"scene_id": {"type": "string"},
|
| 814 |
+
},
|
| 815 |
+
"required": ["scene_id"],
|
| 816 |
+
},
|
| 817 |
+
),
|
| 818 |
+
]
|
| 819 |
+
|
| 820 |
+
# Combine all tools
|
| 821 |
+
ALL_TOOLS = SCENE_TOOLS + PLAYER_TOOLS + RENDERING_TOOLS + POST_PROCESSING_TOOLS + CAMERA_EFFECTS_TOOLS
|
| 822 |
+
|
| 823 |
+
|
| 824 |
+
# =============================================================================
|
| 825 |
+
# MCP Handlers
|
| 826 |
+
# =============================================================================
|
| 827 |
+
|
| 828 |
+
@server.list_tools()
|
| 829 |
+
async def list_tools() -> list[Tool]:
|
| 830 |
+
"""Return all available GCP tools."""
|
| 831 |
+
return ALL_TOOLS
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
@server.call_tool()
|
| 835 |
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
| 836 |
+
"""Handle tool calls from MCP clients."""
|
| 837 |
+
try:
|
| 838 |
+
result = await _execute_tool(name, arguments)
|
| 839 |
+
return [TextContent(type="text", text=str(result))]
|
| 840 |
+
except Exception as e:
|
| 841 |
+
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
| 842 |
+
|
| 843 |
+
|
| 844 |
+
async def _execute_tool(name: str, args: dict) -> Any:
|
| 845 |
+
"""Route tool calls to their implementations."""
|
| 846 |
+
|
| 847 |
+
# Scene tools
|
| 848 |
+
if name == "create_scene":
|
| 849 |
+
return create_game_scene(
|
| 850 |
+
name=args.get("name", "New Scene"),
|
| 851 |
+
description=args.get("description"),
|
| 852 |
+
world_width=args.get("world_width", 100.0),
|
| 853 |
+
world_height=args.get("world_height", 100.0),
|
| 854 |
+
world_depth=args.get("world_depth", 100.0),
|
| 855 |
+
lighting_preset=args.get("lighting_preset", "day"),
|
| 856 |
+
base_url=BASE_URL,
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
elif name == "add_object":
|
| 860 |
+
return add_game_object(
|
| 861 |
+
scene_id=args["scene_id"],
|
| 862 |
+
object_type=args.get("object_type", "cube"),
|
| 863 |
+
name=args.get("name"),
|
| 864 |
+
position=create_vector3(
|
| 865 |
+
args.get("x", 0.0),
|
| 866 |
+
args.get("y", 0.0),
|
| 867 |
+
args.get("z", 0.0)
|
| 868 |
+
),
|
| 869 |
+
rotation=create_vector3(
|
| 870 |
+
args.get("rotation_x", 0.0),
|
| 871 |
+
args.get("rotation_y", 0.0),
|
| 872 |
+
args.get("rotation_z", 0.0)
|
| 873 |
+
),
|
| 874 |
+
scale=create_vector3(
|
| 875 |
+
args.get("scale_x", 1.0),
|
| 876 |
+
args.get("scale_y", 1.0),
|
| 877 |
+
args.get("scale_z", 1.0)
|
| 878 |
+
),
|
| 879 |
+
material=create_material(color=args.get("color", "#ffffff")),
|
| 880 |
+
base_url=BASE_URL,
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
+
elif name == "remove_object":
|
| 884 |
+
return remove_game_object(
|
| 885 |
+
scene_id=args["scene_id"],
|
| 886 |
+
object_id=args["object_id"],
|
| 887 |
+
base_url=BASE_URL,
|
| 888 |
+
)
|
| 889 |
+
|
| 890 |
+
elif name == "set_lighting":
|
| 891 |
+
return set_scene_lighting(
|
| 892 |
+
scene_id=args["scene_id"],
|
| 893 |
+
preset=args.get("preset", "day"),
|
| 894 |
+
base_url=BASE_URL,
|
| 895 |
+
)
|
| 896 |
+
|
| 897 |
+
elif name == "get_scene_info":
|
| 898 |
+
return get_scene_info(args["scene_id"], BASE_URL)
|
| 899 |
+
|
| 900 |
+
# Player tools
|
| 901 |
+
elif name == "set_player_speed":
|
| 902 |
+
return set_player_speed(
|
| 903 |
+
args["scene_id"],
|
| 904 |
+
args.get("walk_speed", 5.0)
|
| 905 |
+
)
|
| 906 |
+
|
| 907 |
+
elif name == "set_jump_force":
|
| 908 |
+
return set_jump_force(
|
| 909 |
+
args["scene_id"],
|
| 910 |
+
args.get("jump_force", 5.0)
|
| 911 |
+
)
|
| 912 |
+
|
| 913 |
+
elif name == "set_mouse_sensitivity":
|
| 914 |
+
return set_mouse_sensitivity(
|
| 915 |
+
args["scene_id"],
|
| 916 |
+
args.get("sensitivity", 0.002),
|
| 917 |
+
args.get("invert_y", False)
|
| 918 |
+
)
|
| 919 |
+
|
| 920 |
+
elif name == "set_gravity":
|
| 921 |
+
return set_gravity(
|
| 922 |
+
args["scene_id"],
|
| 923 |
+
args.get("gravity", -9.82)
|
| 924 |
+
)
|
| 925 |
+
|
| 926 |
+
elif name == "set_player_dimensions":
|
| 927 |
+
return set_player_dimensions(
|
| 928 |
+
args["scene_id"],
|
| 929 |
+
args.get("height", 1.7),
|
| 930 |
+
args.get("radius", 0.3),
|
| 931 |
+
args.get("eye_height")
|
| 932 |
+
)
|
| 933 |
+
|
| 934 |
+
elif name == "set_movement_acceleration":
|
| 935 |
+
return set_movement_acceleration(
|
| 936 |
+
args["scene_id"],
|
| 937 |
+
args.get("acceleration", 0.0),
|
| 938 |
+
args.get("damping", 0.9)
|
| 939 |
+
)
|
| 940 |
+
|
| 941 |
+
elif name == "set_air_control":
|
| 942 |
+
return set_air_control(
|
| 943 |
+
args["scene_id"],
|
| 944 |
+
args.get("air_control_factor", 1.0)
|
| 945 |
+
)
|
| 946 |
+
|
| 947 |
+
elif name == "set_camera_fov":
|
| 948 |
+
return set_camera_fov(
|
| 949 |
+
args["scene_id"],
|
| 950 |
+
args.get("fov", 75.0)
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
elif name == "set_vertical_look_limits":
|
| 954 |
+
return set_vertical_look_limits(
|
| 955 |
+
args["scene_id"],
|
| 956 |
+
args.get("min_pitch", -89.0),
|
| 957 |
+
args.get("max_pitch", 89.0)
|
| 958 |
+
)
|
| 959 |
+
|
| 960 |
+
elif name == "get_player_config":
|
| 961 |
+
return get_player_config(args["scene_id"])
|
| 962 |
+
|
| 963 |
+
# Rendering tools
|
| 964 |
+
elif name == "add_light":
|
| 965 |
+
return add_light(
|
| 966 |
+
args["scene_id"],
|
| 967 |
+
args["light_type"],
|
| 968 |
+
args["name"],
|
| 969 |
+
args.get("color", "#ffffff"),
|
| 970 |
+
args.get("intensity", 1.0),
|
| 971 |
+
args.get("position"),
|
| 972 |
+
args.get("target"),
|
| 973 |
+
args.get("cast_shadow", False),
|
| 974 |
+
args.get("spot_angle")
|
| 975 |
+
)
|
| 976 |
+
|
| 977 |
+
elif name == "remove_light":
|
| 978 |
+
return remove_light(
|
| 979 |
+
args["scene_id"],
|
| 980 |
+
args["light_name"]
|
| 981 |
+
)
|
| 982 |
+
|
| 983 |
+
elif name == "update_light":
|
| 984 |
+
return update_light(
|
| 985 |
+
args["scene_id"],
|
| 986 |
+
args["light_name"],
|
| 987 |
+
args.get("color"),
|
| 988 |
+
args.get("intensity"),
|
| 989 |
+
args.get("position"),
|
| 990 |
+
args.get("cast_shadow")
|
| 991 |
+
)
|
| 992 |
+
|
| 993 |
+
elif name == "get_lights":
|
| 994 |
+
return get_lights(args["scene_id"])
|
| 995 |
+
|
| 996 |
+
elif name == "update_object_material":
|
| 997 |
+
return update_object_material(
|
| 998 |
+
args["scene_id"],
|
| 999 |
+
args["object_id"],
|
| 1000 |
+
args.get("color"),
|
| 1001 |
+
args.get("metalness"),
|
| 1002 |
+
args.get("roughness"),
|
| 1003 |
+
args.get("opacity"),
|
| 1004 |
+
args.get("emissive"),
|
| 1005 |
+
args.get("emissive_intensity")
|
| 1006 |
+
)
|
| 1007 |
+
|
| 1008 |
+
elif name == "set_background_color":
|
| 1009 |
+
return set_background_color(
|
| 1010 |
+
args["scene_id"],
|
| 1011 |
+
args.get("color"),
|
| 1012 |
+
args.get("bg_type", "solid"),
|
| 1013 |
+
args.get("gradient_top"),
|
| 1014 |
+
args.get("gradient_bottom")
|
| 1015 |
+
)
|
| 1016 |
+
|
| 1017 |
+
elif name == "set_fog":
|
| 1018 |
+
return set_fog(
|
| 1019 |
+
args["scene_id"],
|
| 1020 |
+
args["enabled"],
|
| 1021 |
+
args.get("color"),
|
| 1022 |
+
args.get("near"),
|
| 1023 |
+
args.get("far"),
|
| 1024 |
+
args.get("density")
|
| 1025 |
+
)
|
| 1026 |
+
|
| 1027 |
+
# Post-processing tools
|
| 1028 |
+
elif name == "set_bloom":
|
| 1029 |
+
return set_bloom(
|
| 1030 |
+
args["scene_id"],
|
| 1031 |
+
args["enabled"],
|
| 1032 |
+
args.get("strength", 1.0),
|
| 1033 |
+
args.get("radius", 0.4),
|
| 1034 |
+
args.get("threshold", 0.8)
|
| 1035 |
+
)
|
| 1036 |
+
|
| 1037 |
+
elif name == "set_ssao":
|
| 1038 |
+
return set_ssao(
|
| 1039 |
+
args["scene_id"],
|
| 1040 |
+
args["enabled"],
|
| 1041 |
+
args.get("radius", 0.5),
|
| 1042 |
+
args.get("intensity", 1.0),
|
| 1043 |
+
args.get("bias", 0.025)
|
| 1044 |
+
)
|
| 1045 |
+
|
| 1046 |
+
elif name == "set_color_grading":
|
| 1047 |
+
return set_color_grading(
|
| 1048 |
+
args["scene_id"],
|
| 1049 |
+
args["enabled"],
|
| 1050 |
+
args.get("brightness", 0.0),
|
| 1051 |
+
args.get("contrast", 1.0),
|
| 1052 |
+
args.get("saturation", 1.0),
|
| 1053 |
+
args.get("hue", 0.0),
|
| 1054 |
+
args.get("exposure", 1.0),
|
| 1055 |
+
args.get("gamma", 1.0)
|
| 1056 |
+
)
|
| 1057 |
+
|
| 1058 |
+
elif name == "set_vignette":
|
| 1059 |
+
return set_vignette(
|
| 1060 |
+
args["scene_id"],
|
| 1061 |
+
args["enabled"],
|
| 1062 |
+
args.get("intensity", 0.5),
|
| 1063 |
+
args.get("smoothness", 0.5)
|
| 1064 |
+
)
|
| 1065 |
+
|
| 1066 |
+
elif name == "get_post_processing":
|
| 1067 |
+
return get_post_processing(args["scene_id"])
|
| 1068 |
+
|
| 1069 |
+
# Camera effects tools
|
| 1070 |
+
elif name == "set_depth_of_field":
|
| 1071 |
+
return set_depth_of_field(
|
| 1072 |
+
args["scene_id"],
|
| 1073 |
+
args["enabled"],
|
| 1074 |
+
args.get("focus_distance", 10.0),
|
| 1075 |
+
args.get("aperture", 0.025),
|
| 1076 |
+
args.get("max_blur", 0.01)
|
| 1077 |
+
)
|
| 1078 |
+
|
| 1079 |
+
elif name == "set_motion_blur":
|
| 1080 |
+
return set_motion_blur(
|
| 1081 |
+
args["scene_id"],
|
| 1082 |
+
args["enabled"],
|
| 1083 |
+
args.get("intensity", 0.5),
|
| 1084 |
+
args.get("samples", 8)
|
| 1085 |
+
)
|
| 1086 |
+
|
| 1087 |
+
elif name == "set_chromatic_aberration":
|
| 1088 |
+
return set_chromatic_aberration(
|
| 1089 |
+
args["scene_id"],
|
| 1090 |
+
args["enabled"],
|
| 1091 |
+
args.get("intensity", 0.005)
|
| 1092 |
+
)
|
| 1093 |
+
|
| 1094 |
+
elif name == "get_camera_effects":
|
| 1095 |
+
return get_camera_effects(args["scene_id"])
|
| 1096 |
+
|
| 1097 |
+
else:
|
| 1098 |
+
raise ValueError(f"Unknown tool: {name}")
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
# =============================================================================
|
| 1102 |
+
# Server runner (for standalone MCP mode)
|
| 1103 |
+
# =============================================================================
|
| 1104 |
+
|
| 1105 |
+
async def run_stdio():
|
| 1106 |
+
"""Run the MCP server using stdio transport."""
|
| 1107 |
+
from mcp.server.stdio import stdio_server
|
| 1108 |
+
|
| 1109 |
+
async with stdio_server() as (read_stream, write_stream):
|
| 1110 |
+
await server.run(
|
| 1111 |
+
read_stream,
|
| 1112 |
+
write_stream,
|
| 1113 |
+
server.create_initialization_options()
|
| 1114 |
+
)
|
| 1115 |
+
|
| 1116 |
+
|
| 1117 |
+
if __name__ == "__main__":
|
| 1118 |
+
import asyncio
|
| 1119 |
+
asyncio.run(run_stdio())
|
backend/storage.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
In-memory storage for game scenes
|
| 3 |
+
"""
|
| 4 |
+
from typing import Dict, Optional, Any
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Storage:
|
| 9 |
+
"""Simple in-memory storage for game scenes using plain dictionaries"""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self._scenes: Dict[str, Dict[str, Any]] = {}
|
| 13 |
+
|
| 14 |
+
def save(self, scene: Dict[str, Any]) -> Dict[str, Any]:
|
| 15 |
+
"""Save or update a scene"""
|
| 16 |
+
if not isinstance(scene, dict):
|
| 17 |
+
raise ValueError("Scene must be a dictionary")
|
| 18 |
+
|
| 19 |
+
if "scene_id" not in scene:
|
| 20 |
+
raise ValueError("Scene must have a 'scene_id' key")
|
| 21 |
+
|
| 22 |
+
# Update timestamp
|
| 23 |
+
scene["updated_at"] = datetime.utcnow().isoformat()
|
| 24 |
+
|
| 25 |
+
self._scenes[scene["scene_id"]] = scene
|
| 26 |
+
return scene
|
| 27 |
+
|
| 28 |
+
def get(self, scene_id: str) -> Optional[Dict[str, Any]]:
|
| 29 |
+
"""Retrieve a scene by ID"""
|
| 30 |
+
return self._scenes.get(scene_id)
|
| 31 |
+
|
| 32 |
+
def delete(self, scene_id: str) -> bool:
|
| 33 |
+
"""Delete a scene"""
|
| 34 |
+
if scene_id in self._scenes:
|
| 35 |
+
del self._scenes[scene_id]
|
| 36 |
+
return True
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
def list_all(self) -> list[Dict[str, Any]]:
|
| 40 |
+
"""List all scenes"""
|
| 41 |
+
return list(self._scenes.values())
|
| 42 |
+
|
| 43 |
+
def exists(self, scene_id: str) -> bool:
|
| 44 |
+
"""Check if scene exists"""
|
| 45 |
+
return scene_id in self._scenes
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Global storage instance
|
| 49 |
+
storage = Storage()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# Initialize with a clean default Welcome Scene for HuggingFace Spaces
|
| 53 |
+
def initialize_default_scene():
|
| 54 |
+
"""Create a clean default Welcome Scene on startup"""
|
| 55 |
+
from backend.game_models import create_scene, create_light, create_environment, create_vector3
|
| 56 |
+
|
| 57 |
+
# Create lights for day preset
|
| 58 |
+
lights = [
|
| 59 |
+
create_light(
|
| 60 |
+
name="Sun",
|
| 61 |
+
light_type="directional",
|
| 62 |
+
color="#ffffff",
|
| 63 |
+
intensity=1.0,
|
| 64 |
+
position=create_vector3(50, 50, 50),
|
| 65 |
+
),
|
| 66 |
+
create_light(
|
| 67 |
+
name="Ambient",
|
| 68 |
+
light_type="ambient",
|
| 69 |
+
color="#ffffff",
|
| 70 |
+
intensity=0.5,
|
| 71 |
+
),
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
# Create environment
|
| 75 |
+
env = create_environment(lighting_preset="day")
|
| 76 |
+
|
| 77 |
+
# Create clean scene with NO default objects
|
| 78 |
+
# Ground plane and walls are created by physics system in viewer
|
| 79 |
+
scene = create_scene(
|
| 80 |
+
name="Welcome Scene",
|
| 81 |
+
description="Clean 10x10 FPS world with physics - Ready to explore!",
|
| 82 |
+
world_width=10,
|
| 83 |
+
world_height=10,
|
| 84 |
+
world_depth=10,
|
| 85 |
+
lights=lights,
|
| 86 |
+
environment=env,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Use a predictable scene ID for easy linking
|
| 90 |
+
scene["scene_id"] = "welcome"
|
| 91 |
+
|
| 92 |
+
# Save to storage
|
| 93 |
+
storage.save(scene)
|
| 94 |
+
print(f"✓ Initialized default Welcome Scene (ID: welcome)")
|
| 95 |
+
print(f" - 10x10 world with FPS physics controller")
|
| 96 |
+
print(f" - Ground plane + boundary walls (created by viewer)")
|
| 97 |
+
print(f" - Player spawn at (0, 1, 0)")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# Initialize on module load
|
| 101 |
+
initialize_default_scene()
|
backend/tools/__init__.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GCP Tools - Game Context Protocol Tool Implementations
|
| 3 |
+
|
| 4 |
+
Organized by domain:
|
| 5 |
+
- scene_tools: Create, modify, and query 3D scenes
|
| 6 |
+
- player_tools: Configure FPS player controller
|
| 7 |
+
- rendering_tools: Lights, materials, backgrounds, fog
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from backend.tools.scene_tools import (
|
| 11 |
+
create_game_scene,
|
| 12 |
+
add_game_object,
|
| 13 |
+
remove_game_object,
|
| 14 |
+
set_scene_lighting,
|
| 15 |
+
get_scene_info,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
from backend.tools.player_tools import (
|
| 19 |
+
set_player_speed,
|
| 20 |
+
set_jump_force,
|
| 21 |
+
set_mouse_sensitivity,
|
| 22 |
+
set_gravity,
|
| 23 |
+
set_player_dimensions,
|
| 24 |
+
set_movement_acceleration,
|
| 25 |
+
set_air_control,
|
| 26 |
+
set_camera_fov,
|
| 27 |
+
set_vertical_look_limits,
|
| 28 |
+
get_player_config,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
from backend.tools.rendering_tools import (
|
| 32 |
+
add_light,
|
| 33 |
+
remove_light,
|
| 34 |
+
update_light,
|
| 35 |
+
get_lights,
|
| 36 |
+
update_object_material,
|
| 37 |
+
set_background_color,
|
| 38 |
+
set_fog,
|
| 39 |
+
# Post-processing
|
| 40 |
+
set_bloom,
|
| 41 |
+
set_ssao,
|
| 42 |
+
set_color_grading,
|
| 43 |
+
set_vignette,
|
| 44 |
+
get_post_processing,
|
| 45 |
+
# Camera effects
|
| 46 |
+
set_depth_of_field,
|
| 47 |
+
set_motion_blur,
|
| 48 |
+
set_chromatic_aberration,
|
| 49 |
+
get_camera_effects,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
__all__ = [
|
| 53 |
+
# Scene tools
|
| 54 |
+
"create_game_scene",
|
| 55 |
+
"add_game_object",
|
| 56 |
+
"remove_game_object",
|
| 57 |
+
"set_scene_lighting",
|
| 58 |
+
"get_scene_info",
|
| 59 |
+
# Player tools
|
| 60 |
+
"set_player_speed",
|
| 61 |
+
"set_jump_force",
|
| 62 |
+
"set_mouse_sensitivity",
|
| 63 |
+
"set_gravity",
|
| 64 |
+
"set_player_dimensions",
|
| 65 |
+
"set_movement_acceleration",
|
| 66 |
+
"set_air_control",
|
| 67 |
+
"set_camera_fov",
|
| 68 |
+
"set_vertical_look_limits",
|
| 69 |
+
"get_player_config",
|
| 70 |
+
# Rendering tools
|
| 71 |
+
"add_light",
|
| 72 |
+
"remove_light",
|
| 73 |
+
"update_light",
|
| 74 |
+
"get_lights",
|
| 75 |
+
"update_object_material",
|
| 76 |
+
"set_background_color",
|
| 77 |
+
"set_fog",
|
| 78 |
+
# Post-processing tools
|
| 79 |
+
"set_bloom",
|
| 80 |
+
"set_ssao",
|
| 81 |
+
"set_color_grading",
|
| 82 |
+
"set_vignette",
|
| 83 |
+
"get_post_processing",
|
| 84 |
+
# Camera effects tools
|
| 85 |
+
"set_depth_of_field",
|
| 86 |
+
"set_motion_blur",
|
| 87 |
+
"set_chromatic_aberration",
|
| 88 |
+
"get_camera_effects",
|
| 89 |
+
]
|
backend/tools/player_tools.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Player Controller Tools
|
| 3 |
+
Configure first-person character movement, physics, and camera settings
|
| 4 |
+
"""
|
| 5 |
+
from typing import Dict, Any, Optional
|
| 6 |
+
from backend.storage import storage
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def set_player_speed(scene_id: str, walk_speed: float) -> Dict[str, Any]:
|
| 10 |
+
"""
|
| 11 |
+
Implementation: Set player walking/movement speed
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
scene_id: ID of the scene
|
| 15 |
+
walk_speed: Movement speed in units/second
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
Dictionary with updated speed and message
|
| 19 |
+
"""
|
| 20 |
+
scene = storage.get(scene_id)
|
| 21 |
+
if not scene:
|
| 22 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 23 |
+
|
| 24 |
+
# Initialize player_config if it doesn't exist
|
| 25 |
+
if "player_config" not in scene:
|
| 26 |
+
scene["player_config"] = {}
|
| 27 |
+
|
| 28 |
+
scene["player_config"]["move_speed"] = walk_speed
|
| 29 |
+
storage.save(scene)
|
| 30 |
+
|
| 31 |
+
return {
|
| 32 |
+
"scene_id": scene_id,
|
| 33 |
+
"message": f"Set player movement speed to {walk_speed} units/sec",
|
| 34 |
+
"move_speed": walk_speed,
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def set_jump_force(scene_id: str, jump_force: float) -> Dict[str, Any]:
|
| 39 |
+
"""
|
| 40 |
+
Implementation: Configure jump height via initial upward velocity
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
scene_id: ID of the scene
|
| 44 |
+
jump_force: Initial jump velocity in m/s (higher = higher jumps)
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
Dictionary with updated jump force and message
|
| 48 |
+
"""
|
| 49 |
+
scene = storage.get(scene_id)
|
| 50 |
+
if not scene:
|
| 51 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 52 |
+
|
| 53 |
+
if "player_config" not in scene:
|
| 54 |
+
scene["player_config"] = {}
|
| 55 |
+
|
| 56 |
+
scene["player_config"]["jump_force"] = jump_force
|
| 57 |
+
storage.save(scene)
|
| 58 |
+
|
| 59 |
+
return {
|
| 60 |
+
"scene_id": scene_id,
|
| 61 |
+
"message": f"Set jump force to {jump_force} m/s",
|
| 62 |
+
"jump_force": jump_force,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def set_mouse_sensitivity(
|
| 67 |
+
scene_id: str,
|
| 68 |
+
sensitivity: float,
|
| 69 |
+
invert_y: bool
|
| 70 |
+
) -> Dict[str, Any]:
|
| 71 |
+
"""
|
| 72 |
+
Implementation: Configure mouse look sensitivity and Y-axis inversion
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
scene_id: ID of the scene
|
| 76 |
+
sensitivity: Mouse sensitivity multiplier (lower = slower/more precise)
|
| 77 |
+
invert_y: Whether to invert vertical look (flight sim style)
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
Dictionary with updated settings and message
|
| 81 |
+
"""
|
| 82 |
+
scene = storage.get(scene_id)
|
| 83 |
+
if not scene:
|
| 84 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 85 |
+
|
| 86 |
+
if "player_config" not in scene:
|
| 87 |
+
scene["player_config"] = {}
|
| 88 |
+
|
| 89 |
+
scene["player_config"]["mouse_sensitivity"] = sensitivity
|
| 90 |
+
scene["player_config"]["invert_y"] = invert_y
|
| 91 |
+
storage.save(scene)
|
| 92 |
+
|
| 93 |
+
invert_msg = "enabled" if invert_y else "disabled"
|
| 94 |
+
return {
|
| 95 |
+
"scene_id": scene_id,
|
| 96 |
+
"message": f"Set mouse sensitivity to {sensitivity}, Y-invert {invert_msg}",
|
| 97 |
+
"mouse_sensitivity": sensitivity,
|
| 98 |
+
"invert_y": invert_y,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def set_gravity(scene_id: str, gravity: float) -> Dict[str, Any]:
|
| 103 |
+
"""
|
| 104 |
+
Implementation: Set world gravity strength
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
scene_id: ID of the scene
|
| 108 |
+
gravity: Gravity acceleration in m/s² (negative = downward, -9.82 = Earth)
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
Dictionary with updated gravity and message
|
| 112 |
+
"""
|
| 113 |
+
scene = storage.get(scene_id)
|
| 114 |
+
if not scene:
|
| 115 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 116 |
+
|
| 117 |
+
if "player_config" not in scene:
|
| 118 |
+
scene["player_config"] = {}
|
| 119 |
+
|
| 120 |
+
scene["player_config"]["gravity"] = gravity
|
| 121 |
+
storage.save(scene)
|
| 122 |
+
|
| 123 |
+
gravity_desc = "Earth-like" if abs(gravity + 9.82) < 0.1 else "custom"
|
| 124 |
+
return {
|
| 125 |
+
"scene_id": scene_id,
|
| 126 |
+
"message": f"Set gravity to {gravity} m/s² ({gravity_desc})",
|
| 127 |
+
"gravity": gravity,
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def set_player_dimensions(
|
| 132 |
+
scene_id: str,
|
| 133 |
+
height: float,
|
| 134 |
+
radius: float,
|
| 135 |
+
eye_height: Optional[float] = None
|
| 136 |
+
) -> Dict[str, Any]:
|
| 137 |
+
"""
|
| 138 |
+
Implementation: Configure player collision capsule dimensions
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
scene_id: ID of the scene
|
| 142 |
+
height: Player height in meters (affects collision capsule)
|
| 143 |
+
radius: Player radius in meters (affects collision capsule width)
|
| 144 |
+
eye_height: Camera height from feet (defaults to height - 0.1m if not specified)
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
Dictionary with updated dimensions and message
|
| 148 |
+
"""
|
| 149 |
+
scene = storage.get(scene_id)
|
| 150 |
+
if not scene:
|
| 151 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 152 |
+
|
| 153 |
+
if "player_config" not in scene:
|
| 154 |
+
scene["player_config"] = {}
|
| 155 |
+
|
| 156 |
+
# Default eye height to slightly below top of head
|
| 157 |
+
if eye_height is None:
|
| 158 |
+
eye_height = height - 0.1
|
| 159 |
+
|
| 160 |
+
scene["player_config"]["player_height"] = height
|
| 161 |
+
scene["player_config"]["player_radius"] = radius
|
| 162 |
+
scene["player_config"]["eye_height"] = eye_height
|
| 163 |
+
storage.save(scene)
|
| 164 |
+
|
| 165 |
+
return {
|
| 166 |
+
"scene_id": scene_id,
|
| 167 |
+
"message": f"Set player dimensions: height={height}m, radius={radius}m, eye_height={eye_height}m",
|
| 168 |
+
"player_height": height,
|
| 169 |
+
"player_radius": radius,
|
| 170 |
+
"eye_height": eye_height,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def set_movement_acceleration(
|
| 175 |
+
scene_id: str,
|
| 176 |
+
acceleration: float,
|
| 177 |
+
damping: float
|
| 178 |
+
) -> Dict[str, Any]:
|
| 179 |
+
"""
|
| 180 |
+
Implementation: Configure movement acceleration and damping
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
scene_id: ID of the scene
|
| 184 |
+
acceleration: How quickly player reaches max speed (0.0=instant, higher=slower)
|
| 185 |
+
damping: Linear damping/air resistance (0.0-1.0, higher=more friction)
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Dictionary with updated settings and message
|
| 189 |
+
"""
|
| 190 |
+
scene = storage.get(scene_id)
|
| 191 |
+
if not scene:
|
| 192 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 193 |
+
|
| 194 |
+
if "player_config" not in scene:
|
| 195 |
+
scene["player_config"] = {}
|
| 196 |
+
|
| 197 |
+
scene["player_config"]["movement_acceleration"] = acceleration
|
| 198 |
+
scene["player_config"]["linear_damping"] = damping
|
| 199 |
+
storage.save(scene)
|
| 200 |
+
|
| 201 |
+
feel = "instant" if acceleration < 0.1 else ("snappy" if acceleration < 0.5 else "sliding")
|
| 202 |
+
return {
|
| 203 |
+
"scene_id": scene_id,
|
| 204 |
+
"message": f"Set movement acceleration={acceleration}, damping={damping} ({feel} feel)",
|
| 205 |
+
"movement_acceleration": acceleration,
|
| 206 |
+
"linear_damping": damping,
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def set_air_control(scene_id: str, air_control_factor: float) -> Dict[str, Any]:
|
| 211 |
+
"""
|
| 212 |
+
Implementation: Configure movement control while airborne
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
scene_id: ID of the scene
|
| 216 |
+
air_control_factor: Movement control while airborne (0.0-1.0)
|
| 217 |
+
1.0 = full control, 0.0 = no air steering
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Dictionary with updated setting and message
|
| 221 |
+
"""
|
| 222 |
+
scene = storage.get(scene_id)
|
| 223 |
+
if not scene:
|
| 224 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 225 |
+
|
| 226 |
+
if "player_config" not in scene:
|
| 227 |
+
scene["player_config"] = {}
|
| 228 |
+
|
| 229 |
+
# Clamp between 0 and 1
|
| 230 |
+
air_control_factor = max(0.0, min(1.0, air_control_factor))
|
| 231 |
+
|
| 232 |
+
scene["player_config"]["air_control"] = air_control_factor
|
| 233 |
+
storage.save(scene)
|
| 234 |
+
|
| 235 |
+
control_desc = "full" if air_control_factor >= 0.9 else ("limited" if air_control_factor >= 0.3 else "none")
|
| 236 |
+
return {
|
| 237 |
+
"scene_id": scene_id,
|
| 238 |
+
"message": f"Set air control to {air_control_factor} ({control_desc} control)",
|
| 239 |
+
"air_control": air_control_factor,
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def set_camera_fov(scene_id: str, fov: float) -> Dict[str, Any]:
|
| 244 |
+
"""
|
| 245 |
+
Implementation: Set camera field of view
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
scene_id: ID of the scene
|
| 249 |
+
fov: Field of view in degrees (typical: 60-90, default: 75)
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
Dictionary with updated FOV and message
|
| 253 |
+
"""
|
| 254 |
+
scene = storage.get(scene_id)
|
| 255 |
+
if not scene:
|
| 256 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 257 |
+
|
| 258 |
+
if "player_config" not in scene:
|
| 259 |
+
scene["player_config"] = {}
|
| 260 |
+
|
| 261 |
+
# Clamp FOV to reasonable range
|
| 262 |
+
fov = max(30.0, min(120.0, fov))
|
| 263 |
+
|
| 264 |
+
scene["player_config"]["camera_fov"] = fov
|
| 265 |
+
storage.save(scene)
|
| 266 |
+
|
| 267 |
+
style = "narrow" if fov < 70 else ("normal" if fov < 90 else "wide")
|
| 268 |
+
return {
|
| 269 |
+
"scene_id": scene_id,
|
| 270 |
+
"message": f"Set camera FOV to {fov}° ({style})",
|
| 271 |
+
"camera_fov": fov,
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def set_vertical_look_limits(
|
| 276 |
+
scene_id: str,
|
| 277 |
+
min_pitch: float,
|
| 278 |
+
max_pitch: float
|
| 279 |
+
) -> Dict[str, Any]:
|
| 280 |
+
"""
|
| 281 |
+
Implementation: Configure vertical look angle limits
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
scene_id: ID of the scene
|
| 285 |
+
min_pitch: Minimum pitch angle in degrees (looking down, negative)
|
| 286 |
+
max_pitch: Maximum pitch angle in degrees (looking up, positive)
|
| 287 |
+
|
| 288 |
+
Returns:
|
| 289 |
+
Dictionary with updated limits and message
|
| 290 |
+
"""
|
| 291 |
+
scene = storage.get(scene_id)
|
| 292 |
+
if not scene:
|
| 293 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 294 |
+
|
| 295 |
+
if "player_config" not in scene:
|
| 296 |
+
scene["player_config"] = {}
|
| 297 |
+
|
| 298 |
+
scene["player_config"]["min_pitch"] = min_pitch
|
| 299 |
+
scene["player_config"]["max_pitch"] = max_pitch
|
| 300 |
+
storage.save(scene)
|
| 301 |
+
|
| 302 |
+
return {
|
| 303 |
+
"scene_id": scene_id,
|
| 304 |
+
"message": f"Set vertical look limits: {min_pitch}° to {max_pitch}°",
|
| 305 |
+
"min_pitch": min_pitch,
|
| 306 |
+
"max_pitch": max_pitch,
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def get_player_config(scene_id: str) -> Dict[str, Any]:
|
| 311 |
+
"""
|
| 312 |
+
Implementation: Get current player configuration
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
scene_id: ID of the scene
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
Dictionary with all player config values
|
| 319 |
+
"""
|
| 320 |
+
scene = storage.get(scene_id)
|
| 321 |
+
if not scene:
|
| 322 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 323 |
+
|
| 324 |
+
player_config = scene.get("player_config", {})
|
| 325 |
+
|
| 326 |
+
# Return with defaults if not set
|
| 327 |
+
return {
|
| 328 |
+
"scene_id": scene_id,
|
| 329 |
+
"player_config": {
|
| 330 |
+
# Phase 1
|
| 331 |
+
"move_speed": player_config.get("move_speed", 5.0),
|
| 332 |
+
"jump_force": player_config.get("jump_force", 5.0),
|
| 333 |
+
"mouse_sensitivity": player_config.get("mouse_sensitivity", 0.002),
|
| 334 |
+
"invert_y": player_config.get("invert_y", False),
|
| 335 |
+
"gravity": player_config.get("gravity", -9.82),
|
| 336 |
+
"player_height": player_config.get("player_height", 1.7),
|
| 337 |
+
"player_radius": player_config.get("player_radius", 0.3),
|
| 338 |
+
"eye_height": player_config.get("eye_height", 1.6),
|
| 339 |
+
# Phase 2
|
| 340 |
+
"movement_acceleration": player_config.get("movement_acceleration", 0.0),
|
| 341 |
+
"linear_damping": player_config.get("linear_damping", 0.9),
|
| 342 |
+
"air_control": player_config.get("air_control", 1.0),
|
| 343 |
+
"camera_fov": player_config.get("camera_fov", 75.0),
|
| 344 |
+
"min_pitch": player_config.get("min_pitch", -89.0),
|
| 345 |
+
"max_pitch": player_config.get("max_pitch", 89.0),
|
| 346 |
+
}
|
| 347 |
+
}
|
backend/tools/rendering_tools.py
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rendering & Lighting Tools
|
| 3 |
+
Fine-grained control over scene lighting, materials, background, fog,
|
| 4 |
+
post-processing effects, and camera effects.
|
| 5 |
+
"""
|
| 6 |
+
from typing import Dict, Any, Optional
|
| 7 |
+
from backend.storage import storage
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def add_light(
|
| 11 |
+
scene_id: str,
|
| 12 |
+
light_type: str,
|
| 13 |
+
name: str,
|
| 14 |
+
color: str = "#ffffff",
|
| 15 |
+
intensity: float = 1.0,
|
| 16 |
+
position: Optional[Dict[str, float]] = None,
|
| 17 |
+
target: Optional[Dict[str, float]] = None,
|
| 18 |
+
cast_shadow: bool = False,
|
| 19 |
+
spot_angle: Optional[float] = None
|
| 20 |
+
) -> Dict[str, Any]:
|
| 21 |
+
"""
|
| 22 |
+
Implementation: Add a new light source to the scene
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
scene_id: ID of the scene
|
| 26 |
+
light_type: "ambient" | "directional" | "point" | "spot"
|
| 27 |
+
name: Light identifier (e.g., "Torch1", "MainLight")
|
| 28 |
+
color: Hex color (default: "#ffffff")
|
| 29 |
+
intensity: Brightness 0.0-2.0 (default: 1.0)
|
| 30 |
+
position: Position for directional/point/spot lights
|
| 31 |
+
target: Target position for directional/spot lights
|
| 32 |
+
cast_shadow: Enable shadows (default: False)
|
| 33 |
+
spot_angle: Cone angle in degrees (spot lights only)
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
Dictionary with light details and message
|
| 37 |
+
"""
|
| 38 |
+
scene = storage.get(scene_id)
|
| 39 |
+
if not scene:
|
| 40 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 41 |
+
|
| 42 |
+
# Validate light type
|
| 43 |
+
valid_types = ["ambient", "directional", "point", "spot"]
|
| 44 |
+
if light_type not in valid_types:
|
| 45 |
+
raise ValueError(f"Invalid light_type '{light_type}'. Must be one of: {valid_types}")
|
| 46 |
+
|
| 47 |
+
# Check for duplicate name
|
| 48 |
+
if "lights" not in scene:
|
| 49 |
+
scene["lights"] = []
|
| 50 |
+
|
| 51 |
+
for light in scene["lights"]:
|
| 52 |
+
if light.get("name") == name:
|
| 53 |
+
raise ValueError(f"Light with name '{name}' already exists. Use update_light() to modify it.")
|
| 54 |
+
|
| 55 |
+
# Create light object
|
| 56 |
+
light_obj = {
|
| 57 |
+
"name": name,
|
| 58 |
+
"light_type": light_type,
|
| 59 |
+
"color": color,
|
| 60 |
+
"intensity": intensity,
|
| 61 |
+
"cast_shadow": cast_shadow
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Add position for non-ambient lights
|
| 65 |
+
if light_type != "ambient":
|
| 66 |
+
if position:
|
| 67 |
+
light_obj["position"] = position
|
| 68 |
+
else:
|
| 69 |
+
# Default positions
|
| 70 |
+
defaults = {
|
| 71 |
+
"directional": {"x": 50, "y": 50, "z": 50},
|
| 72 |
+
"point": {"x": 0, "y": 5, "z": 0},
|
| 73 |
+
"spot": {"x": 0, "y": 5, "z": 0}
|
| 74 |
+
}
|
| 75 |
+
light_obj["position"] = defaults.get(light_type, {"x": 0, "y": 5, "z": 0})
|
| 76 |
+
|
| 77 |
+
# Add target for directional/spot lights
|
| 78 |
+
if light_type in ["directional", "spot"] and target:
|
| 79 |
+
light_obj["target"] = target
|
| 80 |
+
|
| 81 |
+
# Add spot angle for spot lights
|
| 82 |
+
if light_type == "spot":
|
| 83 |
+
light_obj["spot_angle"] = spot_angle if spot_angle else 45.0
|
| 84 |
+
|
| 85 |
+
scene["lights"].append(light_obj)
|
| 86 |
+
storage.save(scene)
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"scene_id": scene_id,
|
| 90 |
+
"message": f"Added {light_type} light '{name}' to scene",
|
| 91 |
+
"light": light_obj
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def remove_light(scene_id: str, light_name: str) -> Dict[str, Any]:
|
| 96 |
+
"""
|
| 97 |
+
Implementation: Remove a light from the scene
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
scene_id: ID of the scene
|
| 101 |
+
light_name: Name of light to remove
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
Dictionary with confirmation message
|
| 105 |
+
"""
|
| 106 |
+
scene = storage.get(scene_id)
|
| 107 |
+
if not scene:
|
| 108 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 109 |
+
|
| 110 |
+
if "lights" not in scene or not scene["lights"]:
|
| 111 |
+
raise ValueError(f"Scene has no lights to remove")
|
| 112 |
+
|
| 113 |
+
# Find and remove light
|
| 114 |
+
original_count = len(scene["lights"])
|
| 115 |
+
scene["lights"] = [light for light in scene["lights"] if light.get("name") != light_name]
|
| 116 |
+
|
| 117 |
+
if len(scene["lights"]) == original_count:
|
| 118 |
+
raise ValueError(f"Light '{light_name}' not found in scene")
|
| 119 |
+
|
| 120 |
+
storage.save(scene)
|
| 121 |
+
|
| 122 |
+
return {
|
| 123 |
+
"scene_id": scene_id,
|
| 124 |
+
"message": f"Removed light '{light_name}' from scene",
|
| 125 |
+
"light_name": light_name
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def update_light(
|
| 130 |
+
scene_id: str,
|
| 131 |
+
light_name: str,
|
| 132 |
+
color: Optional[str] = None,
|
| 133 |
+
intensity: Optional[float] = None,
|
| 134 |
+
position: Optional[Dict[str, float]] = None,
|
| 135 |
+
cast_shadow: Optional[bool] = None
|
| 136 |
+
) -> Dict[str, Any]:
|
| 137 |
+
"""
|
| 138 |
+
Implementation: Update existing light properties
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
scene_id: ID of the scene
|
| 142 |
+
light_name: Name of light to update
|
| 143 |
+
color: New color (optional)
|
| 144 |
+
intensity: New intensity (optional)
|
| 145 |
+
position: New position (optional)
|
| 146 |
+
cast_shadow: Enable/disable shadows (optional)
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
Dictionary with updated light and message
|
| 150 |
+
"""
|
| 151 |
+
scene = storage.get(scene_id)
|
| 152 |
+
if not scene:
|
| 153 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 154 |
+
|
| 155 |
+
if "lights" not in scene or not scene["lights"]:
|
| 156 |
+
raise ValueError(f"Scene has no lights")
|
| 157 |
+
|
| 158 |
+
# Find light
|
| 159 |
+
light = None
|
| 160 |
+
for l in scene["lights"]:
|
| 161 |
+
if l.get("name") == light_name:
|
| 162 |
+
light = l
|
| 163 |
+
break
|
| 164 |
+
|
| 165 |
+
if not light:
|
| 166 |
+
raise ValueError(f"Light '{light_name}' not found in scene")
|
| 167 |
+
|
| 168 |
+
# Update properties
|
| 169 |
+
updated_props = []
|
| 170 |
+
if color is not None:
|
| 171 |
+
light["color"] = color
|
| 172 |
+
updated_props.append(f"color={color}")
|
| 173 |
+
|
| 174 |
+
if intensity is not None:
|
| 175 |
+
light["intensity"] = intensity
|
| 176 |
+
updated_props.append(f"intensity={intensity}")
|
| 177 |
+
|
| 178 |
+
if position is not None:
|
| 179 |
+
light["position"] = position
|
| 180 |
+
updated_props.append(f"position={position}")
|
| 181 |
+
|
| 182 |
+
if cast_shadow is not None:
|
| 183 |
+
light["cast_shadow"] = cast_shadow
|
| 184 |
+
updated_props.append(f"shadows={'on' if cast_shadow else 'off'}")
|
| 185 |
+
|
| 186 |
+
storage.save(scene)
|
| 187 |
+
|
| 188 |
+
return {
|
| 189 |
+
"scene_id": scene_id,
|
| 190 |
+
"message": f"Updated light '{light_name}': {', '.join(updated_props)}",
|
| 191 |
+
"light": light
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def get_lights(scene_id: str) -> Dict[str, Any]:
|
| 196 |
+
"""
|
| 197 |
+
Implementation: Get all lights in the scene
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
scene_id: ID of the scene
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
Dictionary with list of all lights
|
| 204 |
+
"""
|
| 205 |
+
scene = storage.get(scene_id)
|
| 206 |
+
if not scene:
|
| 207 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 208 |
+
|
| 209 |
+
lights = scene.get("lights", [])
|
| 210 |
+
|
| 211 |
+
return {
|
| 212 |
+
"scene_id": scene_id,
|
| 213 |
+
"lights": lights,
|
| 214 |
+
"count": len(lights)
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def update_object_material(
|
| 219 |
+
scene_id: str,
|
| 220 |
+
object_id: str,
|
| 221 |
+
color: Optional[str] = None,
|
| 222 |
+
metalness: Optional[float] = None,
|
| 223 |
+
roughness: Optional[float] = None,
|
| 224 |
+
opacity: Optional[float] = None,
|
| 225 |
+
emissive: Optional[str] = None,
|
| 226 |
+
emissive_intensity: Optional[float] = None
|
| 227 |
+
) -> Dict[str, Any]:
|
| 228 |
+
"""
|
| 229 |
+
Implementation: Update an object's material properties
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
scene_id: ID of the scene
|
| 233 |
+
object_id: Object to update
|
| 234 |
+
color: Hex color (optional)
|
| 235 |
+
metalness: 0.0-1.0 (optional)
|
| 236 |
+
roughness: 0.0-1.0 (optional)
|
| 237 |
+
opacity: 0.0-1.0 (optional)
|
| 238 |
+
emissive: Emissive color for glow (optional)
|
| 239 |
+
emissive_intensity: Glow strength (optional)
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
Dictionary with updated material and message
|
| 243 |
+
"""
|
| 244 |
+
scene = storage.get(scene_id)
|
| 245 |
+
if not scene:
|
| 246 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 247 |
+
|
| 248 |
+
if "objects" not in scene or not scene["objects"]:
|
| 249 |
+
raise ValueError(f"Scene has no objects")
|
| 250 |
+
|
| 251 |
+
# Find object
|
| 252 |
+
obj = None
|
| 253 |
+
for o in scene["objects"]:
|
| 254 |
+
if o.get("object_id") == object_id:
|
| 255 |
+
obj = o
|
| 256 |
+
break
|
| 257 |
+
|
| 258 |
+
if not obj:
|
| 259 |
+
raise ValueError(f"Object '{object_id}' not found in scene")
|
| 260 |
+
|
| 261 |
+
# Ensure material exists
|
| 262 |
+
if "material" not in obj:
|
| 263 |
+
obj["material"] = {}
|
| 264 |
+
|
| 265 |
+
# Update material properties
|
| 266 |
+
updated_props = []
|
| 267 |
+
|
| 268 |
+
if color is not None:
|
| 269 |
+
obj["material"]["color"] = color
|
| 270 |
+
updated_props.append(f"color={color}")
|
| 271 |
+
|
| 272 |
+
if metalness is not None:
|
| 273 |
+
obj["material"]["metalness"] = max(0.0, min(1.0, metalness))
|
| 274 |
+
updated_props.append(f"metalness={metalness}")
|
| 275 |
+
|
| 276 |
+
if roughness is not None:
|
| 277 |
+
obj["material"]["roughness"] = max(0.0, min(1.0, roughness))
|
| 278 |
+
updated_props.append(f"roughness={roughness}")
|
| 279 |
+
|
| 280 |
+
if opacity is not None:
|
| 281 |
+
obj["material"]["opacity"] = max(0.0, min(1.0, opacity))
|
| 282 |
+
updated_props.append(f"opacity={opacity}")
|
| 283 |
+
|
| 284 |
+
if emissive is not None:
|
| 285 |
+
obj["material"]["emissive"] = emissive
|
| 286 |
+
updated_props.append(f"emissive={emissive}")
|
| 287 |
+
|
| 288 |
+
if emissive_intensity is not None:
|
| 289 |
+
obj["material"]["emissive_intensity"] = emissive_intensity
|
| 290 |
+
updated_props.append(f"emissive_intensity={emissive_intensity}")
|
| 291 |
+
|
| 292 |
+
storage.save(scene)
|
| 293 |
+
|
| 294 |
+
return {
|
| 295 |
+
"scene_id": scene_id,
|
| 296 |
+
"object_id": object_id,
|
| 297 |
+
"message": f"Updated material: {', '.join(updated_props)}",
|
| 298 |
+
"material": obj["material"]
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def set_background_color(
|
| 303 |
+
scene_id: str,
|
| 304 |
+
color: Optional[str] = None,
|
| 305 |
+
bg_type: str = "solid",
|
| 306 |
+
gradient_top: Optional[str] = None,
|
| 307 |
+
gradient_bottom: Optional[str] = None
|
| 308 |
+
) -> Dict[str, Any]:
|
| 309 |
+
"""
|
| 310 |
+
Implementation: Set scene background color
|
| 311 |
+
|
| 312 |
+
Args:
|
| 313 |
+
scene_id: ID of the scene
|
| 314 |
+
color: Hex color for solid background
|
| 315 |
+
bg_type: "solid" | "gradient"
|
| 316 |
+
gradient_top: Top color for gradient
|
| 317 |
+
gradient_bottom: Bottom color for gradient
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
Dictionary with background settings and message
|
| 321 |
+
"""
|
| 322 |
+
scene = storage.get(scene_id)
|
| 323 |
+
if not scene:
|
| 324 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 325 |
+
|
| 326 |
+
if "environment" not in scene:
|
| 327 |
+
scene["environment"] = {}
|
| 328 |
+
|
| 329 |
+
if bg_type == "gradient":
|
| 330 |
+
if not gradient_top or not gradient_bottom:
|
| 331 |
+
raise ValueError("gradient_top and gradient_bottom are required for gradient backgrounds")
|
| 332 |
+
|
| 333 |
+
scene["environment"]["background_type"] = "gradient"
|
| 334 |
+
scene["environment"]["background_gradient_top"] = gradient_top
|
| 335 |
+
scene["environment"]["background_gradient_bottom"] = gradient_bottom
|
| 336 |
+
|
| 337 |
+
message = f"Set background to gradient: {gradient_top} → {gradient_bottom}"
|
| 338 |
+
else:
|
| 339 |
+
if not color:
|
| 340 |
+
raise ValueError("color is required for solid backgrounds")
|
| 341 |
+
|
| 342 |
+
scene["environment"]["background_type"] = "solid"
|
| 343 |
+
scene["environment"]["background_color"] = color
|
| 344 |
+
|
| 345 |
+
message = f"Set background to {color}"
|
| 346 |
+
|
| 347 |
+
storage.save(scene)
|
| 348 |
+
|
| 349 |
+
return {
|
| 350 |
+
"scene_id": scene_id,
|
| 351 |
+
"message": message,
|
| 352 |
+
"background": scene["environment"]
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def set_fog(
|
| 357 |
+
scene_id: str,
|
| 358 |
+
enabled: bool,
|
| 359 |
+
color: Optional[str] = None,
|
| 360 |
+
near: Optional[float] = None,
|
| 361 |
+
far: Optional[float] = None,
|
| 362 |
+
density: Optional[float] = None
|
| 363 |
+
) -> Dict[str, Any]:
|
| 364 |
+
"""
|
| 365 |
+
Implementation: Set atmospheric fog
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
scene_id: ID of the scene
|
| 369 |
+
enabled: Enable/disable fog
|
| 370 |
+
color: Fog color (default: "#aaaaaa")
|
| 371 |
+
near: Start distance for linear fog
|
| 372 |
+
far: End distance for linear fog
|
| 373 |
+
density: Density for exponential fog
|
| 374 |
+
|
| 375 |
+
Returns:
|
| 376 |
+
Dictionary with fog settings and message
|
| 377 |
+
"""
|
| 378 |
+
scene = storage.get(scene_id)
|
| 379 |
+
if not scene:
|
| 380 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 381 |
+
|
| 382 |
+
if "environment" not in scene:
|
| 383 |
+
scene["environment"] = {}
|
| 384 |
+
|
| 385 |
+
if "fog" not in scene["environment"]:
|
| 386 |
+
scene["environment"]["fog"] = {}
|
| 387 |
+
|
| 388 |
+
fog = scene["environment"]["fog"]
|
| 389 |
+
fog["enabled"] = enabled
|
| 390 |
+
|
| 391 |
+
if enabled:
|
| 392 |
+
fog["color"] = color if color else "#aaaaaa"
|
| 393 |
+
|
| 394 |
+
# Determine fog type
|
| 395 |
+
if density is not None:
|
| 396 |
+
fog["type"] = "exponential"
|
| 397 |
+
fog["density"] = density
|
| 398 |
+
message = f"Enabled exponential fog (density={density}, color={fog['color']})"
|
| 399 |
+
elif near is not None and far is not None:
|
| 400 |
+
fog["type"] = "linear"
|
| 401 |
+
fog["near"] = near
|
| 402 |
+
fog["far"] = far
|
| 403 |
+
message = f"Enabled linear fog (near={near}, far={far}, color={fog['color']})"
|
| 404 |
+
else:
|
| 405 |
+
# Default linear fog
|
| 406 |
+
fog["type"] = "linear"
|
| 407 |
+
fog["near"] = 10
|
| 408 |
+
fog["far"] = 50
|
| 409 |
+
message = f"Enabled linear fog (near=10, far=50, color={fog['color']})"
|
| 410 |
+
else:
|
| 411 |
+
message = "Disabled fog"
|
| 412 |
+
|
| 413 |
+
storage.save(scene)
|
| 414 |
+
|
| 415 |
+
return {
|
| 416 |
+
"scene_id": scene_id,
|
| 417 |
+
"message": message,
|
| 418 |
+
"fog": fog
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
# =============================================================================
|
| 423 |
+
# Post-Processing Tools
|
| 424 |
+
# =============================================================================
|
| 425 |
+
|
| 426 |
+
def set_bloom(
|
| 427 |
+
scene_id: str,
|
| 428 |
+
enabled: bool,
|
| 429 |
+
strength: float = 1.0,
|
| 430 |
+
radius: float = 0.4,
|
| 431 |
+
threshold: float = 0.8
|
| 432 |
+
) -> Dict[str, Any]:
|
| 433 |
+
"""
|
| 434 |
+
Configure bloom (glow) post-processing effect.
|
| 435 |
+
|
| 436 |
+
Bloom creates a glow effect around bright areas of the scene,
|
| 437 |
+
simulating how cameras capture bright light sources.
|
| 438 |
+
|
| 439 |
+
Args:
|
| 440 |
+
scene_id: ID of the scene
|
| 441 |
+
enabled: Enable/disable bloom
|
| 442 |
+
strength: Bloom intensity (0.0-3.0, default: 1.0)
|
| 443 |
+
radius: Bloom spread/blur radius (0.0-1.0, default: 0.4)
|
| 444 |
+
threshold: Brightness threshold to trigger bloom (0.0-1.0, default: 0.8)
|
| 445 |
+
|
| 446 |
+
Returns:
|
| 447 |
+
Dictionary with bloom settings and message
|
| 448 |
+
"""
|
| 449 |
+
scene = storage.get(scene_id)
|
| 450 |
+
if not scene:
|
| 451 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 452 |
+
|
| 453 |
+
if "post_processing" not in scene:
|
| 454 |
+
scene["post_processing"] = {}
|
| 455 |
+
|
| 456 |
+
bloom = {
|
| 457 |
+
"enabled": enabled,
|
| 458 |
+
"strength": max(0.0, min(3.0, strength)),
|
| 459 |
+
"radius": max(0.0, min(1.0, radius)),
|
| 460 |
+
"threshold": max(0.0, min(1.0, threshold))
|
| 461 |
+
}
|
| 462 |
+
scene["post_processing"]["bloom"] = bloom
|
| 463 |
+
|
| 464 |
+
storage.save(scene)
|
| 465 |
+
|
| 466 |
+
if enabled:
|
| 467 |
+
message = f"Enabled bloom (strength={strength}, radius={radius}, threshold={threshold})"
|
| 468 |
+
else:
|
| 469 |
+
message = "Disabled bloom"
|
| 470 |
+
|
| 471 |
+
return {
|
| 472 |
+
"scene_id": scene_id,
|
| 473 |
+
"message": message,
|
| 474 |
+
"bloom": bloom
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def set_ssao(
|
| 479 |
+
scene_id: str,
|
| 480 |
+
enabled: bool,
|
| 481 |
+
radius: float = 0.5,
|
| 482 |
+
intensity: float = 1.0,
|
| 483 |
+
bias: float = 0.025
|
| 484 |
+
) -> Dict[str, Any]:
|
| 485 |
+
"""
|
| 486 |
+
Configure Screen Space Ambient Occlusion (SSAO).
|
| 487 |
+
|
| 488 |
+
SSAO adds soft shadows in corners and crevices where ambient light
|
| 489 |
+
would naturally be occluded, adding depth and realism.
|
| 490 |
+
|
| 491 |
+
Args:
|
| 492 |
+
scene_id: ID of the scene
|
| 493 |
+
enabled: Enable/disable SSAO
|
| 494 |
+
radius: Sample radius in world units (0.1-2.0, default: 0.5)
|
| 495 |
+
intensity: Shadow intensity (0.0-2.0, default: 1.0)
|
| 496 |
+
bias: Depth bias to prevent self-occlusion (0.001-0.1, default: 0.025)
|
| 497 |
+
|
| 498 |
+
Returns:
|
| 499 |
+
Dictionary with SSAO settings and message
|
| 500 |
+
"""
|
| 501 |
+
scene = storage.get(scene_id)
|
| 502 |
+
if not scene:
|
| 503 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 504 |
+
|
| 505 |
+
if "post_processing" not in scene:
|
| 506 |
+
scene["post_processing"] = {}
|
| 507 |
+
|
| 508 |
+
ssao = {
|
| 509 |
+
"enabled": enabled,
|
| 510 |
+
"radius": max(0.1, min(2.0, radius)),
|
| 511 |
+
"intensity": max(0.0, min(2.0, intensity)),
|
| 512 |
+
"bias": max(0.001, min(0.1, bias))
|
| 513 |
+
}
|
| 514 |
+
scene["post_processing"]["ssao"] = ssao
|
| 515 |
+
|
| 516 |
+
storage.save(scene)
|
| 517 |
+
|
| 518 |
+
if enabled:
|
| 519 |
+
message = f"Enabled SSAO (radius={radius}, intensity={intensity})"
|
| 520 |
+
else:
|
| 521 |
+
message = "Disabled SSAO"
|
| 522 |
+
|
| 523 |
+
return {
|
| 524 |
+
"scene_id": scene_id,
|
| 525 |
+
"message": message,
|
| 526 |
+
"ssao": ssao
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def set_color_grading(
|
| 531 |
+
scene_id: str,
|
| 532 |
+
enabled: bool,
|
| 533 |
+
brightness: float = 0.0,
|
| 534 |
+
contrast: float = 1.0,
|
| 535 |
+
saturation: float = 1.0,
|
| 536 |
+
hue: float = 0.0,
|
| 537 |
+
exposure: float = 1.0,
|
| 538 |
+
gamma: float = 1.0
|
| 539 |
+
) -> Dict[str, Any]:
|
| 540 |
+
"""
|
| 541 |
+
Configure color grading post-processing.
|
| 542 |
+
|
| 543 |
+
Adjust overall image colors for cinematic looks or stylized effects.
|
| 544 |
+
|
| 545 |
+
Args:
|
| 546 |
+
scene_id: ID of the scene
|
| 547 |
+
enabled: Enable/disable color grading
|
| 548 |
+
brightness: Brightness adjustment (-1.0 to 1.0, default: 0.0)
|
| 549 |
+
contrast: Contrast multiplier (0.0-2.0, default: 1.0)
|
| 550 |
+
saturation: Color saturation (0.0=grayscale, 1.0=normal, 2.0=vivid)
|
| 551 |
+
hue: Hue shift in degrees (-180 to 180, default: 0)
|
| 552 |
+
exposure: Exposure adjustment (0.0-3.0, default: 1.0)
|
| 553 |
+
gamma: Gamma correction (0.5-2.5, default: 1.0)
|
| 554 |
+
|
| 555 |
+
Returns:
|
| 556 |
+
Dictionary with color grading settings and message
|
| 557 |
+
"""
|
| 558 |
+
scene = storage.get(scene_id)
|
| 559 |
+
if not scene:
|
| 560 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 561 |
+
|
| 562 |
+
if "post_processing" not in scene:
|
| 563 |
+
scene["post_processing"] = {}
|
| 564 |
+
|
| 565 |
+
color_grading = {
|
| 566 |
+
"enabled": enabled,
|
| 567 |
+
"brightness": max(-1.0, min(1.0, brightness)),
|
| 568 |
+
"contrast": max(0.0, min(2.0, contrast)),
|
| 569 |
+
"saturation": max(0.0, min(2.0, saturation)),
|
| 570 |
+
"hue": max(-180, min(180, hue)),
|
| 571 |
+
"exposure": max(0.0, min(3.0, exposure)),
|
| 572 |
+
"gamma": max(0.5, min(2.5, gamma))
|
| 573 |
+
}
|
| 574 |
+
scene["post_processing"]["color_grading"] = color_grading
|
| 575 |
+
|
| 576 |
+
storage.save(scene)
|
| 577 |
+
|
| 578 |
+
if enabled:
|
| 579 |
+
adjustments = []
|
| 580 |
+
if brightness != 0.0:
|
| 581 |
+
adjustments.append(f"brightness={brightness}")
|
| 582 |
+
if contrast != 1.0:
|
| 583 |
+
adjustments.append(f"contrast={contrast}")
|
| 584 |
+
if saturation != 1.0:
|
| 585 |
+
adjustments.append(f"saturation={saturation}")
|
| 586 |
+
if hue != 0.0:
|
| 587 |
+
adjustments.append(f"hue={hue}°")
|
| 588 |
+
if exposure != 1.0:
|
| 589 |
+
adjustments.append(f"exposure={exposure}")
|
| 590 |
+
if gamma != 1.0:
|
| 591 |
+
adjustments.append(f"gamma={gamma}")
|
| 592 |
+
|
| 593 |
+
if adjustments:
|
| 594 |
+
message = f"Enabled color grading ({', '.join(adjustments)})"
|
| 595 |
+
else:
|
| 596 |
+
message = "Enabled color grading (default settings)"
|
| 597 |
+
else:
|
| 598 |
+
message = "Disabled color grading"
|
| 599 |
+
|
| 600 |
+
return {
|
| 601 |
+
"scene_id": scene_id,
|
| 602 |
+
"message": message,
|
| 603 |
+
"color_grading": color_grading
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def set_vignette(
|
| 608 |
+
scene_id: str,
|
| 609 |
+
enabled: bool,
|
| 610 |
+
intensity: float = 0.5,
|
| 611 |
+
smoothness: float = 0.5
|
| 612 |
+
) -> Dict[str, Any]:
|
| 613 |
+
"""
|
| 614 |
+
Configure vignette effect (darkened edges).
|
| 615 |
+
|
| 616 |
+
Vignette darkens the corners and edges of the screen,
|
| 617 |
+
drawing focus to the center of the image.
|
| 618 |
+
|
| 619 |
+
Args:
|
| 620 |
+
scene_id: ID of the scene
|
| 621 |
+
enabled: Enable/disable vignette
|
| 622 |
+
intensity: Darkness of the vignette (0.0-1.0, default: 0.5)
|
| 623 |
+
smoothness: Softness of the vignette edge (0.0-1.0, default: 0.5)
|
| 624 |
+
|
| 625 |
+
Returns:
|
| 626 |
+
Dictionary with vignette settings and message
|
| 627 |
+
"""
|
| 628 |
+
scene = storage.get(scene_id)
|
| 629 |
+
if not scene:
|
| 630 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 631 |
+
|
| 632 |
+
if "post_processing" not in scene:
|
| 633 |
+
scene["post_processing"] = {}
|
| 634 |
+
|
| 635 |
+
vignette = {
|
| 636 |
+
"enabled": enabled,
|
| 637 |
+
"intensity": max(0.0, min(1.0, intensity)),
|
| 638 |
+
"smoothness": max(0.0, min(1.0, smoothness))
|
| 639 |
+
}
|
| 640 |
+
scene["post_processing"]["vignette"] = vignette
|
| 641 |
+
|
| 642 |
+
storage.save(scene)
|
| 643 |
+
|
| 644 |
+
if enabled:
|
| 645 |
+
message = f"Enabled vignette (intensity={intensity}, smoothness={smoothness})"
|
| 646 |
+
else:
|
| 647 |
+
message = "Disabled vignette"
|
| 648 |
+
|
| 649 |
+
return {
|
| 650 |
+
"scene_id": scene_id,
|
| 651 |
+
"message": message,
|
| 652 |
+
"vignette": vignette
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
def get_post_processing(scene_id: str) -> Dict[str, Any]:
|
| 657 |
+
"""
|
| 658 |
+
Get all post-processing settings for the scene.
|
| 659 |
+
|
| 660 |
+
Args:
|
| 661 |
+
scene_id: ID of the scene
|
| 662 |
+
|
| 663 |
+
Returns:
|
| 664 |
+
Dictionary with all post-processing settings
|
| 665 |
+
"""
|
| 666 |
+
scene = storage.get(scene_id)
|
| 667 |
+
if not scene:
|
| 668 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 669 |
+
|
| 670 |
+
post_processing = scene.get("post_processing", {})
|
| 671 |
+
|
| 672 |
+
return {
|
| 673 |
+
"scene_id": scene_id,
|
| 674 |
+
"post_processing": post_processing
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
# =============================================================================
|
| 679 |
+
# Camera Effects Tools
|
| 680 |
+
# =============================================================================
|
| 681 |
+
|
| 682 |
+
def set_depth_of_field(
|
| 683 |
+
scene_id: str,
|
| 684 |
+
enabled: bool,
|
| 685 |
+
focus_distance: float = 10.0,
|
| 686 |
+
aperture: float = 0.025,
|
| 687 |
+
max_blur: float = 0.01
|
| 688 |
+
) -> Dict[str, Any]:
|
| 689 |
+
"""
|
| 690 |
+
Configure depth of field (DoF) camera effect.
|
| 691 |
+
|
| 692 |
+
Depth of field blurs objects that are not at the focus distance,
|
| 693 |
+
simulating how real camera lenses focus on a specific plane.
|
| 694 |
+
|
| 695 |
+
Args:
|
| 696 |
+
scene_id: ID of the scene
|
| 697 |
+
enabled: Enable/disable depth of field
|
| 698 |
+
focus_distance: Distance to the focal plane in units (default: 10.0)
|
| 699 |
+
aperture: Aperture size, affects blur amount (0.001-0.1, default: 0.025)
|
| 700 |
+
max_blur: Maximum blur strength (0.0-0.05, default: 0.01)
|
| 701 |
+
|
| 702 |
+
Returns:
|
| 703 |
+
Dictionary with DoF settings and message
|
| 704 |
+
"""
|
| 705 |
+
scene = storage.get(scene_id)
|
| 706 |
+
if not scene:
|
| 707 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 708 |
+
|
| 709 |
+
if "camera_effects" not in scene:
|
| 710 |
+
scene["camera_effects"] = {}
|
| 711 |
+
|
| 712 |
+
dof = {
|
| 713 |
+
"enabled": enabled,
|
| 714 |
+
"focus_distance": max(0.1, focus_distance),
|
| 715 |
+
"aperture": max(0.001, min(0.1, aperture)),
|
| 716 |
+
"max_blur": max(0.0, min(0.05, max_blur))
|
| 717 |
+
}
|
| 718 |
+
scene["camera_effects"]["depth_of_field"] = dof
|
| 719 |
+
|
| 720 |
+
storage.save(scene)
|
| 721 |
+
|
| 722 |
+
if enabled:
|
| 723 |
+
message = f"Enabled depth of field (focus={focus_distance}m, aperture={aperture})"
|
| 724 |
+
else:
|
| 725 |
+
message = "Disabled depth of field"
|
| 726 |
+
|
| 727 |
+
return {
|
| 728 |
+
"scene_id": scene_id,
|
| 729 |
+
"message": message,
|
| 730 |
+
"depth_of_field": dof
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
|
| 734 |
+
def set_motion_blur(
|
| 735 |
+
scene_id: str,
|
| 736 |
+
enabled: bool,
|
| 737 |
+
intensity: float = 0.5,
|
| 738 |
+
samples: int = 8
|
| 739 |
+
) -> Dict[str, Any]:
|
| 740 |
+
"""
|
| 741 |
+
Configure motion blur camera effect.
|
| 742 |
+
|
| 743 |
+
Motion blur adds blur in the direction of camera or object movement,
|
| 744 |
+
creating a sense of speed and smooth motion.
|
| 745 |
+
|
| 746 |
+
Args:
|
| 747 |
+
scene_id: ID of the scene
|
| 748 |
+
enabled: Enable/disable motion blur
|
| 749 |
+
intensity: Blur intensity (0.0-2.0, default: 0.5)
|
| 750 |
+
samples: Quality samples for blur (4-32, default: 8)
|
| 751 |
+
|
| 752 |
+
Returns:
|
| 753 |
+
Dictionary with motion blur settings and message
|
| 754 |
+
"""
|
| 755 |
+
scene = storage.get(scene_id)
|
| 756 |
+
if not scene:
|
| 757 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 758 |
+
|
| 759 |
+
if "camera_effects" not in scene:
|
| 760 |
+
scene["camera_effects"] = {}
|
| 761 |
+
|
| 762 |
+
motion_blur = {
|
| 763 |
+
"enabled": enabled,
|
| 764 |
+
"intensity": max(0.0, min(2.0, intensity)),
|
| 765 |
+
"samples": max(4, min(32, samples))
|
| 766 |
+
}
|
| 767 |
+
scene["camera_effects"]["motion_blur"] = motion_blur
|
| 768 |
+
|
| 769 |
+
storage.save(scene)
|
| 770 |
+
|
| 771 |
+
if enabled:
|
| 772 |
+
message = f"Enabled motion blur (intensity={intensity}, samples={samples})"
|
| 773 |
+
else:
|
| 774 |
+
message = "Disabled motion blur"
|
| 775 |
+
|
| 776 |
+
return {
|
| 777 |
+
"scene_id": scene_id,
|
| 778 |
+
"message": message,
|
| 779 |
+
"motion_blur": motion_blur
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
def set_chromatic_aberration(
|
| 784 |
+
scene_id: str,
|
| 785 |
+
enabled: bool,
|
| 786 |
+
intensity: float = 0.005
|
| 787 |
+
) -> Dict[str, Any]:
|
| 788 |
+
"""
|
| 789 |
+
Configure chromatic aberration effect.
|
| 790 |
+
|
| 791 |
+
Chromatic aberration simulates lens imperfection by separating
|
| 792 |
+
color channels at the edges of the screen.
|
| 793 |
+
|
| 794 |
+
Args:
|
| 795 |
+
scene_id: ID of the scene
|
| 796 |
+
enabled: Enable/disable chromatic aberration
|
| 797 |
+
intensity: Effect strength (0.0-0.05, default: 0.005)
|
| 798 |
+
|
| 799 |
+
Returns:
|
| 800 |
+
Dictionary with chromatic aberration settings and message
|
| 801 |
+
"""
|
| 802 |
+
scene = storage.get(scene_id)
|
| 803 |
+
if not scene:
|
| 804 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 805 |
+
|
| 806 |
+
if "camera_effects" not in scene:
|
| 807 |
+
scene["camera_effects"] = {}
|
| 808 |
+
|
| 809 |
+
chromatic = {
|
| 810 |
+
"enabled": enabled,
|
| 811 |
+
"intensity": max(0.0, min(0.05, intensity))
|
| 812 |
+
}
|
| 813 |
+
scene["camera_effects"]["chromatic_aberration"] = chromatic
|
| 814 |
+
|
| 815 |
+
storage.save(scene)
|
| 816 |
+
|
| 817 |
+
if enabled:
|
| 818 |
+
message = f"Enabled chromatic aberration (intensity={intensity})"
|
| 819 |
+
else:
|
| 820 |
+
message = "Disabled chromatic aberration"
|
| 821 |
+
|
| 822 |
+
return {
|
| 823 |
+
"scene_id": scene_id,
|
| 824 |
+
"message": message,
|
| 825 |
+
"chromatic_aberration": chromatic
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
def get_camera_effects(scene_id: str) -> Dict[str, Any]:
|
| 830 |
+
"""
|
| 831 |
+
Get all camera effects settings for the scene.
|
| 832 |
+
|
| 833 |
+
Args:
|
| 834 |
+
scene_id: ID of the scene
|
| 835 |
+
|
| 836 |
+
Returns:
|
| 837 |
+
Dictionary with all camera effects settings
|
| 838 |
+
"""
|
| 839 |
+
scene = storage.get(scene_id)
|
| 840 |
+
if not scene:
|
| 841 |
+
raise ValueError(f"Scene '{scene_id}' not found")
|
| 842 |
+
|
| 843 |
+
camera_effects = scene.get("camera_effects", {})
|
| 844 |
+
|
| 845 |
+
return {
|
| 846 |
+
"scene_id": scene_id,
|
| 847 |
+
"camera_effects": camera_effects
|
| 848 |
+
}
|
backend/tools/scene_tools.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Scene Tools
|
| 3 |
+
Create, modify, and query 3D scenes
|
| 4 |
+
"""
|
| 5 |
+
from typing import Optional, Dict, Any
|
| 6 |
+
from backend.game_models import (
|
| 7 |
+
create_scene,
|
| 8 |
+
create_game_object,
|
| 9 |
+
create_light,
|
| 10 |
+
create_environment,
|
| 11 |
+
create_vector3,
|
| 12 |
+
create_material,
|
| 13 |
+
)
|
| 14 |
+
from backend.storage import storage
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def generate_viewer_url(scene_id: str, base_url: str = "http://localhost:8000") -> str:
|
| 18 |
+
"""Generate viewer URL for a scene"""
|
| 19 |
+
return f"{base_url}/view/scene/{scene_id}"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def create_game_scene(
|
| 23 |
+
name: str = "New Scene",
|
| 24 |
+
description: Optional[str] = None,
|
| 25 |
+
world_width: float = 100.0,
|
| 26 |
+
world_height: float = 100.0,
|
| 27 |
+
world_depth: float = 100.0,
|
| 28 |
+
lighting_preset: str = "day",
|
| 29 |
+
base_url: str = "http://localhost:8000"
|
| 30 |
+
) -> Dict[str, Any]:
|
| 31 |
+
"""
|
| 32 |
+
Create a new 3D scene
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
name: Scene name
|
| 36 |
+
description: Scene description
|
| 37 |
+
world_width: Width of the world
|
| 38 |
+
world_height: Height of the world
|
| 39 |
+
world_depth: Depth of the world
|
| 40 |
+
lighting_preset: Lighting preset (day, night, sunset, studio)
|
| 41 |
+
base_url: Base URL for the deployed space
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
Dict with scene_id, viewer_url, and message
|
| 45 |
+
"""
|
| 46 |
+
# Create default lights based on preset
|
| 47 |
+
lights = []
|
| 48 |
+
if lighting_preset == "day":
|
| 49 |
+
lights = [
|
| 50 |
+
create_light(
|
| 51 |
+
name="Sun",
|
| 52 |
+
light_type="directional",
|
| 53 |
+
color="#ffffff",
|
| 54 |
+
intensity=1.0,
|
| 55 |
+
position=create_vector3(50, 50, 50),
|
| 56 |
+
),
|
| 57 |
+
create_light(
|
| 58 |
+
name="Ambient",
|
| 59 |
+
light_type="ambient",
|
| 60 |
+
color="#ffffff",
|
| 61 |
+
intensity=0.5,
|
| 62 |
+
),
|
| 63 |
+
]
|
| 64 |
+
elif lighting_preset == "night":
|
| 65 |
+
lights = [
|
| 66 |
+
create_light(
|
| 67 |
+
name="Moon",
|
| 68 |
+
light_type="directional",
|
| 69 |
+
color="#6699cc",
|
| 70 |
+
intensity=0.3,
|
| 71 |
+
position=create_vector3(-50, 50, -50),
|
| 72 |
+
),
|
| 73 |
+
create_light(
|
| 74 |
+
name="Ambient",
|
| 75 |
+
light_type="ambient",
|
| 76 |
+
color="#1a1a3a",
|
| 77 |
+
intensity=0.2,
|
| 78 |
+
),
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
# Create environment
|
| 82 |
+
env = create_environment(lighting_preset=lighting_preset)
|
| 83 |
+
if lighting_preset == "night":
|
| 84 |
+
env["background_color"] = "#0a0a1a"
|
| 85 |
+
elif lighting_preset == "sunset":
|
| 86 |
+
env["background_color"] = "#ff6b35"
|
| 87 |
+
|
| 88 |
+
# Create scene
|
| 89 |
+
scene = create_scene(
|
| 90 |
+
name=name,
|
| 91 |
+
description=description,
|
| 92 |
+
world_width=world_width,
|
| 93 |
+
world_height=world_height,
|
| 94 |
+
world_depth=world_depth,
|
| 95 |
+
lights=lights,
|
| 96 |
+
environment=env,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Save to storage
|
| 100 |
+
storage.save(scene)
|
| 101 |
+
|
| 102 |
+
# Generate viewer URL
|
| 103 |
+
viewer_url = generate_viewer_url(scene["scene_id"], base_url)
|
| 104 |
+
|
| 105 |
+
return {
|
| 106 |
+
"scene_id": scene["scene_id"],
|
| 107 |
+
"viewer_url": viewer_url,
|
| 108 |
+
"message": f"Created scene '{scene['name']}' with 10x10 world (white ground plane and boundary walls)",
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def add_game_object(
|
| 113 |
+
scene_id: str,
|
| 114 |
+
object_type: str = "cube",
|
| 115 |
+
name: Optional[str] = None,
|
| 116 |
+
position: Optional[Dict[str, float]] = None,
|
| 117 |
+
rotation: Optional[Dict[str, float]] = None,
|
| 118 |
+
scale: Optional[Dict[str, float]] = None,
|
| 119 |
+
material: Optional[Dict[str, Any]] = None,
|
| 120 |
+
model_path: Optional[str] = None,
|
| 121 |
+
base_url: str = "http://localhost:8000"
|
| 122 |
+
) -> Dict[str, Any]:
|
| 123 |
+
"""
|
| 124 |
+
Add an object to the scene
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
scene_id: ID of the scene
|
| 128 |
+
object_type: Type of object (cube, sphere, cylinder, etc.)
|
| 129 |
+
name: Object name
|
| 130 |
+
position: Position vector {x, y, z}
|
| 131 |
+
rotation: Rotation vector {x, y, z}
|
| 132 |
+
scale: Scale vector {x, y, z}
|
| 133 |
+
material: Material properties dict
|
| 134 |
+
model_path: Path to 3D model file
|
| 135 |
+
base_url: Base URL for the deployed space
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
Dict with object_id, scene_id, viewer_url, and message
|
| 139 |
+
"""
|
| 140 |
+
# Get existing scene
|
| 141 |
+
scene = storage.get(scene_id)
|
| 142 |
+
if not scene:
|
| 143 |
+
raise ValueError(f"Scene with ID '{scene_id}' not found")
|
| 144 |
+
|
| 145 |
+
# Validate position is within 10x10 world bounds (-5 to 5 in X and Z)
|
| 146 |
+
if position:
|
| 147 |
+
x = position.get('x', 0)
|
| 148 |
+
z = position.get('z', 0)
|
| 149 |
+
WORLD_HALF = 5.0
|
| 150 |
+
if abs(x) > WORLD_HALF or abs(z) > WORLD_HALF:
|
| 151 |
+
raise ValueError(
|
| 152 |
+
f"Object position ({x}, {z}) is outside the 10x10 world bounds. "
|
| 153 |
+
f"X and Z must be between -{WORLD_HALF} and {WORLD_HALF}."
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Create game object
|
| 157 |
+
obj = create_game_object(
|
| 158 |
+
object_type=object_type,
|
| 159 |
+
name=name or f"{object_type}_{len(scene['objects'])}",
|
| 160 |
+
position=position,
|
| 161 |
+
rotation=rotation,
|
| 162 |
+
scale=scale,
|
| 163 |
+
material=material,
|
| 164 |
+
model_path=model_path,
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Add to scene
|
| 168 |
+
scene["objects"].append(obj)
|
| 169 |
+
|
| 170 |
+
# Save updated scene
|
| 171 |
+
storage.save(scene)
|
| 172 |
+
|
| 173 |
+
# Generate viewer URL
|
| 174 |
+
viewer_url = generate_viewer_url(scene["scene_id"], base_url)
|
| 175 |
+
|
| 176 |
+
pos = obj["position"]
|
| 177 |
+
return {
|
| 178 |
+
"object_id": obj["id"],
|
| 179 |
+
"scene_id": scene["scene_id"],
|
| 180 |
+
"viewer_url": viewer_url,
|
| 181 |
+
"message": f"Added {obj['name']} ({object_type}) at position ({pos['x']}, {pos['y']}, {pos['z']})",
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def remove_game_object(
|
| 186 |
+
scene_id: str,
|
| 187 |
+
object_id: str,
|
| 188 |
+
base_url: str = "http://localhost:8000"
|
| 189 |
+
) -> Dict[str, Any]:
|
| 190 |
+
"""
|
| 191 |
+
Remove an object from the scene
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
scene_id: ID of the scene
|
| 195 |
+
object_id: ID of the object to remove
|
| 196 |
+
base_url: Base URL for the deployed space
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
Dict with scene_id, viewer_url, and message
|
| 200 |
+
"""
|
| 201 |
+
# Get existing scene
|
| 202 |
+
scene = storage.get(scene_id)
|
| 203 |
+
if not scene:
|
| 204 |
+
raise ValueError(f"Scene with ID '{scene_id}' not found")
|
| 205 |
+
|
| 206 |
+
# Find and remove object
|
| 207 |
+
original_count = len(scene["objects"])
|
| 208 |
+
scene["objects"] = [obj for obj in scene["objects"] if obj["id"] != object_id]
|
| 209 |
+
|
| 210 |
+
if len(scene["objects"]) == original_count:
|
| 211 |
+
raise ValueError(f"Object with ID '{object_id}' not found in scene")
|
| 212 |
+
|
| 213 |
+
# Save updated scene
|
| 214 |
+
storage.save(scene)
|
| 215 |
+
|
| 216 |
+
# Generate viewer URL
|
| 217 |
+
viewer_url = generate_viewer_url(scene["scene_id"], base_url)
|
| 218 |
+
|
| 219 |
+
return {
|
| 220 |
+
"scene_id": scene["scene_id"],
|
| 221 |
+
"viewer_url": viewer_url,
|
| 222 |
+
"message": f"Removed object {object_id}",
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def set_scene_lighting(
|
| 227 |
+
scene_id: str,
|
| 228 |
+
preset: str = "day",
|
| 229 |
+
base_url: str = "http://localhost:8000"
|
| 230 |
+
) -> Dict[str, Any]:
|
| 231 |
+
"""
|
| 232 |
+
Set lighting preset for the scene
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
scene_id: ID of the scene
|
| 236 |
+
preset: Lighting preset (day, night, sunset, studio)
|
| 237 |
+
base_url: Base URL for the deployed space
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
Dict with scene_id, viewer_url, and message
|
| 241 |
+
"""
|
| 242 |
+
# Get existing scene
|
| 243 |
+
scene = storage.get(scene_id)
|
| 244 |
+
if not scene:
|
| 245 |
+
raise ValueError(f"Scene with ID '{scene_id}' not found")
|
| 246 |
+
|
| 247 |
+
# Update lighting preset
|
| 248 |
+
scene["environment"]["lighting_preset"] = preset
|
| 249 |
+
|
| 250 |
+
# Update lights based on preset
|
| 251 |
+
if preset == "day":
|
| 252 |
+
scene["lights"] = [
|
| 253 |
+
create_light(
|
| 254 |
+
name="Sun",
|
| 255 |
+
light_type="directional",
|
| 256 |
+
color="#ffffff",
|
| 257 |
+
intensity=1.0,
|
| 258 |
+
position=create_vector3(50, 50, 50),
|
| 259 |
+
),
|
| 260 |
+
create_light(
|
| 261 |
+
name="Ambient",
|
| 262 |
+
light_type="ambient",
|
| 263 |
+
color="#ffffff",
|
| 264 |
+
intensity=0.5,
|
| 265 |
+
),
|
| 266 |
+
]
|
| 267 |
+
scene["environment"]["background_color"] = "#87CEEB"
|
| 268 |
+
elif preset == "night":
|
| 269 |
+
scene["lights"] = [
|
| 270 |
+
create_light(
|
| 271 |
+
name="Moon",
|
| 272 |
+
light_type="directional",
|
| 273 |
+
color="#6699cc",
|
| 274 |
+
intensity=0.3,
|
| 275 |
+
position=create_vector3(-50, 50, -50),
|
| 276 |
+
),
|
| 277 |
+
create_light(
|
| 278 |
+
name="Ambient",
|
| 279 |
+
light_type="ambient",
|
| 280 |
+
color="#1a1a3a",
|
| 281 |
+
intensity=0.2,
|
| 282 |
+
),
|
| 283 |
+
]
|
| 284 |
+
scene["environment"]["background_color"] = "#0a0a1a"
|
| 285 |
+
elif preset == "sunset":
|
| 286 |
+
scene["lights"] = [
|
| 287 |
+
create_light(
|
| 288 |
+
name="Sun",
|
| 289 |
+
light_type="directional",
|
| 290 |
+
color="#ff6b35",
|
| 291 |
+
intensity=0.8,
|
| 292 |
+
position=create_vector3(100, 20, 50),
|
| 293 |
+
),
|
| 294 |
+
create_light(
|
| 295 |
+
name="Ambient",
|
| 296 |
+
light_type="ambient",
|
| 297 |
+
color="#ff9966",
|
| 298 |
+
intensity=0.4,
|
| 299 |
+
),
|
| 300 |
+
]
|
| 301 |
+
scene["environment"]["background_color"] = "#ff6b35"
|
| 302 |
+
|
| 303 |
+
# Save updated scene
|
| 304 |
+
storage.save(scene)
|
| 305 |
+
|
| 306 |
+
# Generate viewer URL
|
| 307 |
+
viewer_url = generate_viewer_url(scene["scene_id"], base_url)
|
| 308 |
+
|
| 309 |
+
return {
|
| 310 |
+
"scene_id": scene["scene_id"],
|
| 311 |
+
"viewer_url": viewer_url,
|
| 312 |
+
"message": f"Set lighting to {preset}",
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def get_scene_info(
|
| 317 |
+
scene_id: str,
|
| 318 |
+
base_url: str = "http://localhost:8000"
|
| 319 |
+
) -> Dict[str, Any]:
|
| 320 |
+
"""
|
| 321 |
+
Get information about a scene
|
| 322 |
+
|
| 323 |
+
Args:
|
| 324 |
+
scene_id: ID of the scene to retrieve
|
| 325 |
+
base_url: Base URL for the deployed space
|
| 326 |
+
|
| 327 |
+
Returns:
|
| 328 |
+
Dict with scene details including objects and lights
|
| 329 |
+
"""
|
| 330 |
+
# Get scene
|
| 331 |
+
scene = storage.get(scene_id)
|
| 332 |
+
if not scene:
|
| 333 |
+
raise ValueError(f"Scene with ID '{scene_id}' not found")
|
| 334 |
+
|
| 335 |
+
# Generate viewer URL
|
| 336 |
+
viewer_url = generate_viewer_url(scene["scene_id"], base_url)
|
| 337 |
+
|
| 338 |
+
# Format object list
|
| 339 |
+
objects_info = [
|
| 340 |
+
{
|
| 341 |
+
"id": obj["id"],
|
| 342 |
+
"name": obj["name"],
|
| 343 |
+
"type": obj["type"],
|
| 344 |
+
"position": obj["position"],
|
| 345 |
+
"color": obj["material"]["color"],
|
| 346 |
+
}
|
| 347 |
+
for obj in scene["objects"]
|
| 348 |
+
]
|
| 349 |
+
|
| 350 |
+
return {
|
| 351 |
+
"scene_id": scene["scene_id"],
|
| 352 |
+
"name": scene["name"],
|
| 353 |
+
"viewer_url": viewer_url,
|
| 354 |
+
"object_count": len(scene["objects"]),
|
| 355 |
+
"light_count": len(scene["lights"]),
|
| 356 |
+
"world_bounds": {
|
| 357 |
+
"width": scene["world_width"],
|
| 358 |
+
"height": scene["world_height"],
|
| 359 |
+
"depth": scene["world_depth"],
|
| 360 |
+
},
|
| 361 |
+
"objects": objects_info,
|
| 362 |
+
}
|
chat_client.py
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GPT-powered Chat Client for GCP (Game Context Protocol)
|
| 3 |
+
|
| 4 |
+
This module provides an intelligent chat interface that uses OpenAI's GPT
|
| 5 |
+
with function calling to interact with the GCP tools.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
from typing import Optional, Dict, Any, List
|
| 10 |
+
|
| 11 |
+
# Load .env file if present
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
from openai import OpenAI
|
| 16 |
+
|
| 17 |
+
# Import GCP tools
|
| 18 |
+
from backend.tools.scene_tools import (
|
| 19 |
+
create_game_scene,
|
| 20 |
+
add_game_object,
|
| 21 |
+
remove_game_object,
|
| 22 |
+
set_scene_lighting,
|
| 23 |
+
get_scene_info,
|
| 24 |
+
)
|
| 25 |
+
from backend.tools.player_tools import (
|
| 26 |
+
set_player_speed,
|
| 27 |
+
set_jump_force,
|
| 28 |
+
set_mouse_sensitivity,
|
| 29 |
+
set_gravity,
|
| 30 |
+
set_player_dimensions,
|
| 31 |
+
set_movement_acceleration,
|
| 32 |
+
set_air_control,
|
| 33 |
+
set_camera_fov,
|
| 34 |
+
set_vertical_look_limits,
|
| 35 |
+
get_player_config,
|
| 36 |
+
)
|
| 37 |
+
from backend.tools.rendering_tools import (
|
| 38 |
+
add_light,
|
| 39 |
+
remove_light,
|
| 40 |
+
update_light,
|
| 41 |
+
get_lights,
|
| 42 |
+
update_object_material,
|
| 43 |
+
set_background_color,
|
| 44 |
+
set_fog,
|
| 45 |
+
# Post-processing
|
| 46 |
+
set_bloom,
|
| 47 |
+
set_ssao,
|
| 48 |
+
set_color_grading,
|
| 49 |
+
set_vignette,
|
| 50 |
+
get_post_processing,
|
| 51 |
+
# Camera effects
|
| 52 |
+
set_depth_of_field,
|
| 53 |
+
set_motion_blur,
|
| 54 |
+
set_chromatic_aberration,
|
| 55 |
+
get_camera_effects,
|
| 56 |
+
)
|
| 57 |
+
from backend.game_models import create_vector3, create_material
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Tool definitions for OpenAI function calling
|
| 61 |
+
TOOLS = [
|
| 62 |
+
# Scene Tools
|
| 63 |
+
{
|
| 64 |
+
"type": "function",
|
| 65 |
+
"function": {
|
| 66 |
+
"name": "create_scene",
|
| 67 |
+
"description": "Create a new 3D scene/level",
|
| 68 |
+
"parameters": {
|
| 69 |
+
"type": "object",
|
| 70 |
+
"properties": {
|
| 71 |
+
"name": {"type": "string", "description": "Name of the scene"},
|
| 72 |
+
"description": {"type": "string", "description": "Description of the scene"},
|
| 73 |
+
"world_width": {"type": "number", "description": "Width of the world in units (default: 100)"},
|
| 74 |
+
"world_height": {"type": "number", "description": "Height of the world in units (default: 100)"},
|
| 75 |
+
"world_depth": {"type": "number", "description": "Depth of the world in units (default: 100)"},
|
| 76 |
+
"lighting_preset": {"type": "string", "enum": ["day", "night", "sunset", "studio"], "description": "Lighting preset"},
|
| 77 |
+
},
|
| 78 |
+
"required": []
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"type": "function",
|
| 84 |
+
"function": {
|
| 85 |
+
"name": "add_object",
|
| 86 |
+
"description": "Add a 3D object to the scene",
|
| 87 |
+
"parameters": {
|
| 88 |
+
"type": "object",
|
| 89 |
+
"properties": {
|
| 90 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 91 |
+
"object_type": {"type": "string", "enum": ["cube", "sphere", "cylinder", "plane", "cone", "torus"], "description": "Type of object"},
|
| 92 |
+
"name": {"type": "string", "description": "Name for the object"},
|
| 93 |
+
"x": {"type": "number", "description": "X position"},
|
| 94 |
+
"y": {"type": "number", "description": "Y position"},
|
| 95 |
+
"z": {"type": "number", "description": "Z position"},
|
| 96 |
+
"scale_x": {"type": "number", "description": "X scale (default: 1)"},
|
| 97 |
+
"scale_y": {"type": "number", "description": "Y scale (default: 1)"},
|
| 98 |
+
"scale_z": {"type": "number", "description": "Z scale (default: 1)"},
|
| 99 |
+
"color": {"type": "string", "description": "Hex color code (e.g., #ff0000 for red)"},
|
| 100 |
+
},
|
| 101 |
+
"required": ["scene_id"]
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"type": "function",
|
| 107 |
+
"function": {
|
| 108 |
+
"name": "remove_object",
|
| 109 |
+
"description": "Remove an object from the scene",
|
| 110 |
+
"parameters": {
|
| 111 |
+
"type": "object",
|
| 112 |
+
"properties": {
|
| 113 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 114 |
+
"object_id": {"type": "string", "description": "ID of the object to remove"},
|
| 115 |
+
},
|
| 116 |
+
"required": ["scene_id", "object_id"]
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"type": "function",
|
| 122 |
+
"function": {
|
| 123 |
+
"name": "set_lighting",
|
| 124 |
+
"description": "Set the lighting preset for the scene",
|
| 125 |
+
"parameters": {
|
| 126 |
+
"type": "object",
|
| 127 |
+
"properties": {
|
| 128 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 129 |
+
"preset": {"type": "string", "enum": ["day", "night", "sunset", "studio"], "description": "Lighting preset"},
|
| 130 |
+
},
|
| 131 |
+
"required": ["scene_id"]
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"type": "function",
|
| 137 |
+
"function": {
|
| 138 |
+
"name": "get_scene_info",
|
| 139 |
+
"description": "Get detailed information about a scene including all objects and settings",
|
| 140 |
+
"parameters": {
|
| 141 |
+
"type": "object",
|
| 142 |
+
"properties": {
|
| 143 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 144 |
+
},
|
| 145 |
+
"required": ["scene_id"]
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
# Player Tools
|
| 150 |
+
{
|
| 151 |
+
"type": "function",
|
| 152 |
+
"function": {
|
| 153 |
+
"name": "set_player_speed",
|
| 154 |
+
"description": "Set the player's movement speed in units per second",
|
| 155 |
+
"parameters": {
|
| 156 |
+
"type": "object",
|
| 157 |
+
"properties": {
|
| 158 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 159 |
+
"walk_speed": {"type": "number", "description": "Movement speed in units/second"},
|
| 160 |
+
},
|
| 161 |
+
"required": ["scene_id", "walk_speed"]
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"type": "function",
|
| 167 |
+
"function": {
|
| 168 |
+
"name": "set_jump_force",
|
| 169 |
+
"description": "Set the player's jump force (initial upward velocity)",
|
| 170 |
+
"parameters": {
|
| 171 |
+
"type": "object",
|
| 172 |
+
"properties": {
|
| 173 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 174 |
+
"jump_force": {"type": "number", "description": "Jump force in m/s"},
|
| 175 |
+
},
|
| 176 |
+
"required": ["scene_id", "jump_force"]
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"type": "function",
|
| 182 |
+
"function": {
|
| 183 |
+
"name": "set_mouse_sensitivity",
|
| 184 |
+
"description": "Set mouse look sensitivity and Y-axis inversion",
|
| 185 |
+
"parameters": {
|
| 186 |
+
"type": "object",
|
| 187 |
+
"properties": {
|
| 188 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 189 |
+
"sensitivity": {"type": "number", "description": "Mouse sensitivity multiplier"},
|
| 190 |
+
"invert_y": {"type": "boolean", "description": "Invert Y-axis"},
|
| 191 |
+
},
|
| 192 |
+
"required": ["scene_id"]
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"type": "function",
|
| 198 |
+
"function": {
|
| 199 |
+
"name": "set_gravity",
|
| 200 |
+
"description": "Set the world's gravity strength",
|
| 201 |
+
"parameters": {
|
| 202 |
+
"type": "object",
|
| 203 |
+
"properties": {
|
| 204 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 205 |
+
"gravity": {"type": "number", "description": "Gravity in m/s² (negative = downward, e.g., -9.82 for Earth)"},
|
| 206 |
+
},
|
| 207 |
+
"required": ["scene_id", "gravity"]
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"type": "function",
|
| 213 |
+
"function": {
|
| 214 |
+
"name": "set_player_dimensions",
|
| 215 |
+
"description": "Set player collision capsule dimensions",
|
| 216 |
+
"parameters": {
|
| 217 |
+
"type": "object",
|
| 218 |
+
"properties": {
|
| 219 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 220 |
+
"height": {"type": "number", "description": "Player height in meters"},
|
| 221 |
+
"radius": {"type": "number", "description": "Player radius in meters"},
|
| 222 |
+
"eye_height": {"type": "number", "description": "Camera height from feet"},
|
| 223 |
+
},
|
| 224 |
+
"required": ["scene_id"]
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"type": "function",
|
| 230 |
+
"function": {
|
| 231 |
+
"name": "set_camera_fov",
|
| 232 |
+
"description": "Set camera field of view",
|
| 233 |
+
"parameters": {
|
| 234 |
+
"type": "object",
|
| 235 |
+
"properties": {
|
| 236 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 237 |
+
"fov": {"type": "number", "description": "Field of view in degrees (60-120)"},
|
| 238 |
+
},
|
| 239 |
+
"required": ["scene_id", "fov"]
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"type": "function",
|
| 245 |
+
"function": {
|
| 246 |
+
"name": "get_player_config",
|
| 247 |
+
"description": "Get current player configuration including speed, jump force, gravity, dimensions, etc.",
|
| 248 |
+
"parameters": {
|
| 249 |
+
"type": "object",
|
| 250 |
+
"properties": {
|
| 251 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 252 |
+
},
|
| 253 |
+
"required": ["scene_id"]
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
},
|
| 257 |
+
# Rendering Tools
|
| 258 |
+
{
|
| 259 |
+
"type": "function",
|
| 260 |
+
"function": {
|
| 261 |
+
"name": "add_light",
|
| 262 |
+
"description": "Add a light source to the scene",
|
| 263 |
+
"parameters": {
|
| 264 |
+
"type": "object",
|
| 265 |
+
"properties": {
|
| 266 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 267 |
+
"light_type": {"type": "string", "enum": ["ambient", "directional", "point", "spot"], "description": "Type of light"},
|
| 268 |
+
"name": {"type": "string", "description": "Unique name for the light"},
|
| 269 |
+
"color": {"type": "string", "description": "Hex color code"},
|
| 270 |
+
"intensity": {"type": "number", "description": "Light intensity (0-2)"},
|
| 271 |
+
"x": {"type": "number", "description": "X position"},
|
| 272 |
+
"y": {"type": "number", "description": "Y position"},
|
| 273 |
+
"z": {"type": "number", "description": "Z position"},
|
| 274 |
+
"cast_shadow": {"type": "boolean", "description": "Enable shadows"},
|
| 275 |
+
},
|
| 276 |
+
"required": ["scene_id", "light_type", "name"]
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"type": "function",
|
| 282 |
+
"function": {
|
| 283 |
+
"name": "remove_light",
|
| 284 |
+
"description": "Remove a light from the scene",
|
| 285 |
+
"parameters": {
|
| 286 |
+
"type": "object",
|
| 287 |
+
"properties": {
|
| 288 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 289 |
+
"light_name": {"type": "string", "description": "Name of the light to remove"},
|
| 290 |
+
},
|
| 291 |
+
"required": ["scene_id", "light_name"]
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"type": "function",
|
| 297 |
+
"function": {
|
| 298 |
+
"name": "get_lights",
|
| 299 |
+
"description": "Get all lights in the scene",
|
| 300 |
+
"parameters": {
|
| 301 |
+
"type": "object",
|
| 302 |
+
"properties": {
|
| 303 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 304 |
+
},
|
| 305 |
+
"required": ["scene_id"]
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"type": "function",
|
| 311 |
+
"function": {
|
| 312 |
+
"name": "update_object_material",
|
| 313 |
+
"description": "Update an object's material properties (color, metalness, roughness, opacity, emissive glow)",
|
| 314 |
+
"parameters": {
|
| 315 |
+
"type": "object",
|
| 316 |
+
"properties": {
|
| 317 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 318 |
+
"object_id": {"type": "string", "description": "ID of the object"},
|
| 319 |
+
"color": {"type": "string", "description": "Hex color code"},
|
| 320 |
+
"metalness": {"type": "number", "description": "Metalness (0=matte, 1=metal)"},
|
| 321 |
+
"roughness": {"type": "number", "description": "Roughness (0=shiny, 1=rough)"},
|
| 322 |
+
"opacity": {"type": "number", "description": "Opacity (0=invisible, 1=solid)"},
|
| 323 |
+
"emissive": {"type": "string", "description": "Emissive color for glow"},
|
| 324 |
+
"emissive_intensity": {"type": "number", "description": "Emissive intensity (0-1)"},
|
| 325 |
+
},
|
| 326 |
+
"required": ["scene_id", "object_id"]
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"type": "function",
|
| 332 |
+
"function": {
|
| 333 |
+
"name": "set_background_color",
|
| 334 |
+
"description": "Set scene background color or gradient",
|
| 335 |
+
"parameters": {
|
| 336 |
+
"type": "object",
|
| 337 |
+
"properties": {
|
| 338 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 339 |
+
"color": {"type": "string", "description": "Hex color for solid background"},
|
| 340 |
+
"bg_type": {"type": "string", "enum": ["solid", "gradient"], "description": "Background type"},
|
| 341 |
+
"gradient_top": {"type": "string", "description": "Top color for gradient"},
|
| 342 |
+
"gradient_bottom": {"type": "string", "description": "Bottom color for gradient"},
|
| 343 |
+
},
|
| 344 |
+
"required": ["scene_id"]
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
},
|
| 348 |
+
{
|
| 349 |
+
"type": "function",
|
| 350 |
+
"function": {
|
| 351 |
+
"name": "set_fog",
|
| 352 |
+
"description": "Add or remove atmospheric fog",
|
| 353 |
+
"parameters": {
|
| 354 |
+
"type": "object",
|
| 355 |
+
"properties": {
|
| 356 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 357 |
+
"enabled": {"type": "boolean", "description": "Enable or disable fog"},
|
| 358 |
+
"color": {"type": "string", "description": "Fog color"},
|
| 359 |
+
"near": {"type": "number", "description": "Fog start distance"},
|
| 360 |
+
"far": {"type": "number", "description": "Fog end distance"},
|
| 361 |
+
"density": {"type": "number", "description": "Fog density (for exponential fog)"},
|
| 362 |
+
},
|
| 363 |
+
"required": ["scene_id", "enabled"]
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
},
|
| 367 |
+
# Post-processing Tools
|
| 368 |
+
{
|
| 369 |
+
"type": "function",
|
| 370 |
+
"function": {
|
| 371 |
+
"name": "set_bloom",
|
| 372 |
+
"description": "Configure bloom (glow) post-processing effect. Creates glow around bright areas.",
|
| 373 |
+
"parameters": {
|
| 374 |
+
"type": "object",
|
| 375 |
+
"properties": {
|
| 376 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 377 |
+
"enabled": {"type": "boolean", "description": "Enable or disable bloom"},
|
| 378 |
+
"strength": {"type": "number", "description": "Bloom intensity (0-3, default: 1)"},
|
| 379 |
+
"radius": {"type": "number", "description": "Bloom spread radius (0-1, default: 0.4)"},
|
| 380 |
+
"threshold": {"type": "number", "description": "Brightness threshold (0-1, default: 0.8)"},
|
| 381 |
+
},
|
| 382 |
+
"required": ["scene_id", "enabled"]
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"type": "function",
|
| 388 |
+
"function": {
|
| 389 |
+
"name": "set_ssao",
|
| 390 |
+
"description": "Configure Screen Space Ambient Occlusion (SSAO). Adds soft shadows in corners for depth.",
|
| 391 |
+
"parameters": {
|
| 392 |
+
"type": "object",
|
| 393 |
+
"properties": {
|
| 394 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 395 |
+
"enabled": {"type": "boolean", "description": "Enable or disable SSAO"},
|
| 396 |
+
"radius": {"type": "number", "description": "Sample radius (0.1-2, default: 0.5)"},
|
| 397 |
+
"intensity": {"type": "number", "description": "Shadow intensity (0-2, default: 1)"},
|
| 398 |
+
"bias": {"type": "number", "description": "Depth bias (0.001-0.1, default: 0.025)"},
|
| 399 |
+
},
|
| 400 |
+
"required": ["scene_id", "enabled"]
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
},
|
| 404 |
+
{
|
| 405 |
+
"type": "function",
|
| 406 |
+
"function": {
|
| 407 |
+
"name": "set_color_grading",
|
| 408 |
+
"description": "Configure color grading for cinematic looks. Adjust brightness, contrast, saturation, etc.",
|
| 409 |
+
"parameters": {
|
| 410 |
+
"type": "object",
|
| 411 |
+
"properties": {
|
| 412 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 413 |
+
"enabled": {"type": "boolean", "description": "Enable or disable color grading"},
|
| 414 |
+
"brightness": {"type": "number", "description": "Brightness (-1 to 1, default: 0)"},
|
| 415 |
+
"contrast": {"type": "number", "description": "Contrast (0-2, default: 1)"},
|
| 416 |
+
"saturation": {"type": "number", "description": "Color saturation (0-2, default: 1)"},
|
| 417 |
+
"hue": {"type": "number", "description": "Hue shift in degrees (-180 to 180, default: 0)"},
|
| 418 |
+
"exposure": {"type": "number", "description": "Exposure (0-3, default: 1)"},
|
| 419 |
+
"gamma": {"type": "number", "description": "Gamma correction (0.5-2.5, default: 1)"},
|
| 420 |
+
},
|
| 421 |
+
"required": ["scene_id", "enabled"]
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
},
|
| 425 |
+
{
|
| 426 |
+
"type": "function",
|
| 427 |
+
"function": {
|
| 428 |
+
"name": "set_vignette",
|
| 429 |
+
"description": "Configure vignette effect (darkened edges) to focus attention on center.",
|
| 430 |
+
"parameters": {
|
| 431 |
+
"type": "object",
|
| 432 |
+
"properties": {
|
| 433 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 434 |
+
"enabled": {"type": "boolean", "description": "Enable or disable vignette"},
|
| 435 |
+
"intensity": {"type": "number", "description": "Darkness (0-1, default: 0.5)"},
|
| 436 |
+
"smoothness": {"type": "number", "description": "Edge softness (0-1, default: 0.5)"},
|
| 437 |
+
},
|
| 438 |
+
"required": ["scene_id", "enabled"]
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"type": "function",
|
| 444 |
+
"function": {
|
| 445 |
+
"name": "get_post_processing",
|
| 446 |
+
"description": "Get all post-processing settings (bloom, SSAO, color grading, vignette)",
|
| 447 |
+
"parameters": {
|
| 448 |
+
"type": "object",
|
| 449 |
+
"properties": {
|
| 450 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 451 |
+
},
|
| 452 |
+
"required": ["scene_id"]
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
},
|
| 456 |
+
# Camera Effects Tools
|
| 457 |
+
{
|
| 458 |
+
"type": "function",
|
| 459 |
+
"function": {
|
| 460 |
+
"name": "set_depth_of_field",
|
| 461 |
+
"description": "Configure depth of field (DoF). Blurs objects not at the focus distance.",
|
| 462 |
+
"parameters": {
|
| 463 |
+
"type": "object",
|
| 464 |
+
"properties": {
|
| 465 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 466 |
+
"enabled": {"type": "boolean", "description": "Enable or disable DoF"},
|
| 467 |
+
"focus_distance": {"type": "number", "description": "Distance to focal plane (default: 10)"},
|
| 468 |
+
"aperture": {"type": "number", "description": "Aperture size (0.001-0.1, default: 0.025)"},
|
| 469 |
+
"max_blur": {"type": "number", "description": "Maximum blur (0-0.05, default: 0.01)"},
|
| 470 |
+
},
|
| 471 |
+
"required": ["scene_id", "enabled"]
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"type": "function",
|
| 477 |
+
"function": {
|
| 478 |
+
"name": "set_motion_blur",
|
| 479 |
+
"description": "Configure motion blur effect for sense of speed and smooth motion.",
|
| 480 |
+
"parameters": {
|
| 481 |
+
"type": "object",
|
| 482 |
+
"properties": {
|
| 483 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 484 |
+
"enabled": {"type": "boolean", "description": "Enable or disable motion blur"},
|
| 485 |
+
"intensity": {"type": "number", "description": "Blur intensity (0-2, default: 0.5)"},
|
| 486 |
+
"samples": {"type": "integer", "description": "Quality samples (4-32, default: 8)"},
|
| 487 |
+
},
|
| 488 |
+
"required": ["scene_id", "enabled"]
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"type": "function",
|
| 494 |
+
"function": {
|
| 495 |
+
"name": "set_chromatic_aberration",
|
| 496 |
+
"description": "Configure chromatic aberration. Simulates lens color separation at edges.",
|
| 497 |
+
"parameters": {
|
| 498 |
+
"type": "object",
|
| 499 |
+
"properties": {
|
| 500 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 501 |
+
"enabled": {"type": "boolean", "description": "Enable or disable effect"},
|
| 502 |
+
"intensity": {"type": "number", "description": "Effect strength (0-0.05, default: 0.005)"},
|
| 503 |
+
},
|
| 504 |
+
"required": ["scene_id", "enabled"]
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"type": "function",
|
| 510 |
+
"function": {
|
| 511 |
+
"name": "get_camera_effects",
|
| 512 |
+
"description": "Get all camera effects settings (depth of field, motion blur, chromatic aberration)",
|
| 513 |
+
"parameters": {
|
| 514 |
+
"type": "object",
|
| 515 |
+
"properties": {
|
| 516 |
+
"scene_id": {"type": "string", "description": "ID of the scene"},
|
| 517 |
+
},
|
| 518 |
+
"required": ["scene_id"]
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
]
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
class GCPChatClient:
|
| 526 |
+
"""GPT-powered chat client for GCP"""
|
| 527 |
+
|
| 528 |
+
def __init__(self, scene_id: str, base_url: str = "http://localhost:8000"):
|
| 529 |
+
self.client = OpenAI() # Uses OPENAI_API_KEY env var
|
| 530 |
+
self.scene_id = scene_id
|
| 531 |
+
self.base_url = base_url
|
| 532 |
+
self.conversation_history: List[Dict[str, Any]] = []
|
| 533 |
+
|
| 534 |
+
# System prompt
|
| 535 |
+
self.system_prompt = f"""You are a helpful assistant for GCP (Game Context Protocol), a 3D scene building system.
|
| 536 |
+
|
| 537 |
+
You help users create and modify 3D scenes using natural language. You have access to tools for:
|
| 538 |
+
- Creating scenes and adding/removing objects (cubes, spheres, cylinders, etc.)
|
| 539 |
+
- Configuring player movement (speed, jump, gravity, dimensions)
|
| 540 |
+
- Managing lights (ambient, directional, point, spot)
|
| 541 |
+
- Updating materials (color, metalness, roughness, opacity, glow)
|
| 542 |
+
- Setting backgrounds and fog effects
|
| 543 |
+
- Post-processing effects (bloom, SSAO, color grading, vignette)
|
| 544 |
+
- Camera effects (depth of field, motion blur, chromatic aberration)
|
| 545 |
+
|
| 546 |
+
The current scene ID is: {scene_id}
|
| 547 |
+
|
| 548 |
+
When users ask to modify something relatively (like "half the speed" or "make it twice as big"),
|
| 549 |
+
ALWAYS first get the current state using the appropriate get_* function, then calculate the new value,
|
| 550 |
+
then apply it.
|
| 551 |
+
|
| 552 |
+
Be concise but helpful. After making changes, briefly confirm what was done."""
|
| 553 |
+
|
| 554 |
+
def execute_tool(self, name: str, args: Dict[str, Any]) -> Any:
|
| 555 |
+
"""Execute a GCP tool and return the result"""
|
| 556 |
+
|
| 557 |
+
# Inject scene_id if not provided
|
| 558 |
+
if "scene_id" not in args:
|
| 559 |
+
args["scene_id"] = self.scene_id
|
| 560 |
+
|
| 561 |
+
# Scene tools
|
| 562 |
+
if name == "create_scene":
|
| 563 |
+
return create_game_scene(
|
| 564 |
+
name=args.get("name", "New Scene"),
|
| 565 |
+
description=args.get("description"),
|
| 566 |
+
world_width=args.get("world_width", 100.0),
|
| 567 |
+
world_height=args.get("world_height", 100.0),
|
| 568 |
+
world_depth=args.get("world_depth", 100.0),
|
| 569 |
+
lighting_preset=args.get("lighting_preset", "day"),
|
| 570 |
+
base_url=self.base_url,
|
| 571 |
+
)
|
| 572 |
+
|
| 573 |
+
elif name == "add_object":
|
| 574 |
+
position = create_vector3(
|
| 575 |
+
args.get("x", 0),
|
| 576 |
+
args.get("y", 0),
|
| 577 |
+
args.get("z", 0)
|
| 578 |
+
)
|
| 579 |
+
scale = create_vector3(
|
| 580 |
+
args.get("scale_x", 1),
|
| 581 |
+
args.get("scale_y", 1),
|
| 582 |
+
args.get("scale_z", 1)
|
| 583 |
+
)
|
| 584 |
+
material = create_material(color=args.get("color", "#ffffff"))
|
| 585 |
+
return add_game_object(
|
| 586 |
+
scene_id=args["scene_id"],
|
| 587 |
+
object_type=args.get("object_type", "cube"),
|
| 588 |
+
name=args.get("name"),
|
| 589 |
+
position=position,
|
| 590 |
+
scale=scale,
|
| 591 |
+
material=material,
|
| 592 |
+
base_url=self.base_url,
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
elif name == "remove_object":
|
| 596 |
+
return remove_game_object(
|
| 597 |
+
scene_id=args["scene_id"],
|
| 598 |
+
object_id=args["object_id"],
|
| 599 |
+
base_url=self.base_url,
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
elif name == "set_lighting":
|
| 603 |
+
return set_scene_lighting(
|
| 604 |
+
scene_id=args["scene_id"],
|
| 605 |
+
preset=args.get("preset", "day"),
|
| 606 |
+
base_url=self.base_url,
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
elif name == "get_scene_info":
|
| 610 |
+
return get_scene_info(args["scene_id"], self.base_url)
|
| 611 |
+
|
| 612 |
+
# Player tools
|
| 613 |
+
elif name == "set_player_speed":
|
| 614 |
+
return set_player_speed(args["scene_id"], args["walk_speed"])
|
| 615 |
+
|
| 616 |
+
elif name == "set_jump_force":
|
| 617 |
+
return set_jump_force(args["scene_id"], args["jump_force"])
|
| 618 |
+
|
| 619 |
+
elif name == "set_mouse_sensitivity":
|
| 620 |
+
return set_mouse_sensitivity(
|
| 621 |
+
args["scene_id"],
|
| 622 |
+
args.get("sensitivity", 0.002),
|
| 623 |
+
args.get("invert_y", False)
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
elif name == "set_gravity":
|
| 627 |
+
return set_gravity(args["scene_id"], args["gravity"])
|
| 628 |
+
|
| 629 |
+
elif name == "set_player_dimensions":
|
| 630 |
+
return set_player_dimensions(
|
| 631 |
+
args["scene_id"],
|
| 632 |
+
args.get("height", 1.7),
|
| 633 |
+
args.get("radius", 0.3),
|
| 634 |
+
args.get("eye_height")
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
elif name == "set_camera_fov":
|
| 638 |
+
return set_camera_fov(args["scene_id"], args["fov"])
|
| 639 |
+
|
| 640 |
+
elif name == "get_player_config":
|
| 641 |
+
return get_player_config(args["scene_id"])
|
| 642 |
+
|
| 643 |
+
# Rendering tools
|
| 644 |
+
elif name == "add_light":
|
| 645 |
+
position = None
|
| 646 |
+
if "x" in args or "y" in args or "z" in args:
|
| 647 |
+
position = {"x": args.get("x", 0), "y": args.get("y", 5), "z": args.get("z", 0)}
|
| 648 |
+
return add_light(
|
| 649 |
+
args["scene_id"],
|
| 650 |
+
args["light_type"],
|
| 651 |
+
args["name"],
|
| 652 |
+
args.get("color", "#ffffff"),
|
| 653 |
+
args.get("intensity", 1.0),
|
| 654 |
+
position,
|
| 655 |
+
None, # target
|
| 656 |
+
args.get("cast_shadow", False),
|
| 657 |
+
None # spot_angle
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
elif name == "remove_light":
|
| 661 |
+
return remove_light(args["scene_id"], args["light_name"])
|
| 662 |
+
|
| 663 |
+
elif name == "get_lights":
|
| 664 |
+
return get_lights(args["scene_id"])
|
| 665 |
+
|
| 666 |
+
elif name == "update_object_material":
|
| 667 |
+
return update_object_material(
|
| 668 |
+
args["scene_id"],
|
| 669 |
+
args["object_id"],
|
| 670 |
+
args.get("color"),
|
| 671 |
+
args.get("metalness"),
|
| 672 |
+
args.get("roughness"),
|
| 673 |
+
args.get("opacity"),
|
| 674 |
+
args.get("emissive"),
|
| 675 |
+
args.get("emissive_intensity")
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
elif name == "set_background_color":
|
| 679 |
+
return set_background_color(
|
| 680 |
+
args["scene_id"],
|
| 681 |
+
args.get("color"),
|
| 682 |
+
args.get("bg_type", "solid"),
|
| 683 |
+
args.get("gradient_top"),
|
| 684 |
+
args.get("gradient_bottom")
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
elif name == "set_fog":
|
| 688 |
+
return set_fog(
|
| 689 |
+
args["scene_id"],
|
| 690 |
+
args["enabled"],
|
| 691 |
+
args.get("color"),
|
| 692 |
+
args.get("near"),
|
| 693 |
+
args.get("far"),
|
| 694 |
+
args.get("density")
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
# Post-processing tools
|
| 698 |
+
elif name == "set_bloom":
|
| 699 |
+
return set_bloom(
|
| 700 |
+
args["scene_id"],
|
| 701 |
+
args["enabled"],
|
| 702 |
+
args.get("strength", 1.0),
|
| 703 |
+
args.get("radius", 0.4),
|
| 704 |
+
args.get("threshold", 0.8)
|
| 705 |
+
)
|
| 706 |
+
|
| 707 |
+
elif name == "set_ssao":
|
| 708 |
+
return set_ssao(
|
| 709 |
+
args["scene_id"],
|
| 710 |
+
args["enabled"],
|
| 711 |
+
args.get("radius", 0.5),
|
| 712 |
+
args.get("intensity", 1.0),
|
| 713 |
+
args.get("bias", 0.025)
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
elif name == "set_color_grading":
|
| 717 |
+
return set_color_grading(
|
| 718 |
+
args["scene_id"],
|
| 719 |
+
args["enabled"],
|
| 720 |
+
args.get("brightness", 0.0),
|
| 721 |
+
args.get("contrast", 1.0),
|
| 722 |
+
args.get("saturation", 1.0),
|
| 723 |
+
args.get("hue", 0.0),
|
| 724 |
+
args.get("exposure", 1.0),
|
| 725 |
+
args.get("gamma", 1.0)
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
elif name == "set_vignette":
|
| 729 |
+
return set_vignette(
|
| 730 |
+
args["scene_id"],
|
| 731 |
+
args["enabled"],
|
| 732 |
+
args.get("intensity", 0.5),
|
| 733 |
+
args.get("smoothness", 0.5)
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
elif name == "get_post_processing":
|
| 737 |
+
return get_post_processing(args["scene_id"])
|
| 738 |
+
|
| 739 |
+
# Camera effects tools
|
| 740 |
+
elif name == "set_depth_of_field":
|
| 741 |
+
return set_depth_of_field(
|
| 742 |
+
args["scene_id"],
|
| 743 |
+
args["enabled"],
|
| 744 |
+
args.get("focus_distance", 10.0),
|
| 745 |
+
args.get("aperture", 0.025),
|
| 746 |
+
args.get("max_blur", 0.01)
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
elif name == "set_motion_blur":
|
| 750 |
+
return set_motion_blur(
|
| 751 |
+
args["scene_id"],
|
| 752 |
+
args["enabled"],
|
| 753 |
+
args.get("intensity", 0.5),
|
| 754 |
+
args.get("samples", 8)
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
elif name == "set_chromatic_aberration":
|
| 758 |
+
return set_chromatic_aberration(
|
| 759 |
+
args["scene_id"],
|
| 760 |
+
args["enabled"],
|
| 761 |
+
args.get("intensity", 0.005)
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
elif name == "get_camera_effects":
|
| 765 |
+
return get_camera_effects(args["scene_id"])
|
| 766 |
+
|
| 767 |
+
else:
|
| 768 |
+
return {"error": f"Unknown tool: {name}"}
|
| 769 |
+
|
| 770 |
+
def chat(self, user_message: str) -> tuple[str, Optional[Dict[str, Any]]]:
|
| 771 |
+
"""
|
| 772 |
+
Process a user message and return the response.
|
| 773 |
+
|
| 774 |
+
Returns:
|
| 775 |
+
tuple: (response_text, action_data)
|
| 776 |
+
- response_text: The assistant's response
|
| 777 |
+
- action_data: Optional dict with action info for the frontend
|
| 778 |
+
"""
|
| 779 |
+
# Add user message to history
|
| 780 |
+
self.conversation_history.append({
|
| 781 |
+
"role": "user",
|
| 782 |
+
"content": user_message
|
| 783 |
+
})
|
| 784 |
+
|
| 785 |
+
# Build messages with system prompt
|
| 786 |
+
messages = [{"role": "system", "content": self.system_prompt}] + self.conversation_history
|
| 787 |
+
|
| 788 |
+
# Track actions for frontend
|
| 789 |
+
actions = []
|
| 790 |
+
|
| 791 |
+
# Call GPT with tools
|
| 792 |
+
while True:
|
| 793 |
+
response = self.client.chat.completions.create(
|
| 794 |
+
model="gpt-4o-mini", # or "gpt-4o" for better reasoning
|
| 795 |
+
messages=messages,
|
| 796 |
+
tools=TOOLS,
|
| 797 |
+
tool_choice="auto"
|
| 798 |
+
)
|
| 799 |
+
|
| 800 |
+
assistant_message = response.choices[0].message
|
| 801 |
+
|
| 802 |
+
# Check if there are tool calls
|
| 803 |
+
if assistant_message.tool_calls:
|
| 804 |
+
# Add assistant message with tool calls to history
|
| 805 |
+
messages.append({
|
| 806 |
+
"role": "assistant",
|
| 807 |
+
"content": assistant_message.content,
|
| 808 |
+
"tool_calls": [
|
| 809 |
+
{
|
| 810 |
+
"id": tc.id,
|
| 811 |
+
"type": "function",
|
| 812 |
+
"function": {
|
| 813 |
+
"name": tc.function.name,
|
| 814 |
+
"arguments": tc.function.arguments
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
for tc in assistant_message.tool_calls
|
| 818 |
+
]
|
| 819 |
+
})
|
| 820 |
+
|
| 821 |
+
# Execute each tool call
|
| 822 |
+
for tool_call in assistant_message.tool_calls:
|
| 823 |
+
function_name = tool_call.function.name
|
| 824 |
+
function_args = json.loads(tool_call.function.arguments)
|
| 825 |
+
|
| 826 |
+
# Execute the tool
|
| 827 |
+
try:
|
| 828 |
+
result = self.execute_tool(function_name, function_args)
|
| 829 |
+
actions.append({
|
| 830 |
+
"tool": function_name,
|
| 831 |
+
"args": function_args,
|
| 832 |
+
"result": result
|
| 833 |
+
})
|
| 834 |
+
result_str = json.dumps(result)
|
| 835 |
+
except Exception as e:
|
| 836 |
+
result_str = json.dumps({"error": str(e)})
|
| 837 |
+
|
| 838 |
+
# Add tool result to messages
|
| 839 |
+
messages.append({
|
| 840 |
+
"role": "tool",
|
| 841 |
+
"tool_call_id": tool_call.id,
|
| 842 |
+
"content": result_str
|
| 843 |
+
})
|
| 844 |
+
else:
|
| 845 |
+
# No more tool calls, we have the final response
|
| 846 |
+
final_response = assistant_message.content or "Done!"
|
| 847 |
+
|
| 848 |
+
# Add to conversation history
|
| 849 |
+
self.conversation_history.append({
|
| 850 |
+
"role": "assistant",
|
| 851 |
+
"content": final_response
|
| 852 |
+
})
|
| 853 |
+
|
| 854 |
+
# Build action data for frontend
|
| 855 |
+
action_data = None
|
| 856 |
+
if actions:
|
| 857 |
+
# Return the last significant action for the frontend
|
| 858 |
+
last_action = actions[-1]
|
| 859 |
+
action_data = self._build_frontend_action(last_action)
|
| 860 |
+
|
| 861 |
+
return final_response, action_data
|
| 862 |
+
|
| 863 |
+
def _build_frontend_action(self, action: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
| 864 |
+
"""Convert tool result to frontend action"""
|
| 865 |
+
tool = action["tool"]
|
| 866 |
+
result = action["result"]
|
| 867 |
+
|
| 868 |
+
# Map tool names to frontend actions
|
| 869 |
+
if tool == "add_object":
|
| 870 |
+
from backend.storage import storage
|
| 871 |
+
scene = storage.get(self.scene_id)
|
| 872 |
+
if scene and scene.get("objects"):
|
| 873 |
+
return {"action": "addObject", "data": scene["objects"][-1]}
|
| 874 |
+
|
| 875 |
+
elif tool == "set_lighting":
|
| 876 |
+
from backend.storage import storage
|
| 877 |
+
scene = storage.get(self.scene_id)
|
| 878 |
+
if scene and scene.get("lighting"):
|
| 879 |
+
return {"action": "setLighting", "data": scene["lighting"]}
|
| 880 |
+
|
| 881 |
+
elif tool in ["set_player_speed", "set_jump_force", "set_gravity",
|
| 882 |
+
"set_camera_fov", "set_player_dimensions"]:
|
| 883 |
+
return {"action": "reload", "url": f"{self.base_url}/view/scene/{self.scene_id}"}
|
| 884 |
+
|
| 885 |
+
elif tool == "add_light":
|
| 886 |
+
return {"action": "addLight", "data": result.get("light")}
|
| 887 |
+
|
| 888 |
+
elif tool == "remove_light":
|
| 889 |
+
return {"action": "removeLight", "data": {"light_name": action["args"].get("light_name")}}
|
| 890 |
+
|
| 891 |
+
elif tool == "update_object_material":
|
| 892 |
+
return {"action": "updateMaterial", "data": result}
|
| 893 |
+
|
| 894 |
+
elif tool == "set_background_color":
|
| 895 |
+
return {"action": "setBackground", "data": result.get("background")}
|
| 896 |
+
|
| 897 |
+
elif tool == "set_fog":
|
| 898 |
+
return {"action": "setFog", "data": result.get("fog")}
|
| 899 |
+
|
| 900 |
+
# Post-processing effects (require reload for now)
|
| 901 |
+
elif tool in ["set_bloom", "set_ssao", "set_color_grading", "set_vignette"]:
|
| 902 |
+
return {"action": "reload", "url": f"{self.base_url}/view/scene/{self.scene_id}"}
|
| 903 |
+
|
| 904 |
+
# Camera effects (require reload for now)
|
| 905 |
+
elif tool in ["set_depth_of_field", "set_motion_blur", "set_chromatic_aberration"]:
|
| 906 |
+
return {"action": "reload", "url": f"{self.base_url}/view/scene/{self.scene_id}"}
|
| 907 |
+
|
| 908 |
+
return None
|
| 909 |
+
|
| 910 |
+
def clear_history(self):
|
| 911 |
+
"""Clear conversation history"""
|
| 912 |
+
self.conversation_history = []
|
| 913 |
+
|
| 914 |
+
|
| 915 |
+
# Convenience function for simple usage
|
| 916 |
+
def create_chat_client(scene_id: str = "welcome", base_url: str = "http://localhost:8000") -> GCPChatClient:
|
| 917 |
+
"""Create a new GCP chat client"""
|
| 918 |
+
return GCPChatClient(scene_id, base_url)
|
frontend/game_viewer.html
ADDED
|
@@ -0,0 +1,1515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>3D Game Viewer</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 16 |
+
overflow: hidden;
|
| 17 |
+
background: #0a0a0a;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
#viewer-container {
|
| 21 |
+
width: 100vw;
|
| 22 |
+
height: 100vh;
|
| 23 |
+
position: relative;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/* Crosshair for FPS mode */
|
| 27 |
+
#crosshair {
|
| 28 |
+
position: absolute;
|
| 29 |
+
top: 50%;
|
| 30 |
+
left: 50%;
|
| 31 |
+
transform: translate(-50%, -50%);
|
| 32 |
+
width: 6px;
|
| 33 |
+
height: 6px;
|
| 34 |
+
background: #000000;
|
| 35 |
+
border-radius: 50%;
|
| 36 |
+
pointer-events: none;
|
| 37 |
+
display: none; /* Show only in FPS mode */
|
| 38 |
+
z-index: 100;
|
| 39 |
+
box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
|
| 40 |
+
}
|
| 41 |
+
</style>
|
| 42 |
+
</head>
|
| 43 |
+
<body>
|
| 44 |
+
<div id="viewer-container">
|
| 45 |
+
<div id="crosshair"></div>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<script type="importmap">
|
| 49 |
+
{
|
| 50 |
+
"imports": {
|
| 51 |
+
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
| 52 |
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
|
| 53 |
+
"cannon-es": "https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.js"
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
</script>
|
| 57 |
+
|
| 58 |
+
<!-- Stats library for FPS counter -->
|
| 59 |
+
<script src="https://cdn.jsdelivr.net/npm/stats.js@0.17.0/build/stats.min.js"></script>
|
| 60 |
+
|
| 61 |
+
<script type="module">
|
| 62 |
+
import * as THREE from 'three';
|
| 63 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 64 |
+
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
| 65 |
+
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
| 66 |
+
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';
|
| 67 |
+
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
|
| 68 |
+
import * as CANNON from 'cannon-es';
|
| 69 |
+
|
| 70 |
+
// Get scene ID from URL
|
| 71 |
+
const sceneId = window.location.pathname.split('/').pop();
|
| 72 |
+
const baseUrl = window.location.origin;
|
| 73 |
+
|
| 74 |
+
// Check for control mode in URL params
|
| 75 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 76 |
+
const initialMode = urlParams.get('mode') || 'fps'; // Default to FPS mode
|
| 77 |
+
|
| 78 |
+
console.log('3D Game Viewer - Initializing...');
|
| 79 |
+
console.log('Scene ID:', sceneId);
|
| 80 |
+
console.log('Fetch URL:', `${baseUrl}/api/scenes/${sceneId}`);
|
| 81 |
+
console.log('Initial control mode:', initialMode);
|
| 82 |
+
|
| 83 |
+
let scene, camera, renderer;
|
| 84 |
+
let orbitControls;
|
| 85 |
+
let controlMode = initialMode;
|
| 86 |
+
let sceneData = null;
|
| 87 |
+
|
| 88 |
+
// Postprocessing for outlines
|
| 89 |
+
let composer, outlinePass;
|
| 90 |
+
|
| 91 |
+
// Scene controls
|
| 92 |
+
let gridHelper = null;
|
| 93 |
+
let stats = null;
|
| 94 |
+
let wireframeEnabled = false;
|
| 95 |
+
|
| 96 |
+
// Object selection system (FPS mode)
|
| 97 |
+
let raycaster = new THREE.Raycaster();
|
| 98 |
+
let mouse = new THREE.Vector2();
|
| 99 |
+
let selectedObject = null;
|
| 100 |
+
let selectedObjectId = null;
|
| 101 |
+
const MAX_SELECT_DISTANCE = 10; // Max raycast distance for selection
|
| 102 |
+
|
| 103 |
+
// FPS movement and look variables (configurable via player_config)
|
| 104 |
+
let moveSpeed = 5.0;
|
| 105 |
+
const velocity = new THREE.Vector3();
|
| 106 |
+
let isMouseLocked = false;
|
| 107 |
+
let cameraRotationX = 0; // Pitch (up/down)
|
| 108 |
+
let cameraRotationY = 0; // Yaw (left/right)
|
| 109 |
+
let mouseSensitivity = 0.002;
|
| 110 |
+
let invertY = false;
|
| 111 |
+
let movementAcceleration = 0.0; // Phase 2
|
| 112 |
+
let airControl = 1.0; // Phase 2
|
| 113 |
+
let cameraFOV = 75.0; // Phase 2
|
| 114 |
+
let minPitch = -89.0; // Phase 2
|
| 115 |
+
let maxPitch = 89.0; // Phase 2
|
| 116 |
+
|
| 117 |
+
// Physics variables (configurable via player_config)
|
| 118 |
+
let physicsWorld;
|
| 119 |
+
let playerBody;
|
| 120 |
+
let groundBody;
|
| 121 |
+
let wallBodies = [];
|
| 122 |
+
let objectBodies = new Map(); // Maps object IDs to physics bodies
|
| 123 |
+
let PLAYER_HEIGHT = 1.7;
|
| 124 |
+
let PLAYER_RADIUS = 0.3;
|
| 125 |
+
let EYE_HEIGHT = 1.6; // Eye level for camera
|
| 126 |
+
let JUMP_FORCE = 5.0;
|
| 127 |
+
let GRAVITY = -9.82;
|
| 128 |
+
let PLAYER_MASS = 80.0;
|
| 129 |
+
let LINEAR_DAMPING = 0.9;
|
| 130 |
+
const WORLD_SIZE = 10; // 10x10 world (fixed)
|
| 131 |
+
const WORLD_HALF = WORLD_SIZE / 2; // -5 to 5 (fixed)
|
| 132 |
+
let isGrounded = false;
|
| 133 |
+
let canJump = true;
|
| 134 |
+
|
| 135 |
+
// Movement direction and keyboard state
|
| 136 |
+
const direction = new THREE.Vector3();
|
| 137 |
+
const moveForward = { value: false };
|
| 138 |
+
const moveBackward = { value: false };
|
| 139 |
+
const moveLeft = { value: false };
|
| 140 |
+
const moveRight = { value: false };
|
| 141 |
+
const moveUp = { value: false };
|
| 142 |
+
const moveDown = { value: false };
|
| 143 |
+
let prevTime = performance.now();
|
| 144 |
+
|
| 145 |
+
function applyPlayerConfig() {
|
| 146 |
+
/**
|
| 147 |
+
* Apply player configuration from scene data to runtime variables
|
| 148 |
+
* Allows MCP tools to customize player controller behavior
|
| 149 |
+
*/
|
| 150 |
+
if (!sceneData || !sceneData.player_config) {
|
| 151 |
+
console.log('No player_config in scene data, using defaults');
|
| 152 |
+
return;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const config = sceneData.player_config;
|
| 156 |
+
|
| 157 |
+
// Apply movement settings
|
| 158 |
+
if (config.move_speed !== undefined) {
|
| 159 |
+
moveSpeed = config.move_speed;
|
| 160 |
+
console.log(`Applied player speed: ${moveSpeed} units/sec`);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
if (config.jump_force !== undefined) {
|
| 164 |
+
JUMP_FORCE = config.jump_force;
|
| 165 |
+
console.log(`Applied jump force: ${JUMP_FORCE} m/s`);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Apply camera settings
|
| 169 |
+
if (config.mouse_sensitivity !== undefined) {
|
| 170 |
+
mouseSensitivity = config.mouse_sensitivity;
|
| 171 |
+
console.log(`Applied mouse sensitivity: ${mouseSensitivity}`);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
if (config.invert_y !== undefined) {
|
| 175 |
+
invertY = config.invert_y;
|
| 176 |
+
console.log(`Applied Y-axis inversion: ${invertY}`);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Apply physics settings
|
| 180 |
+
if (config.gravity !== undefined) {
|
| 181 |
+
GRAVITY = config.gravity;
|
| 182 |
+
console.log(`Applied gravity: ${GRAVITY} m/s²`);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Apply player dimensions
|
| 186 |
+
if (config.player_height !== undefined) {
|
| 187 |
+
PLAYER_HEIGHT = config.player_height;
|
| 188 |
+
console.log(`Applied player height: ${PLAYER_HEIGHT}m`);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
if (config.player_radius !== undefined) {
|
| 192 |
+
PLAYER_RADIUS = config.player_radius;
|
| 193 |
+
console.log(`Applied player radius: ${PLAYER_RADIUS}m`);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
if (config.eye_height !== undefined) {
|
| 197 |
+
EYE_HEIGHT = config.eye_height;
|
| 198 |
+
console.log(`Applied eye height: ${EYE_HEIGHT}m`);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
if (config.player_mass !== undefined) {
|
| 202 |
+
PLAYER_MASS = config.player_mass;
|
| 203 |
+
console.log(`Applied player mass: ${PLAYER_MASS}kg`);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
if (config.linear_damping !== undefined) {
|
| 207 |
+
LINEAR_DAMPING = config.linear_damping;
|
| 208 |
+
console.log(`Applied linear damping: ${LINEAR_DAMPING}`);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Apply Phase 2 settings
|
| 212 |
+
if (config.movement_acceleration !== undefined) {
|
| 213 |
+
movementAcceleration = config.movement_acceleration;
|
| 214 |
+
console.log(`Applied movement acceleration: ${movementAcceleration}`);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
if (config.air_control !== undefined) {
|
| 218 |
+
airControl = config.air_control;
|
| 219 |
+
console.log(`Applied air control: ${airControl}`);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
if (config.camera_fov !== undefined) {
|
| 223 |
+
cameraFOV = config.camera_fov;
|
| 224 |
+
console.log(`Applied camera FOV: ${cameraFOV}°`);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
if (config.min_pitch !== undefined) {
|
| 228 |
+
minPitch = config.min_pitch;
|
| 229 |
+
console.log(`Applied min pitch: ${minPitch}°`);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
if (config.max_pitch !== undefined) {
|
| 233 |
+
maxPitch = config.max_pitch;
|
| 234 |
+
console.log(`Applied max pitch: ${maxPitch}°`);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
console.log('✅ Player configuration applied successfully');
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
async function init() {
|
| 241 |
+
try {
|
| 242 |
+
// Fetch scene data
|
| 243 |
+
console.log('Fetching scene data...');
|
| 244 |
+
const response = await fetch(`${baseUrl}/api/scenes/${sceneId}`);
|
| 245 |
+
console.log('Response status:', response.status);
|
| 246 |
+
|
| 247 |
+
if (!response.ok) {
|
| 248 |
+
const errorText = await response.text();
|
| 249 |
+
console.error('Failed to fetch scene:', errorText);
|
| 250 |
+
throw new Error(`Scene not found (${response.status}): ${errorText}`);
|
| 251 |
+
}
|
| 252 |
+
sceneData = await response.json();
|
| 253 |
+
console.log('Scene data loaded:', sceneData);
|
| 254 |
+
|
| 255 |
+
// Apply player configuration from scene data
|
| 256 |
+
applyPlayerConfig();
|
| 257 |
+
|
| 258 |
+
// Setup Three.js scene
|
| 259 |
+
setupScene();
|
| 260 |
+
|
| 261 |
+
// Setup physics world
|
| 262 |
+
setupPhysics();
|
| 263 |
+
|
| 264 |
+
// Render all game objects
|
| 265 |
+
renderGameObjects();
|
| 266 |
+
|
| 267 |
+
// Start animation loop
|
| 268 |
+
animate();
|
| 269 |
+
|
| 270 |
+
} catch (error) {
|
| 271 |
+
console.error('Error initializing viewer:', error);
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
function setupScene() {
|
| 276 |
+
// Create scene
|
| 277 |
+
scene = new THREE.Scene();
|
| 278 |
+
const bgColor = sceneData.environment?.background_color || '#87CEEB';
|
| 279 |
+
scene.background = new THREE.Color(bgColor);
|
| 280 |
+
|
| 281 |
+
// Create camera (FOV from player_config)
|
| 282 |
+
camera = new THREE.PerspectiveCamera(
|
| 283 |
+
cameraFOV,
|
| 284 |
+
window.innerWidth / window.innerHeight,
|
| 285 |
+
0.1,
|
| 286 |
+
1000
|
| 287 |
+
);
|
| 288 |
+
|
| 289 |
+
// Position camera at player eye height (will be synced with physics in animate loop)
|
| 290 |
+
camera.position.set(0, EYE_HEIGHT, 0);
|
| 291 |
+
|
| 292 |
+
// Create renderer
|
| 293 |
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 294 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 295 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
| 296 |
+
document.getElementById('viewer-container').appendChild(renderer.domElement);
|
| 297 |
+
|
| 298 |
+
// Setup postprocessing for object outlines
|
| 299 |
+
composer = new EffectComposer(renderer);
|
| 300 |
+
|
| 301 |
+
const renderPass = new RenderPass(scene, camera);
|
| 302 |
+
composer.addPass(renderPass);
|
| 303 |
+
|
| 304 |
+
outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
|
| 305 |
+
outlinePass.edgeStrength = 5.0; // Increased for better visibility
|
| 306 |
+
outlinePass.edgeGlow = 1.0; // Increased glow
|
| 307 |
+
outlinePass.edgeThickness = 3.0; // Thicker edge
|
| 308 |
+
outlinePass.pulsePeriod = 0; // No pulsing
|
| 309 |
+
outlinePass.visibleEdgeColor.set('#ff8800'); // Orange outline
|
| 310 |
+
outlinePass.hiddenEdgeColor.set('#ff4400'); // Darker orange for hidden edges
|
| 311 |
+
composer.addPass(outlinePass);
|
| 312 |
+
|
| 313 |
+
console.log('OutlinePass configured - orange outline ready');
|
| 314 |
+
|
| 315 |
+
const outputPass = new OutputPass();
|
| 316 |
+
composer.addPass(outputPass);
|
| 317 |
+
|
| 318 |
+
console.log('PostProcessing composer initialized with OutlinePass');
|
| 319 |
+
|
| 320 |
+
// Add lights from scene data
|
| 321 |
+
sceneData.lights.forEach(lightData => {
|
| 322 |
+
let light;
|
| 323 |
+
|
| 324 |
+
if (lightData.type === 'ambient') {
|
| 325 |
+
light = new THREE.AmbientLight(lightData.color, lightData.intensity);
|
| 326 |
+
} else if (lightData.type === 'directional') {
|
| 327 |
+
light = new THREE.DirectionalLight(lightData.color, lightData.intensity);
|
| 328 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 329 |
+
// Directional lights need their target in the scene to work properly
|
| 330 |
+
light.target.position.set(0, 0, 0);
|
| 331 |
+
scene.add(light.target);
|
| 332 |
+
if (lightData.cast_shadow) {
|
| 333 |
+
light.castShadow = true;
|
| 334 |
+
}
|
| 335 |
+
} else if (lightData.type === 'point') {
|
| 336 |
+
light = new THREE.PointLight(lightData.color, lightData.intensity);
|
| 337 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
if (light) {
|
| 341 |
+
scene.add(light);
|
| 342 |
+
console.log('Added light:', lightData.type, lightData.name, 'at', lightData.position || 'default');
|
| 343 |
+
}
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
// Add grid (initially hidden, can be toggled)
|
| 347 |
+
const gridSize = sceneData.grid_size || 100;
|
| 348 |
+
const divisions = sceneData.grid_divisions || 20;
|
| 349 |
+
gridHelper = new THREE.GridHelper(gridSize, divisions, 0x444444, 0x222222);
|
| 350 |
+
gridHelper.visible = sceneData.show_grid || false;
|
| 351 |
+
scene.add(gridHelper);
|
| 352 |
+
|
| 353 |
+
// Initialize stats (FPS counter) - initially hidden
|
| 354 |
+
if (typeof Stats !== 'undefined') {
|
| 355 |
+
stats = new Stats();
|
| 356 |
+
stats.dom.style.position = 'absolute';
|
| 357 |
+
stats.dom.style.top = '0px';
|
| 358 |
+
stats.dom.style.left = '0px';
|
| 359 |
+
stats.dom.style.display = 'none'; // Hidden by default
|
| 360 |
+
document.getElementById('viewer-container').appendChild(stats.dom);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// Setup Orbit controls
|
| 364 |
+
orbitControls = new OrbitControls(camera, renderer.domElement);
|
| 365 |
+
orbitControls.enableDamping = true;
|
| 366 |
+
orbitControls.dampingFactor = 0.05;
|
| 367 |
+
|
| 368 |
+
// Setup FPS mouse-look controls
|
| 369 |
+
setupFPSControls();
|
| 370 |
+
|
| 371 |
+
// Click handler for object inspection in orbit mode
|
| 372 |
+
renderer.domElement.addEventListener('click', (event) => {
|
| 373 |
+
// In orbit mode, allow object inspection
|
| 374 |
+
if (controlMode === 'orbit') {
|
| 375 |
+
onObjectClick(event);
|
| 376 |
+
}
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
// Keyboard controls for FPS movement
|
| 380 |
+
const onKeyDown = (event) => {
|
| 381 |
+
switch (event.code) {
|
| 382 |
+
case 'KeyW': moveForward.value = true; break;
|
| 383 |
+
case 'KeyA': moveLeft.value = true; break;
|
| 384 |
+
case 'KeyS': moveBackward.value = true; break;
|
| 385 |
+
case 'KeyD': moveRight.value = true; break;
|
| 386 |
+
case 'Space':
|
| 387 |
+
event.preventDefault(); // Prevent page scroll
|
| 388 |
+
if (canJump && isGrounded && playerBody) {
|
| 389 |
+
playerBody.velocity.y = JUMP_FORCE;
|
| 390 |
+
canJump = false;
|
| 391 |
+
isGrounded = false;
|
| 392 |
+
}
|
| 393 |
+
break;
|
| 394 |
+
case 'KeyC': toggleControlMode(); break;
|
| 395 |
+
}
|
| 396 |
+
};
|
| 397 |
+
|
| 398 |
+
const onKeyUp = (event) => {
|
| 399 |
+
switch (event.code) {
|
| 400 |
+
case 'KeyW': moveForward.value = false; break;
|
| 401 |
+
case 'KeyA': moveLeft.value = false; break;
|
| 402 |
+
case 'KeyS': moveBackward.value = false; break;
|
| 403 |
+
case 'KeyD': moveRight.value = false; break;
|
| 404 |
+
}
|
| 405 |
+
};
|
| 406 |
+
|
| 407 |
+
document.addEventListener('keydown', onKeyDown);
|
| 408 |
+
document.addEventListener('keyup', onKeyUp);
|
| 409 |
+
|
| 410 |
+
// Set initial control mode
|
| 411 |
+
setControlMode(controlMode);
|
| 412 |
+
|
| 413 |
+
// Handle window resize
|
| 414 |
+
window.addEventListener('resize', onWindowResize);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
function setupFPSControls() {
|
| 418 |
+
// Mouse-look controls for FPS mode
|
| 419 |
+
renderer.domElement.addEventListener('mousedown', (event) => {
|
| 420 |
+
if (controlMode === 'fps' && event.button === 0) {
|
| 421 |
+
isMouseLocked = true;
|
| 422 |
+
renderer.domElement.requestPointerLock();
|
| 423 |
+
}
|
| 424 |
+
});
|
| 425 |
+
|
| 426 |
+
renderer.domElement.addEventListener('mouseup', () => {
|
| 427 |
+
// Keep mouse locked until Escape is pressed
|
| 428 |
+
});
|
| 429 |
+
|
| 430 |
+
// Mouse movement for camera rotation
|
| 431 |
+
document.addEventListener('mousemove', (event) => {
|
| 432 |
+
if (!isMouseLocked || controlMode !== 'fps') return;
|
| 433 |
+
|
| 434 |
+
const movementX = event.movementX || 0;
|
| 435 |
+
const movementY = event.movementY || 0;
|
| 436 |
+
|
| 437 |
+
// Update rotation (yaw and pitch)
|
| 438 |
+
cameraRotationY -= movementX * mouseSensitivity; // Yaw (left/right)
|
| 439 |
+
const pitchMultiplier = invertY ? 1 : -1; // Invert Y if configured
|
| 440 |
+
cameraRotationX += movementY * mouseSensitivity * pitchMultiplier; // Pitch (up/down)
|
| 441 |
+
|
| 442 |
+
// Clamp vertical rotation to configured limits
|
| 443 |
+
const minPitchRad = THREE.MathUtils.degToRad(minPitch);
|
| 444 |
+
const maxPitchRad = THREE.MathUtils.degToRad(maxPitch);
|
| 445 |
+
cameraRotationX = Math.max(minPitchRad, Math.min(maxPitchRad, cameraRotationX));
|
| 446 |
+
});
|
| 447 |
+
|
| 448 |
+
// Handle pointer lock changes
|
| 449 |
+
document.addEventListener('pointerlockchange', () => {
|
| 450 |
+
isMouseLocked = document.pointerLockElement === renderer.domElement;
|
| 451 |
+
});
|
| 452 |
+
|
| 453 |
+
document.addEventListener('pointerlockerror', () => {
|
| 454 |
+
console.error('Pointer lock failed');
|
| 455 |
+
isMouseLocked = false;
|
| 456 |
+
});
|
| 457 |
+
|
| 458 |
+
console.log('FPS controls initialized - click in viewer to enable mouse-look');
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function setupPhysics() {
|
| 462 |
+
console.log('Setting up physics world...');
|
| 463 |
+
|
| 464 |
+
// Create physics world
|
| 465 |
+
physicsWorld = new CANNON.World();
|
| 466 |
+
physicsWorld.gravity.set(0, GRAVITY, 0);
|
| 467 |
+
|
| 468 |
+
// Set up collision materials for better physics response
|
| 469 |
+
const defaultMaterial = new CANNON.Material('default');
|
| 470 |
+
const defaultContactMaterial = new CANNON.ContactMaterial(
|
| 471 |
+
defaultMaterial,
|
| 472 |
+
defaultMaterial,
|
| 473 |
+
{
|
| 474 |
+
friction: 0.3,
|
| 475 |
+
restitution: 0.0, // No bounce
|
| 476 |
+
}
|
| 477 |
+
);
|
| 478 |
+
physicsWorld.addContactMaterial(defaultContactMaterial);
|
| 479 |
+
physicsWorld.defaultContactMaterial = defaultContactMaterial;
|
| 480 |
+
|
| 481 |
+
// Create ground plane (10x10 centered at origin)
|
| 482 |
+
const groundGeometry = new THREE.PlaneGeometry(WORLD_SIZE, WORLD_SIZE);
|
| 483 |
+
const groundMaterial = new THREE.MeshStandardMaterial({
|
| 484 |
+
color: 0xffffff,
|
| 485 |
+
roughness: 0.8,
|
| 486 |
+
metalness: 0.2
|
| 487 |
+
});
|
| 488 |
+
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
|
| 489 |
+
groundMesh.rotation.x = -Math.PI / 2; // Rotate to be horizontal
|
| 490 |
+
groundMesh.position.y = 0;
|
| 491 |
+
groundMesh.receiveShadow = true;
|
| 492 |
+
groundMesh.userData = { isGround: true };
|
| 493 |
+
scene.add(groundMesh);
|
| 494 |
+
|
| 495 |
+
// Ground physics body
|
| 496 |
+
const groundShape = new CANNON.Plane();
|
| 497 |
+
groundBody = new CANNON.Body({ mass: 0, material: defaultMaterial });
|
| 498 |
+
groundBody.addShape(groundShape);
|
| 499 |
+
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
|
| 500 |
+
physicsWorld.addBody(groundBody);
|
| 501 |
+
|
| 502 |
+
// Create 4 boundary walls (white cubes, 5 units high, 0.5 thick)
|
| 503 |
+
const wallHeight = 5;
|
| 504 |
+
const wallThickness = 0.5;
|
| 505 |
+
const wallMaterial = new THREE.MeshStandardMaterial({
|
| 506 |
+
color: 0xffffff,
|
| 507 |
+
roughness: 0.7,
|
| 508 |
+
metalness: 0.1
|
| 509 |
+
});
|
| 510 |
+
|
| 511 |
+
// North wall (z = 5)
|
| 512 |
+
const northWallGeometry = new THREE.BoxGeometry(WORLD_SIZE + wallThickness * 2, wallHeight, wallThickness);
|
| 513 |
+
const northWallMesh = new THREE.Mesh(northWallGeometry, wallMaterial);
|
| 514 |
+
northWallMesh.position.set(0, wallHeight / 2, WORLD_HALF);
|
| 515 |
+
northWallMesh.userData = { isWall: true };
|
| 516 |
+
scene.add(northWallMesh);
|
| 517 |
+
|
| 518 |
+
const northWallShape = new CANNON.Box(new CANNON.Vec3((WORLD_SIZE + wallThickness * 2) / 2, wallHeight / 2, wallThickness / 2));
|
| 519 |
+
const northWallBody = new CANNON.Body({ mass: 0, material: defaultMaterial });
|
| 520 |
+
northWallBody.addShape(northWallShape);
|
| 521 |
+
northWallBody.position.copy(northWallMesh.position);
|
| 522 |
+
physicsWorld.addBody(northWallBody);
|
| 523 |
+
wallBodies.push(northWallBody);
|
| 524 |
+
|
| 525 |
+
// South wall (z = -5)
|
| 526 |
+
const southWallMesh = new THREE.Mesh(northWallGeometry, wallMaterial);
|
| 527 |
+
southWallMesh.position.set(0, wallHeight / 2, -WORLD_HALF);
|
| 528 |
+
southWallMesh.userData = { isWall: true };
|
| 529 |
+
scene.add(southWallMesh);
|
| 530 |
+
|
| 531 |
+
const southWallBody = new CANNON.Body({ mass: 0, material: defaultMaterial });
|
| 532 |
+
southWallBody.addShape(northWallShape);
|
| 533 |
+
southWallBody.position.copy(southWallMesh.position);
|
| 534 |
+
physicsWorld.addBody(southWallBody);
|
| 535 |
+
wallBodies.push(southWallBody);
|
| 536 |
+
|
| 537 |
+
// East wall (x = 5)
|
| 538 |
+
const eastWallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, WORLD_SIZE);
|
| 539 |
+
const eastWallMesh = new THREE.Mesh(eastWallGeometry, wallMaterial);
|
| 540 |
+
eastWallMesh.position.set(WORLD_HALF, wallHeight / 2, 0);
|
| 541 |
+
eastWallMesh.userData = { isWall: true };
|
| 542 |
+
scene.add(eastWallMesh);
|
| 543 |
+
|
| 544 |
+
const eastWallShape = new CANNON.Box(new CANNON.Vec3(wallThickness / 2, wallHeight / 2, WORLD_SIZE / 2));
|
| 545 |
+
const eastWallBody = new CANNON.Body({ mass: 0, material: defaultMaterial });
|
| 546 |
+
eastWallBody.addShape(eastWallShape);
|
| 547 |
+
eastWallBody.position.copy(eastWallMesh.position);
|
| 548 |
+
physicsWorld.addBody(eastWallBody);
|
| 549 |
+
wallBodies.push(eastWallBody);
|
| 550 |
+
|
| 551 |
+
// West wall (x = -5)
|
| 552 |
+
const westWallMesh = new THREE.Mesh(eastWallGeometry, wallMaterial);
|
| 553 |
+
westWallMesh.position.set(-WORLD_HALF, wallHeight / 2, 0);
|
| 554 |
+
westWallMesh.userData = { isWall: true };
|
| 555 |
+
scene.add(westWallMesh);
|
| 556 |
+
|
| 557 |
+
const westWallBody = new CANNON.Body({ mass: 0, material: defaultMaterial });
|
| 558 |
+
westWallBody.addShape(eastWallShape);
|
| 559 |
+
westWallBody.position.copy(westWallMesh.position);
|
| 560 |
+
physicsWorld.addBody(westWallBody);
|
| 561 |
+
wallBodies.push(westWallBody);
|
| 562 |
+
|
| 563 |
+
// Create player physics body (capsule approximated with cylinder)
|
| 564 |
+
const playerShape = new CANNON.Cylinder(PLAYER_RADIUS, PLAYER_RADIUS, PLAYER_HEIGHT, 8);
|
| 565 |
+
playerBody = new CANNON.Body({
|
| 566 |
+
mass: PLAYER_MASS, // kg (configurable via player_config)
|
| 567 |
+
material: defaultMaterial,
|
| 568 |
+
fixedRotation: true, // Prevent player from tipping over
|
| 569 |
+
linearDamping: LINEAR_DAMPING, // Air resistance for movement (configurable)
|
| 570 |
+
});
|
| 571 |
+
playerBody.addShape(playerShape);
|
| 572 |
+
|
| 573 |
+
// Set player starting position (0, 1, 0) - y=1 means feet at y=1, top at y=2.7
|
| 574 |
+
playerBody.position.set(0, 1 + PLAYER_HEIGHT / 2, 0);
|
| 575 |
+
physicsWorld.addBody(playerBody);
|
| 576 |
+
|
| 577 |
+
// Add collision detection for grounded check
|
| 578 |
+
playerBody.addEventListener('collide', (e) => {
|
| 579 |
+
// Check if colliding with ground
|
| 580 |
+
if (e.body === groundBody) {
|
| 581 |
+
isGrounded = true;
|
| 582 |
+
canJump = true;
|
| 583 |
+
}
|
| 584 |
+
});
|
| 585 |
+
|
| 586 |
+
console.log('Physics world created with ground, walls, and player');
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
function toggleControlMode() {
|
| 590 |
+
const newMode = controlMode === 'orbit' ? 'fps' : 'orbit';
|
| 591 |
+
setControlMode(newMode);
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
function setControlMode(mode) {
|
| 595 |
+
controlMode = mode;
|
| 596 |
+
const crosshair = document.getElementById('crosshair');
|
| 597 |
+
|
| 598 |
+
if (mode === 'fps') {
|
| 599 |
+
// Switch to FPS
|
| 600 |
+
orbitControls.enabled = false;
|
| 601 |
+
// Exit pointer lock if active
|
| 602 |
+
if (document.pointerLockElement) {
|
| 603 |
+
document.exitPointerLock();
|
| 604 |
+
}
|
| 605 |
+
isMouseLocked = false;
|
| 606 |
+
|
| 607 |
+
// Show crosshair
|
| 608 |
+
if (crosshair) crosshair.style.display = 'block';
|
| 609 |
+
|
| 610 |
+
console.log('Switched to FPS controls - click in viewer to enable mouse-look, WASD to move');
|
| 611 |
+
} else {
|
| 612 |
+
// Switch to Orbit
|
| 613 |
+
if (document.pointerLockElement) {
|
| 614 |
+
document.exitPointerLock();
|
| 615 |
+
}
|
| 616 |
+
isMouseLocked = false;
|
| 617 |
+
orbitControls.enabled = true;
|
| 618 |
+
|
| 619 |
+
// Hide crosshair
|
| 620 |
+
if (crosshair) crosshair.style.display = 'none';
|
| 621 |
+
|
| 622 |
+
// Clear selection
|
| 623 |
+
if (outlinePass) outlinePass.selectedObjects = [];
|
| 624 |
+
selectedObject = null;
|
| 625 |
+
selectedObjectId = null;
|
| 626 |
+
|
| 627 |
+
console.log('Switched to Orbit controls');
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
function createPhysicsShape(objType, scale) {
|
| 632 |
+
// Create Cannon.js physics shape based on object type
|
| 633 |
+
switch (objType) {
|
| 634 |
+
case 'cube':
|
| 635 |
+
return new CANNON.Box(new CANNON.Vec3(scale.x / 2, scale.y / 2, scale.z / 2));
|
| 636 |
+
case 'sphere':
|
| 637 |
+
return new CANNON.Sphere(scale.x);
|
| 638 |
+
case 'cylinder':
|
| 639 |
+
return new CANNON.Cylinder(scale.x, scale.x, scale.y, 8);
|
| 640 |
+
case 'plane':
|
| 641 |
+
// For planes, create a thin box
|
| 642 |
+
return new CANNON.Box(new CANNON.Vec3(scale.x / 2, 0.01, scale.y / 2));
|
| 643 |
+
case 'cone':
|
| 644 |
+
// Approximate cone with cylinder (Cannon doesn't have cone shape)
|
| 645 |
+
return new CANNON.Cylinder(0, scale.x, scale.y, 8);
|
| 646 |
+
case 'torus':
|
| 647 |
+
// Approximate torus with sphere
|
| 648 |
+
return new CANNON.Sphere(scale.x);
|
| 649 |
+
default:
|
| 650 |
+
// Default to box
|
| 651 |
+
return new CANNON.Box(new CANNON.Vec3(scale.x / 2, scale.y / 2, scale.z / 2));
|
| 652 |
+
}
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
function renderGameObjects() {
|
| 656 |
+
console.log('Rendering', sceneData.objects.length, 'game objects...');
|
| 657 |
+
|
| 658 |
+
sceneData.objects.forEach(obj => {
|
| 659 |
+
// Validate object is within bounds
|
| 660 |
+
if (Math.abs(obj.position.x) > WORLD_HALF || Math.abs(obj.position.z) > WORLD_HALF) {
|
| 661 |
+
console.warn(`Object ${obj.name} at (${obj.position.x}, ${obj.position.z}) is outside 10x10 world bounds - skipping`);
|
| 662 |
+
return;
|
| 663 |
+
}
|
| 664 |
+
let geometry, mesh;
|
| 665 |
+
|
| 666 |
+
// Create geometry based on type
|
| 667 |
+
switch (obj.type) {
|
| 668 |
+
case 'cube':
|
| 669 |
+
geometry = new THREE.BoxGeometry(
|
| 670 |
+
obj.scale.x,
|
| 671 |
+
obj.scale.y,
|
| 672 |
+
obj.scale.z
|
| 673 |
+
);
|
| 674 |
+
break;
|
| 675 |
+
case 'sphere':
|
| 676 |
+
geometry = new THREE.SphereGeometry(obj.scale.x, 32, 32);
|
| 677 |
+
break;
|
| 678 |
+
case 'cylinder':
|
| 679 |
+
geometry = new THREE.CylinderGeometry(
|
| 680 |
+
obj.scale.x,
|
| 681 |
+
obj.scale.x,
|
| 682 |
+
obj.scale.y,
|
| 683 |
+
32
|
| 684 |
+
);
|
| 685 |
+
break;
|
| 686 |
+
case 'plane':
|
| 687 |
+
geometry = new THREE.PlaneGeometry(obj.scale.x, obj.scale.y);
|
| 688 |
+
break;
|
| 689 |
+
case 'cone':
|
| 690 |
+
geometry = new THREE.ConeGeometry(obj.scale.x, obj.scale.y, 32);
|
| 691 |
+
break;
|
| 692 |
+
case 'torus':
|
| 693 |
+
geometry = new THREE.TorusGeometry(obj.scale.x, obj.scale.x * 0.4, 16, 100);
|
| 694 |
+
break;
|
| 695 |
+
default:
|
| 696 |
+
console.warn('Unknown object type:', obj.type);
|
| 697 |
+
return;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
// Create material
|
| 701 |
+
const material = new THREE.MeshStandardMaterial({
|
| 702 |
+
color: obj.material.color,
|
| 703 |
+
metalness: obj.material.metalness || 0.5,
|
| 704 |
+
roughness: obj.material.roughness || 0.5,
|
| 705 |
+
opacity: obj.material.opacity || 1.0,
|
| 706 |
+
transparent: obj.material.opacity < 1.0,
|
| 707 |
+
wireframe: obj.material.wireframe || false,
|
| 708 |
+
});
|
| 709 |
+
|
| 710 |
+
// Create mesh
|
| 711 |
+
mesh = new THREE.Mesh(geometry, material);
|
| 712 |
+
|
| 713 |
+
// Set position
|
| 714 |
+
mesh.position.set(obj.position.x, obj.position.y, obj.position.z);
|
| 715 |
+
|
| 716 |
+
// Set rotation (convert degrees to radians)
|
| 717 |
+
mesh.rotation.set(
|
| 718 |
+
THREE.MathUtils.degToRad(obj.rotation.x),
|
| 719 |
+
THREE.MathUtils.degToRad(obj.rotation.y),
|
| 720 |
+
THREE.MathUtils.degToRad(obj.rotation.z)
|
| 721 |
+
);
|
| 722 |
+
|
| 723 |
+
// Store metadata
|
| 724 |
+
mesh.userData = {
|
| 725 |
+
id: obj.id,
|
| 726 |
+
name: obj.name,
|
| 727 |
+
type: obj.type,
|
| 728 |
+
isSceneObject: true,
|
| 729 |
+
};
|
| 730 |
+
|
| 731 |
+
scene.add(mesh);
|
| 732 |
+
|
| 733 |
+
// Create physics body for collision
|
| 734 |
+
const physicsShape = createPhysicsShape(obj.type, obj.scale);
|
| 735 |
+
const physicsBody = new CANNON.Body({
|
| 736 |
+
mass: 0, // Static objects
|
| 737 |
+
position: new CANNON.Vec3(obj.position.x, obj.position.y, obj.position.z),
|
| 738 |
+
});
|
| 739 |
+
physicsBody.addShape(physicsShape);
|
| 740 |
+
|
| 741 |
+
// Apply rotation to physics body
|
| 742 |
+
const quaternion = new CANNON.Quaternion();
|
| 743 |
+
quaternion.setFromEuler(
|
| 744 |
+
THREE.MathUtils.degToRad(obj.rotation.x),
|
| 745 |
+
THREE.MathUtils.degToRad(obj.rotation.y),
|
| 746 |
+
THREE.MathUtils.degToRad(obj.rotation.z),
|
| 747 |
+
'XYZ'
|
| 748 |
+
);
|
| 749 |
+
physicsBody.quaternion.copy(quaternion);
|
| 750 |
+
|
| 751 |
+
physicsWorld.addBody(physicsBody);
|
| 752 |
+
objectBodies.set(obj.id, physicsBody);
|
| 753 |
+
|
| 754 |
+
console.log('Added object:', obj.name || obj.id, obj.type, 'with physics collider');
|
| 755 |
+
});
|
| 756 |
+
|
| 757 |
+
// Camera position is managed by physics system in FPS mode
|
| 758 |
+
console.log('Scene objects loaded. Camera controlled by player physics body.');
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
function onWindowResize() {
|
| 762 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 763 |
+
camera.updateProjectionMatrix();
|
| 764 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 765 |
+
|
| 766 |
+
// Update composer
|
| 767 |
+
if (composer) {
|
| 768 |
+
composer.setSize(window.innerWidth, window.innerHeight);
|
| 769 |
+
}
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
function updateLookedAtObject() {
|
| 773 |
+
if (controlMode !== 'fps') {
|
| 774 |
+
if (selectedObject) {
|
| 775 |
+
outlinePass.selectedObjects = [];
|
| 776 |
+
selectedObject = null;
|
| 777 |
+
selectedObjectId = null;
|
| 778 |
+
}
|
| 779 |
+
return;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
// Raycast from camera center (crosshair position)
|
| 783 |
+
const raycaster = new THREE.Raycaster();
|
| 784 |
+
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); // Center of screen
|
| 785 |
+
|
| 786 |
+
const selectableObjects = scene.children.filter(obj => obj.userData.isSceneObject && obj.userData.id);
|
| 787 |
+
const intersects = raycaster.intersectObjects(selectableObjects);
|
| 788 |
+
|
| 789 |
+
if (intersects.length > 0 && intersects[0].distance < MAX_SELECT_DISTANCE) {
|
| 790 |
+
const newSelected = intersects[0].object;
|
| 791 |
+
if (newSelected !== selectedObject) {
|
| 792 |
+
selectedObject = newSelected;
|
| 793 |
+
selectedObjectId = newSelected.userData.id;
|
| 794 |
+
outlinePass.selectedObjects = [selectedObject];
|
| 795 |
+
|
| 796 |
+
console.log('🎯 Object selected:', selectedObjectId, newSelected.userData.type,
|
| 797 |
+
`distance: ${intersects[0].distance.toFixed(2)}m`,
|
| 798 |
+
`outline array length: ${outlinePass.selectedObjects.length}`);
|
| 799 |
+
|
| 800 |
+
// Send selection to parent (for chat commands)
|
| 801 |
+
if (window.parent) {
|
| 802 |
+
window.parent.postMessage({
|
| 803 |
+
action: 'objectSelected',
|
| 804 |
+
data: {
|
| 805 |
+
object_id: selectedObjectId,
|
| 806 |
+
object_type: newSelected.userData.type,
|
| 807 |
+
distance: intersects[0].distance.toFixed(2)
|
| 808 |
+
}
|
| 809 |
+
}, '*');
|
| 810 |
+
}
|
| 811 |
+
}
|
| 812 |
+
} else {
|
| 813 |
+
// No object in view
|
| 814 |
+
if (selectedObject) {
|
| 815 |
+
console.log('🎯 Object deselected');
|
| 816 |
+
outlinePass.selectedObjects = [];
|
| 817 |
+
selectedObject = null;
|
| 818 |
+
selectedObjectId = null;
|
| 819 |
+
|
| 820 |
+
// Notify deselection
|
| 821 |
+
if (window.parent) {
|
| 822 |
+
window.parent.postMessage({
|
| 823 |
+
action: 'objectDeselected',
|
| 824 |
+
data: {}
|
| 825 |
+
}, '*');
|
| 826 |
+
}
|
| 827 |
+
}
|
| 828 |
+
}
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
function animate() {
|
| 832 |
+
requestAnimationFrame(animate);
|
| 833 |
+
|
| 834 |
+
// Update stats
|
| 835 |
+
if (stats) stats.begin();
|
| 836 |
+
|
| 837 |
+
const time = performance.now();
|
| 838 |
+
const delta = (time - prevTime) / 1000;
|
| 839 |
+
|
| 840 |
+
// Step physics simulation
|
| 841 |
+
if (physicsWorld) {
|
| 842 |
+
physicsWorld.step(1/60, delta, 3);
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
if (controlMode === 'fps' && playerBody) {
|
| 846 |
+
// Apply camera rotation from mouse-look
|
| 847 |
+
camera.rotation.order = 'YXZ'; // Ensure correct rotation order
|
| 848 |
+
camera.rotation.y = cameraRotationY;
|
| 849 |
+
camera.rotation.x = cameraRotationX;
|
| 850 |
+
camera.rotation.z = 0;
|
| 851 |
+
|
| 852 |
+
// Get input direction from keyboard
|
| 853 |
+
direction.z = Number(moveForward.value) - Number(moveBackward.value);
|
| 854 |
+
direction.x = Number(moveRight.value) - Number(moveLeft.value);
|
| 855 |
+
direction.y = 0; // No vertical movement from input
|
| 856 |
+
direction.normalize();
|
| 857 |
+
|
| 858 |
+
// Calculate movement direction relative to camera orientation
|
| 859 |
+
const forward = new THREE.Vector3(0, 0, -1);
|
| 860 |
+
const right = new THREE.Vector3(1, 0, 0);
|
| 861 |
+
|
| 862 |
+
// Apply camera rotation to get movement direction
|
| 863 |
+
forward.applyQuaternion(camera.quaternion);
|
| 864 |
+
right.applyQuaternion(camera.quaternion);
|
| 865 |
+
|
| 866 |
+
// Project to horizontal plane
|
| 867 |
+
forward.y = 0;
|
| 868 |
+
right.y = 0;
|
| 869 |
+
forward.normalize();
|
| 870 |
+
right.normalize();
|
| 871 |
+
|
| 872 |
+
// Apply movement to physics body (keep current Y velocity for gravity/jump)
|
| 873 |
+
// Reduce effectiveness when airborne based on air control setting
|
| 874 |
+
const controlFactor = isGrounded ? 1.0 : airControl;
|
| 875 |
+
const moveX = (direction.x * right.x + direction.z * forward.x) * moveSpeed * controlFactor;
|
| 876 |
+
const moveZ = (direction.x * right.z + direction.z * forward.z) * moveSpeed * controlFactor;
|
| 877 |
+
|
| 878 |
+
playerBody.velocity.x = moveX;
|
| 879 |
+
playerBody.velocity.z = moveZ;
|
| 880 |
+
// Don't modify playerBody.velocity.y - let physics handle gravity and jumping
|
| 881 |
+
|
| 882 |
+
// Check if grounded using a small raycast downward
|
| 883 |
+
const groundRaycaster = new THREE.Raycaster(
|
| 884 |
+
new THREE.Vector3(playerBody.position.x, playerBody.position.y, playerBody.position.z),
|
| 885 |
+
new THREE.Vector3(0, -1, 0),
|
| 886 |
+
0,
|
| 887 |
+
PLAYER_HEIGHT / 2 + 0.1
|
| 888 |
+
);
|
| 889 |
+
const groundIntersects = groundRaycaster.intersectObjects(
|
| 890 |
+
scene.children.filter(obj => obj.userData.isGround || obj.userData.isWall || obj.userData.isSceneObject)
|
| 891 |
+
);
|
| 892 |
+
if (groundIntersects.length > 0) {
|
| 893 |
+
isGrounded = true;
|
| 894 |
+
canJump = true;
|
| 895 |
+
} else {
|
| 896 |
+
isGrounded = false;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
// Sync camera position to physics body (at eye height)
|
| 900 |
+
camera.position.x = playerBody.position.x;
|
| 901 |
+
camera.position.y = playerBody.position.y - PLAYER_HEIGHT / 2 + EYE_HEIGHT;
|
| 902 |
+
camera.position.z = playerBody.position.z;
|
| 903 |
+
|
| 904 |
+
prevTime = time;
|
| 905 |
+
} else if (controlMode === 'orbit') {
|
| 906 |
+
// Orbit controls
|
| 907 |
+
orbitControls.update();
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
// Update looked-at object (FPS mode only)
|
| 911 |
+
updateLookedAtObject();
|
| 912 |
+
|
| 913 |
+
// Render using composer (for outlines) instead of direct renderer
|
| 914 |
+
if (composer) {
|
| 915 |
+
composer.render();
|
| 916 |
+
} else {
|
| 917 |
+
renderer.render(scene, camera);
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
// End stats measurement
|
| 921 |
+
if (stats) stats.end();
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
// ==================== Scene Control Functions ====================
|
| 925 |
+
|
| 926 |
+
/**
|
| 927 |
+
* Toggle grid helper visibility
|
| 928 |
+
*/
|
| 929 |
+
function toggleGrid(enabled) {
|
| 930 |
+
if (gridHelper) {
|
| 931 |
+
gridHelper.visible = enabled;
|
| 932 |
+
console.log('Grid helper:', enabled ? 'enabled' : 'disabled');
|
| 933 |
+
}
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
/**
|
| 937 |
+
* Toggle wireframe mode for all objects
|
| 938 |
+
*/
|
| 939 |
+
function toggleWireframe(enabled) {
|
| 940 |
+
wireframeEnabled = enabled;
|
| 941 |
+
scene.traverse((object) => {
|
| 942 |
+
if (object.isMesh && object.material) {
|
| 943 |
+
object.material.wireframe = enabled;
|
| 944 |
+
}
|
| 945 |
+
});
|
| 946 |
+
console.log('Wireframe mode:', enabled ? 'enabled' : 'disabled');
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
/**
|
| 950 |
+
* Toggle stats (FPS counter) display
|
| 951 |
+
*/
|
| 952 |
+
function toggleStats(enabled) {
|
| 953 |
+
if (stats) {
|
| 954 |
+
stats.dom.style.display = enabled ? 'block' : 'none';
|
| 955 |
+
console.log('Stats:', enabled ? 'enabled' : 'disabled');
|
| 956 |
+
}
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
/**
|
| 960 |
+
* Capture screenshot of the current scene
|
| 961 |
+
*/
|
| 962 |
+
function captureScreenshot() {
|
| 963 |
+
if (!renderer) {
|
| 964 |
+
console.error('Renderer not initialized');
|
| 965 |
+
return;
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
// Render one more frame to ensure we have the latest scene
|
| 969 |
+
renderer.render(scene, camera);
|
| 970 |
+
|
| 971 |
+
// Get the canvas data as PNG
|
| 972 |
+
const dataURL = renderer.domElement.toDataURL('image/png');
|
| 973 |
+
|
| 974 |
+
// Send screenshot data back to parent window
|
| 975 |
+
window.parent.postMessage({
|
| 976 |
+
action: 'screenshot',
|
| 977 |
+
data: {
|
| 978 |
+
dataURL: dataURL,
|
| 979 |
+
timestamp: Date.now(),
|
| 980 |
+
sceneName: sceneData?.name || 'scene'
|
| 981 |
+
}
|
| 982 |
+
}, '*');
|
| 983 |
+
|
| 984 |
+
console.log('Screenshot captured and sent to parent window');
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
/**
|
| 988 |
+
* Handle object click for inspection
|
| 989 |
+
*/
|
| 990 |
+
function onObjectClick(event) {
|
| 991 |
+
// Calculate mouse position in normalized device coordinates (-1 to +1)
|
| 992 |
+
const rect = renderer.domElement.getBoundingClientRect();
|
| 993 |
+
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
| 994 |
+
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
| 995 |
+
|
| 996 |
+
// Update raycaster
|
| 997 |
+
raycaster.setFromCamera(mouse, camera);
|
| 998 |
+
|
| 999 |
+
// Find intersections (only check mesh objects, not lights or helpers)
|
| 1000 |
+
const intersects = raycaster.intersectObjects(scene.children.filter(obj => obj.isMesh));
|
| 1001 |
+
|
| 1002 |
+
if (intersects.length > 0) {
|
| 1003 |
+
const clickedObject = intersects[0].object;
|
| 1004 |
+
|
| 1005 |
+
// Deselect previous object
|
| 1006 |
+
if (selectedObject && selectedObject !== clickedObject) {
|
| 1007 |
+
if (selectedObject.userData.originalColor) {
|
| 1008 |
+
selectedObject.material.emissive.copy(selectedObject.userData.originalColor);
|
| 1009 |
+
selectedObject.material.emissiveIntensity = 0;
|
| 1010 |
+
}
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
// Select new object
|
| 1014 |
+
selectedObject = clickedObject;
|
| 1015 |
+
|
| 1016 |
+
// Highlight selected object with persistent glow
|
| 1017 |
+
if (!selectedObject.userData.originalColor) {
|
| 1018 |
+
selectedObject.userData.originalColor = selectedObject.material.emissive.clone();
|
| 1019 |
+
}
|
| 1020 |
+
selectedObject.material.emissive = new THREE.Color(0x4444ff);
|
| 1021 |
+
selectedObject.material.emissiveIntensity = 0.3;
|
| 1022 |
+
|
| 1023 |
+
// Get object properties
|
| 1024 |
+
const objectInfo = {
|
| 1025 |
+
id: clickedObject.userData.id,
|
| 1026 |
+
name: clickedObject.userData.name || 'Unnamed Object',
|
| 1027 |
+
type: clickedObject.userData.type || 'unknown',
|
| 1028 |
+
position: {
|
| 1029 |
+
x: clickedObject.position.x.toFixed(2),
|
| 1030 |
+
y: clickedObject.position.y.toFixed(2),
|
| 1031 |
+
z: clickedObject.position.z.toFixed(2)
|
| 1032 |
+
},
|
| 1033 |
+
rotation: {
|
| 1034 |
+
x: THREE.MathUtils.radToDeg(clickedObject.rotation.x).toFixed(2),
|
| 1035 |
+
y: THREE.MathUtils.radToDeg(clickedObject.rotation.y).toFixed(2),
|
| 1036 |
+
z: THREE.MathUtils.radToDeg(clickedObject.rotation.z).toFixed(2)
|
| 1037 |
+
},
|
| 1038 |
+
scale: {
|
| 1039 |
+
x: clickedObject.scale.x.toFixed(2),
|
| 1040 |
+
y: clickedObject.scale.y.toFixed(2),
|
| 1041 |
+
z: clickedObject.scale.z.toFixed(2)
|
| 1042 |
+
},
|
| 1043 |
+
color: '#' + clickedObject.material.color.getHexString()
|
| 1044 |
+
};
|
| 1045 |
+
|
| 1046 |
+
// Send object info to parent window
|
| 1047 |
+
window.parent.postMessage({
|
| 1048 |
+
action: 'objectInspect',
|
| 1049 |
+
data: objectInfo
|
| 1050 |
+
}, '*');
|
| 1051 |
+
|
| 1052 |
+
console.log('Object selected:', objectInfo);
|
| 1053 |
+
}
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
// ==================== PostMessage API for Dynamic Updates ====================
|
| 1057 |
+
|
| 1058 |
+
/**
|
| 1059 |
+
* Listen for messages from parent window (Gradio) to update scene dynamically
|
| 1060 |
+
* This eliminates the need for iframe reloads
|
| 1061 |
+
*/
|
| 1062 |
+
window.addEventListener('message', (event) => {
|
| 1063 |
+
// Security: verify origin in production
|
| 1064 |
+
// if (event.origin !== window.location.origin) return;
|
| 1065 |
+
|
| 1066 |
+
const { action, data } = event.data;
|
| 1067 |
+
console.log('📨 Received postMessage:', action, data);
|
| 1068 |
+
|
| 1069 |
+
switch (action) {
|
| 1070 |
+
case 'addObject':
|
| 1071 |
+
handleAddObject(data);
|
| 1072 |
+
break;
|
| 1073 |
+
case 'removeObject':
|
| 1074 |
+
handleRemoveObject(data);
|
| 1075 |
+
break;
|
| 1076 |
+
case 'setLighting':
|
| 1077 |
+
handleSetLighting(data);
|
| 1078 |
+
break;
|
| 1079 |
+
case 'updateScene':
|
| 1080 |
+
handleUpdateScene(data);
|
| 1081 |
+
break;
|
| 1082 |
+
case 'setControlMode':
|
| 1083 |
+
setControlMode(data.mode);
|
| 1084 |
+
break;
|
| 1085 |
+
case 'toggleGrid':
|
| 1086 |
+
toggleGrid(data.enabled);
|
| 1087 |
+
break;
|
| 1088 |
+
case 'toggleWireframe':
|
| 1089 |
+
toggleWireframe(data.enabled);
|
| 1090 |
+
break;
|
| 1091 |
+
case 'toggleStats':
|
| 1092 |
+
toggleStats(data.enabled);
|
| 1093 |
+
break;
|
| 1094 |
+
case 'takeScreenshot':
|
| 1095 |
+
captureScreenshot();
|
| 1096 |
+
break;
|
| 1097 |
+
case 'addLight':
|
| 1098 |
+
addLightToScene(data);
|
| 1099 |
+
break;
|
| 1100 |
+
case 'removeLight':
|
| 1101 |
+
removeLightFromScene(data.light_name);
|
| 1102 |
+
break;
|
| 1103 |
+
case 'updateLight':
|
| 1104 |
+
updateSceneLight(data.light_name, data);
|
| 1105 |
+
break;
|
| 1106 |
+
case 'updateMaterial':
|
| 1107 |
+
updateObjectMaterial(data.object_id, data);
|
| 1108 |
+
break;
|
| 1109 |
+
case 'setBackground':
|
| 1110 |
+
setSceneBackground(data);
|
| 1111 |
+
break;
|
| 1112 |
+
case 'setFog':
|
| 1113 |
+
setSceneFog(data);
|
| 1114 |
+
break;
|
| 1115 |
+
default:
|
| 1116 |
+
console.warn('Unknown postMessage action:', action);
|
| 1117 |
+
}
|
| 1118 |
+
});
|
| 1119 |
+
|
| 1120 |
+
/**
|
| 1121 |
+
* Dynamically add an object to the scene
|
| 1122 |
+
*/
|
| 1123 |
+
function handleAddObject(objData) {
|
| 1124 |
+
if (!scene || !sceneData) {
|
| 1125 |
+
console.error('Scene not initialized yet');
|
| 1126 |
+
return;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
// Validate object is within bounds
|
| 1130 |
+
if (Math.abs(objData.position.x) > WORLD_HALF || Math.abs(objData.position.z) > WORLD_HALF) {
|
| 1131 |
+
console.error(`Cannot add object at (${objData.position.x}, ${objData.position.z}) - outside 10x10 world bounds`);
|
| 1132 |
+
return;
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
// Add to scene data
|
| 1136 |
+
sceneData.objects.push(objData);
|
| 1137 |
+
|
| 1138 |
+
// Create and add to Three.js scene
|
| 1139 |
+
let geometry;
|
| 1140 |
+
switch (objData.type) {
|
| 1141 |
+
case 'cube':
|
| 1142 |
+
geometry = new THREE.BoxGeometry(objData.scale.x, objData.scale.y, objData.scale.z);
|
| 1143 |
+
break;
|
| 1144 |
+
case 'sphere':
|
| 1145 |
+
geometry = new THREE.SphereGeometry(objData.scale.x, 32, 32);
|
| 1146 |
+
break;
|
| 1147 |
+
case 'cylinder':
|
| 1148 |
+
geometry = new THREE.CylinderGeometry(objData.scale.x, objData.scale.x, objData.scale.y, 32);
|
| 1149 |
+
break;
|
| 1150 |
+
case 'plane':
|
| 1151 |
+
geometry = new THREE.PlaneGeometry(objData.scale.x, objData.scale.y);
|
| 1152 |
+
break;
|
| 1153 |
+
case 'cone':
|
| 1154 |
+
geometry = new THREE.ConeGeometry(objData.scale.x, objData.scale.y, 32);
|
| 1155 |
+
break;
|
| 1156 |
+
case 'torus':
|
| 1157 |
+
geometry = new THREE.TorusGeometry(objData.scale.x, objData.scale.x * 0.4, 16, 100);
|
| 1158 |
+
break;
|
| 1159 |
+
default:
|
| 1160 |
+
console.warn('Unknown object type:', objData.type);
|
| 1161 |
+
return;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
const material = new THREE.MeshStandardMaterial({
|
| 1165 |
+
color: objData.material.color,
|
| 1166 |
+
metalness: objData.material.metalness || 0.5,
|
| 1167 |
+
roughness: objData.material.roughness || 0.5,
|
| 1168 |
+
opacity: objData.material.opacity || 1.0,
|
| 1169 |
+
transparent: objData.material.opacity < 1.0,
|
| 1170 |
+
wireframe: wireframeEnabled || objData.material.wireframe || false,
|
| 1171 |
+
});
|
| 1172 |
+
|
| 1173 |
+
const mesh = new THREE.Mesh(geometry, material);
|
| 1174 |
+
mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
|
| 1175 |
+
mesh.rotation.set(
|
| 1176 |
+
THREE.MathUtils.degToRad(objData.rotation.x),
|
| 1177 |
+
THREE.MathUtils.degToRad(objData.rotation.y),
|
| 1178 |
+
THREE.MathUtils.degToRad(objData.rotation.z)
|
| 1179 |
+
);
|
| 1180 |
+
mesh.userData = {
|
| 1181 |
+
id: objData.id,
|
| 1182 |
+
name: objData.name,
|
| 1183 |
+
type: objData.type,
|
| 1184 |
+
isSceneObject: true,
|
| 1185 |
+
};
|
| 1186 |
+
|
| 1187 |
+
scene.add(mesh);
|
| 1188 |
+
|
| 1189 |
+
// Create physics body for collision
|
| 1190 |
+
const physicsShape = createPhysicsShape(objData.type, objData.scale);
|
| 1191 |
+
const physicsBody = new CANNON.Body({
|
| 1192 |
+
mass: 0, // Static objects
|
| 1193 |
+
position: new CANNON.Vec3(objData.position.x, objData.position.y, objData.position.z),
|
| 1194 |
+
});
|
| 1195 |
+
physicsBody.addShape(physicsShape);
|
| 1196 |
+
|
| 1197 |
+
// Apply rotation to physics body
|
| 1198 |
+
const quaternion = new CANNON.Quaternion();
|
| 1199 |
+
quaternion.setFromEuler(
|
| 1200 |
+
THREE.MathUtils.degToRad(objData.rotation.x),
|
| 1201 |
+
THREE.MathUtils.degToRad(objData.rotation.y),
|
| 1202 |
+
THREE.MathUtils.degToRad(objData.rotation.z),
|
| 1203 |
+
'XYZ'
|
| 1204 |
+
);
|
| 1205 |
+
physicsBody.quaternion.copy(quaternion);
|
| 1206 |
+
|
| 1207 |
+
physicsWorld.addBody(physicsBody);
|
| 1208 |
+
objectBodies.set(objData.id, physicsBody);
|
| 1209 |
+
|
| 1210 |
+
// Add highlight effect
|
| 1211 |
+
animateObjectHighlight(mesh);
|
| 1212 |
+
|
| 1213 |
+
console.log('Added object dynamically:', objData.name || objData.id);
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
/**
|
| 1217 |
+
* Dynamically remove an object from the scene
|
| 1218 |
+
*/
|
| 1219 |
+
function handleRemoveObject(data) {
|
| 1220 |
+
if (!scene || !sceneData) {
|
| 1221 |
+
console.error('Scene not initialized yet');
|
| 1222 |
+
return;
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
const { object_id } = data;
|
| 1226 |
+
|
| 1227 |
+
// Remove from Three.js scene
|
| 1228 |
+
const objectToRemove = scene.children.find(obj => obj.userData && obj.userData.id === object_id);
|
| 1229 |
+
if (objectToRemove) {
|
| 1230 |
+
scene.remove(objectToRemove);
|
| 1231 |
+
if (objectToRemove.geometry) objectToRemove.geometry.dispose();
|
| 1232 |
+
if (objectToRemove.material) objectToRemove.material.dispose();
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
// Remove physics body
|
| 1236 |
+
const physicsBody = objectBodies.get(object_id);
|
| 1237 |
+
if (physicsBody) {
|
| 1238 |
+
physicsWorld.removeBody(physicsBody);
|
| 1239 |
+
objectBodies.delete(object_id);
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
// Remove from scene data
|
| 1243 |
+
sceneData.objects = sceneData.objects.filter(obj => obj.id !== object_id);
|
| 1244 |
+
|
| 1245 |
+
console.log('Removed object dynamically:', object_id);
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
/**
|
| 1249 |
+
* Dynamically update lighting
|
| 1250 |
+
*/
|
| 1251 |
+
function handleSetLighting(data) {
|
| 1252 |
+
if (!scene || !sceneData) {
|
| 1253 |
+
console.error('Scene not initialized yet');
|
| 1254 |
+
return;
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
const { lights } = data;
|
| 1258 |
+
|
| 1259 |
+
// Remove all existing lights
|
| 1260 |
+
const lightsToRemove = scene.children.filter(obj => obj.isLight);
|
| 1261 |
+
lightsToRemove.forEach(light => scene.remove(light));
|
| 1262 |
+
|
| 1263 |
+
// Add new lights
|
| 1264 |
+
lights.forEach(lightData => {
|
| 1265 |
+
let light;
|
| 1266 |
+
|
| 1267 |
+
if (lightData.type === 'ambient') {
|
| 1268 |
+
light = new THREE.AmbientLight(lightData.color, lightData.intensity);
|
| 1269 |
+
} else if (lightData.type === 'directional') {
|
| 1270 |
+
light = new THREE.DirectionalLight(lightData.color, lightData.intensity);
|
| 1271 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 1272 |
+
light.target.position.set(0, 0, 0);
|
| 1273 |
+
scene.add(light.target);
|
| 1274 |
+
if (lightData.cast_shadow) {
|
| 1275 |
+
light.castShadow = true;
|
| 1276 |
+
}
|
| 1277 |
+
} else if (lightData.type === 'point') {
|
| 1278 |
+
light = new THREE.PointLight(lightData.color, lightData.intensity);
|
| 1279 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
if (light) {
|
| 1283 |
+
scene.add(light);
|
| 1284 |
+
}
|
| 1285 |
+
});
|
| 1286 |
+
|
| 1287 |
+
// Update scene data
|
| 1288 |
+
sceneData.lights = lights;
|
| 1289 |
+
|
| 1290 |
+
console.log('Updated lighting dynamically');
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
/**
|
| 1294 |
+
* Fully reload the scene from new data
|
| 1295 |
+
*/
|
| 1296 |
+
function handleUpdateScene(data) {
|
| 1297 |
+
// For major updates, we can reload the entire scene
|
| 1298 |
+
// This is a fallback for complex changes
|
| 1299 |
+
location.reload();
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
/**
|
| 1303 |
+
* Animate object highlight with enhanced color pulse and scale effect
|
| 1304 |
+
*/
|
| 1305 |
+
function animateObjectHighlight(mesh) {
|
| 1306 |
+
const originalColor = mesh.material.color.clone();
|
| 1307 |
+
const originalScale = mesh.scale.clone();
|
| 1308 |
+
|
| 1309 |
+
// Color pulse sequence: yellow -> cyan -> yellow
|
| 1310 |
+
const pulseColors = [
|
| 1311 |
+
new THREE.Color(0xffff00), // Yellow
|
| 1312 |
+
new THREE.Color(0x00ffff), // Cyan
|
| 1313 |
+
new THREE.Color(0xffff00), // Yellow
|
| 1314 |
+
];
|
| 1315 |
+
|
| 1316 |
+
let progress = 0;
|
| 1317 |
+
const duration = 90; // frames (1.5 seconds at 60fps)
|
| 1318 |
+
const pulseIntensity = 0.6; // How much to mix highlight colors
|
| 1319 |
+
|
| 1320 |
+
function animateHighlight() {
|
| 1321 |
+
if (progress < duration) {
|
| 1322 |
+
// Calculate normalized progress (0 to 1)
|
| 1323 |
+
const t = progress / duration;
|
| 1324 |
+
|
| 1325 |
+
// Color animation - cycle through pulse colors
|
| 1326 |
+
const colorPhase = t * (pulseColors.length - 1);
|
| 1327 |
+
const colorIndex = Math.floor(colorPhase);
|
| 1328 |
+
const colorBlend = colorPhase - colorIndex;
|
| 1329 |
+
|
| 1330 |
+
if (colorIndex < pulseColors.length - 1) {
|
| 1331 |
+
const color1 = pulseColors[colorIndex];
|
| 1332 |
+
const color2 = pulseColors[colorIndex + 1];
|
| 1333 |
+
const blendedColor = color1.clone().lerp(color2, colorBlend);
|
| 1334 |
+
|
| 1335 |
+
// Mix with original color using sine wave for smooth pulse
|
| 1336 |
+
const pulseMix = Math.sin(t * Math.PI) * pulseIntensity;
|
| 1337 |
+
mesh.material.color.lerpColors(originalColor, blendedColor, pulseMix);
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
+
// Scale animation - subtle "pop" effect
|
| 1341 |
+
const scaleAmount = 1.0 + Math.sin(t * Math.PI) * 0.15; // 15% scale increase
|
| 1342 |
+
mesh.scale.copy(originalScale).multiplyScalar(scaleAmount);
|
| 1343 |
+
|
| 1344 |
+
// Increase emissive for glow effect
|
| 1345 |
+
if (mesh.material.emissive) {
|
| 1346 |
+
const emissiveIntensity = Math.sin(t * Math.PI * 2) * 0.3;
|
| 1347 |
+
mesh.material.emissiveIntensity = emissiveIntensity;
|
| 1348 |
+
}
|
| 1349 |
+
|
| 1350 |
+
progress++;
|
| 1351 |
+
requestAnimationFrame(animateHighlight);
|
| 1352 |
+
} else {
|
| 1353 |
+
// Reset to original state
|
| 1354 |
+
mesh.material.color.copy(originalColor);
|
| 1355 |
+
mesh.scale.copy(originalScale);
|
| 1356 |
+
if (mesh.material.emissive) {
|
| 1357 |
+
mesh.material.emissiveIntensity = 0;
|
| 1358 |
+
}
|
| 1359 |
+
}
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
// Enable emissive if not already set
|
| 1363 |
+
if (!mesh.material.emissive) {
|
| 1364 |
+
mesh.material.emissive = new THREE.Color(originalColor);
|
| 1365 |
+
mesh.material.emissiveIntensity = 0;
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
animateHighlight();
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
// ==================== Rendering & Lighting Handler Functions ====================
|
| 1372 |
+
|
| 1373 |
+
function addLightToScene(lightData) {
|
| 1374 |
+
console.log('💡 addLightToScene called with:', lightData);
|
| 1375 |
+
let light;
|
| 1376 |
+
|
| 1377 |
+
if (lightData.light_type === 'ambient') {
|
| 1378 |
+
light = new THREE.AmbientLight(lightData.color, lightData.intensity);
|
| 1379 |
+
} else if (lightData.light_type === 'directional') {
|
| 1380 |
+
light = new THREE.DirectionalLight(lightData.color, lightData.intensity);
|
| 1381 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 1382 |
+
if (lightData.target) {
|
| 1383 |
+
light.target.position.set(lightData.target.x, lightData.target.y, lightData.target.z);
|
| 1384 |
+
scene.add(light.target);
|
| 1385 |
+
}
|
| 1386 |
+
} else if (lightData.light_type === 'point') {
|
| 1387 |
+
light = new THREE.PointLight(lightData.color, lightData.intensity);
|
| 1388 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 1389 |
+
} else if (lightData.light_type === 'spot') {
|
| 1390 |
+
light = new THREE.SpotLight(lightData.color, lightData.intensity);
|
| 1391 |
+
light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
|
| 1392 |
+
light.angle = THREE.MathUtils.degToRad(lightData.spot_angle || 45);
|
| 1393 |
+
if (lightData.target) {
|
| 1394 |
+
light.target.position.set(lightData.target.x, lightData.target.y, lightData.target.z);
|
| 1395 |
+
scene.add(light.target);
|
| 1396 |
+
}
|
| 1397 |
+
}
|
| 1398 |
+
|
| 1399 |
+
if (light) {
|
| 1400 |
+
light.name = lightData.name;
|
| 1401 |
+
if (lightData.cast_shadow) light.castShadow = true;
|
| 1402 |
+
scene.add(light);
|
| 1403 |
+
console.log('💡 Light added to scene:', lightData.name, 'type:', lightData.light_type);
|
| 1404 |
+
} else {
|
| 1405 |
+
console.error('❌ Failed to create light:', lightData);
|
| 1406 |
+
}
|
| 1407 |
+
}
|
| 1408 |
+
|
| 1409 |
+
function removeLightFromScene(lightName) {
|
| 1410 |
+
const light = scene.getObjectByName(lightName);
|
| 1411 |
+
if (light) {
|
| 1412 |
+
scene.remove(light);
|
| 1413 |
+
console.log('Removed light:', lightName);
|
| 1414 |
+
}
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
function updateSceneLight(lightName, updates) {
|
| 1418 |
+
const light = scene.getObjectByName(lightName);
|
| 1419 |
+
if (!light) return;
|
| 1420 |
+
|
| 1421 |
+
if (updates.color) light.color.set(updates.color);
|
| 1422 |
+
if (updates.intensity !== undefined) light.intensity = updates.intensity;
|
| 1423 |
+
if (updates.position) {
|
| 1424 |
+
light.position.set(updates.position.x, updates.position.y, updates.position.z);
|
| 1425 |
+
}
|
| 1426 |
+
if (updates.cast_shadow !== undefined) light.castShadow = updates.cast_shadow;
|
| 1427 |
+
|
| 1428 |
+
console.log('Updated light:', lightName);
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
function updateObjectMaterial(objectId, materialData) {
|
| 1432 |
+
console.log('updateObjectMaterial called with:', objectId, materialData);
|
| 1433 |
+
const obj = scene.children.find(child => child.userData.id === objectId);
|
| 1434 |
+
if (!obj || !obj.material) {
|
| 1435 |
+
console.error('Object not found or has no material:', objectId);
|
| 1436 |
+
return;
|
| 1437 |
+
}
|
| 1438 |
+
|
| 1439 |
+
console.log('Found object:', obj.userData.name, 'Current color:', obj.material.color.getHexString());
|
| 1440 |
+
|
| 1441 |
+
if (materialData.color) {
|
| 1442 |
+
obj.material.color.set(materialData.color);
|
| 1443 |
+
console.log('Set color to:', materialData.color);
|
| 1444 |
+
}
|
| 1445 |
+
if (materialData.metalness !== undefined) {
|
| 1446 |
+
obj.material.metalness = materialData.metalness;
|
| 1447 |
+
console.log('Set metalness to:', materialData.metalness);
|
| 1448 |
+
}
|
| 1449 |
+
if (materialData.roughness !== undefined) {
|
| 1450 |
+
obj.material.roughness = materialData.roughness;
|
| 1451 |
+
console.log('Set roughness to:', materialData.roughness);
|
| 1452 |
+
}
|
| 1453 |
+
if (materialData.opacity !== undefined) {
|
| 1454 |
+
obj.material.opacity = materialData.opacity;
|
| 1455 |
+
obj.material.transparent = materialData.opacity < 1.0;
|
| 1456 |
+
console.log('Set opacity to:', materialData.opacity);
|
| 1457 |
+
}
|
| 1458 |
+
if (materialData.emissive) {
|
| 1459 |
+
obj.material.emissive = new THREE.Color(materialData.emissive);
|
| 1460 |
+
console.log('Set emissive to:', materialData.emissive);
|
| 1461 |
+
}
|
| 1462 |
+
if (materialData.emissive_intensity !== undefined) {
|
| 1463 |
+
obj.material.emissiveIntensity = materialData.emissive_intensity;
|
| 1464 |
+
console.log('Set emissive intensity to:', materialData.emissive_intensity);
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
obj.material.needsUpdate = true;
|
| 1468 |
+
console.log('Material updated successfully! New color:', obj.material.color.getHexString());
|
| 1469 |
+
}
|
| 1470 |
+
|
| 1471 |
+
function setSceneBackground(bgData) {
|
| 1472 |
+
if (bgData.background_type === 'gradient') {
|
| 1473 |
+
// Create gradient canvas
|
| 1474 |
+
const canvas = document.createElement('canvas');
|
| 1475 |
+
canvas.width = 2;
|
| 1476 |
+
canvas.height = 256;
|
| 1477 |
+
const ctx = canvas.getContext('2d');
|
| 1478 |
+
const gradient = ctx.createLinearGradient(0, 0, 0, 256);
|
| 1479 |
+
gradient.addColorStop(0, bgData.background_gradient_top);
|
| 1480 |
+
gradient.addColorStop(1, bgData.background_gradient_bottom);
|
| 1481 |
+
ctx.fillStyle = gradient;
|
| 1482 |
+
ctx.fillRect(0, 0, 2, 256);
|
| 1483 |
+
|
| 1484 |
+
const texture = new THREE.CanvasTexture(canvas);
|
| 1485 |
+
scene.background = texture;
|
| 1486 |
+
} else {
|
| 1487 |
+
scene.background = new THREE.Color(bgData.background_color);
|
| 1488 |
+
}
|
| 1489 |
+
|
| 1490 |
+
console.log('Updated background');
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
function setSceneFog(fogData) {
|
| 1494 |
+
if (!fogData.enabled) {
|
| 1495 |
+
scene.fog = null;
|
| 1496 |
+
console.log('Fog disabled');
|
| 1497 |
+
return;
|
| 1498 |
+
}
|
| 1499 |
+
|
| 1500 |
+
const color = new THREE.Color(fogData.color);
|
| 1501 |
+
|
| 1502 |
+
if (fogData.type === 'exponential') {
|
| 1503 |
+
scene.fog = new THREE.FogExp2(color, fogData.density);
|
| 1504 |
+
} else {
|
| 1505 |
+
scene.fog = new THREE.Fog(color, fogData.near, fogData.far);
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
console.log('Fog enabled:', fogData.type);
|
| 1509 |
+
}
|
| 1510 |
+
|
| 1511 |
+
// Start the application
|
| 1512 |
+
init();
|
| 1513 |
+
</script>
|
| 1514 |
+
</body>
|
| 1515 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Backend
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
requests
|
| 5 |
+
|
| 6 |
+
# MCP Server (official Anthropic SDK)
|
| 7 |
+
mcp>=1.0.0
|
| 8 |
+
|
| 9 |
+
# Chat Interface
|
| 10 |
+
gradio
|
| 11 |
+
|
| 12 |
+
# LLM
|
| 13 |
+
openai
|
| 14 |
+
python-dotenv
|