ArturoNereu commited on
Commit
5b29015
·
1 Parent(s): 39f3262

Added tools, and improved player movement

Browse files
README.md CHANGED
@@ -41,15 +41,28 @@ Simply describe what you want:
41
 
42
  ### 🎨 Scene Building
43
  - **6 primitive types**: cube, sphere, cylinder, plane, cone, torus
 
44
  - **Flexible positioning**: Place objects anywhere in 3D space
45
- - **Material system**: Colors, metalness, roughness, opacity
46
  - **Dynamic scaling**: Custom size for each object
47
 
48
- ### 💡 Lighting System
49
- - **4 presets**: day, night, sunset, studio
 
50
  - **Multiple light types**: ambient, directional, point, spot
 
51
  - **Automatic shadows**: Realistic lighting effects
52
 
 
 
 
 
 
 
 
 
 
 
53
  ### 🎮 FPS Controller
54
  - **Physics-based movement**: Cannon.js integration with gravity, jumping, collisions
55
  - **WASD controls**: Smooth keyboard-based movement
@@ -65,9 +78,9 @@ Simply describe what you want:
65
 
66
  ### 🤖 AI Integration
67
  - **MCP protocol**: Works with Claude, GPT, and other AI assistants
68
- - **Natural language**: Simple commands like "add a blue sphere" or "set speed to 10"
69
  - **Context aware**: Builds on existing scenes
70
- - **16 MCP tools**: Scene building (5) + Player controller (11)
71
  - **No coding required**: Pure natural language scene building
72
 
73
  ---
@@ -99,9 +112,14 @@ Open `http://localhost:7860` in your browser.
99
  ```
100
  Add a red cube at 0,2,0
101
  Add a blue sphere at 5,1,5
102
- Add a green cylinder at -3,1,0
 
 
 
 
 
 
103
  Set lighting to night
104
- Create a level 100 units wide
105
  ```
106
 
107
  ---
@@ -110,30 +128,59 @@ Create a level 100 units wide
110
 
111
  ### For AI Assistants (MCP)
112
 
113
- The MCP server exposes 16 tools that AI assistants can call:
114
-
115
- **Scene Building (5 tools):**
116
- - `create_scene_tool` - Create a new 3D scene/level
117
- - `add_object_tool` - Add objects to the scene
118
- - `remove_object_tool` - Remove objects from scene
119
- - `set_lighting_tool` - Change lighting preset
120
- - `get_scene_info_tool` - Get scene details
121
-
122
- **Player Controller Phase 1 (5 tools):**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  - `set_player_speed` - Movement speed
124
  - `set_jump_force` - Jump height
125
- - `set_mouse_sensitivity` - Mouse look sensitivity + Y-invert
126
  - `set_gravity` - World gravity
127
  - `set_player_dimensions` - Player size
128
-
129
- **Player Controller Phase 2 (4 tools):**
130
  - `set_movement_acceleration` - Movement feel
131
  - `set_air_control` - Airborne control
132
  - `set_camera_fov` - Field of view
133
  - `set_vertical_look_limits` - Look angle limits
134
-
135
- **Configuration (1 tool):**
136
- - `get_player_config` - Get all player settings
 
 
 
 
 
 
 
 
137
 
138
  ### For Developers (HTTP API)
139
 
@@ -246,25 +293,26 @@ app.py # Gradio chat interface
246
  ### ✅ Completed
247
  - **Phase 1**: Player Controller - Core Controls (5 tools)
248
  - **Phase 2**: Player Controller - Enhanced Feel (4 tools)
 
 
 
 
 
 
249
  - Physics engine integration (Cannon.js)
250
  - FPS controls (WASD + mouse look)
251
 
252
- ### 🚧 Next Phase: Rendering & Lighting Tools
253
- - Add/remove individual lights
254
- - Update light properties (color, intensity, position)
255
- - Change object materials (color, metalness, roughness)
256
- - Set background color
257
-
258
- ### 🔮 Phase 3: World Building
259
- - glTF model loading (Kenney assets)
260
- - Prefab system (props, buildings, terrain)
261
- - Scene templates
262
  - Export to Unity/Unreal
 
263
 
264
  ### 💭 Future Ideas
265
  - NPC system with behaviors
266
  - Multiplayer support
267
  - Procedural generation
 
268
 
269
  ---
270
 
 
41
 
42
  ### 🎨 Scene Building
43
  - **6 primitive types**: cube, sphere, cylinder, plane, cone, torus
44
+ - **LEGO-style bricks**: 10 brick types from Kenney kit (1x1, 2x4, slopes, etc.)
45
  - **Flexible positioning**: Place objects anywhere in 3D space
46
+ - **Material system**: Colors, metalness, roughness, opacity, toon shading
47
  - **Dynamic scaling**: Custom size for each object
48
 
49
+ ### 💡 Lighting & Environment
50
+ - **4 lighting presets**: day, night, sunset, studio
51
+ - **Procedural skybox**: Realistic sky with sun positioning
52
  - **Multiple light types**: ambient, directional, point, spot
53
+ - **Fog effects**: Linear and exponential fog
54
  - **Automatic shadows**: Realistic lighting effects
55
 
56
+ ### ✨ Particle Effects
57
+ - **5 presets**: fire, smoke, sparkle, rain, snow
58
+ - **Localized effects**: Fire, smoke at specific positions
59
+ - **Weather effects**: Rain and snow cover entire world
60
+
61
+ ### 📊 UI System
62
+ - **2D text overlay**: Render text on screen at any position
63
+ - **Progress bars**: Health bars, mana bars with labels
64
+ - **HUD elements**: Score displays, status indicators
65
+
66
  ### 🎮 FPS Controller
67
  - **Physics-based movement**: Cannon.js integration with gravity, jumping, collisions
68
  - **WASD controls**: Smooth keyboard-based movement
 
78
 
79
  ### 🤖 AI Integration
80
  - **MCP protocol**: Works with Claude, GPT, and other AI assistants
81
+ - **Natural language**: Simple commands like "add a blue sphere" or "add rain"
82
  - **Context aware**: Builds on existing scenes
83
+ - **40+ MCP tools**: Scene (6) + Player (10) + Rendering (10) + Environment (4) + UI (4) + Post-processing (8)
84
  - **No coding required**: Pure natural language scene building
85
 
86
  ---
 
112
  ```
113
  Add a red cube at 0,2,0
114
  Add a blue sphere at 5,1,5
115
+ Add a sunset skybox
116
+ Add fire particles at 0,1,0
117
+ Add rain
118
+ Render "Score: 100" at the top of the screen
119
+ Add a health bar with value 75
120
+ Add a red brick_2x4 at 0,0,0
121
+ Apply toon shading to the cube
122
  Set lighting to night
 
123
  ```
124
 
125
  ---
 
128
 
129
  ### For AI Assistants (MCP)
130
 
131
+ The MCP server exposes 40+ tools that AI assistants can call:
132
+
133
+ **Scene Building (6 tools):**
134
+ - `create_scene` - Create a new 3D scene/level
135
+ - `add_object` - Add primitive objects (cube, sphere, etc.)
136
+ - `add_brick` - Add LEGO-style bricks from Kenney kit
137
+ - `remove_object` - Remove objects from scene
138
+ - `set_lighting` - Change lighting preset
139
+ - `get_scene_info` - Get scene details
140
+
141
+ **Environment (4 tools):**
142
+ - `add_skybox` - Add procedural sky (day, sunset, night, etc.)
143
+ - `remove_skybox` - Remove skybox
144
+ - `add_particles` - Add particle effects (fire, smoke, rain, snow)
145
+ - `remove_particles` - Remove particle systems
146
+
147
+ **UI Overlay (4 tools):**
148
+ - `render_text_on_screen` - Display 2D text
149
+ - `render_bar_on_screen` - Display health/progress bars
150
+ - `remove_ui_element` - Remove UI elements
151
+ - `get_ui_elements` - List all UI elements
152
+
153
+ **Rendering (10 tools):**
154
+ - `add_light` - Add light sources
155
+ - `remove_light` - Remove lights
156
+ - `update_light` - Modify light properties
157
+ - `get_lights` - List all lights
158
+ - `update_object_material` - Change material properties
159
+ - `update_material_to_toon` - Apply toon/cel shading
160
+ - `set_background_color` - Set solid or gradient background
161
+ - `set_fog` - Add atmospheric fog
162
+
163
+ **Player Controller (10 tools):**
164
  - `set_player_speed` - Movement speed
165
  - `set_jump_force` - Jump height
166
+ - `set_mouse_sensitivity` - Mouse look + Y-invert
167
  - `set_gravity` - World gravity
168
  - `set_player_dimensions` - Player size
 
 
169
  - `set_movement_acceleration` - Movement feel
170
  - `set_air_control` - Airborne control
171
  - `set_camera_fov` - Field of view
172
  - `set_vertical_look_limits` - Look angle limits
173
+ - `get_player_config` - Get all settings
174
+
175
+ **Post-Processing (8 tools):**
176
+ - `set_bloom` - Glow effect
177
+ - `set_ssao` - Ambient occlusion
178
+ - `set_color_grading` - Color adjustments
179
+ - `set_vignette` - Vignette effect
180
+ - `set_depth_of_field` - Focus blur
181
+ - `set_motion_blur` - Movement blur
182
+ - `set_chromatic_aberration` - Lens effect
183
+ - `get_post_processing` - Get all effects
184
 
185
  ### For Developers (HTTP API)
186
 
 
293
  ### ✅ Completed
294
  - **Phase 1**: Player Controller - Core Controls (5 tools)
295
  - **Phase 2**: Player Controller - Enhanced Feel (4 tools)
296
+ - **Phase 3**: Rendering & Lighting Tools (10 tools)
297
+ - **Phase 4**: Post-Processing Effects (8 tools)
298
+ - **Phase 5**: Environment Tools - Skybox & Particles (4 tools)
299
+ - **Phase 6**: UI Overlay System (4 tools)
300
+ - **Phase 7**: LEGO Brick Kit Integration (Kenney assets)
301
+ - **Phase 8**: Toon/Cel Shading Materials
302
  - Physics engine integration (Cannon.js)
303
  - FPS controls (WASD + mouse look)
304
 
305
+ ### 🔮 Next Steps
306
+ - Transform controls for object manipulation
307
+ - Scene templates and prefabs
 
 
 
 
 
 
 
308
  - Export to Unity/Unreal
309
+ - Persistent scene storage
310
 
311
  ### 💭 Future Ideas
312
  - NPC system with behaviors
313
  - Multiplayer support
314
  - Procedural generation
315
+ - Audio system
316
 
317
  ---
318
 
app.py CHANGED
@@ -421,49 +421,70 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
421
  # Tools panel - full width below chat and viewer
422
  with gr.Row(elem_id="tools-panel"):
