ArturoNereu commited on
Commit
7aa89e4
·
1 Parent(s): 69242aa

Improved lighting, removed debug text

Browse files
Files changed (4) hide show
  1. app.py +122 -86
  2. backend/game_models.py +34 -5
  3. backend/storage.py +68 -9
  4. frontend/game_viewer.html +114 -100
app.py CHANGED
@@ -242,15 +242,6 @@ APP_CSS = """
242
  border: none;
243
  }
244
 
245
- /* PostMessage Container - must be invisible */
246
- #postmessage-container {
247
- position: absolute;
248
- width: 0;
249
- height: 0;
250
- overflow: hidden;
251
- pointer-events: none;
252
- }
253
-
254
  /* Toast Notifications */
255
  #toast-container {
256
  position: fixed;
@@ -271,14 +262,50 @@ APP_CSS = """
271
  .toast.success { border-left-color: #4caf50; }
272
  .toast.error { border-left-color: #f44336; }
273
 
274
- /* Hide the action data textbox but keep it in DOM for events to fire */
275
- .hidden-action,
276
- .hidden-action textarea,
277
- .hidden-action input,
278
- .hidden-action label,
279
- .hidden-action .wrap,
280
- #action-data,
281
- #crosshair-pos {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  position: absolute !important;
283
  width: 1px !important;
284
  height: 1px !important;
@@ -295,6 +322,9 @@ APP_CSS = """
295
 
296
  # Build immersive chat interface with overlay
297
  with gr.Blocks(title="GCP - Game Context Protocol") as demo:
 
 
 
298
  # Initialize JavaScript functionality (minimal essentials only)
299
  gr.HTML("""
300
  <script>
@@ -372,13 +402,8 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
372
  }
373
 
374
  if (event.data && event.data.action === 'crosshairPosition') {
 
375
  window.crosshairPosition = event.data.data;
376
- // Update hidden Gradio textbox so Python can read it
377
- const crosshairInput = document.querySelector('#crosshair-pos textarea');
378
- if (crosshairInput) {
379
- crosshairInput.value = event.data.data ? JSON.stringify(event.data.data) : '';
380
- crosshairInput.dispatchEvent(new Event('input', { bubbles: true }));
381
- }
382
  }
383
  });
384
 
@@ -395,23 +420,11 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
395
  </script>
396
  """)
397
 
398
- # State for file handling
399
- file_state = gr.State([])
400
-
401
- # Component for passing action data to JavaScript via .change() event
402
- #
403
- # IMPORTANT: DO NOT set visible=False!
404
- # Gradio's visible=False removes the component from the DOM or renders it in a way
405
- # that prevents .change() events from firing when the value is updated programmatically.
406
- # Instead, we use visible=True and hide it with CSS (see .hidden-action in APP_CSS).
407
- # This keeps the element in the DOM so events fire properly.
408
- #
409
- # The flow: bot() returns JSON → action_data updates → .change() fires → JS sends postMessage to iframe
410
- action_data = gr.Textbox(value="", elem_id="action-data", visible=True, elem_classes=["hidden-action"])
411
 
412
- # Hidden textbox to receive crosshair position from JavaScript
413
- # JS updates this via: document.querySelector('#crosshair-pos textarea').value = JSON.stringify(pos)
414
- crosshair_pos = gr.Textbox(value="", elem_id="crosshair-pos", visible=True, elem_classes=["hidden-action"])
415
 
416
  # Main container - side by side layout: Chat (left) | Viewer (right)
417
  with gr.Row(elem_id="main-container", equal_height=True):
@@ -419,25 +432,20 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
419
  with gr.Column(elem_id="chat-column", scale=1, min_width=350):
420
  gr.Markdown("### 🎮 GCP - Game Context Protocol")
421
  chatbot = gr.Chatbot(
422
- height=450, # Slightly shorter to make room for provider dropdown
423
  show_label=False,
424
  elem_id="chatbot",
425
  )
426
- with gr.Row():
427
- msg = gr.Textbox(
428
- placeholder="'add a red cube' • 'set lighting to night' • 'help'",
429
- show_label=False,
430
- container=False,
431
- elem_id="chat-input",
432
- scale=4
433
- )
434
- provider_dropdown = gr.Dropdown(
435
- choices=["openai", "gemini"],
436
- value="openai",
437
- label="LLM",
438
- scale=1,
439
- min_width=100
440
- )
441
 
442
  # Right column: 3D Viewer (scale=3 = ~75% width)
443
  with gr.Column(elem_id="viewer-column", scale=3):
@@ -532,7 +540,7 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
532
  history.append({"role": "user", "content": user_message})
533
  return "", history
534
 
535
- def bot(history, crosshair_position, provider):
536
  """Generate bot response using selected LLM provider"""
537
  # Gradio 6: content can be a string or list of content blocks
538
  content = history[-1]["content"]
@@ -545,34 +553,25 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
545
  else:
546
  user_message = content
547
 
548
- # Parse crosshair position from JSON string
 
549
  crosshair_pos_dict = None
550
- if crosshair_position:
551
- try:
552
- crosshair_pos_dict = json.loads(crosshair_position)
553
- except:
554
- pass
555
 
556
  # Process command with crosshair context and selected provider
557
  bot_message, action_result = chat_response(user_message, [], crosshair_pos_dict, provider)
558
  history.append({"role": "assistant", "content": bot_message})
559
 
560
  # Handle action_result
561
- viewer_html = viewer.value # Default: keep current viewer
562
  action_json = "" # Default: no action (empty string)
563
 
564
  if action_result:
565
  action_type = action_result.get("action")
566
 
567
- if action_type == "reload":
568
- # Full reload: update iframe src
569
- 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>'
570
-
571
- else:
572
  # Generate toast message for the action
573
  toast_message = _get_toast_message(action_type, action_result.get("data", {}))
574
 
575
- # Send action to viewer via postMessage
576
  action_json = json.dumps({
577
  "action": action_result["action"],
578
  "data": action_result["data"],
@@ -581,18 +580,64 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
581
  "timestamp": time.time()
582
  })
583
 
584
- return history, viewer_html, action_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
- # When action_data changes, this handler sends postMessage to the iframe
587
- def handle_action_change(action_json):
588
- return action_json # Pass through for JS
 
 
 
 
 
 
 
589
 
 
590
  action_data.change(
591
- fn=handle_action_change,
592
  inputs=[action_data],
593
  outputs=[action_data],
594
  js="""
