ginipick commited on
Commit
61b805a
ยท
verified ยท
1 Parent(s): 6a6eb1e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +523 -96
app.py CHANGED
@@ -1,4 +1,134 @@
1
- import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import time
3
  import glob
4
  import json
@@ -75,43 +205,76 @@ print(f"Output directory: {LOG_PATH}")
75
 
76
  def normalize_mesh(mesh):
77
  """Normalize mesh to fit in a unit cube centered at origin"""
78
- if isinstance(mesh, trimesh.Scene):
79
- # Scene ๊ฐ์ฒด ์ฒ˜๋ฆฌ
80
- # ์”ฌ์—์„œ ๋ชจ๋“  ๋ฉ”์‹œ ์ถ”์ถœ
81
- meshes = []
82
- for geometry in mesh.geometry.values():
83
- if isinstance(geometry, trimesh.Trimesh):
84
- meshes.append(geometry)
85
-
86
- if not meshes:
87
- raise ValueError("No meshes found in scene")
88
-
89
- # ๋ชจ๋“  ๋ฉ”์‹œ์˜ ์ •์ ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๊ฒฝ๊ณ„ ์ƒ์ž ๊ณ„์‚ฐ
90
- all_vertices = np.vstack([m.vertices for m in meshes])
91
- bounds = np.array([all_vertices.min(axis=0), all_vertices.max(axis=0)])
92
- center = (bounds[0] + bounds[1]) / 2
93
- scale = 1.0 / (bounds[1] - bounds[0]).max()
94
-
95
- # ๊ฐ ๋ฉ”์‹œ๋ฅผ ์ •๊ทœํ™”ํ•˜์—ฌ ์ƒˆ ์”ฌ ์ƒ์„ฑ
96
- normalized_scene = trimesh.Scene()
97
- for mesh_obj in meshes:
98
- normalized_mesh = mesh_obj.copy()
99
- normalized_mesh.vertices = (normalized_mesh.vertices - center) * scale
100
- normalized_scene.add_geometry(normalized_mesh)
101
-
102
- return normalized_scene, center, scale
103
- else:
104
- # ์ผ๋ฐ˜ Trimesh ๊ฐ์ฒด ์ฒ˜๋ฆฌ
105
- vertices = mesh.vertices
106
- bounds = np.array([vertices.min(axis=0), vertices.max(axis=0)])
107
- center = (bounds[0] + bounds[1]) / 2
108
- scale = 1.0 / (bounds[1] - bounds[0]).max()
109
-
110
- # ๋ณต์‚ฌ๋ณธ ์ƒ์„ฑํ•˜์—ฌ ์›๋ณธ ๋ณ€๊ฒฝ ๋ฐฉ์ง€
111
- normalized_mesh = mesh.copy()
112
- normalized_mesh.vertices = (vertices - center) * scale
113
-
114
- return normalized_mesh, center, scale
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def create_rotation_animation(mesh, duration=3.0, fps=30):
117
  """Create a rotation animation around the Y axis"""
@@ -119,7 +282,13 @@ def create_rotation_animation(mesh, duration=3.0, fps=30):
119
  frames = []
120
 
121
  # Normalize the mesh for consistent animation
122
- mesh, original_center, original_scale = normalize_mesh(mesh)
 
 
 
 
 
 
123
 
124
  for frame_idx in range(num_frames):
125
  t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
