chingshuai commited on
Commit
5b6aa4b
·
1 Parent(s): bcc06ab

更新静态页面生成逻辑

Browse files
hymotion/utils/t2m_runtime.py CHANGED
@@ -10,7 +10,7 @@ import yaml
10
 
11
  from ..prompt_engineering.prompt_rewrite import PromptRewriter
12
  from .loaders import load_object
13
- from .visualize_mesh_web import save_visualization_data
14
 
15
  try:
16
  import fbx
@@ -75,12 +75,10 @@ class T2MRuntime:
75
 
76
  self.prompt_rewriter = PromptRewriter(backend="our_rewriter", host=self.prompt_engineering_host)
77
 
78
- # Skip model loading if checkpoint not found
79
  if self.skip_model_loading:
80
- print(">>> [WARNING] Model loading skipped - checkpoint not found")
81
- self._loaded = True # Mark as loaded to prevent further load attempts
82
- else:
83
- self.load()
84
  self.fbx_available = FBX_AVAILABLE
85
  if self.fbx_available:
86
  try:
@@ -97,7 +95,7 @@ class T2MRuntime:
97
 
98
  device_info = self.device_ids if self.device_ids else 'cpu'
99
  if self.skip_model_loading:
100
- print(f">>> T2MRuntime initialized (model NOT loaded) in IP {self.local_ip}, devices={device_info}")
101
  else:
102
  print(f">>> T2MRuntime loaded in IP {self.local_ip}, devices={device_info}")
103
 
@@ -109,6 +107,9 @@ class T2MRuntime:
109
  with open(self.config_path, "r") as f:
110
  config = yaml.load(f, Loader=yaml.FullLoader)
111
 
 
 
 
112
  if not self.device_ids:
113
  pipeline = load_object(
114
  config["train_pipeline"],
@@ -118,7 +119,7 @@ class T2MRuntime:
118
  )
119
  device = torch.device("cpu")
120
  pipeline.load_in_demo(
121
- self.ckpt_name, os.path.dirname(self.ckpt_name), build_text_encoder=not self.skip_text
122
  )
123
  pipeline.to(device)
124
  self.pipelines = [pipeline]
@@ -131,7 +132,7 @@ class T2MRuntime:
131
  network_module=config["network_module"],
132
  network_module_args=config["network_module_args"],
133
  )
134
- p.load_in_demo(self.ckpt_name, os.path.dirname(self.ckpt_name), build_text_encoder=not self.skip_text)
135
  p.to(torch.device(f"cuda:{gid}"))
136
  self.pipelines.append(p)
137
  self._gpu_load = [0] * len(self.pipelines)
@@ -260,13 +261,6 @@ class T2MRuntime:
260
  original_text: Optional[str] = None,
261
  use_special_game_feat: bool = False,
262
  ) -> Tuple[Union[str, list[str]], dict]:
263
- # Check if model was skipped due to missing checkpoint
264
- if self.skip_model_loading:
265
- raise RuntimeError(
266
- "Motion generation is not available: model checkpoint was not found. "
267
- "Please ensure the checkpoint file exists at the specified path."
268
- )
269
-
270
  self.load()
271
  seeds = [int(s.strip()) for s in seeds_csv.split(",") if s.strip() != ""]
272
  pi = self._acquire_pipeline()
@@ -318,8 +312,8 @@ class T2MRuntime:
318
  )
319
 
320
  if output_format == "fbx" and not self.fbx_available:
321
- print(">>> Warning: FBX export requested but FBX SDK is not available. Falling back to html.")
322
- output_format = "html"
323
 
324
  if output_format == "fbx" and self.fbx_available:
325
  fbx_files = self._generate_fbx_files(
@@ -328,6 +322,9 @@ class T2MRuntime:
328
  fbx_filename=output_filename,
329
  )
330
  return view_url, fbx_files, model_output
 
 
 
331
  else:
332
  raise ValueError(f">>> Invalid output format: {output_format}")
333
 
@@ -337,10 +334,46 @@ class T2MRuntime:
337
  file_path: str,
338
  output_dir: Optional[str] = None,
339
  ) -> str:
340
- print(f">>> HTML ready, timestamp: {timestamp}")
 
 
 
 
 
 
 
 
 
 
 
 
341
  gradio_dir = output_dir if output_dir is not None else "output/gradio"
342
- view_url = f"/view/{gradio_dir}/{file_path}"
343
- return view_url
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
  def _generate_fbx_files(
346
  self,
 
10
 
11
  from ..prompt_engineering.prompt_rewrite import PromptRewriter
12
  from .loaders import load_object
13
+ from .visualize_mesh_web import save_visualization_data, generate_static_html
14
 
15
  try:
16
  import fbx
 
75
 
76
  self.prompt_rewriter = PromptRewriter(backend="our_rewriter", host=self.prompt_engineering_host)
77
 
78
+ # Load model (with or without checkpoint)
79
  if self.skip_model_loading:
80
+ print(">>> [WARNING] Checkpoint not found, will use randomly initialized model weights")
81
+ self.load()
 
 
82
  self.fbx_available = FBX_AVAILABLE
83
  if self.fbx_available:
84
  try:
 
95
 
96
  device_info = self.device_ids if self.device_ids else 'cpu'
97
  if self.skip_model_loading:
98
+ print(f">>> T2MRuntime initialized (using randomly initialized weights) in IP {self.local_ip}, devices={device_info}")
99
  else:
100
  print(f">>> T2MRuntime loaded in IP {self.local_ip}, devices={device_info}")
101
 
 
107
  with open(self.config_path, "r") as f:
108
  config = yaml.load(f, Loader=yaml.FullLoader)
109
 
110
+ # Use allow_empty_ckpt=True when skip_model_loading is True
111
+ allow_empty_ckpt = self.skip_model_loading
112
+
113
  if not self.device_ids:
114
  pipeline = load_object(
115
  config["train_pipeline"],
 
119
  )
120
  device = torch.device("cpu")
121
  pipeline.load_in_demo(
122
+ self.ckpt_name, os.path.dirname(self.ckpt_name), build_text_encoder=not self.skip_text, allow_empty_ckpt=allow_empty_ckpt
123
  )
124
  pipeline.to(device)
125
  self.pipelines = [pipeline]
 
132
  network_module=config["network_module"],
133
  network_module_args=config["network_module_args"],
134
  )
135
+ p.load_in_demo(self.ckpt_name, os.path.dirname(self.ckpt_name), build_text_encoder=not self.skip_text, allow_empty_ckpt=allow_empty_ckpt)
136
  p.to(torch.device(f"cuda:{gid}"))
137
  self.pipelines.append(p)
138
  self._gpu_load = [0] * len(self.pipelines)
 
261
  original_text: Optional[str] = None,
262
  use_special_game_feat: bool = False,
263
  ) -> Tuple[Union[str, list[str]], dict]:
 
 
 
 
 
 
 
264
  self.load()
265
  seeds = [int(s.strip()) for s in seeds_csv.split(",") if s.strip() != ""]
266
  pi = self._acquire_pipeline()
 
312
  )
313
 
314
  if output_format == "fbx" and not self.fbx_available:
315
+ print(">>> Warning: FBX export requested but FBX SDK is not available. Falling back to dict format.")
316
+ output_format = "dict"
317
 
318
  if output_format == "fbx" and self.fbx_available:
319
  fbx_files = self._generate_fbx_files(
 
322
  fbx_filename=output_filename,
323
  )