423
  gr.Markdown("""
424
- ### GCP Available Tools
425
 
426
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 10px;">
427
 
428
  <div>
429
 
430
  **🎬 Scene Tools**
431
- - `create_scene` - Create a new 3D scene
432
- - `add_object` - Add object (cube, sphere, cylinder, cone, torus, plane)
433
- - `remove_object` - Remove an object by ID
434
- - `set_lighting` - Set lighting preset (day, night, sunset, studio)
435
- - `get_scene_info` - Get current scene information
 
 
 
 
 
 
 
 
 
 
436
 
437
  </div>
438
 
439
  <div>
440
 
441
  **🎮 Player Tools**
442
- - `set_player_speed` - Set movement speed
443
- - `set_jump_force` - Set jump strength
444
- - `set_mouse_sensitivity` - Set look sensitivity
445
- - `set_gravity` - Set world gravity
446
- - `set_player_dimensions` - Set player height/radius
447
- - `set_camera_fov` - Set field of view
448
- - `get_player_config` - Get current player settings
449
 
450
  </div>
451
 
452
  <div>
453
 
454
- **💡 Lighting Tools**
455
  - `add_light` - Add light (ambient, directional, point, spot)
456
- - `remove_light` - Remove a light by name
457
- - `get_lights` - List all lights in scene
 
 
 
 
 
 
 
 
 
 
 
458
 
459
  </div>
460
 
461
  <div>
462
 
463
- **🎨 Material & Environment**
464
- - `update_object_material` - Change object color, metalness, roughness
465
- - `set_background_color` - Set background color or gradient
466
- - `set_fog` - Add/configure fog effect
 
467
 
468
  </div>
469
 
@@ -517,7 +538,15 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
517
  "setBackground", "setFog",
518
  # Player tools
519
  "setPlayerSpeed", "setJumpForce", "setGravity",
520
- "setCameraFov", "setMouseSensitivity", "setPlayerDimensions"]:
 
 
 
 
 
 
 
 
521
  # Build action JSON for the JavaScript watcher
522
  import json
523
  import time
@@ -566,6 +595,34 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
566
  elif action_type == "setPlayerDimensions":
567
  height = action_result["data"].get("height", 1.7)
568
  toast_message = f"Player height: {height}m"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  # Create JSON payload for the .then() JavaScript handler
571
  # Include timestamp to ensure Gradio detects change even for repeated actions
 
421
  # Tools panel - full width below chat and viewer
422
  with gr.Row(elem_id="tools-panel"):
423
  gr.Markdown("""
424
+ ### GCP Available Tools (40+)
425
 
426
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 15px; margin-top: 10px; font-size: 0.9em;">
427
 
428
  <div>
429
 
430
  **🎬 Scene Tools**
431
+ - `add_object` - Add primitives (cube, sphere, cylinder, cone, torus, plane)
432
+ - `add_brick` - Add LEGO bricks (1x1, 2x4, slopes, plates)
433
+ - `remove_object` - Remove an object
434
+ - `set_lighting` - Presets: day, night, sunset, studio
435
+ - `get_scene_info` - Get scene details
436
+
437
+ </div>
438
+
439
+ <div>
440
+
441
+ **🌅 Environment**
442
+ - `add_skybox` - Add sky (day, sunset, night, dawn)
443
+ - `remove_skybox` - Remove skybox
444
+ - `add_particles` - Effects: fire, smoke, rain, snow, sparkle
445
+ - `remove_particles` - Remove particles
446
 
447
  </div>
448
 
449
  <div>
450
 
451
  **🎮 Player Tools**
452
+ - `set_player_speed` - Movement speed
453
+ - `set_jump_force` - Jump strength
454
+ - `set_mouse_sensitivity` - Look sensitivity
455
+ - `set_gravity` - World gravity
456
+ - `set_camera_fov` - Field of view
457
+ - `get_player_config` - Get settings
 
458
 
459
  </div>
460
 
461
  <div>
462
 
463
+ **💡 Lighting**
464
  - `add_light` - Add light (ambient, directional, point, spot)
465
+ - `remove_light` - Remove light
466
+ - `update_light` - Modify light properties
467
+ - `get_lights` - List all lights
468
+
469
+ </div>
470
+
471
+ <div>
472
+
473
+ **🎨 Materials**
474
+ - `update_object_material` - Color, metalness, roughness, opacity
475
+ - `update_material_to_toon` - Apply cel-shading
476
+ - `set_background_color` - Solid or gradient
477
+ - `set_fog` - Atmospheric fog
478
 
479
  </div>
480
 
481
  <div>
482
 
483
+ **📊 UI Overlay**
484
+ - `render_text_on_screen` - 2D text at any position
485
+ - `render_bar_on_screen` - Health/progress bars
486
+ - `remove_ui_element` - Remove UI element
487
+ - `get_ui_elements` - List UI elements
488
 
489
  </div>
490
 
 
538
  "setBackground", "setFog",
539
  # Player tools
540
  "setPlayerSpeed", "setJumpForce", "setGravity",
541
+ "setCameraFov", "setMouseSensitivity", "setPlayerDimensions",
542
+ # Environment tools
543
+ "addSkybox", "removeSkybox", "addParticles", "removeParticles",
544
+ # UI tools
545
+ "renderText", "renderBar", "removeUIElement",
546
+ # Toon shading
547
+ "updateToonMaterial",
548
+ # Brick blocks
549
+ "addBrick"]:
550
  # Build action JSON for the JavaScript watcher
551
  import json
552
  import time
 
595
  elif action_type == "setPlayerDimensions":
596
  height = action_result["data"].get("height", 1.7)
597
  toast_message = f"Player height: {height}m"
598
+ # Environment tool toast messages
599
+ elif action_type == "addSkybox":
600
+ preset = action_result["data"].get("preset", "custom")
601
+ toast_message = f"Skybox added: {preset}"
602
+ elif action_type == "removeSkybox":
603
+ toast_message = "Skybox removed"
604
+ elif action_type == "addParticles":
605
+ preset = action_result["data"].get("preset", "effect")
606
+ toast_message = f"Particles added: {preset}"
607
+ elif action_type == "removeParticles":
608
+ toast_message = "Particles removed"
609
+ # UI tool toast messages
610
+ elif action_type == "renderText":
611
+ text = action_result["data"].get("text", "")[:20]
612
+ toast_message = f"Text rendered: {text}..."
613
+ elif action_type == "renderBar":
614
+ label = action_result["data"].get("label", "Bar")
615
+ toast_message = f"Bar rendered: {label}"
616
+ elif action_type == "removeUIElement":
617
+ toast_message = "UI element removed"
618
+ # Toon shading toast
619
+ elif action_type == "updateToonMaterial":
620
+ enabled = action_result["data"].get("enabled", True)
621
+ toast_message = "Toon shading " + ("enabled" if enabled else "disabled")
622
+ # Brick toast
623
+ elif action_type == "addBrick":
624
+ brick_type = action_result["data"].get("brick_type", "brick")
625
+ toast_message = f"Added {brick_type.replace('_', ' ')}"
626
 
627
  # Create JSON payload for the .then() JavaScript handler
628
  # Include timestamp to ensure Gradio detects change even for repeated actions
backend/game_models.py CHANGED
@@ -127,7 +127,7 @@ def create_player(
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,
@@ -136,7 +136,7 @@ def create_player_config(
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,
 
127
 
128
 
129
  def create_player_config(
130
+ move_speed: float = 8.0,
131
  jump_force: float = 5.0,
132
  mouse_sensitivity: float = 0.002,
133
  invert_y: bool = False,
 
136
  player_radius: float = 0.3,
137
  eye_height: float = 1.6,
138
  player_mass: float = 80.0,
139
+ linear_damping: float = 0.0,
140
  movement_acceleration: float = 0.0,
141
  air_control: float = 1.0,
142
  camera_fov: float = 75.0,
backend/main.py CHANGED
@@ -4,12 +4,17 @@ 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",
@@ -26,6 +31,11 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
 
 
 
 
 
29
 
30
  # =============================================================================
31
  # HTTP Endpoints (for viewer and health checks)
 
4
  MCP tools are defined in mcp_server.py
5
  """
6
  import os
7
+ from pathlib import Path
8
  from fastapi import FastAPI, HTTPException
9
  from fastapi.responses import HTMLResponse, JSONResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.staticfiles import StaticFiles
12
 
13
  from backend.storage import storage
14
 
15
+ # Get project root directory
16
+ PROJECT_ROOT = Path(__file__).parent.parent
17
+
18
  # Create FastAPI app
19
  app = FastAPI(
20
  title="GCP - Game Context Protocol",
 
31
  allow_headers=["*"],
32
  )
33
 
34
+ # Mount static files for models
35
+ models_path = PROJECT_ROOT / "models"
36
+ if models_path.exists():
37
+ app.mount("/static/models", StaticFiles(directory=str(models_path)), name="models")
38
+
39
 
40
  # =============================================================================
41
  # HTTP Endpoints (for viewer and health checks)
backend/mcp_server.py CHANGED
@@ -16,6 +16,8 @@ from backend.tools.scene_tools import (
16
  remove_game_object,
17
  set_scene_lighting,
18
  get_scene_info,
 
 
19
  )
20
  from backend.tools.player_tools import (
21
  set_player_speed,
@@ -48,6 +50,20 @@ from backend.tools.rendering_tools import (
48
  set_motion_blur,
49
  set_chromatic_aberration,
50
  get_camera_effects,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  )
52
  from backend.game_models import create_vector3, create_material
53
 
@@ -186,6 +202,53 @@ Returns: scene details including name, objects, lights, and viewer_url""",
186
  "required": ["scene_id"],
187
  },
188
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  ]
190
 
191
  PLAYER_TOOLS = [
@@ -817,8 +880,249 @@ Returns: All camera effects settings (depth of field, motion blur, chromatic abe
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
  # =============================================================================
@@ -897,6 +1201,17 @@ async def _execute_tool(name: str, args: dict) -> Any:
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(
@@ -1094,6 +1409,86 @@ async def _execute_tool(name: str, args: dict) -> Any:
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
 
 
16
  remove_game_object,
17
  set_scene_lighting,
18
  get_scene_info,
19
+ add_brick,
20
+ BRICK_TYPES,
21
  )
22
  from backend.tools.player_tools import (
23
  set_player_speed,
 
50
  set_motion_blur,
51
  set_chromatic_aberration,
52
  get_camera_effects,
53
+ # Toon shading
54
+ update_material_to_toon,
55
+ )
56
+ from backend.tools.environment_tools import (
57
+ add_skybox,
58
+ remove_skybox,
59
+ add_particles,
60
+ remove_particles,
61
+ )
62
+ from backend.tools.ui_tools import (
63
+ render_text_on_screen,
64
+ render_bar_on_screen,
65
+ remove_ui_element,
66
+ get_ui_elements,
67
  )
68
  from backend.game_models import create_vector3, create_material
69
 
 
202
  "required": ["scene_id"],
203
  },
204
  ),
205
+ Tool(
206
+ name="add_brick",
207
+ description="""Add a LEGO-style brick from the Kenney brick kit.
208
+
209
+ Args:
210
+ scene_id: ID of the scene (required)
211
+ brick_type: Type of brick (required) - brick_1x1, brick_1x2, brick_1x4, brick_2x2, brick_2x4,
212
+ plate_1x2, plate_2x2, plate_2x4, plate_4x4, slope_2x2
213
+ position: {x, y, z} position
214
+ rotation: {x, y, z} rotation in degrees
215
+ color: Hex color code (default: #ff0000 red)
216
+ name: Optional name for the brick
217
+
218
+ Examples:
219
+ add_brick(scene_id, "brick_2x4", position={x:0, y:0, z:0}, color="#ff0000")
220
+ add_brick(scene_id, "slope_2x2", position={x:1, y:0.5, z:0}, color="#0000ff")""",
221
+ inputSchema={
222
+ "type": "object",
223
+ "properties": {
224
+ "scene_id": {"type": "string"},
225
+ "brick_type": {
226
+ "type": "string",
227
+ "enum": ["brick_1x1", "brick_1x2", "brick_1x4", "brick_2x2", "brick_2x4",
228
+ "plate_1x2", "plate_2x2", "plate_2x4", "plate_4x4", "slope_2x2"]
229
+ },
230
+ "position": {
231
+ "type": "object",
232
+ "properties": {
233
+ "x": {"type": "number"},
234
+ "y": {"type": "number"},
235
+ "z": {"type": "number"},
236
+ }
237
+ },
238
+ "rotation": {
239
+ "type": "object",
240
+ "properties": {
241
+ "x": {"type": "number"},
242
+ "y": {"type": "number"},
243
+ "z": {"type": "number"},
244
+ }
245
+ },
246
+ "color": {"type": "string", "default": "#ff0000"},
247
+ "name": {"type": "string"},
248
+ },
249
+ "required": ["scene_id", "brick_type"],
250
+ },
251
+ ),
252
  ]