595
  (actionJson) => {
 
596
  if (actionJson && actionJson.length > 2) {
597
  try {
598
  const actionData = JSON.parse(actionJson);
@@ -605,9 +650,11 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
605
  if (actionData.toast && window.showToast) {
606
  window.showToast(actionData.toast, actionData.toastType || 'success');
607
  }
 
 
608
  }
609
  } catch (e) {
610
- // Silently ignore parse errors
611
  }
612
  }
613
  return actionJson;
@@ -615,17 +662,6 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
615
  """
616
  )
617
 
618
- msg.submit(
619
- user,
620
- [msg, chatbot],
621
- [msg, chatbot],
622
- queue=False
623
- ).then(
624
- bot,
625
- [chatbot, crosshair_pos, provider_dropdown],
626
- [chatbot, viewer, action_data]
627
- )
628
-
629
 
630
  if __name__ == "__main__":
631
  demo.queue()
 
242
  border: none;
243
  }
244
 
 
 
 
 
 
 
 
 
 
245
  /* Toast Notifications */
246
  #toast-container {
247
  position: fixed;
 
262
  .toast.success { border-left-color: #4caf50; }
263
  .toast.error { border-left-color: #f44336; }
264
 
265
+ /* LLM Toggle Buttons */
266
+ #llm-toggle-row {
267
+ margin-top: 8px;
268
+ gap: 0 !important;
269
+ }
270
+
271
+ .llm-btn {
272
+ border-radius: 0 !important;
273
+ border: 1px solid #444 !important;
274
+ background: #2a2a2a !important;
275
+ color: #888 !important;
276
+ font-size: 13px !important;
277
+ padding: 8px 16px !important;
278
+ min-height: 36px !important;
279
+ transition: all 0.2s ease !important;
280
+ }
281
+
282
+ .llm-btn:first-child {
283
+ border-radius: 6px 0 0 6px !important;
284
+ border-right: none !important;
285
+ }
286
+
287
+ .llm-btn:last-child {
288
+ border-radius: 0 6px 6px 0 !important;
289
+ }
290
+
291
+ .llm-btn:hover {
292
+ background: #3a3a3a !important;
293
+ color: #aaa !important;
294
+ }
295
+
296
+ .llm-btn-active {
297
+ background: #4a6fa5 !important;
298
+ color: #fff !important;
299
+ border-color: #5a7fb5 !important;
300
+ }
301
+
302
+ .llm-btn-active:hover {
303
+ background: #5a7fb5 !important;
304
+ color: #fff !important;
305
+ }
306
+
307
+ /* Hidden action textbox - must be in DOM but invisible for .change() to fire */
308
+ .hidden-action {
309
  position: absolute !important;
310
  width: 1px !important;
311
  height: 1px !important;
 
322
 
323
  # Build immersive chat interface with overlay
324
  with gr.Blocks(title="GCP - Game Context Protocol") as demo:
325
+ # Inject custom CSS via HTML style tag (Gradio 6 doesn't support css= parameter)
326
+ gr.HTML(f"<style>{APP_CSS}</style>")
327
+
328
  # Initialize JavaScript functionality (minimal essentials only)
329
  gr.HTML("""
330
  <script>
 
402
  }
403
 
404
  if (event.data && event.data.action === 'crosshairPosition') {
405
+ // Store crosshair position in window for JS access
406
  window.crosshairPosition = event.data.data;
 
 
 
 
 
 
407
  }
408
  });
409
 
 
420
  </script>