@@ -353,95 +522,206 @@ def generate_gif_from_frames(frames, output_path, fps=30, resolution=(640, 480),
353
  """Generate a GIF from animation frames"""
354
  gif_frames = []
355
 
 
 
356
  for frame in frames:
357
- # Create a scene with the frame
358
- if not isinstance(frame, trimesh.Scene):
359
- scene = trimesh.Scene(frame)
360
- else:
361
- scene = frame
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
- # ์ž๋™ ์นด๋ฉ”๋ผ ์„ค์ • (์ „์ฒด ์žฅ๋ฉด์ด ๋ณด์ด๋„๋ก)
364
- try:
365
- scene.camera
366
- except:
367
- # ๊ธฐ๋ณธ ์นด๋ฉ”๋ผ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
368
- scene.set_camera()
369
 
370
- # ๋ชจ๋“  ๊ฐ์ฒด๊ฐ€ ๋ณด์ด๋„๋ก ์นด๋ฉ”๋ผ ์œ„์น˜ ์กฐ์ •
371
- scene.camera_transform = scene.camera_transform
372
-
373
- # Render the frame
 
 
 
 
 
374
  try:
375
- rendered_img = scene.save_image(resolution=resolution, background=background_color)
376
- pil_img = Image.open(rendered_img)
377
- gif_frames.append(pil_img)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  except Exception as e:
379
- print(f"Error rendering frame: {str(e)}")
380
- # ๋ Œ๋”๋ง ์‹คํŒจ ์‹œ ๋นˆ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
381
  gif_frames.append(Image.new('RGB', resolution, (255, 255, 255)))
382
 
383
- # Save as GIF
384
  if gif_frames:
385
- gif_frames[0].save(
386
- output_path,
387
- save_all=True,
388
- append_images=gif_frames[1:],
389
- optimize=False,
390
- duration=int(1000 / fps),
391
- loop=0
392
- )
393
- return output_path
 
 
 
 
 
 
 
 
 
394
  else:
 
395
  return None
396
 
397
  def create_animation_mesh(input_mesh_path, animation_type='rotate', duration=3.0, fps=30):
398
  """Create animation from input mesh based on animation type"""
399
  # Load the mesh
400
  try:
 
401
  # ๋จผ์ € Scene์œผ๋กœ ๋กœ๋“œ ์‹œ๋„
402
  loaded_obj = trimesh.load(input_mesh_path)
 
403
 
404
  # Scene์ธ ๊ฒฝ์šฐ ๋‹จ์ผ ๋ฉ”์‹œ๋กœ ๋ณ€ํ™˜ ์‹œ๋„
405
  if isinstance(loaded_obj, trimesh.Scene):
406
  print("Loaded a scene, extracting meshes...")
407
  # ์”ฌ์—์„œ ๋ชจ๋“  ๋ฉ”์‹œ ์ถ”์ถœ
408
  meshes = []
409
- for geometry in loaded_obj.geometry.values():
410
  if isinstance(geometry, trimesh.Trimesh):
 
411
  meshes.append(geometry)
412
 
413
  if not meshes:
414
- print("No meshes found in scene")
415
- return None, None
416
-
417
- # ๋ชจ๋“  ๋ฉ”์‹œ๋ฅผ ํ•˜๋‚˜๋กœ ๊ฒฐํ•ฉ
418
- mesh = trimesh.util.concatenate(meshes)
 
 
 
 
419
  elif isinstance(loaded_obj, trimesh.Trimesh):
420
  mesh = loaded_obj
 
421
  else:
422
  print(f"Unsupported object type: {type(loaded_obj)}")
423
- return None, None
424
-
 
 
425
  except Exception as e:
426
  print(f"Error loading mesh: {str(e)}")
427
- return None, None
 
 
 
428
 
429
  # Generate animation frames based on animation type
430
- if animation_type == 'rotate':
431
- frames = create_rotation_animation(mesh, duration, fps)
432
- elif animation_type == 'float':
433
- frames = create_float_animation(mesh, duration, fps)
434
- elif animation_type == 'explode':
435
- frames = create_explode_animation(mesh, duration, fps)
436
- elif animation_type == 'assemble':
437
- frames = create_assemble_animation(mesh, duration, fps)
438
- elif animation_type == 'pulse':
439
- frames = create_pulse_animation(mesh, duration, fps)
440
- elif animation_type == 'swing':
441
- frames = create_swing_animation(mesh, duration, fps)
442
- else:
443
- print(f"Unknown animation type: {animation_type}")
444
- return None, None
 
 
 
 
 
 
 
 
 
 
 
445
 
446
  base_filename = os.path.basename(input_mesh_path).rsplit('.', 1)[0]
447
 
@@ -455,8 +735,12 @@ def create_animation_mesh(input_mesh_path, animation_type='rotate', duration=3.0
455
  # First frame for static GLB
456
  first_frame = frames[0]
457
  # Export as GLB
458
- scene = trimesh.Scene(first_frame)
 
 
 
459
  scene.export(animated_glb_path)
 
460
  else:
461
  return None, None
462
  except Exception as e:
@@ -466,6 +750,7 @@ def create_animation_mesh(input_mesh_path, animation_type='rotate', duration=3.0
466
  # Create GIF for preview
467
  try:
468
  animated_gif_path = os.path.join(LOG_PATH, f'animated_{base_filename}.gif')
 
469
  generate_gif_from_frames(frames, animated_gif_path, fps)
470
  except Exception as e:
471
  print(f"Error creating GIF: {str(e)}")
@@ -487,8 +772,51 @@ def process_3d_model(input_3d, animation_type, animation_duration, fps):
487
  fps=fps
488
  )
489
 
490
- if not animated_glb_path or not animated_gif_path:
491
- return "Error creating animation", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
  # Create a simple JSON metadata file
494
  metadata = {
@@ -503,6 +831,105 @@ def process_3d_model(input_3d, animation_type, animation_duration, fps):
503
  with open(json_path, 'w') as f:
504
  json.dump(metadata, f, indent=4)
505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  return animated_glb_path, animated_gif_path, json_path
507
  except Exception as e:
508
  error_msg = f"Error processing file: {str(e)}"
 
1
+ def create_textual_animation_gif(output_path, model_name, animation_type, duration=3.0, fps=30):
2
+ """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์˜ ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ GIF ์ƒ์„ฑ - ๋ Œ๋”๋ง ์‹คํŒจ ์‹œ ๋Œ€์ฒด์šฉ"""
3
+ try:
4
+ # ๊ฐ„๋‹จํ•œ ํ”„๋ ˆ์ž„ ์‹œํ€€์Šค ์ƒ์„ฑ
5
+ frames = []
6
+ num_frames = int(duration * fps)
7
+ if num_frames > 60: # ๋„ˆ๋ฌด ๋งŽ์€ ํ”„๋ ˆ์ž„์€ ํšจ์œจ์ ์ด์ง€ ์•Š์Œ
8
+ num_frames = 60
9
+
10
+ for i in range(num_frames):
11
+ t = i / (num_frames - 1) # 0~1 ๋ฒ”์œ„
12
+ angle = t * 360 # ์ „์ฒด ํšŒ์ „
13
+
14
+ # ์ƒˆ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
15
+ img = Image.new('RGB', (640, 480), color=(240, 240, 240))
16
+ draw = ImageDraw.Draw(img)
17
+
18
+ # ์ •๋ณด ํ…์ŠคํŠธ
19
+ draw.text((50, 50), f"Model: {os.path.basename(model_name)}", fill=(0, 0, 0))
20
+ draw.text((50, 100), f"Animation Type: {animation_type}", fill=(0, 0, 0))
21
+ draw.text((50, 150), f"Frame: {i+1}/{num_frames}", fill=(0, 0, 0))
22
+
23
+ # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜•์— ๋”ฐ๋ฅธ ์‹œ๊ฐ์  ํšจ๊ณผ
24
+ center_x, center_y = 320, 240
25
+ if animation_type == 'rotate':
26
+ # ํšŒ์ „ํ•˜๋Š” ์‚ฌ๊ฐํ˜•
27
+ radius = 100
28
+ x = center_x + radius * math.cos(math.radians(angle))
29
+ y = center_y + radius * math.sin(math.radians(angle))
30
+ draw.rectangle((x-40, y-40, x+40, y+40), outline=(0, 0, 0), fill=(255, 0, 0))
31
+
32
+ elif animation_type == 'float':
33
+ # ์œ„์•„๋ž˜๋กœ ์›€์ง์ด๋Š” ์›
34
+ offset_y = 50 * math.sin(2 * math.pi * t)
35
+ draw.ellipse((center_x-50, center_y-50+offset_y, center_x+50, center_y+50+offset_y),
36
+ outline=(0, 0, 0), fill=(0, 0, 255))
37
+
38
+ elif animation_type == 'explode' or animation_type == 'assemble':
39
+ # ๋ฐ”๊นฅ์ชฝ/์•ˆ์ชฝ์œผ๋กœ ์›€์ง์ด๋Š” ์—ฌ๋Ÿฌ ๋„ํ˜•
40
+ scale = t if animation_type == 'explode' else 1 - t
41
+ for j in range(8):
42
+ angle_j = j * 45
43
+ dist = 120 * scale
44
+ x = center_x + dist * math.cos(math.radians(angle_j))
45
+ y = center_y + dist * math.sin(math.radians(angle_j))
46
+
47
+ if j % 3 == 0:
48
+ draw.rectangle((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(255, 0, 0))
49
+ elif j % 3 == 1:
50
+ draw.ellipse((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(0, 255, 0))
51
+ else:
52
+ draw.polygon([(x, y-20), (x+20, y+20), (x-20, y+20)], outline=(0, 0, 0), fill=(0, 0, 255))
53
+
54
+ elif animation_type == 'pulse':
55
+ # ํฌ๊ธฐ๊ฐ€ ๋ณ€ํ•˜๋Š” ์›
56
+ scale = 0.5 + 0.5 * math.sin(2 * math.pi * t)
57
+ radius = 100 * scale
58
+ draw.ellipse((center_x-radius, center_y-radius, center_x+radius, center_y+radius),
59
+ outline=(0, 0, 0), fill=(0, 255, 0))
60
+
61
+ elif animation_type == 'swing':
62
+ # ์ขŒ์šฐ๋กœ ์›€์ง์ด๋Š” ์‚ผ๊ฐํ˜•
63
+ angle_offset = 30 * math.sin(2 * math.pi * t)
64
+ points = [
65
+ (center_x + 100 * math.cos(math.radians(angle_offset)), center_y - 80),
66
+ (center_x + 100 * math.cos(math.radians(120 + angle_offset)), center_y + 40),
67
+ (center_x + 100 * math.cos(math.radians(240 + angle_offset)), center_y + 40)
68
+ ]
69
+ draw.polygon(points, outline=(0, 0, 0), fill=(255, 165, 0))
70
+
71
+ # ํ”„๋ ˆ์ž„ ์ถ”๊ฐ€
72
+ frames.append(img)
73
+
74
+ # GIF๋กœ ์ €์žฅ
75
+ frames[0].save(
76
+ output_path,
77
+ save_all=True,
78
+ append_images=frames[1:],
79
+ optimize=False,
80
+ duration=int(1000 / fps),
81
+ loop=0
82
+ )
83
+ print(f"Created textual animation GIF at {output_path}")
84
+ return output_path
85
+ except Exception as e:
86
+ print(f"Error creating textual animation: {str(e)}")
87
+ return Nonedef create_simple_animation_frames(input_mesh_path, animation_type='rotate', num_frames=30):
88
+ """๊ฐ„๋‹จํ•œ ๋ฐฉ์‹์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„ ์ƒ์„ฑ - ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•๋“ค์ด ์‹คํŒจํ•  ๊ฒฝ์šฐ ๋Œ€์ฒด์šฉ"""
89
+ try:
90
+ # ๋ฉ”์‹œ ๋กœ๋“œ
91
+ mesh = trimesh.load(input_mesh_path)
92
+
93
+ # ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ˜•ํƒœ๋กœ ๋ฉ”์‹œ/์”ฌ ์ถ”์ถœ
94
+ if isinstance(mesh, trimesh.Scene):
95
+ # ์”ฌ์—์„œ ์ฒซ ๋ฒˆ์งธ ๋ฉ”์‹œ ์ถ”์ถœ
96
+ geometries = list(mesh.geometry.values())
97
+ if geometries:
98
+ base_mesh = geometries[0]
99
+ else:
100
+ print("No geometries found in scene")
101
+ return None
102
+ else:
103
+ base_mesh = mesh
104
+
105
+ # ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„ ์ค€๋น„
106
+ frames = []
107
+
108
+ # ๋‹จ์ˆœ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ
109
+ for i in range(num_frames):
110
+ angle = i * 2 * math.pi / num_frames
111
+
112
+ # ์ƒˆ ์”ฌ ์ƒ์„ฑ
113
+ scene = trimesh.Scene()
114
+
115
+ # ๋ฉ”์‹œ ๋ณต์‚ฌ ๋ฐ ํšŒ์ „
116
+ rotated_mesh = base_mesh.copy()
117
+ rotation = tf.rotation_matrix(angle, [0, 1, 0])
118
+ rotated_mesh.apply_transform(rotation)
119
+
120
+ # ์”ฌ์— ์ถ”๊ฐ€
121
+ scene.add_geometry(rotated_mesh)
122
+
123
+ # ์นด๋ฉ”๋ผ ์„ค์ •
124
+ scene.set_camera()
125
+
126
+ frames.append(scene)
127
+
128
+ return frames
129
+ except Exception as e:
130
+ print(f"Error in simple animation: {str(e)}")
131
+ return Noneimport os
132
  import time
133
  import glob
134
  import json
 
205
 
206
  def normalize_mesh(mesh):
207
  """Normalize mesh to fit in a unit cube centered at origin"""
208
+ try:
209
+ if isinstance(mesh, trimesh.Scene):
210
+ # Scene ๊ฐ์ฒด ์ฒ˜๋ฆฌ
211
+ # ์”ฌ์—์„œ ๋ชจ๋“  ๋ฉ”์‹œ ์ถ”์ถœ
212
+ meshes = []
213
+ for geometry in mesh.geometry.values():
214
+ if isinstance(geometry, trimesh.Trimesh):
215
+ meshes.append(geometry)
216
+
217
+ if not meshes:
218
+ print("Warning: No meshes found in scene during normalization")
219
+ return mesh, np.zeros(3), 1.0
220
+
221
+ # ๋ชจ๋“  ๋ฉ”์‹œ์˜ ์ •์ ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๊ฒฝ๊ณ„ ์ƒ์ž ๊ณ„์‚ฐ
222
+ try:
223
+ all_vertices = np.vstack([m.vertices for m in meshes if hasattr(m, 'vertices') and m.vertices.shape[0] > 0])
224
+ if len(all_vertices) == 0:
225
+ print("Warning: No vertices found in meshes during normalization")
226
+ return mesh, np.zeros(3), 1.0
227
+
228
+ bounds = np.array([all_vertices.min(axis=0), all_vertices.max(axis=0)])
229
+ center = (bounds[0] + bounds[1]) / 2
230
+ scale_value = (bounds[1] - bounds[0]).max()
231
+ if scale_value < 1e-10:
232
+ print("Warning: Mesh is too small, using default scale")
233
+ scale = 1.0
234
+ else:
235
+ scale = 1.0 / scale_value
236
+
237
+ # ๊ฐ ๋ฉ”์‹œ๋ฅผ ์ •๊ทœํ™”ํ•˜์—ฌ ์ƒˆ ์”ฌ ์ƒ์„ฑ
238
+ normalized_scene = trimesh.Scene()
239
+ for mesh_idx, mesh_obj in enumerate(meshes):
240
+ normalized_mesh = mesh_obj.copy()
241
+ try:
242
+ normalized_mesh.vertices = (normalized_mesh.vertices - center) * scale
243
+ normalized_scene.add_geometry(normalized_mesh, node_name=f"normalized_mesh_{mesh_idx}")
244
+ except Exception as e:
245
+ print(f"Error normalizing mesh {mesh_idx}: {str(e)}")
246
+ # ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์›๋ณธ ๋ฉ”์‹œ ์ถ”๊ฐ€
247
+ normalized_scene.add_geometry(mesh_obj, node_name=f"original_mesh_{mesh_idx}")
248
+
249
+ return normalized_scene, center, scale
250
+ except Exception as e:
251
+ print(f"Error during scene normalization: {str(e)}")
252
+ return mesh, np.zeros(3), 1.0
253
+ else:
254
+ # ์ผ๋ฐ˜ Trimesh ๊ฐ์ฒด ์ฒ˜๋ฆฌ
255
+ if not hasattr(mesh, 'vertices') or mesh.vertices.shape[0] == 0:
256
+ print("Warning: Mesh has no vertices")
257
+ return mesh, np.zeros(3), 1.0
258
+
259
+ vertices = mesh.vertices
260
+ bounds = np.array([vertices.min(axis=0), vertices.max(axis=0)])
261
+ center = (bounds[0] + bounds[1]) / 2
262
+ scale_value = (bounds[1] - bounds[0]).max()
263
+ if scale_value < 1e-10:
264
+ print("Warning: Mesh is too small, using default scale")
265
+ scale = 1.0
266
+ else:
267
+ scale = 1.0 / scale_value
268
+
269
+ # ๋ณต์‚ฌ๋ณธ ์ƒ์„ฑํ•˜์—ฌ ์›๋ณธ ๋ณ€๊ฒฝ ๋ฐฉ์ง€
270
+ normalized_mesh = mesh.copy()
271
+ normalized_mesh.vertices = (vertices - center) * scale
272
+
273
+ return normalized_mesh, center, scale
274
+ except Exception as e:
275
+ print(f"Unexpected error in normalize_mesh: {str(e)}")
276
+ # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜
277
+ return mesh, np.zeros(3), 1.0
278
 
279
  def create_rotation_animation(mesh, duration=3.0, fps=30):
280
  """Create a rotation animation around the Y axis"""
 
282
  frames = []
283
 
284
  # Normalize the mesh for consistent animation
285
+ try:
286
+ mesh, original_center, original_scale = normalize_mesh(mesh)
287
+ print(f"Normalized mesh center: {original_center}, scale: {original_scale}")
288
+ except Exception as e:
289
+ print(f"Error normalizing mesh: {str(e)}")
290
+ # ์ •๊ทœํ™”์— ์‹คํŒจํ•˜๋”๋ผ๋„ ๊ณ„์† ์ง„ํ–‰
291
+ pass
292
 
293
  for frame_idx in range(num_frames):
294
  t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
 
522
  """Generate a GIF from animation frames"""
523
  gif_frames = []
524
 
525
+ # ๋ชจ๋“  ํ”„๋ ˆ์ž„์˜ ๊ฒฝ๊ณ„ ์ƒ์ž๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ์ผ๊ด€๋œ ์นด๋ฉ”๋ผ ์„ค์ • ๊ตฌ์„ฑ
526
+ all_bounds = []
527
  for frame in frames:
528
+ if isinstance(frame, trimesh.Scene):
529
+ # ์”ฌ์—์„œ ๋ชจ๋“  ๋ฉ”์‹œ์˜ ์ •์ ์„ ์ถ”์ถœํ•˜์—ฌ ๊ฒฝ๊ณ„์ƒ์ž ๊ณ„์‚ฐ
530
+ all_points = []
531
+ for geom in frame.geometry.values():
532
+ if hasattr(geom, 'vertices') and geom.vertices.shape[0] > 0:
533
+ all_points.append(geom.vertices)
534
+
535
+ if all_points:
536
+ all_vertices = np.vstack(all_points)
537
+ bounds = np.array([all_vertices.min(axis=0), all_vertices.max(axis=0)])
538
+ all_bounds.append(bounds)
539
+ elif hasattr(frame, 'vertices') and frame.vertices.shape[0] > 0:
540
+ # ์ผ๋ฐ˜ ๋ฉ”์‹œ์—์„œ ๊ฒฝ๊ณ„์ƒ์ž ๊ณ„์‚ฐ
541
+ bounds = np.array([frame.vertices.min(axis=0), frame.vertices.max(axis=0)])
542
+ all_bounds.append(bounds)
543
+
544
+ # ๋ชจ๋“  ํ”„๋ ˆ์ž„์„ ํฌํ•จํ•˜๋Š” ์ „์ฒด ๊ฒฝ๊ณ„์ƒ์ž ๊ณ„์‚ฐ
545
+ if all_bounds:
546
+ min_corner = np.min(np.array([b[0] for b in all_bounds]), axis=0)
547
+ max_corner = np.max(np.array([b[1] for b in all_bounds]), axis=0)
548
+ total_bounds = np.array([min_corner, max_corner])
549
 
550
+ # ๊ฒฝ๊ณ„์ƒ์ž ์ค‘์‹ฌ๊ณผ ํฌ๊ธฐ ๊ณ„์‚ฐ
551
+ center = (total_bounds[0] + total_bounds[1]) / 2
552
+ size = np.max(total_bounds[1] - total_bounds[0])
 
 
 
553
 
554
+ # ์นด๋ฉ”๋ผ ์œ„์น˜ ๊ณ„์‚ฐ (์ ์ ˆํ•œ ๊ฑฐ๋ฆฌ์—์„œ ๊ฐ์ฒด ๋ฐ”๋ผ๋ณด๊ธฐ)
555
+ camera_distance = size * 2.5 # ๊ฐ์ฒด ํฌ๊ธฐ์˜ 2.5๋ฐฐ ๊ฑฐ๋ฆฌ
556
+ else:
557
+ # ๊ฒฝ๊ณ„์ƒ์ž๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
558
+ center = np.zeros(3)
559
+ camera_distance = 2.0
560
+
561
+ # ๊ฐ ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง
562
+ for i, frame in enumerate(frames):
563
  try:
564
+ # ์”ฌ ๊ฐ์ฒด ์ƒ์„ฑ
565
+ if not isinstance(frame, trimesh.Scene):
566
+ scene = trimesh.Scene(frame)
567
+ else:
568
+ scene = frame
569
+
570
+ # ๋” ๊ฐ•๋ ฅํ•œ ์นด๋ฉ”๋ผ ์„ค์ • ๋ฐฉ๋ฒ•
571
+ try:
572
+ # ๊ฐ์ฒด๊ฐ€ ๋ณด์ด๋Š” ์œ„์น˜์— ์นด๋ฉ”๋ผ ์„ค์ •
573
+ camera_fov = 60.0 # ์‹œ์•ผ๊ฐ (๊ฐ๋„)
574
+ camera_pos = np.array([0, 0, camera_distance]) # ์นด๋ฉ”๋ผ ์œ„์น˜
575
+
576
+ # ์นด๋ฉ”๋ผ ๋ณ€ํ™˜ ํ–‰๋ ฌ ์ƒ์„ฑ
577
+ camera_transform = np.eye(4)
578
+ camera_transform[:3, 3] = camera_pos
579
+
580
+ # ์นด๋ฉ”๋ผ๊ฐ€ ์›์ ์„ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
581
+ scene.camera = trimesh.scene.Camera(
582
+ resolution=resolution,
583
+ fov=[camera_fov, camera_fov * (resolution[1] / resolution[0])]
584
+ )
585
+ scene.camera_transform = camera_transform
586
+ except Exception as e:
587
+ print(f"Error setting camera: {str(e)}")
588
+ # ๊ธฐ๋ณธ ์นด๋ฉ”๋ผ ์„ค์ •
589
+ scene.set_camera(angles=(0, 0, 0), distance=camera_distance)
590
+
591
+ # ๋ Œ๋”๋ง ์‹œ๋„
592
+ try:
593
+ # PyOpenGL ๊ฒฝ๊ณ  ์ˆจ๊ธฐ๊ธฐ
594
+ import warnings
595
+ warnings.filterwarnings("ignore", category=UserWarning)
596
+
597
+ # ์”ฌ ๋ Œ๋”๋ง
598
+ rendered_img = scene.save_image(resolution=resolution, background=background_color)
599
+ pil_img = Image.open(rendered_img)
600
+
601
+ # ์ด๋ฏธ์ง€๊ฐ€ ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ
602
+ if np.array(pil_img).std() < 1.0: # ํ‘œ์ค€ํŽธ์ฐจ๊ฐ€ ์ž‘์œผ๋ฉด ๋Œ€๋ถ€๋ถ„ ๋™์ผํ•œ ์ƒ‰์ƒ (๋นˆ ์ด๋ฏธ์ง€)
603
+ print(f"Warning: Frame {i} seems to be empty, trying different angle")
604
+ # ๋‹ค๋ฅธ ๊ฐ๋„์—์„œ ๋‹ค์‹œ ์‹œ๋„
605
+ scene.set_camera(angles=(np.pi/4, np.pi/4, 0), distance=camera_distance*1.2)
606
+ rendered_img = scene.save_image(resolution=resolution, background=background_color)
607
+ pil_img = Image.open(rendered_img)
608
+
609
+ gif_frames.append(pil_img)
610
+ except Exception as e:
611
+ print(f"Error in main rendering: {str(e)}")
612
+ # ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋”๋ง ์‹คํŒจ ์‹œ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์‹œ๋„
613
+ try:
614
+ # ๋Œ€์ฒด ๋ Œ๋”๋ง ๋ฉ”์„œ๋“œ
615
+ from PIL import Image, ImageDraw
616
+ img = Image.new('RGB', resolution, color=(255, 255, 255))
617
+ draw = ImageDraw.Draw(img)
618
+
619
+ # ํ”„๋ ˆ์ž„ ๋ฒˆํ˜ธ๋ฅผ ํ…์ŠคํŠธ๋กœ ํ‘œ์‹œ
620
+ draw.text((resolution[0]//2, resolution[1]//2), f"Frame {i}", fill=(0, 0, 0))
621
+ gif_frames.append(img)
622
+ except Exception as e2:
623
+ print(f"Error in fallback rendering: {str(e2)}")
624
+ gif_frames.append(Image.new('RGB', resolution, (255, 255, 255)))
625
  except Exception as e:
626
+ print(f"Unexpected error in frame processing: {str(e)}")
 
627
  gif_frames.append(Image.new('RGB', resolution, (255, 255, 255)))
628
 
629
+ # GIF ์ €์žฅ
630
  if gif_frames:
631
+ try:
632
+ gif_frames[0].save(
633
+ output_path,
634
+ save_all=True,
635
+ append_images=gif_frames[1:],
636
+ optimize=False,
637
+ duration=int(1000 / fps),
638
+ loop=0
639
+ )
640
+ print(f"GIF saved to {output_path}")
641
+ # ํ™•์ธ์„ ์œ„ํ•ด ์ฒซ ํ”„๋ ˆ์ž„๋„ ๋”ฐ๋กœ ์ €์žฅ
642
+ first_frame_path = output_path.replace('.gif', '_first_frame.png')
643
+ gif_frames[0].save(first_frame_path)
644
+ print(f"First frame saved to {first_frame_path}")
645
+ return output_path
646
+ except Exception as e:
647
+ print(f"Error saving GIF: {str(e)}")
648
+ return None
649
  else:
650
+ print("No frames to save")
651
  return None
652
 
653
  def create_animation_mesh(input_mesh_path, animation_type='rotate', duration=3.0, fps=30):
654
  """Create animation from input mesh based on animation type"""
655
  # Load the mesh
656
  try:
657
+ print(f"Loading mesh from {input_mesh_path}")
658
  # ๋จผ์ € Scene์œผ๋กœ ๋กœ๋“œ ์‹œ๋„
659
  loaded_obj = trimesh.load(input_mesh_path)
660
+ print(f"Loaded object type: {type(loaded_obj)}")
661
 
662
  # Scene์ธ ๊ฒฝ์šฐ ๋‹จ์ผ ๋ฉ”์‹œ๋กœ ๋ณ€ํ™˜ ์‹œ๋„
663
  if isinstance(loaded_obj, trimesh.Scene):
664
  print("Loaded a scene, extracting meshes...")
665
  # ์”ฌ์—์„œ ๋ชจ๋“  ๋ฉ”์‹œ ์ถ”์ถœ
666
  meshes = []
667
+ for geometry_name, geometry in loaded_obj.geometry.items():
668
  if isinstance(geometry, trimesh.Trimesh):
669
+ print(f"Found mesh: {geometry_name} with {len(geometry.vertices)} vertices")
670
  meshes.append(geometry)
671
 
672
  if not meshes:
673
+ print("No meshes found in scene, trying simple animation...")
674
+ frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps))
675
+ if not frames:
676
+ print("Simple animation failed too")
677
+ return None, None
678
+ else:
679
+ # ๋ชจ๋“  ๋ฉ”์‹œ๋ฅผ ํ•˜๋‚˜๋กœ ๊ฒฐํ•ฉ
680
+ mesh = loaded_obj
681
+ print(f"Using original scene with {len(meshes)} meshes")
682
  elif isinstance(loaded_obj, trimesh.Trimesh):
683
  mesh = loaded_obj
684
+ print(f"Loaded a single mesh with {len(mesh.vertices)} vertices")
685
  else:
686
  print(f"Unsupported object type: {type(loaded_obj)}")
687
+ # ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ ์‹œ๋„
688
+ frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps))
689
+ if not frames:
690
+ return None, None
691
  except Exception as e:
692
  print(f"Error loading mesh: {str(e)}")
693
+ # ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ ์‹œ๋„
694
+ frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps))
695
+ if not frames:
696
+ return None, None
697
 
698
  # Generate animation frames based on animation type
699
+ try:
700
+ if 'frames' not in locals(): # ์ด๋ฏธ frames๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด
701
+ print(f"Generating {animation_type} animation...")
702
+ if animation_type == 'rotate':
703
+ frames = create_rotation_animation(mesh, duration, fps)
704
+ elif animation_type == 'float':
705
+ frames = create_float_animation(mesh, duration, fps)
706
+ elif animation_type == 'explode':
707
+ frames = create_explode_animation(mesh, duration, fps)
708
+ elif animation_type == 'assemble':
709
+ frames = create_assemble_animation(mesh, duration, fps)
710
+ elif animation_type == 'pulse':
711
+ frames = create_pulse_animation(mesh, duration, fps)
712
+ elif animation_type == 'swing':
713
+ frames = create_swing_animation(mesh, duration, fps)
714
+ else:
715
+ print(f"Unknown animation type: {animation_type}, using default rotate")
716
+ frames = create_rotation_animation(mesh, duration, fps)
717
+ except Exception as e:
718
+ print(f"Error generating animation: {str(e)}")
719
+ # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ ์‹คํŒจ ์‹œ ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ๋„
720
+ frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps))
721
+ if not frames:
722
+ return None, None
723
+
724
+ print(f"Generated {len(frames)} animation frames")
725
 