253
 
254
  PLAYER_TOOLS = [
 
880
  ),
881
  ]
882
 
883
+ ENVIRONMENT_TOOLS = [
884
+ Tool(
885
+ name="add_skybox",
886
+ description="""Add a procedural sky to the scene.
887
+
888
+ Creates realistic outdoor environments with atmospheric scattering.
889
+
890
+ Args:
891
+ scene_id: Scene to modify (required)
892
+ preset: Quick preset - "day", "sunset", "noon", "dawn", "night" (default: "day")
893
+ turbidity: Haziness 2.0=clear, 10.0=hazy, 20.0=foggy (default: 10.0)
894
+ rayleigh: Blue sky intensity 0.0-4.0 (default: 2.0)
895
+ sun_elevation: Sun angle from horizon -90 to 90 degrees (default: 45)
896
+ sun_azimuth: Sun compass direction 0-360 degrees (default: 180)
897
+
898
+ Examples:
899
+ add_skybox(scene_id, preset="sunset")
900
+ add_skybox(scene_id, sun_elevation=5, turbidity=4)""",
901
+ inputSchema={
902
+ "type": "object",
903
+ "properties": {
904
+ "scene_id": {"type": "string"},
905
+ "preset": {
906
+ "type": "string",
907
+ "enum": ["day", "sunset", "noon", "dawn", "night"],
908
+ "default": "day"
909
+ },
910
+ "turbidity": {"type": "number", "default": 10.0},
911
+ "rayleigh": {"type": "number", "default": 2.0},
912
+ "sun_elevation": {"type": "number", "default": 45.0},
913
+ "sun_azimuth": {"type": "number", "default": 180.0},
914
+ },
915
+ "required": ["scene_id"],
916
+ },
917
+ ),
918
+ Tool(
919
+ name="remove_skybox",
920
+ description="""Remove the skybox from the scene.
921
+
922
+ Reverts to solid background color.
923
+
924
+ Args:
925
+ scene_id: Scene to modify (required)""",
926
+ inputSchema={
927
+ "type": "object",
928
+ "properties": {
929
+ "scene_id": {"type": "string"},
930
+ },
931
+ "required": ["scene_id"],
932
+ },
933
+ ),
934
+ Tool(
935
+ name="add_particles",
936
+ description="""Add a particle effect to the scene.
937
+
938
+ Use presets for quick setup of common effects.
939
+
940
+ Args:
941
+ scene_id: Scene to modify (required)
942
+ preset: Effect type - "fire", "smoke", "sparkle", "rain", "snow" (required)
943
+ position: {x, y, z} for localized effects (fire, smoke, sparkle)
944
+ particle_id: Optional unique identifier
945
+
946
+ Examples:
947
+ add_particles(scene_id, "fire", position={x:0, y:1, z:0})
948
+ add_particles(scene_id, "rain") # Weather covers entire world""",
949
+ inputSchema={
950
+ "type": "object",
951
+ "properties": {
952
+ "scene_id": {"type": "string"},
953
+ "preset": {
954
+ "type": "string",
955
+ "enum": ["fire", "smoke", "sparkle", "rain", "snow"]
956
+ },
957
+ "position": {
958
+ "type": "object",
959
+ "properties": {
960
+ "x": {"type": "number"},
961
+ "y": {"type": "number"},
962
+ "z": {"type": "number"},
963
+ }
964
+ },
965
+ "particle_id": {"type": "string"},
966
+ },
967
+ "required": ["scene_id", "preset"],
968
+ },
969
+ ),
970
+ Tool(
971
+ name="remove_particles",
972
+ description="""Remove a particle system from the scene.
973
+
974
+ Args:
975
+ scene_id: Scene to modify (required)
976
+ particle_id: ID of the particle system to remove (required)""",
977
+ inputSchema={
978
+ "type": "object",
979
+ "properties": {
980
+ "scene_id": {"type": "string"},
981
+ "particle_id": {"type": "string"},
982
+ },
983
+ "required": ["scene_id", "particle_id"],
984
+ },
985
+ ),
986
+ ]
987
+
988
+ UI_TOOLS = [
989
+ Tool(
990
+ name="render_text_on_screen",
991
+ description="""Render text on the screen as a 2D overlay.
992
+
993
+ Args:
994
+ scene_id: Scene to modify (required)
995
+ text: The text to display (required)
996
+ x: Horizontal position in % (0=left, 50=center, 100=right, default: 50)
997
+ y: Vertical position in % (0=top, 50=center, 100=bottom, default: 10)
998
+ font_size: Font size in pixels (default: 24)
999
+ color: Text color hex (default: "#ffffff")
1000
+ text_id: Optional unique ID for updates/removal
1001
+ background_color: Optional background color for text box
1002
+
1003
+ Examples:
1004
+ render_text_on_screen(scene_id, "Score: 100", x=10, y=5)
1005
+ render_text_on_screen(scene_id, "Game Over", x=50, y=50, font_size=48)""",
1006
+ inputSchema={
1007
+ "type": "object",
1008
+ "properties": {
1009
+ "scene_id": {"type": "string"},
1010
+ "text": {"type": "string"},
1011
+ "x": {"type": "number", "default": 50.0},
1012
+ "y": {"type": "number", "default": 10.0},
1013
+ "font_size": {"type": "integer", "default": 24},
1014
+ "color": {"type": "string", "default": "#ffffff"},
1015
+ "text_id": {"type": "string"},
1016
+ "font_family": {"type": "string", "default": "Arial"},
1017
+ "text_align": {
1018
+ "type": "string",
1019
+ "enum": ["left", "center", "right"],
1020
+ "default": "center"
1021
+ },
1022
+ "background_color": {"type": "string"},
1023
+ "padding": {"type": "integer", "default": 8},
1024
+ },
1025
+ "required": ["scene_id", "text"],
1026
+ },
1027
+ ),
1028
+ Tool(
1029
+ name="render_bar_on_screen",
1030
+ description="""Render a progress/health bar on the screen.
1031
+
1032
+ Args:
1033
+ scene_id: Scene to modify (required)
1034
+ x: Horizontal position in % (default: 10)
1035
+ y: Vertical position in % (default: 10)
1036
+ width: Bar width in pixels (default: 200)
1037
+ height: Bar height in pixels (default: 20)
1038
+ value: Current value (default: 100)
1039
+ max_value: Maximum value (default: 100)
1040
+ bar_color: Fill color (default: "#00ff00" green)
1041
+ background_color: Background color (default: "#333333")
1042
+ bar_id: Optional unique ID for updates/removal
1043
+ label: Optional label above the bar
1044
+ show_value: Show numeric value on bar (default: false)
1045
+
1046
+ Examples:
1047
+ render_bar_on_screen(scene_id, value=75, bar_color="#ff0000", label="Health")
1048
+ render_bar_on_screen(scene_id, x=10, y=90, value=50, bar_color="#0088ff", label="Mana")""",
1049
+ inputSchema={
1050
+ "type": "object",
1051
+ "properties": {
1052
+ "scene_id": {"type": "string"},
1053
+ "x": {"type": "number", "default": 10.0},
1054
+ "y": {"type": "number", "default": 10.0},
1055
+ "width": {"type": "number", "default": 200.0},
1056
+ "height": {"type": "number", "default": 20.0},
1057
+ "value": {"type": "number", "default": 100.0},
1058
+ "max_value": {"type": "number", "default": 100.0},
1059
+ "bar_color": {"type": "string", "default": "#00ff00"},
1060
+ "background_color": {"type": "string", "default": "#333333"},
1061
+ "border_color": {"type": "string", "default": "#ffffff"},
1062
+ "bar_id": {"type": "string"},
1063
+ "label": {"type": "string"},
1064
+ "show_value": {"type": "boolean", "default": False},
1065
+ },
1066
+ "required": ["scene_id"],
1067
+ },
1068
+ ),
1069
+ Tool(
1070
+ name="remove_ui_element",
1071
+ description="""Remove a UI element from the screen.
1072
+
1073
+ Args:
1074
+ scene_id: Scene to modify (required)
1075
+ element_id: ID of the text or bar to remove (required)""",
1076
+ inputSchema={
1077
+ "type": "object",
1078
+ "properties": {
1079
+ "scene_id": {"type": "string"},
1080
+ "element_id": {"type": "string"},
1081
+ },
1082
+ "required": ["scene_id", "element_id"],
1083
+ },
1084
+ ),
1085
+ ]
1086
+
1087
+ TOON_TOOLS = [
1088
+ Tool(
1089
+ name="update_material_to_toon",
1090
+ description="""Apply toon/cel-shading to an object for cartoon-like appearance.
1091
+
1092
+ Creates discrete shading bands instead of smooth gradients, common in anime styles.
1093
+
1094
+ Args:
1095
+ scene_id: Scene to modify (required)
1096
+ object_id: Object to update (required)
1097
+ enabled: Enable/disable toon shading (default: true)
1098
+ color: Base color (optional, keeps existing)
1099
+ gradient_steps: Shading steps 2=hard, 3=medium, 5=soft (default: 3)
1100
+ outline: Add black outline effect (default: true)
1101
+ outline_color: Outline color (default: "#000000")
1102
+ outline_thickness: Outline thickness 0.01-0.1 (default: 0.03)
1103
+
1104
+ Examples:
1105
+ update_material_to_toon(scene_id, object_id, gradient_steps=2)
1106
+ update_material_to_toon(scene_id, object_id, enabled=False) # Revert""",
1107
+ inputSchema={
1108
+ "type": "object",
1109
+ "properties": {
1110
+ "scene_id": {"type": "string"},
1111
+ "object_id": {"type": "string"},
1112
+ "enabled": {"type": "boolean", "default": True},
1113
+ "color": {"type": "string"},
1114
+ "gradient_steps": {"type": "integer", "default": 3},
1115
+ "outline": {"type": "boolean", "default": True},
1116
+ "outline_color": {"type": "string", "default": "#000000"},
1117
+ "outline_thickness": {"type": "number", "default": 0.03},
1118
+ },
1119
+ "required": ["scene_id", "object_id"],
1120
+ },
1121
+ ),
1122
+ ]
1123
+
1124
  # Combine all tools
1125
+ ALL_TOOLS = SCENE_TOOLS + PLAYER_TOOLS + RENDERING_TOOLS + POST_PROCESSING_TOOLS + CAMERA_EFFECTS_TOOLS + ENVIRONMENT_TOOLS + UI_TOOLS + TOON_TOOLS
1126
 
1127
 
1128
  # =============================================================================
 
1201
  elif name == "get_scene_info":
1202
  return get_scene_info(args["scene_id"], BASE_URL)
1203
 
1204
+ elif name == "add_brick":
1205
+ return add_brick(
1206
+ args["scene_id"],
1207
+ args["brick_type"],
1208
+ args.get("position"),
1209
+ args.get("rotation"),
1210
+ args.get("color", "#ff0000"),
1211
+ args.get("name"),
1212
+ BASE_URL
1213
+ )
1214
+
1215
  # Player tools