421
  """)
422
 
423
+ # State for crosshair position (received from JS via window.crosshairPosition)
424
+ crosshair_state = gr.State("")
 
 
 
 
 
 
 
 
 
 
 
425
 
426
+ # State for selected LLM provider
427
+ provider_state = gr.State("openai")
 
428
 
429
  # Main container - side by side layout: Chat (left) | Viewer (right)
430
  with gr.Row(elem_id="main-container", equal_height=True):
 
432
  with gr.Column(elem_id="chat-column", scale=1, min_width=350):
433
  gr.Markdown("### 🎮 GCP - Game Context Protocol")
434
  chatbot = gr.Chatbot(
435
+ height=480,
436
  show_label=False,
437
  elem_id="chatbot",
438
  )
439
+ msg = gr.Textbox(
440
+ placeholder="'add a red cube' • 'set lighting to night' • 'help'",
441
+ show_label=False,
442
+ container=False,
443
+ elem_id="chat-input",
444
+ )
445
+ # LLM Provider toggle buttons
446
+ with gr.Row(elem_id="llm-toggle-row"):
447
+ btn_openai = gr.Button("OpenAI", elem_id="btn-openai", elem_classes=["llm-btn", "llm-btn-active"], scale=1)
448
+ btn_gemini = gr.Button("Gemini", elem_id="btn-gemini", elem_classes=["llm-btn"], scale=1)
 
 
 
 
 
449
 
450
  # Right column: 3D Viewer (scale=3 = ~75% width)
451
  with gr.Column(elem_id="viewer-column", scale=3):
 
540
  history.append({"role": "user", "content": user_message})
541
  return "", history
542
 
543
+ def bot(history, provider):
544
  """Generate bot response using selected LLM provider"""
545
  # Gradio 6: content can be a string or list of content blocks
546
  content = history[-1]["content"]
 
553
  else:
554
  user_message = content
555
 
556
+ # Crosshair position is passed via JS (see js parameter on .then())
557
+ # It will be injected by the JS code that runs before this function
558
  crosshair_pos_dict = None
 
 
 
 
 
559
 
560
  # Process command with crosshair context and selected provider
561
  bot_message, action_result = chat_response(user_message, [], crosshair_pos_dict, provider)
562
  history.append({"role": "assistant", "content": bot_message})
563
 
564
  # Handle action_result
 
565
  action_json = "" # Default: no action (empty string)
566
 
567
  if action_result:
568
  action_type = action_result.get("action")
569
 
570
+ if action_type != "reload":
 
 
 
 
571
  # Generate toast message for the action
572
  toast_message = _get_toast_message(action_type, action_result.get("data", {}))
573
 
574
+ # Send action to viewer via postMessage (handled by JS in .then())
575
  action_json = json.dumps({
576
  "action": action_result["action"],
577
  "data": action_result["data"],
 
580
  "timestamp": time.time()
581
  })
582
 
583
+ return history, action_json
584
+
585
+ # Toggle button click handlers
586
+ def select_openai(current_provider):
587
+ return "openai"
588
+
589
+ def select_gemini(current_provider):
590
+ return "gemini"
591
+
592
+ btn_openai.click(
593
+ fn=select_openai,
594
+ inputs=[provider_state],
595
+ outputs=[provider_state],
596
+ js="""
597
+ (provider) => {
598
+ document.getElementById('btn-openai').classList.add('llm-btn-active');
599
+ document.getElementById('btn-gemini').classList.remove('llm-btn-active');
600
+ return provider;
601
+ }
602
+ """
603
+ )
604
+
605
+ btn_gemini.click(
606
+ fn=select_gemini,
607
+ inputs=[provider_state],
608
+ outputs=[provider_state],
609
+ js="""
610
+ (provider) => {
611
+ document.getElementById('btn-gemini').classList.add('llm-btn-active');
612
+ document.getElementById('btn-openai').classList.remove('llm-btn-active');
613
+ return provider;
614
+ }
615
+ """
616
+ )
617
+
618
+ # Hidden textbox to trigger JS via .change() event
619
+ # NOTE: Must be visible=True with CSS hiding, as visible=False prevents .change() from firing
620
+ action_data = gr.Textbox(value="", elem_id="action-data", visible=True, elem_classes=["hidden-action"])
621
 
622
+ msg.submit(
623
+ user,
624
+ [msg, chatbot],
625
+ [msg, chatbot],
626
+ queue=False
627
+ ).then(
628
+ bot,
629
+ [chatbot, provider_state],
630
+ [chatbot, action_data],
631
+ )
632
 
633
+ # When action_data changes, send postMessage to iframe
634
  action_data.change(
635
+ fn=lambda x: x,
636
  inputs=[action_data],
637
  outputs=[action_data],
638
  js="""
639
  (actionJson) => {
640
+ // Send postMessage to iframe if there's action data
641
  if (actionJson && actionJson.length > 2) {
642
  try {
643
  const actionData = JSON.parse(actionJson);
 
650
  if (actionData.toast && window.showToast) {
651
  window.showToast(actionData.toast, actionData.toastType || 'success');
652
  }
653
+ } else {
654
+ console.error('❌ iframe not found');
655
  }
656
  } catch (e) {
657
+ console.error('Failed to parse action:', e);
658
  }
659
  }
660
  return actionJson;
 