726
  base_filename = os.path.basename(input_mesh_path).rsplit('.', 1)[0]
727
 
 
735
  # First frame for static GLB
736
  first_frame = frames[0]
737
  # Export as GLB
738
+ if not isinstance(first_frame, trimesh.Scene):
739
+ scene = trimesh.Scene(first_frame)
740
+ else:
741
+ scene = first_frame
742
  scene.export(animated_glb_path)
743
+ print(f"Exported GLB to {animated_glb_path}")
744
  else:
745
  return None, None
746
  except Exception as e:
 
750
  # Create GIF for preview
751
  try:
752
  animated_gif_path = os.path.join(LOG_PATH, f'animated_{base_filename}.gif')
753
+ print(f"Creating GIF at {animated_gif_path}")
754
  generate_gif_from_frames(frames, animated_gif_path, fps)
755
  except Exception as e:
756
  print(f"Error creating GIF: {str(e)}")
 
772
  fps=fps
773
  )
774
 
775
+ if not animated_glb_path:
776
+ # ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ ๊ฐ„๋‹จํ•œ ๋ฐฉ์‹์œผ๋กœ ๋‹ค์‹œ ์‹œ๋„
777
+ print("Primary animation method failed, trying simple animation...")
778
+ frames = create_simple_animation_frames(
779
+ input_3d,
780
+ animation_type,
781
+ int(animation_duration * fps)
782
+ )
783
+
784
+ if frames:
785
+ base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
786
+
787
+ # GLB ์ €์žฅ
788
+ animated_glb_path = os.path.join(LOG_PATH, f'simple_animated_{base_filename}.glb')
789
+ if isinstance(frames[0], trimesh.Scene):
790
+ scene = frames[0]
791
+ else:
792
+ scene = trimesh.Scene(frames[0])
793
+ scene.export(animated_glb_path)
794
+
795
+ # GIF ์ƒ์„ฑ
796
+ animated_gif_path = os.path.join(LOG_PATH, f'simple_animated_{base_filename}.gif')
797
+ generate_gif_from_frames(frames, animated_gif_path, fps)
798
+ else:
799
+ # ๋ชจ๋“  ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํŒจํ•œ ๊ฒฝ์šฐ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ GIF๋ผ๋„ ์ƒ์„ฑ
800
+ base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
801
+ text_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
802
+ animated_gif_path = create_textual_animation_gif(
803
+ text_gif_path,
804
+ os.path.basename(input_3d),
805
+ animation_type,
806
+ animation_duration,
807
+ fps
808
+ )
809
+
810
+ # ์›๋ณธ ํŒŒ์ผ์„ GLB๋กœ ๋ณต์‚ฌ (์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ)
811
+ copy_glb_path = os.path.join(LOG_PATH, f'copy_{base_filename}.glb')
812
+ import shutil
813
+ try:
814
+ shutil.copy(input_3d, copy_glb_path)
815
+ animated_glb_path = copy_glb_path
816
+ print(f"Copied original GLB to {copy_glb_path}")
817
+ except:
818
+ animated_glb_path = input_3d
819
+ print("Could not copy original GLB, using input path")
820
 