1216
  elif name == "set_player_speed":
1217
  return set_player_speed(
 
1409
  elif name == "get_camera_effects":
1410
  return get_camera_effects(args["scene_id"])
1411
 
1412
+ # Environment tools (skybox, particles)
1413
+ elif name == "add_skybox":
1414
+ return add_skybox(
1415
+ args["scene_id"],
1416
+ args.get("preset", "day"),
1417
+ args.get("turbidity", 10.0),
1418
+ args.get("rayleigh", 2.0),
1419
+ args.get("sun_elevation", 45.0),
1420
+ args.get("sun_azimuth", 180.0)
1421
+ )
1422
+
1423
+ elif name == "remove_skybox":
1424
+ return remove_skybox(args["scene_id"])
1425
+
1426
+ elif name == "add_particles":
1427
+ return add_particles(
1428
+ args["scene_id"],
1429
+ args["preset"],
1430
+ args.get("position"),
1431
+ args.get("particle_id")
1432
+ )
1433
+
1434
+ elif name == "remove_particles":
1435
+ return remove_particles(
1436
+ args["scene_id"],
1437
+ args["particle_id"]
1438
+ )
1439
+
1440
+ # UI tools
1441
+ elif name == "render_text_on_screen":
1442
+ return render_text_on_screen(
1443
+ args["scene_id"],
1444
+ args["text"],
1445
+ args.get("x", 50.0),
1446
+ args.get("y", 10.0),
1447
+ args.get("font_size", 24),
1448
+ args.get("color", "#ffffff"),
1449
+ args.get("text_id"),
1450
+ args.get("font_family", "Arial"),
1451
+ args.get("text_align", "center"),
1452
+ args.get("background_color"),
1453
+ args.get("padding", 8)
1454
+ )
1455
+
1456
+ elif name == "render_bar_on_screen":
1457
+ return render_bar_on_screen(
1458
+ args["scene_id"],
1459
+ args.get("x", 10.0),
1460
+ args.get("y", 10.0),
1461
+ args.get("width", 200.0),
1462
+ args.get("height", 20.0),
1463
+ args.get("value", 100.0),
1464
+ args.get("max_value", 100.0),
1465
+ args.get("bar_color", "#00ff00"),
1466
+ args.get("background_color", "#333333"),
1467
+ args.get("border_color", "#ffffff"),
1468
+ args.get("bar_id"),
1469
+ args.get("label"),
1470
+ args.get("show_value", False)
1471
+ )
1472
+
1473
+ elif name == "remove_ui_element":
1474
+ return remove_ui_element(
1475
+ args["scene_id"],
1476
+ args["element_id"]
1477
+ )
1478
+
1479
+ # Toon shading tools
1480
+ elif name == "update_material_to_toon":
1481
+ return update_material_to_toon(
1482
+ args["scene_id"],
1483
+ args["object_id"],
1484
+ args.get("enabled", True),
1485
+ args.get("color"),
1486
+ args.get("gradient_steps", 3),
1487
+ args.get("outline", True),
1488
+ args.get("outline_color", "#000000"),
1489
+ args.get("outline_thickness", 0.03)
1490
+ )
1491
+
1492
  else:
1493
  raise ValueError(f"Unknown tool: {name}")
1494
 
backend/storage.py CHANGED
@@ -49,39 +49,89 @@ class Storage:
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 25x25 FPS world with physics - Ready to explore!",
82
  world_width=25,
83
  world_height=10,
84
  world_depth=25,
 
85
  lights=lights,
86
  environment=env,
87
  )
@@ -89,12 +139,21 @@ def initialize_default_scene():
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" - 25x25 world with FPS physics controller")
96
- print(f" - Ground plane + invisible boundary walls")
97
- print(f" - Player spawn at (0, 1, 0)")
98
 
99
 
100
  # Initialize on module load
 
49
  storage = Storage()
50
 
51
 
52
+ # Initialize with an impressive default Welcome Scene for HuggingFace Spaces
53
  def initialize_default_scene():
54
+ """Create a visually impressive Welcome Scene on startup"""
55
+ from backend.game_models import (
56
+ create_scene, create_light, create_environment, create_vector3,
57
+ create_game_object, create_material
58
+ )
59
 
60
+ # Create lights for sunset preset (warm, dramatic lighting)
61
  lights = [
62
  create_light(
63
  name="Sun",
64
  light_type="directional",
65
+ color="#ff9944", # Warm sunset orange
66
+ intensity=1.2,
67
+ position=create_vector3(50, 30, 50),
68
  ),
69
  create_light(
70
  name="Ambient",
71
  light_type="ambient",
72
+ color="#ffcc88", # Warm ambient
73
+ intensity=0.4,
74
  ),
75
  ]
76
 
77
+ # Create environment with sunset preset
78
+ env = create_environment(
79
+ lighting_preset="sunset",
80
+ background_color="#1a0a20", # Dark purple (will be overridden by skybox)
81
+ )
82
+
83
+ # Create some visually interesting starter objects
84
+ objects = [
85
+ # Red cube - classic demo object
86
+ create_game_object(
87
+ object_type="cube",
88
+ name="RedCube",
89
+ position=create_vector3(3, 1, -3),
90
+ scale=create_vector3(2, 2, 2),
91
+ material=create_material(color="#ff4444", metalness=0.3, roughness=0.4),
92
+ ),
93
+ # Blue sphere - metallic
94
+ create_game_object(
95
+ object_type="sphere",
96
+ name="BlueSphere",
97
+ position=create_vector3(-4, 1.5, -2),
98
+ scale=create_vector3(3, 3, 3),
99
+ material=create_material(color="#4488ff", metalness=0.8, roughness=0.2),
100
+ ),
101
+ # Green cylinder
102
+ create_game_object(
103
+ object_type="cylinder",
104
+ name="GreenCylinder",
105
+ position=create_vector3(0, 1.5, -6),
106
+ scale=create_vector3(1.5, 3, 1.5),
107
+ material=create_material(color="#44ff44", metalness=0.2, roughness=0.6),
108
+ ),
109
+ # Yellow torus
110
+ create_game_object(
111
+ object_type="torus",
112
+ name="YellowTorus",
113
+ position=create_vector3(-3, 2, -7),
114
+ scale=create_vector3(2, 2, 2),
115
+ material=create_material(color="#ffcc00", metalness=0.6, roughness=0.3),
116
+ ),
117
+ # Purple cone
118
+ create_game_object(
119
+ object_type="cone",
120
+ name="PurpleCone",
121
+ position=create_vector3(5, 1, -5),
122
+ scale=create_vector3(1.5, 3, 1.5),
123
+ material=create_material(color="#aa44ff", metalness=0.4, roughness=0.5),
124
+ ),
125
+ ]
126
 
127
+ # Create scene with starter objects
 
128
  scene = create_scene(
129
+ name="Welcome to GCP",
130
+ description="AI-powered 3D scene builder - Try 'add a red sphere' or 'set lighting to night'",
131
  world_width=25,
132
  world_height=10,
133
  world_depth=25,
134
+ objects=objects,
135
  lights=lights,
136
  environment=env,
137
  )
 
139
  # Use a predictable scene ID for easy linking
140
  scene["scene_id"] = "welcome"
141
 
142
+ # Add skybox configuration (sunset preset)
143
+ scene["skybox"] = {
144
+ "preset": "sunset",
145
+ "turbidity": 8,
146
+ "rayleigh": 2,
147
+ "elevation": 5,
148
+ "azimuth": 180
149
+ }
150
+
151
  # Save to storage
152
  storage.save(scene)
153
+ print(f"✓ Initialized Welcome Scene (ID: welcome)")
154
+ print(f" - 25x25 world with sunset skybox")
155
+ print(f" - 5 starter objects (cube, sphere, cylinder, torus, cone)")
156
+ print(f" - FPS physics controller ready")
157
 
158
 
159
  # Initialize on module load
backend/tools/rendering_tools.py CHANGED
@@ -846,3 +846,84 @@ def get_camera_effects(scene_id: str) -> Dict[str, Any]:
846
  "scene_id": scene_id,
847
  "camera_effects": camera_effects
848
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  "scene_id": scene_id,
847
  "camera_effects": camera_effects
848
  }
849
+
850
+
851
+ # =============================================================================
852
+ # Toon/Cel Shading Tools
853
+ # =============================================================================
854
+
855
+ def update_material_to_toon(
856
+ scene_id: str,
857
+ object_id: str,
858
+ enabled: bool = True,
859
+ color: Optional[str] = None,
860
+ gradient_steps: int = 3,
861
+ outline: bool = True,
862
+ outline_color: str = "#000000",
863
+ outline_thickness: float = 0.03
864
+ ) -> Dict[str, Any]:
865
+ """
866
+ Convert an object's material to toon/cel-shaded style.
867
+
868
+ Toon shading creates a cartoon-like appearance with discrete shading bands
869
+ instead of smooth gradients, commonly used in anime and cel-animation styles.
870
+
871
+ Args:
872
+ scene_id: ID of the scene
873
+ object_id: ID of the object to update
874
+ enabled: Enable/disable toon shading (True to apply, False to revert to standard)
875
+ color: Base color for toon material (optional, keeps existing if not set)
876
+ gradient_steps: Number of shading steps (2=hard, 3=medium, 5=soft, default: 3)
877
+ outline: Add black outline effect (default: True)
878
+ outline_color: Color of the outline (default: "#000000")
879
+ outline_thickness: Thickness of outline (0.01-0.1, default: 0.03)
880
+
881
+ Returns:
882
+ Dictionary with updated material info and message
883
+ """
884
+ scene = storage.get(scene_id)
885
+ if not scene:
886
+ raise ValueError(f"Scene '{scene_id}' not found")
887
+
888
+ if "objects" not in scene or not scene["objects"]:
889
+ raise ValueError("Scene has no objects")
890
+
891
+ # Find object
892
+ obj = None
893
+ for o in scene["objects"]:
894
+ if o.get("id") == object_id or o.get("object_id") == object_id:
895
+ obj = o
896
+ break
897
+
898
+ if not obj:
899
+ raise ValueError(f"Object '{object_id}' not found in scene")
900
+
901
+ # Ensure material exists
902
+ if "material" not in obj:
903
+ obj["material"] = {}
904
+
905
+ # Update toon properties
906
+ if enabled:
907
+ obj["material"]["toon"] = {
908
+ "enabled": True,
909
+ "gradient_steps": max(2, min(10, gradient_steps)),
910
+ "outline": outline,
911
+ "outline_color": outline_color,
912
+ "outline_thickness": max(0.01, min(0.1, outline_thickness))
913
+ }
914
+ if color:
915
+ obj["material"]["color"] = color
916
+
917
+ message = f"Applied toon shading to '{object_id}' ({gradient_steps} steps{', with outline' if outline else ''})"
918
+ else:
919
+ obj["material"]["toon"] = {"enabled": False}
920
+ message = f"Disabled toon shading on '{object_id}' (reverted to standard material)"
921
+
922
+ storage.save(scene)
923
+
924
+ return {
925
+ "scene_id": scene_id,
926
+ "object_id": object_id,
927
+ "message": message,
928
+ "material": obj["material"]
929
+ }
backend/tools/scene_tools.py CHANGED
@@ -360,3 +360,106 @@ def get_scene_info(
360
  },
361
  "objects": objects_info,
362
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  },
361
  "objects": objects_info,