324
  return view_url, fbx_files, model_output
325
+ elif output_format == "dict":
326
+ # Return HTML URL and empty list for fbx_files when using dict format
327
+ return view_url, [], model_output
328
  else:
329
  raise ValueError(f">>> Invalid output format: {output_format}")
330
 
 
334
  file_path: str,
335
  output_dir: Optional[str] = None,
336
  ) -> str:
337
+ """
338
+ Generate a static HTML file with embedded data and return the URL for iframe.
339
+ All JavaScript code is embedded directly in the HTML, no external static resources needed.
340
+
341
+ Args:
342
+ timestamp: Timestamp string for logging
343
+ file_path: Base filename (without extension)
344
+ output_dir: Directory where NPZ/meta files are stored and HTML will be saved
345
+
346
+ Returns:
347
+ URL path for the static HTML file (to be used in iframe src)
348
+ """
349
+ print(f">>> Generating static HTML, timestamp: {timestamp}")
350
  gradio_dir = output_dir if output_dir is not None else "output/gradio"
351
+
352
+ try:
353
+ # Generate static HTML file with embedded data (all JS is embedded in template)
354
+ output_html_path = generate_static_html(
355
+ folder_name=gradio_dir,
356
+ file_name=file_path,
357
+ output_dir=gradio_dir,
358
+ hide_captions=False,
359
+ )
360
+
361
+ # The HTML file is saved in output_dir (e.g., output/gradio)
362
+ # We mount output/gradio at /output, so the URL is /output/{filename}_vis.html
363
+ html_filename = f"{file_path}_vis.html"
364
+ view_url = f"/output/{html_filename}"
365
+
366
+ print(f">>> Static HTML generated: {output_html_path}")
367
+ print(f">>> View URL: {view_url}")
368
+ return view_url
369
+
370
+ except Exception as e:
371
+ print(f">>> Failed to generate static HTML: {e}")
372
+ import traceback
373
+ traceback.print_exc()
374
+ # Fallback to old dynamic route
375
+ view_url = f"/view/{gradio_dir}/{file_path}"
376
+ return view_url
377
 