662
  """
663
  )
664
 
 
 
 
 
 
 
 
 
 
 
 
665
 
666
  if __name__ == "__main__":
667
  demo.queue()
backend/game_models.py CHANGED
@@ -9,7 +9,7 @@ import uuid
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"]
@@ -73,10 +73,27 @@ def create_light(
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,
@@ -84,9 +101,21 @@ def create_light(
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",
 
9
 
10
  # Game Types
11
  ObjectType = Literal["cube", "sphere", "cylinder", "plane", "cone", "torus", "model"]
12
+ LightType = Literal["ambient", "directional", "point", "spot", "hemisphere"]
13
  LightingPreset = Literal["day", "night", "sunset", "studio"]
14
  CameraMode = Literal["fps", "orbit", "top_down", "free"]
15
  MaterialType = Literal["standard", "basic", "phong", "toon"]
 
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
+ ground_color: Optional[str] = None, # For hemisphere lights
78
+ distance: Optional[float] = None, # For point/spot lights
79
+ decay: float = 2.0 # Physically correct decay for point lights
80
  ) -> Dict[str, Any]:
81
+ """
82
+ Create a light source.
83
+
84
+ Args:
85
+ light_type: Type of light (ambient, directional, point, spot, hemisphere)
86
+ name: Light name for identification
87
+ color: Light color in hex format (sky color for hemisphere)
88
+ intensity: Light intensity (0.0-2.0 typical, higher for specific effects)
89
+ position: Light position in 3D space
90
+ target: Target position for directional/spot lights
91
+ cast_shadow: Whether the light casts shadows (directional/spot only)
92
+ ground_color: Ground color for hemisphere lights (defaults to darker version of color)
93
+ distance: Maximum range for point/spot lights (0 = infinite)
94
+ decay: Light falloff rate for point/spot (2.0 = physically correct)
95
+ """
96
+ light = {
97
  "id": str(uuid.uuid4()),
98
  "name": name,
99
  "type": light_type,
 
101
  "intensity": intensity,
102
  "position": position or create_vector3(10, 10, 10),
103
  "target": target,
104
+ "cast_shadow": cast_shadow,
105
  }
106
 
107
+ # Add hemisphere-specific properties
108
+ if light_type == "hemisphere":
109
+ light["ground_color"] = ground_color or "#444444"
110
+
111
+ # Add point/spot-specific properties
112
+ if light_type in ["point", "spot"]:
113
+ if distance is not None:
114
+ light["distance"] = distance
115
+ light["decay"] = decay
116
+
117
+ return light
118
+
119
 
120
  def create_environment(
121
  background_color: str = "#87CEEB",
backend/storage.py CHANGED
@@ -57,20 +57,78 @@ def initialize_default_scene():
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
 
@@ -80,7 +138,7 @@ def initialize_default_scene():
80
  background_color="#1a0a20", # Dark purple (will be overridden by skybox)
81
  )
82
 
83
- # Hugging Face emoji model at center (animated)
84
  objects = [
85
  create_game_object(
86
  object_type="model",
@@ -88,7 +146,7 @@ def initialize_default_scene():
88
  position=create_vector3(0, 1.5, 0),
89
  scale=create_vector3(3, 3, 3),
90
  model_path="Norod78/huggingface_emoji.glb",
91
- metadata={"animate": True, "baseY": 1.5},
92
  ),
93
  ]
94
 
@@ -120,7 +178,8 @@ def initialize_default_scene():
120
  storage.save(scene)
121
  print(f"✓ Initialized Welcome Scene (ID: welcome)")
122
  print(f" - 25x25 world with sunset skybox")
123
- print(f" - Hugging Face emoji model at center")
 
124
  print(f" - FPS physics controller ready")
125
 
126
 
 
57
  create_game_object, create_material
58
  )
59
 
60
+ # Create lights using Three.js best practices:
61
+ # - Ambient: Low intensity (~0.1-0.2) to fake light bounce without washing out scene
62
+ # - Directional: Main sun light with warm color for outdoor feel
63
+ # - Secondary directional: Fill light from opposite side to soften shadows
64
+ # - Point lights: Distributed around arena for even coverage
65
  lights = [
66
+ # Main directional light (sun) - warm sunset color, positioned for dramatic shadows
67
  create_light(
68
  name="Sun",
69
  light_type="directional",
70
+ color="#ffd4a3", # Warm sunlight (less saturated orange)
71
+ intensity=1.5,
72
+ position=create_vector3(10, 20, 10),
73
+ cast_shadow=True,
74
  ),
75
+ # Fill light - cooler blue from opposite side to balance shadows
76
+ create_light(
77
+ name="FillLight",
78
+ light_type="directional",
79
+ color="#a3c4ff", # Cool blue fill
80
+ intensity=0.4,
81
+ position=create_vector3(-10, 15, -5),
82
+ ),
83
+ # Ambient light - very low intensity to simulate indirect light bounce
84
  create_light(
85
  name="Ambient",
86
  light_type="ambient",
87
+ color="#404060", # Slightly blue-ish for natural feel
88
+ intensity=0.2,
89
+ ),
90
+ # === Arena Point Lights (distributed for even coverage) ===
91
+ # Center light - main arena illumination
92
+ create_light(
93
+ name="ArenaCenter",
94
+ light_type="point",
95
+ color="#ffffff", # Neutral white
96
+ intensity=1.0,
97
+ position=create_vector3(0, 10, 0),
98
+ distance=30,
99
+ ),
100
+ # Corner lights - warm tones for cozy feel
101
+ create_light(
102
+ name="ArenaCornerNE",
103
+ light_type="point",
104
+ color="#ffcc88", # Warm
105
+ intensity=0.6,
106
+ position=create_vector3(8, 6, 8),
107
+ distance=20,
108
+ ),
109
+ create_light(
110
+ name="ArenaCornerNW",
111
+ light_type="point",
112
+ color="#ffcc88", # Warm
113
+ intensity=0.6,
114
+ position=create_vector3(-8, 6, 8),
115
+ distance=20,
116
+ ),
117
+ create_light(
118
+ name="ArenaCornerSE",
119
+ light_type="point",
120
+ color="#88ccff", # Cool blue
121
+ intensity=0.5,
122
+ position=create_vector3(8, 6, -8),
123
+ distance=20,
124
+ ),
125
+ create_light(
126
+ name="ArenaCornerSW",
127
+ light_type="point",
128
+ color="#88ccff", # Cool blue
129
+ intensity=0.5,
130
+ position=create_vector3(-8, 6, -8),
131
+ distance=20,
132
  ),
133
  ]
134
 
 
138
  background_color="#1a0a20", # Dark purple (will be overridden by skybox)
139
  )
140
 
141
+ # Hugging Face emoji model at center (animated + unlit for true colors)
142
  objects = [
143
  create_game_object(
144
  object_type="model",
 
146
  position=create_vector3(0, 1.5, 0),
147
  scale=create_vector3(3, 3, 3),
148
  model_path="Norod78/huggingface_emoji.glb",
149
+ metadata={"animate": True, "baseY": 1.5, "unlit": True},
150
  ),
151
  ]
152
 
 
178
  storage.save(scene)
179
  print(f"✓ Initialized Welcome Scene (ID: welcome)")
180
  print(f" - 25x25 world with sunset skybox")
181
+ print(f" - Hugging Face emoji model at center (unlit)")
182
+ print(f" - 8-light setup: Sun + Fill + Ambient + 5 arena point lights")
183
  print(f" - FPS physics controller ready")
184
 
185
 
frontend/game_viewer.html CHANGED
@@ -92,10 +92,6 @@
92
  const urlParams = new URLSearchParams(window.location.search);
93
  const initialMode = urlParams.get('mode') || 'fps'; // Default to FPS mode
94
 
95
- console.log('3D Game Viewer - Initializing...');
96
- console.log('Scene ID:', sceneId);
97
- console.log('Fetch URL:', `${baseUrl}/api/scenes/${sceneId}`);
98
- console.log('Initial control mode:', initialMode);
99
 
100
  let scene, camera, renderer;
101
  let orbitControls;
@@ -166,93 +162,27 @@
166
  * Allows MCP tools to customize player controller behavior
167
  */
168
  if (!sceneData || !sceneData.player_config) {
169
- console.log('No player_config in scene data, using defaults');
170
  return;
171
  }
172
 
173
  const config = sceneData.player_config;
174
 
175
  // Apply movement settings
176
- if (config.move_speed !== undefined) {
177
- moveSpeed = config.move_speed;
178
- console.log(`Applied player speed: ${moveSpeed} units/sec`);
179
- }
180
-
181
- if (config.jump_force !== undefined) {
182
- JUMP_FORCE = config.jump_force;
183
- console.log(`Applied jump force: ${JUMP_FORCE} m/s`);
184
- }
185
-
186
- // Apply camera settings
187
- if (config.mouse_sensitivity !== undefined) {
188
- mouseSensitivity = config.mouse_sensitivity;
189
- console.log(`Applied mouse sensitivity: ${mouseSensitivity}`);
190
- }
191
-
192
- if (config.invert_y !== undefined) {
193
- invertY = config.invert_y;
194
- console.log(`Applied Y-axis inversion: ${invertY}`);
195
- }
196
-
197
- // Apply physics settings
198
- if (config.gravity !== undefined) {
199
- GRAVITY = config.gravity;
200
- console.log(`Applied gravity: ${GRAVITY} m/s²`);
201
- }
202
-
203
- // Apply player dimensions
204
- if (config.player_height !== undefined) {
205
- PLAYER_HEIGHT = config.player_height;
206
- console.log(`Applied player height: ${PLAYER_HEIGHT}m`);
207
- }
208
-
209
- if (config.player_radius !== undefined) {
210
- PLAYER_RADIUS = config.player_radius;
211
- console.log(`Applied player radius: ${PLAYER_RADIUS}m`);
212
- }
213
-
214
- if (config.eye_height !== undefined) {
215
- EYE_HEIGHT = config.eye_height;
216
- console.log(`Applied eye height: ${EYE_HEIGHT}m`);
217
- }
218
-
219
- if (config.player_mass !== undefined) {
220
- PLAYER_MASS = config.player_mass;
221
- console.log(`Applied player mass: ${PLAYER_MASS}kg`);
222
- }
223
-
224
- if (config.linear_damping !== undefined) {
225
- LINEAR_DAMPING = config.linear_damping;
226
- console.log(`Applied linear damping: ${LINEAR_DAMPING}`);
227
- }
228
-
229
- // Apply Phase 2 settings
230
- if (config.movement_acceleration !== undefined) {
231
- movementAcceleration = config.movement_acceleration;
232
- console.log(`Applied movement acceleration: ${movementAcceleration}`);
233
- }
234
-
235
- if (config.air_control !== undefined) {
236
- airControl = config.air_control;
237
- console.log(`Applied air control: ${airControl}`);
238
- }
239
-
240
- if (config.camera_fov !== undefined) {
241
- cameraFOV = config.camera_fov;
242
- console.log(`Applied camera FOV: ${cameraFOV}°`);
243
- }
244
-
245
- if (config.min_pitch !== undefined) {
246
- minPitch = config.min_pitch;
247
- console.log(`Applied min pitch: ${minPitch}°`);
248
- }
249
-
250
- if (config.max_pitch !== undefined) {
251
- maxPitch = config.max_pitch;
252
- console.log(`Applied max pitch: ${maxPitch}°`);
253
- }
254
-
255
- console.log('✅ Player configuration applied successfully');
256
  }
257
 
258
  function applyInitialEnvironment() {
@@ -264,14 +194,12 @@
264
 
265
  // Apply skybox if defined in scene data
266
  if (sceneData.skybox) {
267
- console.log('Applying initial skybox:', sceneData.skybox);
268
  handleAddSkybox(sceneData.skybox);
269
  }
270
 
271
  // Apply particles if defined in scene data
272
  if (sceneData.particles && Array.isArray(sceneData.particles)) {
273
  sceneData.particles.forEach(particleConfig => {
274
- console.log('Applying initial particles:', particleConfig);
275
  handleAddParticles(particleConfig);
276
  });
277
  }
@@ -280,29 +208,22 @@
280
  if (sceneData.ui_elements && Array.isArray(sceneData.ui_elements)) {
281
  sceneData.ui_elements.forEach(uiConfig => {
282
  if (uiConfig.type === 'text') {
283
- console.log('Applying initial UI text:', uiConfig);
284
  handleRenderText(uiConfig);
285
  } else if (uiConfig.type === 'bar') {
286
- console.log('Applying initial UI bar:', uiConfig);
287
  handleRenderBar(uiConfig);
288
  }
289
  });
290
  }
291
-
292
- console.log('✅ Initial environment applied');
293
  }
294
 
295
  async function init() {
296
  try {
297
  // Check for embedded scene data first (used when served via Gradio)
298
  if (window.SCENE_DATA) {
299
- console.log('Using embedded scene data');
300
  sceneData = window.SCENE_DATA;
301
  } else {
302
  // Fetch scene data from API (used when served via FastAPI)
303
- console.log('Fetching scene data...');
304
  const response = await fetch(`${baseUrl}/api/scenes/${sceneId}`);
305
- console.log('Response status:', response.status);
306
 
307
  if (!response.ok) {
308
  const errorText = await response.text();
@@ -360,10 +281,18 @@
360
  // Position camera at player eye height (will be synced with physics in animate loop)
361
  camera.position.set(0, EYE_HEIGHT, 0);
362
 
363
- // Create renderer
364
  renderer = new THREE.WebGLRenderer({ antialias: true });
365
  renderer.setSize(window.innerWidth, window.innerHeight);
366
  renderer.setPixelRatio(window.devicePixelRatio);
 
 
 
 
 
 
 
 
367
  document.getElementById('viewer-container').appendChild(renderer.domElement);
368
 
369
  // Setup postprocessing for object outlines
@@ -389,6 +318,7 @@
389
  console.log('PostProcessing composer initialized with OutlinePass');
390
 
391
  // Add lights from scene data
 
392
  sceneData.lights.forEach(lightData => {
393
  let light;
394
 
@@ -402,15 +332,37 @@
402
  scene.add(light.target);
403
  if (lightData.cast_shadow) {
404
  light.castShadow = true;
 
 
 
 
 
 
 
 
 
 
405
  }
406
  } else if (lightData.type === 'point') {
407
- light = new THREE.PointLight(lightData.color, lightData.intensity);
 
 
 
408
  light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
 
 
 
 
 
 
 
 
409
  }
410
 
411
  if (light) {
 
412
  scene.add(light);
413
- console.log('Added light:', lightData.type, lightData.name, 'at', lightData.position || 'default');
414
  }
415
  });
416
 
@@ -850,6 +802,39 @@
850
  THREE.MathUtils.degToRad(obj.rotation.y),
851
  THREE.MathUtils.degToRad(obj.rotation.z)
852
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
  model.userData = {
854
  id: obj.id,
855
  name: obj.name,
@@ -857,6 +842,7 @@
857
  isSceneObject: true,
858
  animate: obj.metadata?.animate || false,
859
  baseY: obj.metadata?.baseY || obj.position.y,
 
860
  };
861
  scene.add(model);
862
 
@@ -1729,8 +1715,24 @@
1729
  light.target.position.set(lightData.target.x, lightData.target.y, lightData.target.z);
1730
  scene.add(light.target);
1731
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1732
  } else if (lightData.light_type === 'point') {
1733
- light = new THREE.PointLight(lightData.color, lightData.intensity);
 
 
 
1734
  light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
1735
  } else if (lightData.light_type === 'spot') {
1736
  light = new THREE.SpotLight(lightData.color, lightData.intensity);
@@ -1740,13 +1742,25 @@
1740
  light.target.position.set(lightData.target.x, lightData.target.y, lightData.target.z);
1741
  scene.add(light.target);
1742
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1743
  }
1744
 
1745
  if (light) {
1746
  light.name = lightData.name;
1747
- if (lightData.cast_shadow) light.castShadow = true;
1748
  scene.add(light);
1749
- console.log('💡 Light added to scene:', lightData.name, 'type:', lightData.light_type);
1750
  } else {
1751
  console.error('❌ Failed to create light:', lightData);
1752
  }
 
92
  const urlParams = new URLSearchParams(window.location.search);
93
  const initialMode = urlParams.get('mode') || 'fps'; // Default to FPS mode
94
 
 
 
 
 
95
 
96
  let scene, camera, renderer;
97
  let orbitControls;
 
162
  * Allows MCP tools to customize player controller behavior
163
  */
164
  if (!sceneData || !sceneData.player_config) {
 
165
  return;
166
  }
167
 
168
  const config = sceneData.player_config;
169
 
170
  // Apply movement settings
171
+ if (config.move_speed !== undefined) moveSpeed = config.move_speed;
172
+ if (config.jump_force !== undefined) JUMP_FORCE = config.jump_force;
173
+ if (config.mouse_sensitivity !== undefined) mouseSensitivity = config.mouse_sensitivity;
174
+ if (config.invert_y !== undefined) invertY = config.invert_y;
175
+ if (config.gravity !== undefined) GRAVITY = config.gravity;
176
+ if (config.player_height !== undefined) PLAYER_HEIGHT = config.player_height;
177
+ if (config.player_radius !== undefined) PLAYER_RADIUS = config.player_radius;
178
+ if (config.eye_height !== undefined) EYE_HEIGHT = config.eye_height;
179
+ if (config.player_mass !== undefined) PLAYER_MASS = config.player_mass;
180
+ if (config.linear_damping !== undefined) LINEAR_DAMPING = config.linear_damping;
181
+ if (config.movement_acceleration !== undefined) movementAcceleration = config.movement_acceleration;
182
+ if (config.air_control !== undefined) airControl = config.air_control;
183
+ if (config.camera_fov !== undefined) cameraFOV = config.camera_fov;
184
+ if (config.min_pitch !== undefined) minPitch = config.min_pitch;
185
+ if (config.max_pitch !== undefined) maxPitch = config.max_pitch;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
 
188
  function applyInitialEnvironment() {
 
194
 
195
  // Apply skybox if defined in scene data
196
  if (sceneData.skybox) {
 
197
  handleAddSkybox(sceneData.skybox);
198
  }
199
 
200
  // Apply particles if defined in scene data
201
  if (sceneData.particles && Array.isArray(sceneData.particles)) {
202
  sceneData.particles.forEach(particleConfig => {
 
203
  handleAddParticles(particleConfig);
204
  });
205
  }
 
208
  if (sceneData.ui_elements && Array.isArray(sceneData.ui_elements)) {
209
  sceneData.ui_elements.forEach(uiConfig => {
210
  if (uiConfig.type === 'text') {
 
211
  handleRenderText(uiConfig);
212
  } else if (uiConfig.type === 'bar') {
 
213
  handleRenderBar(uiConfig);
214
  }
215
  });
216
  }
 
 
217
  }
218
 
219
  async function init() {
220
  try {
221
  // Check for embedded scene data first (used when served via Gradio)
222
  if (window.SCENE_DATA) {
 
223
  sceneData = window.SCENE_DATA;
224
  } else {
225
  // Fetch scene data from API (used when served via FastAPI)
 
226
  const response = await fetch(`${baseUrl}/api/scenes/${sceneId}`);
 
227
 
228
  if (!response.ok) {
229
  const errorText = await response.text();
 
281
  // Position camera at player eye height (will be synced with physics in animate loop)
282
  camera.position.set(0, EYE_HEIGHT, 0);
283
 
284
+ // Create renderer with shadow support
285
  renderer = new THREE.WebGLRenderer({ antialias: true });
286
  renderer.setSize(window.innerWidth, window.innerHeight);
287
  renderer.setPixelRatio(window.devicePixelRatio);
288
+ // Enable shadows for realistic lighting
289
+ renderer.shadowMap.enabled = true;
290
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadows
291
+ // Use physically correct lighting model
292
+ renderer.physicallyCorrectLights = true;
293
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
294
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
295
+ renderer.toneMappingExposure = 1.0;
296
  document.getElementById('viewer-container').appendChild(renderer.domElement);
297
 
298
  // Setup postprocessing for object outlines
 
318
  console.log('PostProcessing composer initialized with OutlinePass');
319
 
320
  // Add lights from scene data
321
+ // Best practices: Combine ambient (low intensity) + directional (sun) + point lights (accents)
322
  sceneData.lights.forEach(lightData => {
323
  let light;
324
 
 
332
  scene.add(light.target);
333
  if (lightData.cast_shadow) {
334
  light.castShadow = true;
335
+ // Configure shadow map for better quality
336
+ light.shadow.mapSize.width = 2048;
337
+ light.shadow.mapSize.height = 2048;
338
+ light.shadow.camera.near = 0.5;
339
+ light.shadow.camera.far = 100;
340
+ light.shadow.camera.left = -30;
341
+ light.shadow.camera.right = 30;
342
+ light.shadow.camera.top = 30;
343
+ light.shadow.camera.bottom = -30;
344
+ light.shadow.bias = -0.0001;
345
  }
346
  } else if (lightData.type === 'point') {
347
+ // Point lights with distance and decay for realistic falloff
348
+ const distance = lightData.distance || 50;
349
+ const decay = lightData.decay || 2; // Physically correct decay
350
+ light = new THREE.PointLight(lightData.color, lightData.intensity, distance, decay);
351
  light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
352
+ } else if (lightData.type === 'hemisphere') {
353
+ // Hemisphere light - great for outdoor scenes (sky + ground colors)
354
+ const skyColor = lightData.color || '#87CEEB';
355
+ const groundColor = lightData.ground_color || '#444444';
356
+ light = new THREE.HemisphereLight(skyColor, groundColor, lightData.intensity);
357
+ if (lightData.position) {
358
+ light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
359
+ }
360
  }
361
 
362
  if (light) {
363
+ light.name = lightData.name || lightData.type;
364
  scene.add(light);
365
+ console.log('Added light:', lightData.type, lightData.name, 'intensity:', lightData.intensity);
366
  }
367
  });
368
 
 
802
  THREE.MathUtils.degToRad(obj.rotation.y),
803
  THREE.MathUtils.degToRad(obj.rotation.z)
804
  );
805
+
806
+ // Apply unlit material if specified in metadata
807
+ // This makes the model render at its true colors without lighting
808
+ if (obj.metadata?.unlit) {
809
+ model.traverse((child) => {
810
+ if (child.isMesh && child.material) {
811
+ // Preserve the original texture/color but make it unlit
812
+ const oldMaterial = child.material;
813
+ const newMaterial = new THREE.MeshBasicMaterial();
814
+
815
+ // Copy texture if exists
816
+ if (oldMaterial.map) {
817
+ newMaterial.map = oldMaterial.map;
818
+ }
819
+ // Copy color if no texture
820
+ if (oldMaterial.color) {
821
+ newMaterial.color = oldMaterial.color.clone();
822
+ }
823
+ // Preserve transparency
824
+ if (oldMaterial.transparent) {
825
+ newMaterial.transparent = true;
826
+ newMaterial.opacity = oldMaterial.opacity;
827
+ }
828
+ if (oldMaterial.alphaMap) {
829
+ newMaterial.alphaMap = oldMaterial.alphaMap;
830
+ }
831
+
832
+ child.material = newMaterial;
833
+ }
834
+ });
835
+ console.log(`Applied unlit material to model: ${obj.name}`);
836
+ }
837
+
838
  model.userData = {
839
  id: obj.id,
840
  name: obj.name,
 
842
  isSceneObject: true,
843
  animate: obj.metadata?.animate || false,
844
  baseY: obj.metadata?.baseY || obj.position.y,
845
+ unlit: obj.metadata?.unlit || false,
846
  };
847
  scene.add(model);
848
 
 
1715
  light.target.position.set(lightData.target.x, lightData.target.y, lightData.target.z);
1716
  scene.add(light.target);
1717
  }
1718
+ if (lightData.cast_shadow) {
1719
+ light.castShadow = true;
1720
+ // Configure shadow map for better quality
1721
+ light.shadow.mapSize.width = 2048;
1722
+ light.shadow.mapSize.height = 2048;
1723
+ light.shadow.camera.near = 0.5;
1724
+ light.shadow.camera.far = 100;
1725
+ light.shadow.camera.left = -30;
1726
+ light.shadow.camera.right = 30;
1727
+ light.shadow.camera.top = 30;
1728
+ light.shadow.camera.bottom = -30;
1729
+ light.shadow.bias = -0.0001;
1730
+ }
1731
  } else if (lightData.light_type === 'point') {
1732
+ // Point lights with distance and decay for realistic falloff
1733
+ const distance = lightData.distance || 50;
1734
+ const decay = lightData.decay || 2;
1735
+ light = new THREE.PointLight(lightData.color, lightData.intensity, distance, decay);
1736
  light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
1737
  } else if (lightData.light_type === 'spot') {
1738
  light = new THREE.SpotLight(lightData.color, lightData.intensity);
 
1742
  light.target.position.set(lightData.target.x, lightData.target.y, lightData.target.z);
1743
  scene.add(light.target);
1744
  }
1745
+ if (lightData.cast_shadow) {
1746
+ light.castShadow = true;
1747
+ light.shadow.mapSize.width = 1024;
1748
+ light.shadow.mapSize.height = 1024;
1749
+ }
1750
+ } else if (lightData.light_type === 'hemisphere') {
1751
+ // Hemisphere light - great for outdoor scenes (sky + ground colors)
1752
+ const skyColor = lightData.color || '#87CEEB';
1753
+ const groundColor = lightData.ground_color || '#444444';
1754
+ light = new THREE.HemisphereLight(skyColor, groundColor, lightData.intensity);
1755
+ if (lightData.position) {
1756
+ light.position.set(lightData.position.x, lightData.position.y, lightData.position.z);
1757
+ }
1758
  }
1759
 
1760
  if (light) {
1761
  light.name = lightData.name;
 
1762
  scene.add(light);
1763
+ console.log('💡 Light added to scene:', lightData.name, 'type:', lightData.light_type, 'intensity:', lightData.intensity);
1764
  } else {
1765
  console.error('❌ Failed to create light:', lightData);
1766
  }