362
  }
363
+
364
+
365
+ # Valid brick types from Kenney brick kit
366
+ BRICK_TYPES = {
367
+ "brick_1x1": "brick_1x1.glb",
368
+ "brick_1x2": "brick_1x2.glb",
369
+ "brick_1x4": "brick_1x4.glb",
370
+ "brick_2x2": "brick_2x2.glb",
371
+ "brick_2x4": "brick_2x4.glb",
372
+ "plate_1x2": "plate_1x2.glb",
373
+ "plate_2x2": "plate_2x2.glb",
374
+ "plate_2x4": "plate_2x4.glb",
375
+ "plate_4x4": "plate_4x4.glb",
376
+ "slope_2x2": "slope_2x2.glb",
377
+ }
378
+
379
+
380
+ def add_brick(
381
+ scene_id: str,
382
+ brick_type: str = "brick_2x4",
383
+ position: Optional[Dict[str, float]] = None,
384
+ rotation: Optional[Dict[str, float]] = None,
385
+ color: str = "#ff0000",
386
+ name: Optional[str] = None,
387
+ base_url: str = "http://localhost:8000"
388
+ ) -> Dict[str, Any]:
389
+ """
390
+ Add a LEGO-style brick from the Kenney brick kit.
391
+
392
+ Args:
393
+ scene_id: ID of the scene
394
+ brick_type: Type of brick - brick_1x1, brick_1x2, brick_1x4, brick_2x2, brick_2x4,
395
+ plate_1x2, plate_2x2, plate_2x4, plate_4x4, slope_2x2
396
+ position: Position {x, y, z} (default: {0, 0, 0})
397
+ rotation: Rotation in degrees {x, y, z} (default: {0, 0, 0})
398
+ color: Hex color code for the brick (default: #ff0000 red)
399
+ name: Optional name for the brick
400
+ base_url: Base URL for the deployed space
401
+
402
+ Returns:
403
+ Dictionary with brick info and message
404
+ """
405
+ scene = storage.get(scene_id)
406
+ if not scene:
407
+ raise ValueError(f"Scene '{scene_id}' not found")
408
+
409
+ if brick_type not in BRICK_TYPES:
410
+ raise ValueError(f"Invalid brick_type '{brick_type}'. Valid types: {list(BRICK_TYPES.keys())}")
411
+
412
+ # Default position
413
+ if position is None:
414
+ position = {"x": 0, "y": 0, "z": 0}
415
+
416
+ # Default rotation
417
+ if rotation is None:
418
+ rotation = {"x": 0, "y": 0, "z": 0}
419
+
420
+ # Validate position is within world bounds
421
+ x = position.get('x', 0)
422
+ z = position.get('z', 0)
423
+ WORLD_HALF = 5.0
424
+ if abs(x) > WORLD_HALF or abs(z) > WORLD_HALF:
425
+ raise ValueError(
426
+ f"Brick position ({x}, {z}) is outside the 10x10 world bounds. "
427
+ f"X and Z must be between -{WORLD_HALF} and {WORLD_HALF}."
428
+ )
429
+
430
+ # Generate unique ID
431
+ brick_id = f"brick_{len(scene['objects'])}_{brick_type}"
432
+
433
+ # Generate name if not provided
434
+ if not name:
435
+ name = f"{brick_type.replace('_', ' ').title()}"
436
+
437
+ # Model path relative to static files
438
+ model_path = f"/static/models/kenney/brick_kit/{BRICK_TYPES[brick_type]}"
439
+
440
+ brick_obj = {
441
+ "id": brick_id,
442
+ "type": "brick",
443
+ "brick_type": brick_type,
444
+ "name": name,
445
+ "position": position,
446
+ "rotation": rotation,
447
+ "scale": {"x": 1, "y": 1, "z": 1},
448
+ "model_path": model_path,
449
+ "material": {
450
+ "color": color,
451
+ "metalness": 0.1,
452
+ "roughness": 0.7
453
+ }
454
+ }
455
+
456
+ scene["objects"].append(brick_obj)
457
+ storage.save(scene)
458
+
459
+ return {
460
+ "scene_id": scene_id,
461
+ "brick_id": brick_id,
462
+ "brick_type": brick_type,
463
+ "message": f"Added {name} at ({position['x']}, {position['y']}, {position['z']})",
464
+ "brick": brick_obj
465
+ }
chat_client.py CHANGED
@@ -21,6 +21,7 @@ from backend.tools.scene_tools import (
21
  remove_game_object,
22
  set_scene_lighting,
23
  get_scene_info,
 
24
  )
25
  from backend.tools.player_tools import (
26
  set_player_speed,
@@ -42,6 +43,18 @@ from backend.tools.rendering_tools import (
42
  update_object_material,
43
  set_background_color,
44
  set_fog,
 
 
 
 
 
 
 
 
 
 
 
 
45
  )
46
  from backend.game_models import create_vector3, create_material
47
 
@@ -353,6 +366,179 @@ TOOLS = [
353
  }
354
  }
355
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  ]
357
 
358
 