378
  def _generate_fbx_files(
379
  self,
hymotion/utils/visualize_mesh_web.py CHANGED
@@ -10,6 +10,12 @@ from torch import Tensor
10
 
11
  _FILE_ACCESS_LOCK = threading.Lock()
12
 
 
 
 
 
 
 
13
 
14
  def sanitize_filename(filename: str) -> str:
15
  """
@@ -340,3 +346,94 @@ def get_cached_smpl_frames(folder_name: str, file_name: str) -> List[list]:
340
  combined_frames.append(frame_content)
341
 
342
  return combined_frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  _FILE_ACCESS_LOCK = threading.Lock()
12
 
13
+ # Template directory path
14
+ _TEMPLATE_DIR = os.path.join(
15
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
16
+ "scripts", "gradio", "templates"
17
+ )
18
+
19
 
20
  def sanitize_filename(filename: str) -> str:
21
  """
 
346
  combined_frames.append(frame_content)
347
 
348
  return combined_frames
349
+
350
+
351
+ def generate_static_html(
352
+ folder_name: str,
353
+ file_name: str,
354
+ output_dir: str,
355
+ hide_captions: bool = False,
356
+ ) -> str:
357
+ """
358
+ Generate a static HTML file with embedded SMPL data and captions.
359
+ All JavaScript code is embedded directly in the HTML template,
360
+ so no external static resources are needed.
361
+
362
+ Args:
363
+ folder_name: The folder name containing the NPZ/meta files
364
+ file_name: The base file name (without extension)
365
+ output_dir: Directory to save the generated HTML file
366
+ hide_captions: Whether to hide captions in the visualization
367
+
368
+ Returns:
369
+ The path to the generated HTML file
370
+ """
371
+ # Load SMPL data
372
+ smpl_frames = get_cached_smpl_frames(folder_name, file_name)
373
+ if not smpl_frames:
374
+ raise ValueError(f"No SMPL data found for {folder_name}/{file_name}")
375
+
376
+ # Load captions
377
+ captions = []
378
+ if not hide_captions:
379
+ captions = get_cached_captions(folder_name, file_name)
380
+
381
+ # Generate caption HTML
382
+ caption_html = _generate_caption_html(captions, hide_captions)
383
+
384
+ # Convert SMPL data to JSON
385
+ smpl_data_json = json.dumps(smpl_frames, ensure_ascii=False)
386
+
387
+ # Load template
388
+ template_path = os.path.join(_TEMPLATE_DIR, "index_wooden_static.html")
389
+ with open(template_path, "r", encoding="utf-8") as f:
390
+ template_content = f.read()
391
+
392
+ # Replace placeholders with actual data
393
+ html_content = template_content.replace("{{ smpl_data_json }}", smpl_data_json)
394
+ html_content = html_content.replace("{{ caption_html }}", caption_html)
395
+
396
+ # Generate output path
397
+ os.makedirs(output_dir, exist_ok=True)
398
+ output_html_path = os.path.join(output_dir, f"{file_name}_vis.html")
399
+
400
+ # Write HTML file
401
+ with _FILE_ACCESS_LOCK:
402
+ with open(output_html_path, "w", encoding="utf-8") as f:
403
+ f.write(html_content)
404
+
405
+ print(f">>> Generated static HTML: {output_html_path}")
406
+ return output_html_path
407
+
408
+
409
+ def _generate_caption_html(captions: List[dict], hide_captions: bool = False) -> str:
410
+ """
411
+ Generate the caption overlay HTML.
412
+
413
+ Args:
414
+ captions: List of caption dictionaries
415
+ hide_captions: Whether to hide captions
416
+
417
+ Returns:
418
+ HTML string for caption overlay
419
+ """
420
+ if hide_captions or not captions:
421
+ return ""
422
+
423
+ caption_items = []
424
+ for caption in captions:
425
+ # Get the display text (prefer rewritten text)
426
+ text = caption.get("short caption+") or caption.get("short caption") or "No caption"
427
+ caption_items.append(f'<div class="caption-item">{text}</div>')
428
+
429
+ captions_html = "\n".join(caption_items)
430
+
431
+ return f'''
432
+ <div class="caption-overlay">
433
+ <div class="motion-info" id="motion-info">
434
+ <div class="captions-section">
435
+ {captions_html}
436
+ </div>
437
+ </div>
438
+ </div>
439
+ '''
scripts/gradio/templates/index_wooden_static.html ADDED
@@ -0,0 +1,1205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Motion Visualization</title>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"></script>
12
+ <style>
13
+ html, body {
14
+ background: #1a1a2e !important;
15
+ color: #e2e8f0;
16
+ margin: 0;
17
+ padding: 0;
18
+ }
19
+ .container {
20
+ padding: 0;
21
+ border: none;
22
+ background: #1a1a2e;
23
+ }
24
+ .alert-success {
25
+ display: none;
26
+ }
27
+ </style>
28
+ </head>
29
+
30
+ <body>
31
+
32
+ <!-- Fullscreen 3D container -->
33
+ <div class="fullscreen-container">
34
+ <!-- 3D viewport -->
35
+ <div id="vis3d"></div>
36
+
37
+ <!-- Floating caption overlay (centered at top) -->
38
+ {{ caption_html }}
39
+
40
+ <!-- Floating progress control panel (centered at bottom) -->
41
+ <div class="control-overlay">
42
+ <div class="control-row-minimal">
43
+ <div class="progress-container">
44
+ <input type="range" id="progressSlider" class="progress-slider-minimal" min="0" max="100" value="0">
45
+ </div>
46
+ <div class="frame-counter">
47
+ <span id="currentFrame">0</span> / <span id="totalFrames">0</span>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Loading status overlay -->
53
+ <div class="loading-overlay" id="loadingStatus">
54
+ <i class="fas fa-spinner fa-spin"></i> Loading...
55
+ </div>
56
+
57
+ <!-- Hidden controls for functionality -->
58
+ <div style="display: none;">
59
+ <button id="playPauseBtn"></button>
60
+ <button id="resetBtn"></button>
61
+ <input type="range" id="speedSlider" min="0.1" max="3" step="0.1" value="1">
62
+ <span id="speedValue">1.0x</span>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Add Font Awesome for icons -->
67
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
68
+
69
+ <script type="importmap">
70
+ {
71
+ "imports": {
72
+ "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
73
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
74
+ }
75
+ }
76
+ </script>
77
+
78
+ <!-- Embedded SMPL Data - Generated by Python -->
79
+ <script type="application/json" id="smpl-data-json">
80
+ {{ smpl_data_json }}
81
+ </script>
82
+
83
+ <script type="module">
84
+ import * as THREE from 'three';
85
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
86
+
87
+ // ============================================================
88
+ // EMBEDDED: create_ground.js functions
89
+ // ============================================================
90
+
91
+ function getAdaptiveGridSize(sample_data, default_size = 5) {
92
+ if (sample_data) {
93
+ const bounds = calculateDataBounds(sample_data);
94
+ const grid_size = Math.max(bounds.maxRange * 3, 5);
95
+ console.log(`Adaptive ground size: ${grid_size.toFixed(2)}, data range: ${bounds.maxRange.toFixed(2)}`);
96
+ return grid_size;
97
+ }
98
+ return default_size;
99
+ }
100
+
101
+ function createBaseChessboard(
102
+ grid_size = 5,
103
+ divisions = 10,
104
+ white = "#ffffff",
105
+ black = "#444444",
106
+ texture_size = 1024,
107
+ sample_data = null,
108
+ ) {
109
+ if (sample_data) {
110
+ grid_size = getAdaptiveGridSize(sample_data, grid_size);
111
+ }
112
+
113
+ var adjusted_texture_size = Math.floor(texture_size / divisions) * divisions;
114
+ var canvas = document.createElement("canvas");
115
+ canvas.width = canvas.height = adjusted_texture_size;
116
+ var context = canvas.getContext("2d");
117
+ context.imageSmoothingEnabled = false;
118
+
119
+ var step = adjusted_texture_size / divisions;
120
+ for (var i = 0; i < divisions; i++) {
121
+ for (var j = 0; j < divisions; j++) {
122
+ context.fillStyle = (i + j) % 2 === 0 ? white : black;
123
+ context.fillRect(i * step, j * step, step, step);
124
+ }
125
+ }
126
+
127
+ var texture = new THREE.CanvasTexture(canvas);
128
+ texture.wrapS = THREE.RepeatWrapping;
129
+ texture.wrapT = THREE.RepeatWrapping;
130
+ texture.magFilter = THREE.NearestFilter;
131
+ texture.minFilter = THREE.NearestFilter;
132
+ texture.generateMipmaps = false;
133
+
134
+ var planeGeometry = new THREE.PlaneGeometry(grid_size, grid_size);
135
+
136
+ var planeMaterial = new THREE.MeshStandardMaterial({
137
+ map: texture,
138
+ side: THREE.DoubleSide,
139
+ transparent: true,
140
+ opacity: 0.85,
141
+ roughness: 0.9,
142
+ metalness: 0.1,
143
+ emissiveIntensity: 0.05,
144
+ });
145
+
146
+ var plane = new THREE.Mesh(planeGeometry, planeMaterial);
147
+ plane.receiveShadow = true;
148
+
149
+ return plane;
150
+ }
151
+
152
+ function getChessboard(...args) {
153
+ var plane = createBaseChessboard(...args);
154
+ plane.rotation.x = -Math.PI;
155
+ return plane;
156
+ }
157
+
158
+ function getChessboardXZ(...args) {
159
+ var plane = createBaseChessboard(...args);
160
+ plane.rotation.x = -Math.PI / 2;
161
+ return plane;
162
+ }
163
+
164
+ function getCoordinate(axisLength) {
165
+ var axes = new THREE.Group();
166
+ var materialX = new THREE.LineBasicMaterial({ color: 0xff0000 });
167
+ var materialY = new THREE.LineBasicMaterial({ color: 0x00ff00 });
168
+ var materialZ = new THREE.LineBasicMaterial({ color: 0x0000ff });
169
+
170
+ var xAxisGeometry = new THREE.BufferGeometry().setFromPoints([
171
+ new THREE.Vector3(0, 0, 0),
172
+ new THREE.Vector3(axisLength, 0, 0),
173
+ ]);
174
+ var yAxisGeometry = new THREE.BufferGeometry().setFromPoints([
175
+ new THREE.Vector3(0, 0, 0),
176
+ new THREE.Vector3(0, axisLength, 0),
177
+ ]);
178
+ var zAxisGeometry = new THREE.BufferGeometry().setFromPoints([
179
+ new THREE.Vector3(0, 0, 0),
180
+ new THREE.Vector3(0, 0, axisLength),
181
+ ]);
182
+
183
+ var xAxis = new THREE.Line(xAxisGeometry, materialX);
184
+ var yAxis = new THREE.Line(yAxisGeometry, materialY);
185
+ var zAxis = new THREE.Line(zAxisGeometry, materialZ);
186
+
187
+ axes.add(xAxis);
188
+ axes.add(yAxis);
189
+ axes.add(zAxis);
190
+
191
+ return axes;
192
+ }
193
+
194
+ function calculateDataBounds(sample_data) {
195
+ let minX = Infinity, maxX = -Infinity;
196
+ let minY = Infinity, maxY = -Infinity;
197
+ let minZ = Infinity, maxZ = -Infinity;
198
+
199
+ if (sample_data && sample_data.length > 0) {
200
+ sample_data.forEach((frame) => {
201
+ if (frame.positions && Array.isArray(frame.positions)) {
202
+ frame.positions.forEach((pos) => {
203
+ let x, y, z;
204
+ if (typeof pos === "object") {
205
+ x = pos.x !== undefined ? pos.x : pos[0];
206
+ y = pos.y !== undefined ? pos.y : pos[1];
207
+ z = pos.z !== undefined ? pos.z : pos[2];
208
+ } else if (Array.isArray(pos)) {
209
+ [x, y, z] = pos;
210
+ }
211
+
212
+ if (x !== undefined && y !== undefined && z !== undefined) {
213
+ minX = Math.min(minX, x);
214
+ maxX = Math.max(maxX, x);
215
+ minY = Math.min(minY, y);
216
+ maxY = Math.max(maxY, y);
217
+ minZ = Math.min(minZ, z);
218
+ maxZ = Math.max(maxZ, z);
219
+ }
220
+ });
221
+ }
222
+ });
223
+ }
224
+
225
+ if (minX === Infinity || maxX === -Infinity) {
226
+ minX = maxX = minY = maxY = minZ = maxZ = 0;
227
+ }
228
+
229
+ const rangeX = Math.abs(maxX - minX);
230
+ const rangeY = Math.abs(maxY - minY);
231
+ const rangeZ = Math.abs(maxZ - minZ);
232
+ const maxRange = Math.max(rangeX, rangeZ);
233
+
234
+ return { minX, maxX, minY, maxY, minZ, maxZ, rangeX, rangeY, rangeZ, maxRange };
235
+ }
236
+
237
+ // ============================================================
238
+ // EMBEDDED: create_scene.js functions
239
+ // ============================================================
240
+
241
+ function create_scene(scene, camera, renderer, use_ground = true, axis_up = "z", axis_forward = "-y") {
242
+ const width = document.querySelector(".container") ? document.querySelector(".container").offsetWidth : window.innerWidth;
243
+ const height = width;
244
+
245
+ if (axis_up == "z") {
246
+ camera.up.set(0, 0, 1);
247
+ if (axis_forward == "-y") {
248
+ camera.position.set(0, -3, 3);
249
+ } else if (axis_forward == "y") {
250
+ camera.position.set(0, 3, 3);
251
+ }
252
+ camera.lookAt(new THREE.Vector3(0, 0, 1.5));
253
+ } else if (axis_up == "y") {
254
+ camera.up.set(0, 1, 0);
255
+ if (axis_forward == "z") {
256
+ camera.position.set(0, 2.5, 5);
257
+ } else if (axis_forward == "-z") {
258
+ camera.position.set(0, 2.5, -5);
259
+ }
260
+ camera.lookAt(new THREE.Vector3(0, 1, 0));
261
+ }
262
+
263
+ scene.background = new THREE.Color(0x000000);
264
+ scene.fog = new THREE.FogExp2(0x424242, 0.06);
265
+
266
+ renderer.shadowMap.enabled = true;
267
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
268
+
269
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.8);
270
+ hemisphereLight.position.set(0, 2, 0);
271
+ scene.add(hemisphereLight);
272
+
273
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
274
+ if (axis_up == "z") {
275
+ if (axis_forward == "-y") {
276
+ directionalLight.position.set(-3, 1, 5);
277
+ } else if (axis_forward == "y") {
278
+ directionalLight.position.set(3, 1, 5);
279
+ }
280
+ } else if (axis_up == "y") {
281
+ if (axis_forward == "z") {
282
+ directionalLight.position.set(3, 5, 4);
283
+ } else if (axis_forward == "-z") {
284
+ directionalLight.position.set(3, 5, -4);
285
+ }
286
+ }
287
+ directionalLight.castShadow = true;
288
+ directionalLight.shadow.mapSize.width = 2048;
289
+ directionalLight.shadow.mapSize.height = 2048;
290
+ directionalLight.shadow.camera.near = 0.5;
291
+ directionalLight.shadow.camera.far = 50;
292
+ directionalLight.shadow.camera.left = -10;
293
+ directionalLight.shadow.camera.right = 10;
294
+ directionalLight.shadow.camera.top = 10;
295
+ directionalLight.shadow.camera.bottom = -10;
296
+ directionalLight.shadow.bias = -0.0001;
297
+ scene.add(directionalLight);
298
+
299
+ const fillLight = new THREE.DirectionalLight(0xaaccff, 0.4);
300
+ fillLight.position.set(-3, 3, -2);
301
+ scene.add(fillLight);
302
+
303
+ const rimLight = new THREE.DirectionalLight(0xffeedd, 0.3);
304
+ rimLight.position.set(0, 4, -5);
305
+ scene.add(rimLight);
306
+
307
+ if (use_ground) {
308
+ if (axis_up == "z") {
309
+ var plane = getChessboard(50, 50, '#ffffff', '#3a3a3a', 1024);
310
+ plane.name = 'ground';
311
+ plane.receiveShadow = true;
312
+ scene.add(plane);
313
+ } else if (axis_up == "y") {
314
+ var plane = getChessboardXZ(50, 50, '#ffffff', '#3a3a3a', 1024);
315
+ plane.name = 'ground';
316
+ plane.receiveShadow = true;
317
+ scene.add(plane);
318
+ }
319
+ }
320
+
321
+ return 0;
322
+ }
323
+
324
+ function fitCameraToScene(scene, camera, controls = null, opts = {}) {
325
+ const { margin = 1.05, axis_up = "y", excludeNames = ["ground"] } = opts;
326
+
327
+ const box = new THREE.Box3();
328
+ const tmp = new THREE.Box3();
329
+ let has = false;
330
+
331
+ scene.traverse((obj) => {
332
+ if (!obj || !obj.visible) return;
333
+ if (obj.isLight) return;
334
+ const t = obj.type || "";
335
+ if (t.endsWith("Helper")) return;
336
+ if (excludeNames && excludeNames.includes(obj.name)) return;
337
+
338
+ if (obj.isMesh) {
339
+ if (obj.geometry && obj.geometry.type === "PlaneGeometry") return;
340
+ try {
341
+ tmp.setFromObject(obj);
342
+ if (!tmp.isEmpty()) {
343
+ if (!has) {
344
+ box.copy(tmp);
345
+ has = true;
346
+ } else {
347
+ box.union(tmp);
348
+ }
349
+ }
350
+ } catch (_) {}
351
+ }
352
+ });
353
+
354
+ if (!has || box.isEmpty()) return;
355
+
356
+ const sphere = new THREE.Sphere();
357
+ box.getBoundingSphere(sphere);
358
+ const center = sphere.center.clone();
359
+ const radius = Math.max(sphere.radius, 1e-3);
360
+
361
+ const vFov = THREE.MathUtils.degToRad(camera.fov);
362
+ const hFov = 2 * Math.atan(Math.tan(vFov / 2) * camera.aspect);
363
+ const distV = radius / Math.sin(vFov / 2);
364
+ const distH = radius / Math.sin(hFov / 2);
365
+ const dist = Math.max(distV, distH) * margin;
366
+
367
+ const elev = THREE.MathUtils.degToRad(25);
368
+ const azim = Math.PI / 4;
369
+ const horiz = Math.cos(elev);
370
+ let dir;
371
+
372
+ if (axis_up === "y") {
373
+ dir = new THREE.Vector3(Math.sin(azim) * horiz, Math.sin(elev), Math.cos(azim) * horiz);
374
+ camera.up.set(0, 1, 0);
375
+ } else {
376
+ dir = new THREE.Vector3(Math.sin(azim) * horiz, Math.cos(azim) * horiz, Math.sin(elev));
377
+ camera.up.set(0, 0, 1);
378
+ }
379
+
380
+ camera.position.copy(center).add(dir.multiplyScalar(dist));
381
+ camera.updateProjectionMatrix();
382
+ camera.lookAt(center);
383
+
384
+ if (controls) {
385
+ controls.target.copy(center);
386
+ controls.minDistance = Math.max(radius * 0.2, 0.1);
387
+ controls.maxDistance = Math.max(dist * 3, controls.minDistance + 0.1);
388
+ controls.update();
389
+ }
390
+ }
391
+
392
+ // ============================================================
393
+ // EMBEDDED: load_wooden.js functions
394
+ // ============================================================
395
+
396
+ const NUM_SKIN_WEIGHTS = 4;
397
+
398
+ const SMPLH_JOINT_NAMES = [
399
+ "Pelvis", "L_Hip", "R_Hip", "Spine1",
400
+ "L_Knee", "R_Knee", "Spine2",
401
+ "L_Ankle", "R_Ankle", "Spine3",
402
+ "L_Foot", "R_Foot", "Neck", "L_Collar", "R_Collar", "Head",
403
+ "L_Shoulder", "R_Shoulder", "L_Elbow", "R_Elbow",
404
+ "L_Wrist", "R_Wrist",
405
+ "L_Index1", "L_Index2", "L_Index3",
406
+ "L_Middle1", "L_Middle2", "L_Middle3",
407
+ "L_Pinky1", "L_Pinky2", "L_Pinky3",
408
+ "L_Ring1", "L_Ring2", "L_Ring3",
409
+ "L_Thumb1", "L_Thumb2", "L_Thumb3",
410
+ "R_Index1", "R_Index2", "R_Index3",
411
+ "R_Middle1", "R_Middle2", "R_Middle3",
412
+ "R_Pinky1", "R_Pinky2", "R_Pinky3",
413
+ "R_Ring1", "R_Ring2", "R_Ring3",
414
+ "R_Thumb1", "R_Thumb2", "R_Thumb3",
415
+ ];
416
+
417
+ const DEFAULT_EDGES = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19, 20, 22, 23, 20, 25, 26, 20, 28, 29, 20, 31, 32, 20, 34, 35, 21, 37, 38, 21, 40, 41, 21, 43, 44, 21, 46, 47, 21, 49, 50];
418
+
419
+ async function load_wooden(shapes, gender, basePath = '/static/assets/dump_wooden') {
420
+ console.log("Loading wooden model...");
421
+ basePath = "https://raw.githubusercontent.com/chingswy/WoodenModel/refs/heads/main/dump_wooden"
422
+ console.log(`Using base path: ${basePath}`);
423
+
424
+ const urls = [
425
+ `${basePath}/v_template.bin`,
426
+ `${basePath}/faces.bin`,
427
+ `${basePath}/skinWeights.bin`,
428
+ `${basePath}/skinIndice.bin`,
429
+ `${basePath}/j_template.bin`,
430
+ `${basePath}/uvs.bin`,
431
+ ];
432
+
433
+ let edges = [...DEFAULT_EDGES];
434
+ try {
435
+ const kintreeResponse = await fetch(`${basePath}/kintree.bin`);
436
+ if (kintreeResponse.ok) {
437
+ const kintreeBuffer = await kintreeResponse.arrayBuffer();
438
+ edges = Array.from(new Int32Array(kintreeBuffer));
439
+ console.log(`Loaded kintree with ${edges.length} joints`);
440
+ }
441
+ } catch (e) {
442
+ console.log('Using default kintree');
443
+ }
444
+
445
+ let jointNames = [...SMPLH_JOINT_NAMES];
446
+ try {
447
+ const namesResponse = await fetch(`${basePath}/joint_names.json`);
448
+ if (namesResponse.ok) {
449
+ jointNames = await namesResponse.json();
450
+ console.log(`Loaded ${jointNames.length} joint names`);
451
+ }
452
+ } catch (e) {
453
+ console.log('Using default joint names');
454
+ }
455
+
456
+ const buffers = await Promise.all(urls.map(url => fetch(url).then(response => response.arrayBuffer())));
457
+ const v_template = new Float32Array(buffers[0]);
458
+ const faces = new Uint16Array(buffers[1]);
459
+ const skinWeights = new Float32Array(buffers[2]);
460
+ const skinIndices = new Uint16Array(buffers[3]);
461
+ const keypoints = new Float32Array(buffers[4]);
462
+ const uvs = new Float32Array(buffers[5]);
463
+
464
+ console.log(`Vertices: ${v_template.length / 3}, Faces: ${faces.length / 3}, Joints: ${keypoints.length / 3}`);
465
+
466
+ const geometry = new THREE.BufferGeometry();
467
+ geometry.setAttribute('position', new THREE.BufferAttribute(v_template, 3));
468
+ geometry.setIndex(new THREE.BufferAttribute(faces, 1));
469
+ geometry.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndices, NUM_SKIN_WEIGHTS));
470
+ geometry.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeights, NUM_SKIN_WEIGHTS));
471
+ geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
472
+
473
+ const numJoints = keypoints.length / 3;
474
+
475
+ while (edges.length < numJoints) {
476
+ edges.push(0);
477
+ }
478
+
479
+ var rootBone = new THREE.Bone();
480
+ rootBone.position.set(keypoints[0], keypoints[1], keypoints[2]);
481
+ rootBone.name = jointNames[0] || 'Pelvis';
482
+ var bones = [rootBone];
483
+
484
+ for (let i = 1; i < numJoints; i++) {
485
+ const bone = new THREE.Bone();
486
+ const parentIndex = edges[i];
487
+
488
+ if (parentIndex >= 0 && parentIndex < i) {
489
+ bone.position.set(
490
+ keypoints[3 * i] - keypoints[3 * parentIndex],
491
+ keypoints[3 * i + 1] - keypoints[3 * parentIndex + 1],
492
+ keypoints[3 * i + 2] - keypoints[3 * parentIndex + 2]
493
+ );
494
+ bone.name = jointNames[i] || `Joint_${i}`;
495
+ bones.push(bone);
496
+ bones[parentIndex].add(bone);
497
+ console.log(`Joint ${i} (${bone.name}): parent=${parentIndex}, pos=${bone.position.toArray()}`);
498
+ } else {
499
+ console.warn(`Invalid parent index ${parentIndex} for joint ${i}, attaching to root`);
500
+ bone.position.set(0, 0, 0);
501
+ bone.name = jointNames[i] || `Joint_${i}`;
502
+ bones.push(bone);
503
+ bones[0].add(bone);
504
+ }
505
+ }
506
+
507
+ var skeleton = new THREE.Skeleton(bones);
508
+
509
+ geometry.computeVertexNormals();
510
+
511
+ const textureLoader = new THREE.TextureLoader();
512
+
513
+ async function loadTextureAsync(url, isSRGB = true) {
514
+ const tex = await textureLoader.loadAsync(url);
515
+ tex.flipY = false;
516
+ if (isSRGB) tex.colorSpace = THREE.SRGBColorSpace;
517
+ return tex;
518
+ }
519
+
520
+ const [baseColorMap] = await Promise.all([
521
+ loadTextureAsync(`${basePath}/Boy_lambert4_BaseColor.webp`, true),
522
+ ]);
523
+
524
+ const material = new THREE.MeshStandardMaterial({
525
+ map: baseColorMap,
526
+ roughness: 0.6,
527
+ metalness: 0.2,
528
+ envMapIntensity: 1.5,
529
+ });
530
+
531
+ var mesh = new THREE.SkinnedMesh(geometry, material);
532
+ mesh.castShadow = true;
533
+ mesh.receiveShadow = true;
534
+ mesh.add(bones[0]);
535
+ mesh.bind(skeleton);
536
+
537
+ console.log(`Wooden model loaded: ${numJoints} joints, ${v_template.length / 3} vertices`);
538
+
539
+ return { bones, skeleton, mesh, jointNames, edges };
540
+ }
541
+
542
+ // ============================================================
543
+ // Main Application Code
544
+ // ============================================================
545
+
546
+ let scene, camera, renderer;
547
+ let controls;
548
+ let infos;
549
+ let currentFrame = 0;
550
+ let total_frame = 0;
551
+ const baseIntervalTime = 30;
552
+ var model_mesh = {};
553
+
554
+ let isPlaying = false;
555
+ let lastFrameTime = 0;
556
+ let playbackSpeed = 1.0;
557
+ let animationId = null;
558
+ let modelsLoaded = false;
559
+ let expectedModelCount = 0;
560
+ let loadedModelCount = 0;
561
+
562
+ let ignoreGlobalTrans = false;
563
+ let currentOffsets = [];
564
+
565
+ const updateFrame = () => {
566
+ if (!infos || currentFrame >= total_frame || !modelsLoaded) return;
567
+
568
+ const info = infos[currentFrame];
569
+ let allModelsReady = true;
570
+
571
+ info.forEach(smpl_params => {
572
+ if (!(smpl_params.id in model_mesh)) {
573
+ allModelsReady = false;
574
+ }
575
+ });
576
+
577
+ if (!allModelsReady) {
578
+ return;
579
+ }
580
+
581
+ const offsets = computeOffsets(info.length);
582
+ currentOffsets = offsets;
583
+
584
+ info.forEach((smpl_params, b) => {
585
+ const bones = model_mesh[smpl_params.id];
586
+ const meshContainer = bones[0].parent;
587
+
588
+ if (ignoreGlobalTrans) {
589
+ meshContainer.position.set(-offsets[b], 0, 0);
590
+ } else {
591
+ meshContainer.position.set(
592
+ smpl_params.Th[0][0] - offsets[b],
593
+ smpl_params.Th[0][1],
594
+ smpl_params.Th[0][2]
595
+ );
596
+ }
597
+
598
+ var axis = new THREE.Vector3(smpl_params.Rh[0][0], smpl_params.Rh[0][1], smpl_params.Rh[0][2]);
599
+ var angle = axis.length();
600
+ axis.normalize();
601
+ var quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle);
602
+ bones[0].quaternion.copy(quaternion);
603
+
604
+ var poses_offset = 0;
605
+
606
+ if (smpl_params.poses[0].length == 69) {
607
+ poses_offset = -3;
608
+ }
609
+
610
+ for (let i = 1; i < bones.length; i++) {
611
+ const startIndex = poses_offset + 3 * i;
612
+
613
+ if (startIndex + 2 < smpl_params.poses[0].length) {
614
+ var axis = new THREE.Vector3(
615
+ smpl_params.poses[0][startIndex],
616
+ smpl_params.poses[0][startIndex + 1],
617
+ smpl_params.poses[0][startIndex + 2]
618
+ );
619
+ var angle = axis.length();
620
+
621
+ if (angle > 1e-6) {
622
+ axis.normalize();
623
+ var quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle);
624
+ bones[i].quaternion.copy(quaternion);
625
+ } else {
626
+ bones[i].quaternion.set(0, 0, 0, 1);
627
+ }
628
+ }
629
+ }
630
+ });
631
+
632
+ updateUI();
633
+ }
634
+
635
+ const playLoop = (currentTime) => {
636
+ if (isPlaying && currentTime - lastFrameTime >= (baseIntervalTime / playbackSpeed)) {
637
+ currentFrame += 1;
638
+ if (currentFrame >= total_frame) {
639
+ currentFrame = 0;
640
+ }
641
+ updateFrame();
642
+ lastFrameTime = currentTime;
643
+ }
644
+
645
+ if (isPlaying) {
646
+ animationId = requestAnimationFrame(playLoop);
647
+ }
648
+ }
649
+
650
+ const updateUI = () => {
651
+ document.getElementById('currentFrame').textContent = currentFrame;
652
+ document.getElementById('totalFrames').textContent = total_frame;
653
+
654
+ if (total_frame > 0) {
655
+ const progress = (currentFrame / total_frame) * 100;
656
+ document.getElementById('progressSlider').value = progress;
657
+ }
658
+ }
659
+
660
+ const updateLoadingStatus = () => {
661
+ const loadingElement = document.getElementById('loadingStatus');
662
+ if (!loadingElement) return;
663
+
664
+ if (modelsLoaded) {
665
+ loadingElement.innerHTML = '<i class="fas fa-check"></i> Ready';
666
+ loadingElement.className = 'loading-overlay complete';
667
+ setTimeout(() => {
668
+ loadingElement.className = 'loading-overlay hidden';
669
+ }, 1500);
670
+ } else {
671
+ loadingElement.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Loading... (${loadedModelCount}/${expectedModelCount})`;
672
+ loadingElement.className = 'loading-overlay';
673
+ }
674
+ }
675
+
676
+ const updatePlayPauseButton = () => {
677
+ const playPauseBtn = document.getElementById('playPauseBtn');
678
+ if (playPauseBtn) {
679
+ if (isPlaying) {
680
+ playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
681
+ playPauseBtn.title = 'Pause';
682
+ } else {
683
+ playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
684
+ playPauseBtn.title = 'Play';
685
+ }
686
+ }
687
+ }
688
+
689
+ const enablePlaybackControls = () => {
690
+ const playPauseBtn = document.getElementById('playPauseBtn');
691
+ const resetBtn = document.getElementById('resetBtn');
692
+ const progressSlider = document.getElementById('progressSlider');
693
+ const speedSlider = document.getElementById('speedSlider');
694
+
695
+ [playPauseBtn, resetBtn, progressSlider, speedSlider].forEach(element => {
696
+ if (element) {
697
+ element.disabled = false;
698
+ element.style.opacity = '1';
699
+ element.style.cursor = 'pointer';
700
+ }
701
+ });
702
+
703
+ updatePlayPauseButton();
704
+ }
705
+
706
+ const playAnimation = () => {
707
+ if (!isPlaying && total_frame > 0 && modelsLoaded) {
708
+ isPlaying = true;
709
+ lastFrameTime = performance.now();
710
+ animationId = requestAnimationFrame(playLoop);
711
+ updatePlayPauseButton();
712
+ }
713
+ }
714
+
715
+ const pauseAnimation = () => {
716
+ isPlaying = false;
717
+ if (animationId) {
718
+ cancelAnimationFrame(animationId);
719
+ animationId = null;
720
+ }
721
+ updatePlayPauseButton();
722
+ }
723
+
724
+ const resetAnimation = () => {
725
+ pauseAnimation();
726
+ currentFrame = 0;
727
+ updateFrame();
728
+ updatePlayPauseButton();
729
+ }
730
+
731
+ const initPlaybackControls = () => {
732
+ const progressSlider = document.getElementById('progressSlider');
733
+
734
+ let wasPlaying = false;
735
+ progressSlider.addEventListener('mousedown', () => {
736
+ if (!modelsLoaded) return;
737
+ wasPlaying = isPlaying;
738
+ if (isPlaying) pauseAnimation();
739
+ });
740
+
741
+ progressSlider.addEventListener('input', (e) => {
742
+ if (!modelsLoaded) return;
743
+ const progress = parseFloat(e.target.value);
744
+ currentFrame = Math.floor((progress / 100) * total_frame);
745
+ if (currentFrame >= total_frame) currentFrame = total_frame - 1;
746
+ if (currentFrame < 0) currentFrame = 0;
747
+ updateFrame();
748
+ });
749
+
750
+ progressSlider.addEventListener('mouseup', () => {
751
+ if (!modelsLoaded) return;
752
+ if (wasPlaying) playAnimation();
753
+ });
754
+
755
+ progressSlider.addEventListener('touchstart', () => {
756
+ if (!modelsLoaded) return;
757
+ wasPlaying = isPlaying;
758
+ if (isPlaying) pauseAnimation();
759
+ });
760
+
761
+ progressSlider.addEventListener('touchend', () => {
762
+ if (!modelsLoaded) return;
763
+ if (wasPlaying) playAnimation();
764
+ });
765
+
766
+ const speedSlider = document.getElementById('speedSlider');
767
+ const speedValue = document.getElementById('speedValue');
768
+ speedSlider.addEventListener('input', (e) => {
769
+ playbackSpeed = parseFloat(e.target.value);
770
+ speedValue.textContent = playbackSpeed.toFixed(1) + 'x';
771
+ });
772
+
773
+ document.addEventListener('keydown', (e) => {
774
+ if (!modelsLoaded) return;
775
+ switch (e.code) {
776
+ case 'Space':
777
+ e.preventDefault();
778
+ if (isPlaying) {
779
+ pauseAnimation();
780
+ } else {
781
+ playAnimation();
782
+ }
783
+ break;
784
+ case 'ArrowLeft':
785
+ e.preventDefault();
786
+ if (currentFrame > 0) {
787
+ currentFrame--;
788
+ updateFrame();
789
+ }
790
+ break;
791
+ case 'ArrowRight':
792
+ e.preventDefault();
793
+ if (currentFrame < total_frame - 1) {
794
+ currentFrame++;
795
+ updateFrame();
796
+ }
797
+ break;
798
+ case 'Home':
799
+ e.preventDefault();
800
+ resetAnimation();
801
+ break;
802
+ }
803
+ });
804
+ }
805
+
806
+ // Load embedded SMPL data directly (no fetch needed)
807
+ function loadEmbeddedData() {
808
+ try {
809
+ const smplDataElement = document.getElementById('smpl-data-json');
810
+ if (!smplDataElement) {
811
+ console.error('SMPL data element not found');
812
+ return;
813
+ }
814
+
815
+ const datas = JSON.parse(smplDataElement.textContent);
816
+
817
+ if (!datas || datas.length === 0) {
818
+ console.error('No SMPL data available');
819
+ return;
820
+ }
821
+
822
+ console.log(`Loaded ${datas.length} frames of embedded SMPL data`);
823
+ infos = datas;
824
+ total_frame = datas.length;
825
+
826
+ document.getElementById('progressSlider').max = 100;
827
+ updateUI();
828
+ updatePlayPauseButton();
829
+
830
+ expectedModelCount = infos[0].length;
831
+
832
+ loadedModelCount = 0;
833
+ modelsLoaded = false;
834
+ updateLoadingStatus();
835
+
836
+ infos[0].forEach(data => {
837
+ load_wooden(null, null).then(result => {
838
+ scene.add(result.mesh);
839
+
840
+ result.mesh.castShadow = true;
841
+ result.mesh.receiveShadow = true;
842
+
843
+ model_mesh[data.id] = result.bones;
844
+
845
+ loadedModelCount++;
846
+
847
+ if (loadedModelCount === expectedModelCount) {
848
+ modelsLoaded = true;
849
+ updateLoadingStatus();
850
+ updateFrame();
851
+ enablePlaybackControls();
852
+ fitCameraToScene(scene, camera, controls, { axis_up: 'y', excludeNames: ['ground'] });
853
+ setTimeout(() => playAnimation(), 500);
854
+ } else {
855
+ updateLoadingStatus();
856
+ }
857
+ }).catch(err => {
858
+ console.error("Failed to load wooden model:", err);
859
+ });
860
+ });
861
+
862
+ initPlaybackControls();
863
+ animate();
864
+ } catch (error) {
865
+ console.error('Error loading embedded data:', error);
866
+ }
867
+ }
868
+
869
+ init();
870
+ loadEmbeddedData();
871
+
872
+ function init() {
873
+ const width = window.innerWidth;
874
+ const height = window.innerHeight;
875
+ scene = new THREE.Scene();
876
+ camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 50);
877
+ renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
878
+
879
+ create_scene(scene, camera, renderer, true, 'y', 'z');
880
+
881
+ renderer.shadowMap.enabled = true;
882
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
883
+
884
+ scene.background = new THREE.Color(0x424242);
885
+ scene.fog = new THREE.FogExp2(0x424242, 0.06);
886
+
887
+ scene.children = scene.children.filter(child => !child.isLight);
888
+
889
+ const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.2);
890
+ hemisphereLight.position.set(0, 2, 0);
891
+ scene.add(hemisphereLight);
892
+
893
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
894
+ directionalLight.position.set(3, 5, 4);
895
+ directionalLight.castShadow = true;
896
+ directionalLight.shadow.mapSize.width = 2048;
897
+ directionalLight.shadow.mapSize.height = 2048;
898
+ directionalLight.shadow.camera.near = 0.5;
899
+ directionalLight.shadow.camera.far = 50;
900
+ directionalLight.shadow.camera.left = -10;
901
+ directionalLight.shadow.camera.right = 10;
902
+ directionalLight.shadow.camera.top = 10;
903
+ directionalLight.shadow.camera.bottom = -10;
904
+ directionalLight.shadow.bias = -0.0001;
905
+ scene.add(directionalLight);
906
+
907
+ const fillLight = new THREE.DirectionalLight(0xaaccff, 0.5);
908
+ fillLight.position.set(-3, 3, -2);
909
+ scene.add(fillLight);
910
+
911
+ const rimLight = new THREE.DirectionalLight(0xffeedd, 0.4);
912
+ rimLight.position.set(0, 4, -5);
913
+ scene.add(rimLight);
914
+
915
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
916
+ renderer.toneMappingExposure = 1.0;
917
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
918
+
919
+ renderer.setPixelRatio(window.devicePixelRatio);
920
+ renderer.setSize(width, height);
921
+ var container = document.getElementById('vis3d');
922
+ container.appendChild(renderer.domElement);
923
+
924
+ window.addEventListener('resize', onWindowResize);
925
+
926
+ controls = new OrbitControls(camera, renderer.domElement);
927
+ controls.minDistance = 1;
928
+ controls.maxDistance = 15;
929
+ controls.enableDamping = true;
930
+ controls.dampingFactor = 0.05;
931
+ controls.target.set(0, 1, 0);
932
+ fitCameraToScene(scene, camera, controls, { axis_up: 'y', excludeNames: ['ground'] });
933
+
934
+ let isDragging = false;
935
+ let mouseDownTime = 0;
936
+
937
+ renderer.domElement.addEventListener('mousedown', () => {
938
+ isDragging = false;
939
+ mouseDownTime = Date.now();
940
+ });
941
+
942
+ renderer.domElement.addEventListener('mousemove', () => {
943
+ if (Date.now() - mouseDownTime > 150) {
944
+ isDragging = true;
945
+ }
946
+ });
947
+
948
+ renderer.domElement.addEventListener('mouseup', (e) => {
949
+ if (!isDragging && Date.now() - mouseDownTime < 300) {
950
+ if (modelsLoaded) {
951
+ isPlaying ? pauseAnimation() : playAnimation();
952
+ }
953
+ }
954
+ });
955
+
956
+ renderer.domElement.addEventListener('dblclick', () => {
957
+ if (modelsLoaded) {
958
+ pauseAnimation();
959
+ currentFrame = 0;
960
+ updateFrame();
961
+ }
962
+ });
963
+ }
964
+
965
+ function animate() {
966
+ requestAnimationFrame(animate);
967
+ if (controls && controls.enableDamping) {
968
+ controls.update();
969
+ }
970
+ renderer.render(scene, camera);
971
+ }
972
+
973
+ function onWindowResize() {
974
+ const width = window.innerWidth;
975
+ const height = window.innerHeight;
976
+ camera.aspect = width / height;
977
+ camera.updateProjectionMatrix();
978
+ renderer.setSize(width, height);
979
+ }
980
+
981
+ function computeOffsets(batchSize) {
982
+ const spacing = 2.0;
983
+ const total_width = (batchSize - 1) * spacing;
984
+ const start_x = -total_width / 2;
985
+ const offsets = [];
986
+ for (let i = 0; i < batchSize; i++) {
987
+ offsets.push(start_x + i * spacing);
988
+ }
989
+ return offsets;
990
+ }
991
+
992
+ </script>
993
+
994
+ <style>
995
+ /* Fullscreen dark mode base styles */
996
+ * {
997
+ margin: 0;
998
+ padding: 0;
999
+ box-sizing: border-box;
1000
+ }
1001
+
1002
+ html, body {
1003
+ width: 100%;
1004
+ height: 100%;
1005
+ overflow: hidden;
1006
+ background: #424242 !important;
1007
+ color: #e2e8f0;
1008
+ }
1009
+
1010
+ /* Fullscreen container for 3D scene */
1011
+ .fullscreen-container {
1012
+ position: fixed;
1013
+ top: 0;
1014
+ left: 0;
1015
+ width: 100vw;
1016
+ height: 100vh;
1017
+ background: #424242;
1018
+ overflow: hidden;
1019
+ }
1020
+
1021
+ #vis3d {
1022
+ position: absolute;
1023
+ top: 0;
1024
+ left: 0;
1025
+ width: 100%;
1026
+ height: 100%;
1027
+ background: #424242;
1028
+ }
1029
+
1030
+ #vis3d canvas {
1031
+ display: block;
1032
+ width: 100% !important;
1033
+ height: 100% !important;
1034
+ }
1035
+
1036
+ /* Floating caption overlay */
1037
+ .caption-overlay {
1038
+ position: absolute;
1039
+ top: 20px;
1040
+ left: 50%;
1041
+ transform: translateX(-50%);
1042
+ width: auto;
1043
+ max-width: 90%;
1044
+ z-index: 100;
1045
+ pointer-events: auto;
1046
+ }
1047
+
1048
+ .motion-info {
1049
+ background-color: rgba(45, 55, 72, 0.85);
1050
+ backdrop-filter: blur(10px);
1051
+ -webkit-backdrop-filter: blur(10px);
1052
+ border-radius: 20px;
1053
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
1054
+ overflow: hidden;
1055
+ max-height: 40vh;
1056
+ overflow-y: auto;
1057
+ display: inline-block;
1058
+ }
1059
+
1060
+ /* Floating progress control panel */
1061
+ .control-overlay {
1062
+ position: absolute;
1063
+ bottom: 30px;
1064
+ left: 50%;
1065
+ transform: translateX(-50%);
1066
+ width: 80%;
1067
+ max-width: 600px;
1068
+ z-index: 100;
1069
+ background: rgba(0, 0, 0, 0.4);
1070
+ backdrop-filter: blur(8px);
1071
+ -webkit-backdrop-filter: blur(8px);
1072
+ padding: 15px 20px;
1073
+ border-radius: 12px;
1074
+ }
1075
+
1076
+ .control-row-minimal {
1077
+ display: flex;
1078
+ align-items: center;
1079
+ gap: 20px;
1080
+ }
1081
+
1082
+ .progress-container {
1083
+ flex: 1;
1084
+ }
1085
+
1086
+ .progress-slider-minimal {
1087
+ width: 100%;
1088
+ height: 8px;
1089
+ border-radius: 4px;
1090
+ background: rgba(255, 255, 255, 0.3);
1091
+ outline: none;
1092
+ cursor: pointer;
1093
+ -webkit-appearance: none;
1094
+ appearance: none;
1095
+ }
1096
+
1097
+ .progress-slider-minimal::-webkit-slider-runnable-track {
1098
+ width: 100%;
1099
+ height: 8px;
1100
+ border-radius: 4px;
1101
+ background: rgba(255, 255, 255, 0.3);
1102
+ }
1103
+
1104
+ .progress-slider-minimal::-webkit-slider-thumb {
1105
+ -webkit-appearance: none;
1106
+ appearance: none;
1107
+ width: 20px;
1108
+ height: 20px;
1109
+ border-radius: 50%;
1110
+ background: #4a9eff;
1111
+ cursor: pointer;
1112
+ border: 2px solid white;
1113
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
1114
+ margin-top: -6px;
1115
+ }
1116
+
1117
+ .progress-slider-minimal::-moz-range-track {
1118
+ width: 100%;
1119
+ height: 8px;
1120
+ border-radius: 4px;
1121
+ background: rgba(255, 255, 255, 0.3);
1122
+ }
1123
+
1124
+ .progress-slider-minimal::-moz-range-thumb {
1125
+ width: 20px;
1126
+ height: 20px;
1127
+ border-radius: 50%;
1128
+ background: #4a9eff;
1129
+ cursor: pointer;
1130
+ border: 2px solid white;
1131
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
1132
+ }
1133
+
1134
+ .frame-counter {
1135
+ font-family: 'SF Mono', 'Consolas', monospace;
1136
+ font-size: 14px;
1137
+ font-weight: 500;
1138
+ color: white;
1139
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
1140
+ white-space: nowrap;
1141
+ min-width: 80px;
1142
+ text-align: right;
1143
+ }
1144
+
1145
+ /* Loading overlay */
1146
+ .loading-overlay {
1147
+ position: absolute;
1148
+ top: 50%;
1149
+ left: 50%;
1150
+ transform: translate(-50%, -50%);
1151
+ background: rgba(0, 0, 0, 0.7);
1152
+ backdrop-filter: blur(8px);
1153
+ -webkit-backdrop-filter: blur(8px);
1154
+ color: white;
1155
+ padding: 15px 25px;
1156
+ border-radius: 10px;
1157
+ font-size: 14px;
1158
+ z-index: 200;
1159
+ display: flex;
1160
+ align-items: center;
1161
+ gap: 10px;
1162
+ }
1163
+
1164
+ .loading-overlay.hidden {
1165
+ display: none;
1166
+ }
1167
+
1168
+ .loading-overlay.complete {
1169
+ background: rgba(76, 175, 80, 0.85);
1170
+ }
1171
+
1172
+ /* Caption content styles */
1173
+ .loading {
1174
+ padding: 10px 18px;
1175
+ text-align: center;
1176
+ color: #a0aec0;
1177
+ font-style: italic;
1178
+ white-space: nowrap;
1179
+ }
1180
+
1181
+ .captions-section {
1182
+ padding: 12px 20px;
1183
+ white-space: nowrap;
1184
+ }
1185
+
1186
+ .caption-item {
1187
+ background: transparent;
1188
+ border: none;
1189
+ border-radius: 0;
1190
+ margin-bottom: 6px;
1191
+ padding: 0;
1192
+ color: #f0f4f8;
1193
+ font-size: 1em;
1194
+ font-weight: 500;
1195
+ line-height: 1.5;
1196
+ text-align: center;
1197
+ }
1198
+
1199
+ .caption-item:last-child {
1200
+ margin-bottom: 0;
1201
+ }
1202
+ </style>
1203
+
1204
+ </body>
1205
+ </html>