821
  # Create a simple JSON metadata file
822
  metadata = {
 
831
  with open(json_path, 'w') as f:
832
  json.dump(metadata, f, indent=4)
833
 
834
+ # GIF๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ Œ๋”๋ง ์‹คํŒจ์ธ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๋ฐฑ์—… GIF ์ƒ์„ฑ
835
+ if not animated_gif_path:
836
+ base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
837
+ text_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
838
+ animated_gif_path = create_textual_animation_gif(
839
+ text_gif_path,
840
+ os.path.basename(input_3d),
841
+ animation_type,
842
+ animation_duration,
843
+ fps
844
+ )
845
+
846
+ return animated_glb_path, animated_gif_path, json_path
847
+ except Exception as e:
848
+ error_msg = f"Error processing file: {str(e)}"
849
+ print(error_msg)
850
+
851
+ # ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ…์ŠคํŠธ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋ผ๋„ ์ƒ์„ฑ
852
+ try:
853
+ base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
854
+ text_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
855
+ animated_gif_path = create_textual_animation_gif(
856
+ text_gif_path,
857
+ os.path.basename(input_3d),
858
+ animation_type,
859
+ animation_duration,
860
+ fps
861
+ )
862
+
863
+ # ์›๋ณธ ํŒŒ์ผ์„ GLB๋กœ ๋ณต์‚ฌ (์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ)
864
+ copy_glb_path = os.path.join(LOG_PATH, f'copy_{base_filename}.glb')
865
+ import shutil
866
+ try:
867
+ shutil.copy(input_3d, copy_glb_path)
868
+ animated_glb_path = copy_glb_path
869
+ except:
870
+ animated_glb_path = input_3d
871
+
872
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ
873
+ metadata = {
874
+ "animation_type": animation_type,
875
+ "duration": animation_duration,
876
+ "fps": fps,
877
+ "original_model": os.path.basename(input_3d),
878
+ "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
879
+ "error": str(e)
880
+ }
881
+
882
+ json_path = os.path.join(LOG_PATH, f'metadata_{base_filename}.json')
883
+ with open(json_path, 'w') as f:
884
+ json.dump(metadata, f, indent=4)
885
+
886
+ return animated_glb_path, animated_gif_path, json_path
887
+ except:
888
+ # ๋ชจ๋“  ๋ฐฉ๋ฒ•์ด ์‹คํŒจํ•œ ๊ฒฝ์šฐ
889
+ return error_msg, None, None, 50), f"Model: {os.path.basename(input_3d)}", fill=(0, 0, 0))
890
+ draw.text((50, 100), f"Animation: {animation_type}", fill=(0, 0, 0))
891
+ draw.text((50, 150), f"Angle: {angle}ยฐ", fill=(0, 0, 0))
892
+ # ๊ฐ„๋‹จํ•œ ํšŒ์ „ ๊ฐ์ฒด ๊ทธ๋ฆฌ๊ธฐ
893
+ center_x, center_y = 200, 180
894
+ radius = 50
895
+ x = center_x + radius * math.cos(math.radians(angle))
896
+ y = center_y + radius * math.sin(math.radians(angle))
897
+ draw.ellipse((x-20, y-20, x+20, y+20), fill=(255, 0, 0))
898
+ simple_frames.append(img)
899
+
900
+ # GIF ์ €์žฅ
901
+ simple_frames[0].save(
902
+ backup_gif_path,
903
+ save_all=True,
904
+ append_images=simple_frames[1:],
905
+ optimize=False,
906
+ duration=int(1000 / fps),
907
+ loop=0
908
+ )
909
+ print(f"Backup GIF created at {backup_gif_path}")
910
+
911
+ # ๊ธฐ์กด GIF๊ฐ€ ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐฑ์—… GIF ์‚ฌ์šฉ
912
+ try:
913
+ # ๊ธฐ์กด GIF ํ™•์ธ
914
+ original_gif = Image.open(animated_gif_path)
915
+ original_frames = []
916
+ try:
917
+ for i in range(100): # ์ตœ๋Œ€ 100 ํ”„๋ ˆ์ž„
918
+ original_gif.seek(i)
919
+ frame = original_gif.copy()
920
+ original_frames.append(frame)
921
+ except EOFError:
922
+ pass # ๋ชจ๋“  ํ”„๋ ˆ์ž„ ์ฝ์Œ
923
+
924
+ if not original_frames:
925
+ print("Original GIF is empty, using backup")
926
+ animated_gif_path = backup_gif_path
927
+ except Exception as e:
928
+ print(f"Error checking original GIF: {str(e)}, using backup")
929
+ animated_gif_path = backup_gif_path
930
+ except Exception as e:
931
+ print(f"Error creating backup GIF: {str(e)}")
932
+
933
  return animated_glb_path, animated_gif_path, json_path
934
  except Exception as e:
935
  error_msg = f"Error processing file: {str(e)}"