@@ -528,6 +714,97 @@ Be concise but helpful. After making changes, briefly confirm what was done."""
528
  args.get("density")
529
  )
530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  else:
532
  return {"error": f"Unknown tool: {name}"}
533
 
@@ -710,6 +987,45 @@ If the user DOES specify a position (e.g., "add a cube at 0, 0, 0"), use their s
710
  elif tool == "set_fog":
711
  return {"action": "setFog", "data": result.get("fog")}
712
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  return None
714
 
715
  def clear_history(self):
 
21
  remove_game_object,
22
  set_scene_lighting,
23
  get_scene_info,
24
+ add_brick,
25
  )
26
  from backend.tools.player_tools import (
27
  set_player_speed,
 
43
  update_object_material,
44
  set_background_color,
45
  set_fog,
46
+ update_material_to_toon,
47
+ )
48
+ from backend.tools.environment_tools import (
49
+ add_skybox,
50
+ remove_skybox,
51
+ add_particles,
52
+ remove_particles,
53
+ )
54
+ from backend.tools.ui_tools import (
55
+ render_text_on_screen,
56
+ render_bar_on_screen,
57
+ remove_ui_element,
58
  )
59
  from backend.game_models import create_vector3, create_material
60
 
 
366
  }
367
  }
368
  },
369
+ # Environment Tools
370
+ {
371
+ "type": "function",
372
+ "function": {
373
+ "name": "add_skybox",
374
+ "description": "Add a procedural sky to the scene. Presets: day, sunset, noon, dawn, night",
375
+ "parameters": {
376
+ "type": "object",
377
+ "properties": {
378
+ "scene_id": {"type": "string", "description": "ID of the scene"},
379
+ "preset": {"type": "string", "enum": ["day", "sunset", "noon", "dawn", "night"], "description": "Sky preset"},
380
+ "turbidity": {"type": "number", "description": "Haziness 2-20 (default: 10)"},
381
+ "rayleigh": {"type": "number", "description": "Blue sky intensity 0-4 (default: 2)"},
382
+ "sun_elevation": {"type": "number", "description": "Sun angle from horizon -90 to 90 (default: 45)"},
383
+ "sun_azimuth": {"type": "number", "description": "Sun compass direction 0-360 (default: 180)"},
384
+ },
385
+ "required": ["scene_id"]
386
+ }
387
+ }
388
+ },
389
+ {
390
+ "type": "function",
391
+ "function": {
392
+ "name": "remove_skybox",
393
+ "description": "Remove the skybox from the scene",
394
+ "parameters": {
395
+ "type": "object",
396
+ "properties": {
397
+ "scene_id": {"type": "string", "description": "ID of the scene"},
398
+ },
399
+ "required": ["scene_id"]
400
+ }
401
+ }
402
+ },
403
+ {
404
+ "type": "function",
405
+ "function": {
406
+ "name": "add_particles",
407
+ "description": "Add particle effects. Presets: fire, smoke, sparkle, rain, snow",
408
+ "parameters": {
409
+ "type": "object",
410
+ "properties": {
411
+ "scene_id": {"type": "string", "description": "ID of the scene"},
412
+ "preset": {"type": "string", "enum": ["fire", "smoke", "sparkle", "rain", "snow"], "description": "Particle preset"},
413
+ "x": {"type": "number", "description": "X position for localized effects"},
414
+ "y": {"type": "number", "description": "Y position for localized effects"},
415
+ "z": {"type": "number", "description": "Z position for localized effects"},
416
+ "particle_id": {"type": "string", "description": "Optional unique ID"},
417
+ },
418
+ "required": ["scene_id", "preset"]
419
+ }
420
+ }
421
+ },
422
+ {
423
+ "type": "function",
424
+ "function": {
425
+ "name": "remove_particles",
426
+ "description": "Remove a particle system from the scene",
427
+ "parameters": {
428
+ "type": "object",
429
+ "properties": {
430
+ "scene_id": {"type": "string", "description": "ID of the scene"},
431
+ "particle_id": {"type": "string", "description": "ID of the particle system to remove"},
432
+ },
433
+ "required": ["scene_id", "particle_id"]
434
+ }
435
+ }
436
+ },
437
+ # UI Tools
438
+ {
439
+ "type": "function",
440
+ "function": {
441
+ "name": "render_text_on_screen",
442
+ "description": "Render text on the screen as a 2D overlay",
443
+ "parameters": {
444
+ "type": "object",
445
+ "properties": {
446
+ "scene_id": {"type": "string", "description": "ID of the scene"},
447
+ "text": {"type": "string", "description": "Text to display"},
448
+ "x": {"type": "number", "description": "Horizontal position 0-100% (default: 50)"},
449
+ "y": {"type": "number", "description": "Vertical position 0-100% (default: 10)"},
450
+ "font_size": {"type": "integer", "description": "Font size in pixels (default: 24)"},
451
+ "color": {"type": "string", "description": "Text color hex (default: #ffffff)"},
452
+ "text_id": {"type": "string", "description": "Optional unique ID for updates"},
453
+ "background_color": {"type": "string", "description": "Optional background color"},
454
+ },
455
+ "required": ["scene_id", "text"]
456
+ }
457
+ }
458
+ },
459
+ {
460
+ "type": "function",
461
+ "function": {
462
+ "name": "render_bar_on_screen",
463
+ "description": "Render a progress/health bar on screen",
464
+ "parameters": {
465
+ "type": "object",
466
+ "properties": {
467
+ "scene_id": {"type": "string", "description": "ID of the scene"},
468
+ "x": {"type": "number", "description": "Horizontal position 0-100% (default: 10)"},
469
+ "y": {"type": "number", "description": "Vertical position 0-100% (default: 10)"},
470
+ "width": {"type": "number", "description": "Bar width in pixels (default: 200)"},
471
+ "height": {"type": "number", "description": "Bar height in pixels (default: 20)"},
472
+ "value": {"type": "number", "description": "Current value (default: 100)"},
473
+ "max_value": {"type": "number", "description": "Max value (default: 100)"},
474
+ "bar_color": {"type": "string", "description": "Fill color (default: #00ff00)"},
475
+ "bar_id": {"type": "string", "description": "Optional unique ID for updates"},
476
+ "label": {"type": "string", "description": "Optional label above bar"},
477
+ "show_value": {"type": "boolean", "description": "Show numeric value"},
478
+ },
479
+ "required": ["scene_id"]
480
+ }
481
+ }
482
+ },
483
+ {
484
+ "type": "function",
485
+ "function": {
486
+ "name": "remove_ui_element",
487
+ "description": "Remove a UI element from the screen",
488
+ "parameters": {
489
+ "type": "object",
490
+ "properties": {
491
+ "scene_id": {"type": "string", "description": "ID of the scene"},
492
+ "element_id": {"type": "string", "description": "ID of the text or bar to remove"},
493
+ },
494
+ "required": ["scene_id", "element_id"]
495
+ }
496
+ }
497
+ },
498
+ # Toon Material
499
+ {
500
+ "type": "function",
501
+ "function": {
502
+ "name": "update_material_to_toon",
503
+ "description": "Apply toon/cel-shading to an object for cartoon-like appearance",
504
+ "parameters": {
505
+ "type": "object",
506
+ "properties": {
507
+ "scene_id": {"type": "string", "description": "ID of the scene"},
508
+ "object_id": {"type": "string", "description": "ID of the object"},
509
+ "enabled": {"type": "boolean", "description": "Enable/disable toon shading (default: true)"},
510
+ "color": {"type": "string", "description": "Base color (optional)"},
511
+ "gradient_steps": {"type": "integer", "description": "Shading steps 2-10 (default: 3)"},
512
+ "outline": {"type": "boolean", "description": "Add outline effect (default: true)"},
513
+ "outline_color": {"type": "string", "description": "Outline color (default: #000000)"},
514
+ "outline_thickness": {"type": "number", "description": "Outline thickness 0.01-0.1 (default: 0.03)"},
515
+ },
516
+ "required": ["scene_id", "object_id"]
517
+ }
518
+ }
519
+ },
520
+ # Brick Blocks
521
+ {
522
+ "type": "function",
523
+ "function": {
524
+ "name": "add_brick",
525
+ "description": "Add a LEGO-style brick from the Kenney brick kit. Types: brick_1x1, brick_1x2, brick_1x4, brick_2x2, brick_2x4, plate_1x2, plate_2x2, plate_2x4, plate_4x4, slope_2x2",
526
+ "parameters": {
527
+ "type": "object",
528
+ "properties": {
529
+ "scene_id": {"type": "string", "description": "ID of the scene"},
530
+ "brick_type": {"type": "string", "enum": ["brick_1x1", "brick_1x2", "brick_1x4", "brick_2x2", "brick_2x4", "plate_1x2", "plate_2x2", "plate_2x4", "plate_4x4", "slope_2x2"], "description": "Type of brick"},
531
+ "x": {"type": "number", "description": "X position"},
532
+ "y": {"type": "number", "description": "Y position"},
533
+ "z": {"type": "number", "description": "Z position"},
534
+ "rotation_y": {"type": "number", "description": "Y rotation in degrees"},
535
+ "color": {"type": "string", "description": "Hex color (default: #ff0000)"},
536
+ "name": {"type": "string", "description": "Optional name"},
537
+ },
538
+ "required": ["scene_id", "brick_type"]
539
+ }
540
+ }
541
+ },
542
  ]
543
 
544
 
 
714
  args.get("density")
715
  )
716
 
717
+ # Environment tools
718
+ elif name == "add_skybox":
719
+ return add_skybox(
720
+ args["scene_id"],
721
+ args.get("preset", "day"),
722
+ args.get("turbidity", 10.0),
723
+ args.get("rayleigh", 2.0),
724
+ args.get("sun_elevation", 45.0),
725
+ args.get("sun_azimuth", 180.0)
726
+ )
727
+
728
+ elif name == "remove_skybox":
729
+ return remove_skybox(args["scene_id"])
730
+
731
+ elif name == "add_particles":
732
+ position = None
733
+ if "x" in args or "y" in args or "z" in args:
734
+ position = {"x": args.get("x", 0), "y": args.get("y", 1), "z": args.get("z", 0)}
735
+ return add_particles(
736
+ args["scene_id"],
737
+ args["preset"],
738
+ position,
739
+ args.get("particle_id")
740
+ )
741
+
742
+ elif name == "remove_particles":
743
+ return remove_particles(args["scene_id"], args["particle_id"])
744
+
745
+ # UI tools
746
+ elif name == "render_text_on_screen":
747
+ return render_text_on_screen(
748
+ args["scene_id"],
749
+ args["text"],
750
+ args.get("x", 50.0),
751
+ args.get("y", 10.0),
752
+ args.get("font_size", 24),
753
+ args.get("color", "#ffffff"),
754
+ args.get("text_id"),
755
+ args.get("font_family", "Arial"),
756
+ args.get("text_align", "center"),
757
+ args.get("background_color"),
758
+ args.get("padding", 8)
759
+ )
760
+
761
+ elif name == "render_bar_on_screen":
762
+ return render_bar_on_screen(
763
+ args["scene_id"],
764
+ args.get("x", 10.0),
765
+ args.get("y", 10.0),
766
+ args.get("width", 200.0),
767
+ args.get("height", 20.0),
768
+ args.get("value", 100.0),
769
+ args.get("max_value", 100.0),
770
+ args.get("bar_color", "#00ff00"),
771
+ args.get("background_color", "#333333"),
772
+ args.get("border_color", "#ffffff"),
773
+ args.get("bar_id"),
774
+ args.get("label"),
775
+ args.get("show_value", False)
776
+ )
777
+
778
+ elif name == "remove_ui_element":
779
+ return remove_ui_element(args["scene_id"], args["element_id"])
780
+
781
+ # Toon material
782
+ elif name == "update_material_to_toon":
783
+ return update_material_to_toon(
784
+ args["scene_id"],
785
+ args["object_id"],
786
+ args.get("enabled", True),
787
+ args.get("color"),
788
+ args.get("gradient_steps", 3),
789
+ args.get("outline", True),
790
+ args.get("outline_color", "#000000"),
791
+ args.get("outline_thickness", 0.03)
792
+ )
793
+
794
+ # Brick blocks
795
+ elif name == "add_brick":
796
+ position = {"x": args.get("x", 0), "y": args.get("y", 0), "z": args.get("z", 0)}
797
+ rotation = {"x": 0, "y": args.get("rotation_y", 0), "z": 0}
798
+ return add_brick(
799
+ args["scene_id"],
800
+ args["brick_type"],
801
+ position,
802
+ rotation,
803
+ args.get("color", "#ff0000"),
804
+ args.get("name"),
805
+ self.base_url
806
+ )
807
+
808
  else:
809
  return {"error": f"Unknown tool: {name}"}
810
 
 
987
  elif tool == "set_fog":
988
  return {"action": "setFog", "data": result.get("fog")}
989
 
990
+ # Environment tools
991
+ elif tool == "add_skybox":
992
+ return {"action": "addSkybox", "data": result.get("skybox")}
993
+
994
+ elif tool == "remove_skybox":
995
+ return {"action": "removeSkybox", "data": {}}
996
+
997
+ elif tool == "add_particles":
998
+ return {"action": "addParticles", "data": result.get("particle_system")}
999
+
1000
+ elif tool == "remove_particles":
1001
+ return {"action": "removeParticles", "data": {"particle_id": action["args"].get("particle_id")}}
1002
+
1003
+ # UI tools
1004
+ elif tool == "render_text_on_screen":
1005
+ return {"action": "renderText", "data": result.get("text_element")}
1006
+
1007
+ elif tool == "render_bar_on_screen":
1008
+ return {"action": "renderBar", "data": result.get("bar_element")}
1009
+
1010
+ elif tool == "remove_ui_element":
1011
+ return {"action": "removeUIElement", "data": {"element_id": action["args"].get("element_id")}}
1012
+
1013
+ # Toon shading
1014
+ elif tool == "update_material_to_toon":
1015
+ return {"action": "updateToonMaterial", "data": {
1016
+ "object_id": action["args"].get("object_id"),
1017
+ "enabled": action["args"].get("enabled", True),
1018
+ "color": action["args"].get("color"),
1019
+ "gradient_steps": action["args"].get("gradient_steps", 3),
1020
+ "outline": action["args"].get("outline", True),
1021
+ "outline_color": action["args"].get("outline_color", "#000000"),
1022
+ "outline_thickness": action["args"].get("outline_thickness", 0.03)
1023
+ }}
1024
+
1025
+ # Brick blocks
1026
+ elif tool == "add_brick":
1027
+ return {"action": "addBrick", "data": result.get("brick")}
1028
+
1029
  return None
1030
 
1031
  def clear_history(self):
frontend/game_viewer.html CHANGED
@@ -65,8 +65,24 @@
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;
@@ -101,7 +117,7 @@
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)
@@ -126,7 +142,7 @@
126
  let JUMP_FORCE = 5.0;
127
  let GRAVITY = -9.82;
128
  let PLAYER_MASS = 80.0;
129
- let LINEAR_DAMPING = 0.9;
130
  // World size from scene data (default 25x25)
131
  let WORLD_SIZE = 25;
132
  let WORLD_HALF = WORLD_SIZE / 2;
@@ -238,6 +254,43 @@
238
  console.log('✅ Player configuration applied successfully');
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  async function init() {
242
  try {
243
  // Check for embedded scene data first (used when served via Gradio)
@@ -278,6 +331,9 @@
278
  // Render all game objects
279
  renderGameObjects();
280
 
 
 
 
281
  // Start animation loop
282
  animate();
283
 
@@ -479,14 +535,14 @@
479
  physicsWorld = new CANNON.World();
480
  physicsWorld.gravity.set(0, GRAVITY, 0);
481
 
482
- // Set up collision materials for better physics response
483
  const defaultMaterial = new CANNON.Material('default');
484
  const defaultContactMaterial = new CANNON.ContactMaterial(
485
  defaultMaterial,
486
  defaultMaterial,
487
  {
488
- friction: 0.3,
489
- restitution: 0.0, // No bounce
490
  }
491
  );
492
  physicsWorld.addContactMaterial(defaultContactMaterial);
@@ -996,6 +1052,9 @@
996
  // Update crosshair floor intersection and send to parent
997
  updateCrosshairPosition();
998
 
 
 
 
999
  // Render using composer (for outlines) instead of direct renderer
1000
  if (composer) {
1001
  composer.render();
@@ -1322,6 +1381,38 @@
1322
  }
1323
  console.log('Player dimensions updated:', { height: PLAYER_HEIGHT, radius: PLAYER_RADIUS });
1324
  break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1325
  default:
1326
  console.warn('Unknown postMessage action:', action);
1327
  }
@@ -1718,6 +1809,465 @@
1718
  console.log('Fog enabled:', fogData.type);
1719
  }
1720
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1721
  // Start the application
1722
  init();
1723
  </script>
 
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 { Sky } from 'three/addons/objects/Sky.js';
69
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
70
  import * as CANNON from 'cannon-es';
71
 
72
+ // Skybox and environment references
73
+ let sky = null;
74
+ let sun = new THREE.Vector3();
75
+
76
+ // Particle systems
77
+ let particleSystems = new Map();
78
+
79
+ // UI overlay container
80
+ let uiContainer = null;
81
+ let uiElements = new Map();
82
+
83
+ // GLTF loader for brick models
84
+ const gltfLoader = new GLTFLoader();
85
+
86
  // Get scene ID from URL
87
  const sceneId = window.location.pathname.split('/').pop();
88
  const baseUrl = window.location.origin;
 
117
  const MAX_SELECT_DISTANCE = 10; // Max raycast distance for selection
118
 
119
  // FPS movement and look variables (configurable via player_config)
120
+ let moveSpeed = 8.0; // Default walking speed in units/sec
121
  const velocity = new THREE.Vector3();
122
  let isMouseLocked = false;
123
  let cameraRotationX = 0; // Pitch (up/down)
 
142
  let JUMP_FORCE = 5.0;
143
  let GRAVITY = -9.82;
144
  let PLAYER_MASS = 80.0;
145
+ let LINEAR_DAMPING = 0.0; // No damping - we control velocity directly
146
  // World size from scene data (default 25x25)
147
  let WORLD_SIZE = 25;
148
  let WORLD_HALF = WORLD_SIZE / 2;
 
254
  console.log('✅ Player configuration applied successfully');
255
  }
256
 
257
+ function applyInitialEnvironment() {
258
+ /**
259
+ * Apply initial environment settings from scene data
260
+ * Loads skybox, particles, and UI elements on startup
261
+ */
262
+ if (!sceneData) return;
263
+
264
+ // Apply skybox if defined in scene data
265
+ if (sceneData.skybox) {
266
+ console.log('Applying initial skybox:', sceneData.skybox);
267
+ handleAddSkybox(sceneData.skybox);
268
+ }
269
+
270
+ // Apply particles if defined in scene data
271
+ if (sceneData.particles && Array.isArray(sceneData.particles)) {
272
+ sceneData.particles.forEach(particleConfig => {
273
+ console.log('Applying initial particles:', particleConfig);
274
+ handleAddParticles(particleConfig);
275
+ });
276
+ }
277
+
278
+ // Apply UI elements if defined in scene data
279
+ if (sceneData.ui_elements && Array.isArray(sceneData.ui_elements)) {
280
+ sceneData.ui_elements.forEach(uiConfig => {
281
+ if (uiConfig.type === 'text') {
282
+ console.log('Applying initial UI text:', uiConfig);
283
+ handleRenderText(uiConfig);
284
+ } else if (uiConfig.type === 'bar') {
285
+ console.log('Applying initial UI bar:', uiConfig);
286
+ handleRenderBar(uiConfig);
287
+ }
288
+ });
289
+ }
290
+
291
+ console.log('✅ Initial environment applied');
292
+ }
293
+
294
  async function init() {
295
  try {
296
  // Check for embedded scene data first (used when served via Gradio)
 
331
  // Render all game objects
332
  renderGameObjects();
333
 
334
+ // Apply initial environment (skybox, particles, UI from scene data)
335
+ applyInitialEnvironment();
336
+
337
  // Start animation loop
338
  animate();
339
 
 
535
  physicsWorld = new CANNON.World();
536
  physicsWorld.gravity.set(0, GRAVITY, 0);
537
 
538
+ // Set up collision materials - zero friction since we control velocity directly
539
  const defaultMaterial = new CANNON.Material('default');
540
  const defaultContactMaterial = new CANNON.ContactMaterial(
541
  defaultMaterial,
542
  defaultMaterial,
543
  {
544
+ friction: 0.0, // No friction - we set velocity directly each frame
545
+ restitution: 0.0, // No bounce
546
  }
547
  );
548
  physicsWorld.addContactMaterial(defaultContactMaterial);
 
1052
  // Update crosshair floor intersection and send to parent
1053
  updateCrosshairPosition();
1054
 
1055
+ // Update particle systems
1056
+ updateParticleSystems(delta);
1057
+
1058
  // Render using composer (for outlines) instead of direct renderer
1059
  if (composer) {
1060
  composer.render();
 
1381
  }
1382
  console.log('Player dimensions updated:', { height: PLAYER_HEIGHT, radius: PLAYER_RADIUS });
1383
  break;
1384
+ // Skybox actions
1385
+ case 'addSkybox':
1386
+ handleAddSkybox(data);
1387
+ break;
1388
+ case 'removeSkybox':
1389
+ handleRemoveSkybox();
1390
+ break;
1391
+ // Particle actions
1392
+ case 'addParticles':
1393
+ handleAddParticles(data);
1394
+ break;
1395
+ case 'removeParticles':
1396
+ handleRemoveParticles(data.particle_id);
1397
+ break;
1398
+ // UI actions
1399
+ case 'renderText':
1400
+ handleRenderText(data);
1401
+ break;
1402
+ case 'renderBar':
1403
+ handleRenderBar(data);
1404
+ break;
1405
+ case 'removeUIElement':
1406
+ handleRemoveUIElement(data.element_id);
1407
+ break;
1408
+ // Toon shading
1409
+ case 'updateToonMaterial':
1410
+ handleUpdateToonMaterial(data);
1411
+ break;
1412
+ // Brick blocks
1413
+ case 'addBrick':
1414
+ handleAddBrick(data);
1415
+ break;
1416
  default:
1417
  console.warn('Unknown postMessage action:', action);
1418
  }
 
1809
  console.log('Fog enabled:', fogData.type);
1810
  }
1811
 
1812
+ // ==================== Skybox Handlers ====================
1813
+
1814
+ function handleAddSkybox(skyboxData) {
1815
+ // Remove existing skybox if any
1816
+ if (sky) {
1817
+ scene.remove(sky);
1818
+ }
1819
+
1820
+ // Create Sky mesh
1821
+ sky = new Sky();
1822
+ sky.scale.setScalar(450000);
1823
+ scene.add(sky);
1824
+
1825
+ const skyUniforms = sky.material.uniforms;
1826
+ skyUniforms['turbidity'].value = skyboxData.turbidity || 10;
1827
+ skyUniforms['rayleigh'].value = skyboxData.rayleigh || 2;
1828
+ skyUniforms['mieCoefficient'].value = 0.005;
1829
+ skyUniforms['mieDirectionalG'].value = 0.8;
1830
+
1831
+ // Calculate sun position from elevation and azimuth
1832
+ const phi = THREE.MathUtils.degToRad(90 - (skyboxData.sun_elevation || 45));
1833
+ const theta = THREE.MathUtils.degToRad(skyboxData.sun_azimuth || 180);
1834
+ sun.setFromSphericalCoords(1, phi, theta);
1835
+ skyUniforms['sunPosition'].value.copy(sun);
1836
+
1837
+ // Update scene background to use sky
1838
+ scene.background = null; // Sky will render as background
1839
+
1840
+ console.log('🌤️ Skybox added:', skyboxData.preset || 'custom',
1841
+ `elevation=${skyboxData.sun_elevation}°`);
1842
+ }
1843
+
1844
+ function handleRemoveSkybox() {
1845
+ if (sky) {
1846
+ scene.remove(sky);
1847
+ sky = null;
1848
+ }
1849
+ // Revert to solid background
1850
+ const bgColor = sceneData?.environment?.background_color || '#87CEEB';
1851
+ scene.background = new THREE.Color(bgColor);
1852
+ console.log('🌤️ Skybox removed');
1853
+ }
1854
+
1855
+ // ==================== Particle System Handlers ====================
1856
+
1857
+ function handleAddParticles(particleData) {
1858
+ const id = particleData.id || particleData.particle_id;
1859
+
1860
+ // Remove existing particle system with same ID
1861
+ if (particleSystems.has(id)) {
1862
+ const existingSystem = particleSystems.get(id);
1863
+ scene.remove(existingSystem.points);
1864
+ particleSystems.delete(id);
1865
+ }
1866
+
1867
+ const config = particleData;
1868
+ const count = config.count || 100;
1869
+
1870
+ // Create particle geometry
1871
+ const geometry = new THREE.BufferGeometry();
1872
+ const positions = new Float32Array(count * 3);
1873
+ const velocities = new Float32Array(count * 3);
1874
+ const lifetimes = new Float32Array(count);
1875
+
1876
+ const spread = config.spread || 1.0;
1877
+ const pos = config.position || { x: 0, y: 0, z: 0 };
1878
+
1879
+ for (let i = 0; i < count; i++) {
1880
+ const i3 = i * 3;
1881
+
1882
+ if (config.localized !== false) {
1883
+ // Localized effect (fire, smoke, sparkle)
1884
+ positions[i3] = pos.x + (Math.random() - 0.5) * spread;
1885
+ positions[i3 + 1] = pos.y + Math.random() * spread;
1886
+ positions[i3 + 2] = pos.z + (Math.random() - 0.5) * spread;
1887
+ } else {
1888
+ // Weather effect (rain, snow) - covers world
1889
+ positions[i3] = (Math.random() - 0.5) * WORLD_SIZE * 2;
1890
+ positions[i3 + 1] = Math.random() * 20;
1891
+ positions[i3 + 2] = (Math.random() - 0.5) * WORLD_SIZE * 2;
1892
+ }
1893
+
1894
+ const vel = config.velocity || { x: 0, y: 1, z: 0 };
1895
+ velocities[i3] = vel.x + (Math.random() - 0.5) * 0.5;
1896
+ velocities[i3 + 1] = vel.y + (Math.random() - 0.5) * 0.5;
1897
+ velocities[i3 + 2] = vel.z + (Math.random() - 0.5) * 0.5;
1898
+
1899
+ lifetimes[i] = Math.random() * (config.lifetime || 2.0);
1900
+ }
1901
+
1902
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1903
+
1904
+ // Create particle material
1905
+ const startColor = new THREE.Color(config.color_start || '#ffffff');
1906
+ const material = new THREE.PointsMaterial({
1907
+ size: config.size || 0.1,
1908
+ color: startColor,
1909
+ transparent: true,
1910
+ opacity: 0.8,
1911
+ blending: THREE.AdditiveBlending,
1912
+ depthWrite: false
1913
+ });
1914
+
1915
+ const points = new THREE.Points(geometry, material);
1916
+ points.name = `particles_${id}`;
1917
+ scene.add(points);
1918
+
1919
+ // Store particle system data for animation
1920
+ particleSystems.set(id, {
1921
+ points,
1922
+ geometry,
1923
+ velocities,
1924
+ lifetimes,
1925
+ config,
1926
+ maxLifetime: config.lifetime || 2.0,
1927
+ startColor,
1928
+ endColor: new THREE.Color(config.color_end || config.color_start || '#ffffff')
1929
+ });
1930
+
1931
+ console.log('✨ Particles added:', id, config.preset);
1932
+ }
1933
+
1934
+ function handleRemoveParticles(particleId) {
1935
+ if (particleSystems.has(particleId)) {
1936
+ const system = particleSystems.get(particleId);
1937
+ scene.remove(system.points);
1938
+ system.geometry.dispose();
1939
+ system.points.material.dispose();
1940
+ particleSystems.delete(particleId);
1941
+ console.log('✨ Particles removed:', particleId);
1942
+ }
1943
+ }
1944
+
1945
+ function updateParticleSystems(delta) {
1946
+ particleSystems.forEach((system, id) => {
1947
+ const positions = system.geometry.attributes.position.array;
1948
+ const count = positions.length / 3;
1949
+ const config = system.config;
1950
+ const pos = config.position || { x: 0, y: 0, z: 0 };
1951
+ const spread = config.spread || 1.0;
1952
+
1953
+ for (let i = 0; i < count; i++) {
1954
+ const i3 = i * 3;
1955
+
1956
+ // Update position based on velocity
1957
+ positions[i3] += system.velocities[i3] * delta;
1958
+ positions[i3 + 1] += system.velocities[i3 + 1] * delta;
1959
+ positions[i3 + 2] += system.velocities[i3 + 2] * delta;
1960
+
1961
+ // Update lifetime
1962
+ system.lifetimes[i] += delta;
1963
+
1964
+ // Reset particle if lifetime exceeded
1965
+ if (system.lifetimes[i] >= system.maxLifetime) {
1966
+ system.lifetimes[i] = 0;
1967
+
1968
+ if (config.localized !== false) {
1969
+ positions[i3] = pos.x + (Math.random() - 0.5) * spread;
1970
+ positions[i3 + 1] = pos.y;
1971
+ positions[i3 + 2] = pos.z + (Math.random() - 0.5) * spread;
1972
+ } else {
1973
+ // Weather - respawn at top
1974
+ positions[i3] = (Math.random() - 0.5) * WORLD_SIZE * 2;
1975
+ positions[i3 + 1] = 20;
1976
+ positions[i3 + 2] = (Math.random() - 0.5) * WORLD_SIZE * 2;
1977
+ }
1978
+ }
1979
+ }
1980
+
1981
+ system.geometry.attributes.position.needsUpdate = true;
1982
+ });
1983
+ }
1984
+
1985
+ // ==================== UI Overlay Handlers ====================
1986
+
1987
+ function ensureUIContainer() {
1988
+ if (!uiContainer) {
1989
+ uiContainer = document.createElement('div');
1990
+ uiContainer.id = 'ui-overlay';
1991
+ uiContainer.style.cssText = `
1992
+ position: absolute;
1993
+ top: 0;
1994
+ left: 0;
1995
+ width: 100%;
1996
+ height: 100%;
1997
+ pointer-events: none;
1998
+ z-index: 50;
1999
+ `;
2000
+ document.getElementById('viewer-container').appendChild(uiContainer);
2001
+ }
2002
+ }
2003
+
2004
+ function handleRenderText(textData) {
2005
+ ensureUIContainer();
2006
+
2007
+ const id = textData.id || textData.text_id;
2008
+
2009
+ // Remove existing element with same ID
2010
+ if (uiElements.has(id)) {
2011
+ uiContainer.removeChild(uiElements.get(id));
2012
+ }
2013
+
2014
+ const element = document.createElement('div');
2015
+ element.id = `ui-${id}`;
2016
+
2017
+ let bgStyle = '';
2018
+ if (textData.background_color) {
2019
+ bgStyle = `background-color: ${textData.background_color}; padding: ${textData.padding || 8}px; border-radius: 4px;`;
2020
+ }
2021
+
2022
+ element.style.cssText = `
2023
+ position: absolute;
2024
+ left: ${textData.x}%;
2025
+ top: ${textData.y}%;
2026
+ transform: translate(-50%, 0);
2027
+ color: ${textData.color || '#ffffff'};
2028
+ font-family: ${textData.font_family || 'Arial'}, sans-serif;
2029
+ font-size: ${textData.font_size || 24}px;
2030
+ text-align: ${textData.text_align || 'center'};
2031
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
2032
+ white-space: nowrap;
2033
+ ${bgStyle}
2034
+ `;
2035
+ element.textContent = textData.text;
2036
+
2037
+ uiContainer.appendChild(element);
2038
+ uiElements.set(id, element);
2039
+
2040
+ console.log('📝 UI text rendered:', id);
2041
+ }
2042
+
2043
+ function handleRenderBar(barData) {
2044
+ ensureUIContainer();
2045
+
2046
+ const id = barData.id || barData.bar_id;
2047
+
2048
+ // Remove existing element with same ID
2049
+ if (uiElements.has(id)) {
2050
+ uiContainer.removeChild(uiElements.get(id));
2051
+ }
2052
+
2053
+ const percentage = barData.percentage ||
2054
+ ((barData.value / barData.max_value) * 100);
2055
+
2056
+ const container = document.createElement('div');
2057
+ container.id = `ui-${id}`;
2058
+ container.style.cssText = `
2059
+ position: absolute;
2060
+ left: ${barData.x}%;
2061
+ top: ${barData.y}%;
2062
+ `;
2063
+
2064
+ // Add label if provided
2065
+ if (barData.label) {
2066
+ const label = document.createElement('div');
2067
+ label.style.cssText = `
2068
+ color: #ffffff;
2069
+ font-family: Arial, sans-serif;
2070
+ font-size: 14px;
2071
+ margin-bottom: 4px;
2072
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
2073
+ `;
2074
+ label.textContent = barData.label;
2075
+ container.appendChild(label);
2076
+ }
2077
+
2078
+ // Create bar container
2079
+ const barContainer = document.createElement('div');
2080
+ barContainer.style.cssText = `
2081
+ width: ${barData.width || 200}px;
2082
+ height: ${barData.height || 20}px;
2083
+ background-color: ${barData.background_color || '#333333'};
2084
+ border: 2px solid ${barData.border_color || '#ffffff'};
2085
+ border-radius: 4px;
2086
+ overflow: hidden;
2087
+ position: relative;
2088
+ `;
2089
+
2090
+ // Create fill bar
2091
+ const fill = document.createElement('div');
2092
+ fill.style.cssText = `
2093
+ width: ${percentage}%;
2094
+ height: 100%;
2095
+ background-color: ${barData.bar_color || '#00ff00'};
2096
+ transition: width 0.3s ease;
2097
+ `;
2098
+ barContainer.appendChild(fill);
2099
+
2100
+ // Show value if requested
2101
+ if (barData.show_value) {
2102
+ const valueText = document.createElement('div');
2103
+ valueText.style.cssText = `
2104
+ position: absolute;
2105
+ top: 50%;
2106
+ left: 50%;
2107
+ transform: translate(-50%, -50%);
2108
+ color: #ffffff;
2109
+ font-family: Arial, sans-serif;
2110
+ font-size: 12px;
2111
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
2112
+ `;
2113
+ valueText.textContent = `${Math.round(barData.value)}/${Math.round(barData.max_value)}`;
2114
+ barContainer.appendChild(valueText);
2115
+ }
2116
+
2117
+ container.appendChild(barContainer);
2118
+ uiContainer.appendChild(container);
2119
+ uiElements.set(id, container);
2120
+
2121
+ console.log('📊 UI bar rendered:', id);
2122
+ }
2123
+
2124
+ function handleRemoveUIElement(elementId) {
2125
+ if (uiElements.has(elementId)) {
2126
+ uiContainer.removeChild(uiElements.get(elementId));
2127
+ uiElements.delete(elementId);
2128
+ console.log('🗑️ UI element removed:', elementId);
2129
+ }
2130
+ }
2131
+
2132
+ // ==================== Toon Material Handler ====================
2133
+
2134
+ function handleUpdateToonMaterial(data) {
2135
+ const obj = scene.children.find(child =>
2136
+ child.userData.id === data.object_id ||
2137
+ child.userData.object_id === data.object_id
2138
+ );
2139
+
2140
+ if (!obj) {
2141
+ console.error('Object not found for toon material:', data.object_id);
2142
+ return;
2143
+ }
2144
+
2145
+ if (data.enabled !== false) {
2146
+ // Create toon material
2147
+ const existingColor = obj.material?.color?.getHex() || 0xffffff;
2148
+ const color = data.color ? new THREE.Color(data.color) : new THREE.Color(existingColor);
2149
+
2150
+ // Create gradient texture for toon shading
2151
+ const steps = data.gradient_steps || 3;
2152
+ const gradientMap = createToonGradientMap(steps);
2153
+
2154
+ const toonMaterial = new THREE.MeshToonMaterial({
2155
+ color: color,
2156
+ gradientMap: gradientMap
2157
+ });
2158
+
2159
+ // Dispose old material
2160
+ if (obj.material) obj.material.dispose();
2161
+ obj.material = toonMaterial;
2162
+
2163
+ // Store toon settings in userData for reference
2164
+ obj.userData.toonEnabled = true;
2165
+ obj.userData.toonSettings = data;
2166
+
2167
+ console.log('🎨 Toon material applied to:', data.object_id);
2168
+ } else {
2169
+ // Revert to standard material
2170
+ const existingColor = obj.material?.color?.getHex() || 0xffffff;
2171
+
2172
+ const standardMaterial = new THREE.MeshStandardMaterial({
2173
+ color: existingColor,
2174
+ roughness: 0.7,
2175
+ metalness: 0.0
2176
+ });
2177
+
2178
+ if (obj.material) obj.material.dispose();
2179
+ obj.material = standardMaterial;
2180
+ obj.userData.toonEnabled = false;
2181
+
2182
+ console.log('🎨 Reverted to standard material:', data.object_id);
2183
+ }
2184
+ }
2185
+
2186
+ function createToonGradientMap(steps) {
2187
+ const canvas = document.createElement('canvas');
2188
+ canvas.width = steps;
2189
+ canvas.height = 1;
2190
+ const ctx = canvas.getContext('2d');
2191
+
2192
+ for (let i = 0; i < steps; i++) {
2193
+ const value = Math.floor((i / (steps - 1)) * 255);
2194
+ ctx.fillStyle = `rgb(${value},${value},${value})`;
2195
+ ctx.fillRect(i, 0, 1, 1);
2196
+ }
2197
+
2198
+ const texture = new THREE.CanvasTexture(canvas);
2199
+ texture.minFilter = THREE.NearestFilter;
2200
+ texture.magFilter = THREE.NearestFilter;
2201
+
2202
+ return texture;
2203
+ }
2204
+
2205
+ // ==================== Brick Block Handler ====================
2206
+
2207
+ function handleAddBrick(brickData) {
2208
+ if (!scene || !sceneData) {
2209
+ console.error('Scene not initialized yet');
2210
+ return;
2211
+ }
2212
+
2213
+ const modelPath = brickData.model_path;
2214
+ const position = brickData.position || { x: 0, y: 0, z: 0 };
2215
+ const rotation = brickData.rotation || { x: 0, y: 0, z: 0 };
2216
+ const color = new THREE.Color(brickData.material?.color || '#ff0000');
2217
+
2218
+ // Load the GLTF model
2219
+ gltfLoader.load(
2220
+ modelPath,
2221
+ (gltf) => {
2222
+ const model = gltf.scene;
2223
+
2224
+ // Apply position
2225
+ model.position.set(position.x, position.y, position.z);
2226
+
2227
+ // Apply rotation (convert degrees to radians)
2228
+ model.rotation.set(
2229
+ THREE.MathUtils.degToRad(rotation.x),
2230
+ THREE.MathUtils.degToRad(rotation.y),
2231
+ THREE.MathUtils.degToRad(rotation.z)
2232
+ );
2233
+
2234
+ // Apply color to all meshes in the model
2235
+ model.traverse((child) => {
2236
+ if (child.isMesh) {
2237
+ child.material = new THREE.MeshStandardMaterial({
2238
+ color: color,
2239
+ metalness: brickData.material?.metalness || 0.1,
2240
+ roughness: brickData.material?.roughness || 0.7
2241
+ });
2242
+ child.castShadow = true;
2243
+ child.receiveShadow = true;
2244
+ }
2245
+ });
2246
+
2247
+ // Store metadata
2248
+ model.userData.id = brickData.id;
2249
+ model.userData.type = 'brick';
2250
+ model.userData.brick_type = brickData.brick_type;
2251
+ model.userData.name = brickData.name;
2252
+
2253
+ // Add to scene
2254
+ scene.add(model);
2255
+
2256
+ // Add to scene data for tracking
2257
+ if (!sceneData.objects) sceneData.objects = [];
2258
+ sceneData.objects.push(brickData);
2259
+
2260
+ console.log('🧱 Brick added:', brickData.brick_type, 'at', position);
2261
+ },
2262
+ (xhr) => {
2263
+ console.log(`Loading brick: ${(xhr.loaded / xhr.total * 100).toFixed(0)}%`);
2264
+ },
2265
+ (error) => {
2266
+ console.error('Error loading brick:', error);
2267
+ }
2268
+ );
2269
+ }
2270
+
2271
  // Start the application
2272
  init();
2273
  </script>