ginipick commited on
Commit
a285b9c
Β·
verified Β·
1 Parent(s): 61b805a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -914
app.py CHANGED
@@ -1,3 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  def create_textual_animation_gif(output_path, model_name, animation_type, duration=3.0, fps=30):
2
  """ν…μŠ€νŠΈ 기반의 κ°„λ‹¨ν•œ μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성 - λ Œλ”λ§ μ‹€νŒ¨ μ‹œ λŒ€μ²΄μš©"""
3
  try:
@@ -84,741 +105,37 @@ def create_textual_animation_gif(output_path, model_name, animation_type, durati
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
135
- import numpy as np
136
- import trimesh
137
- import argparse
138
- from scipy.spatial.transform import Rotation
139
- import PIL.Image
140
- from PIL import Image
141
- import math
142
- import trimesh.transformations as tf
143
- from trimesh.exchange.gltf import export_glb
144
-
145
- os.environ['PYOPENGL_PLATFORM'] = 'egl'
146
-
147
- import gradio as gr
148
- import spaces
149
-
150
- def parse_args():
151
- parser = argparse.ArgumentParser(description='Create animations for 3D models')
152
-
153
- parser.add_argument(
154
- '--input',
155
- type=str,
156
- default='./data/demo_glb/',
157
- help='Input file or directory path (default: ./data/demo_glb/)'
158
- )
159
-
160
- parser.add_argument(
161
- '--log_path',
162
- type=str,
163
- default='./results/demo',
164
- help='Output directory path (default: results/demo)'
165
- )
166
-
167
- parser.add_argument(
168
- '--animation_type',
169
- type=str,
170
- default='rotate',
171
- choices=['rotate', 'float', 'explode', 'assemble', 'pulse', 'swing'],
172
- help='Type of animation to apply'
173
- )
174
-
175
- parser.add_argument(
176
- '--animation_duration',
177
- type=float,
178
- default=3.0,
179
- help='Duration of animation in seconds'
180
- )
181
-
182
- parser.add_argument(
183
- '--fps',
184
- type=int,
185
- default=30,
186
- help='Frames per second for animation'
187
- )
188
-
189
- return parser.parse_args()
190
-
191
- def get_input_files(input_path):
192
- if os.path.isfile(input_path):
193
- return [input_path]
194
- elif os.path.isdir(input_path):
195
- return glob.glob(os.path.join(input_path, '*'))
196
- else:
197
- raise ValueError(f"Input path {input_path} is neither a file nor a directory")
198
-
199
- args = parse_args()
200
-
201
- LOG_PATH = args.log_path
202
- os.makedirs(LOG_PATH, exist_ok=True)
203
-
204
- print(f"Output directory: {LOG_PATH}")
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"""
281
- num_frames = int(duration * fps)
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]
295
- angle = t * 2 * math.pi # Full rotation
296
-
297
- if isinstance(mesh, trimesh.Scene):
298
- # Scene 객체인 경우 각 λ©”μ‹œμ— νšŒμ „ 적용
299
- frame_scene = trimesh.Scene()
300
-
301
- for node_name, transform, geometry_name in mesh.graph.nodes_geometry:
302
- # λ©”μ‹œλ₯Ό λ³΅μ‚¬ν•˜κ³  νšŒμ „ 적용
303
- mesh_copy = mesh.geometry[geometry_name].copy()
304
-
305
- # λ©”μ‹œμ˜ 쀑심점 계산
306
- center_point = mesh_copy.centroid if hasattr(mesh_copy, 'centroid') else np.zeros(3)
307
-
308
- # νšŒμ „ ν–‰λ ¬ 생성
309
- rotation_matrix = tf.rotation_matrix(angle, [0, 1, 0], center_point)
310
-
311
- # λ³€ν™˜ 적용
312
- mesh_copy.apply_transform(rotation_matrix)
313
-
314
- # 씬에 μΆ”κ°€
315
- frame_scene.add_geometry(mesh_copy, node_name=node_name)
316
-
317
- frames.append(frame_scene)
318
- else:
319
- # 일반 Trimesh 객체인 경우 직접 νšŒμ „ 적용
320
- animated_mesh = mesh.copy()
321
- rotation_matrix = tf.rotation_matrix(angle, [0, 1, 0])
322
- animated_mesh.apply_transform(rotation_matrix)
323
- frames.append(animated_mesh)
324
-
325
- return frames
326
-
327
- def create_float_animation(mesh, duration=3.0, fps=30, amplitude=0.2):
328
- """Create a floating animation where the mesh moves up and down"""
329
- num_frames = int(duration * fps)
330
- frames = []
331
-
332
- # Normalize the mesh for consistent animation
333
- mesh, original_center, original_scale = normalize_mesh(mesh)
334
-
335
- for frame_idx in range(num_frames):
336
- t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
337
- y_offset = amplitude * math.sin(2 * math.pi * t)
338
-
339
- if isinstance(mesh, trimesh.Scene):
340
- # Scene 객체인 경우
341
- frame_scene = trimesh.Scene()
342
-
343
- for node_name, transform, geometry_name in mesh.graph.nodes_geometry:
344
- # λ©”μ‹œλ₯Ό 볡사
345
- mesh_copy = mesh.geometry[geometry_name].copy()
346
-
347
- # λ³€ν™˜ 적용
348
- translation_matrix = tf.translation_matrix([0, y_offset, 0])
349
- mesh_copy.apply_transform(translation_matrix)
350
-
351
- # 씬에 μΆ”κ°€
352
- frame_scene.add_geometry(mesh_copy, node_name=node_name)
353
-
354
- frames.append(frame_scene)
355
- else:
356
- # 일반 Trimesh 객체인 경우
357
- animated_mesh = mesh.copy()
358
- translation_matrix = tf.translation_matrix([0, y_offset, 0])
359
- animated_mesh.apply_transform(translation_matrix)
360
- frames.append(animated_mesh)
361
-
362
- return frames
363
-
364
- def create_explode_animation(mesh, duration=3.0, fps=30):
365
- """Create an explode animation where parts of the mesh move outward"""
366
- num_frames = int(duration * fps)
367
- frames = []
368
-
369
- # Normalize the mesh for consistent animation
370
- mesh, original_center, original_scale = normalize_mesh(mesh)
371
-
372
- # 씬인 경우 뢀뢄별 처리
373
- if isinstance(mesh, trimesh.Scene):
374
- for frame_idx in range(num_frames):
375
- t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
376
-
377
- # μƒˆ 씬 생성
378
- frame_scene = trimesh.Scene()
379
-
380
- # 각 λ…Έλ“œλ³„ 폭발 μ• λ‹ˆλ©”μ΄μ…˜ 적용
381
- for node_name, transform, geometry_name in mesh.graph.nodes_geometry:
382
- mesh_copy = mesh.geometry[geometry_name].copy()
383
-
384
- # λ…Έλ“œμ˜ 쀑심점 계산
385
- center_point = mesh_copy.centroid if hasattr(mesh_copy, 'centroid') else np.zeros(3)
386
-
387
- # λ°©ν–₯ 벑터 (μ›μ μ—μ„œ 객체 μ€‘μ‹¬κΉŒμ§€)
388
- direction = center_point
389
- if np.linalg.norm(direction) < 1e-10:
390
- # 쀑심점이 원점과 λ„ˆλ¬΄ κ°€κΉŒμš°λ©΄ 랜덀 λ°©ν–₯ 선택
391
- direction = np.random.rand(3) - 0.5
392
-
393
- direction = direction / np.linalg.norm(direction)
394
-
395
- # 폭발 이동 적용
396
- translation = direction * t * 0.5 # 폭발 강도 쑰절
397
- translation_matrix = tf.translation_matrix(translation)
398
- mesh_copy.apply_transform(translation_matrix)
399
-
400
- # 씬에 μΆ”κ°€
401
- frame_scene.add_geometry(mesh_copy, node_name=f"{node_name}_{frame_idx}")
402
-
403
- frames.append(frame_scene)
404
- else:
405
- # λ©”μ‹œλ₯Ό μ—¬λŸ¬ λΆ€λΆ„μœΌλ‘œ λΆ„ν• 
406
- try:
407
- components = mesh.split(only_watertight=False)
408
- if len(components) <= 1:
409
- # μ»΄ν¬λ„ŒνŠΈκ°€ ν•˜λ‚˜λΏμ΄λ©΄ 정점 기반 폭발 μ• λ‹ˆλ©”μ΄μ…˜ μ‚¬μš©
410
- raise ValueError("Mesh cannot be split into components")
411
- except:
412
- # 뢄할이 μ‹€νŒ¨ν•˜λ©΄ 정점 기반 폭발 μ• λ‹ˆλ©”μ΄μ…˜ μ‚¬μš©
413
- for frame_idx in range(num_frames):
414
- t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
415
-
416
- # λ©”μ‹œ 볡사
417
- animated_mesh = mesh.copy()
418
- vertices = animated_mesh.vertices.copy()
419
-
420
- # 각 정점을 μ€‘μ‹¬μ—μ„œ λ°”κΉ₯μͺ½μœΌλ‘œ 이동
421
- directions = vertices.copy()
422
- norms = np.linalg.norm(directions, axis=1, keepdims=True)
423
- mask = norms > 1e-10
424
- directions[mask] = directions[mask] / norms[mask]
425
- directions[~mask] = np.random.rand(np.sum(~mask), 3) - 0.5
426
-
427
- # 폭발 κ³„μˆ˜ 적용
428
- vertices += directions * t * 0.3
429
- animated_mesh.vertices = vertices
430
-
431
- frames.append(animated_mesh)
432
- else:
433
- # μ»΄ν¬λ„ŒνŠΈ 기반 폭발 μ• λ‹ˆλ©”μ΄μ…˜
434
- for frame_idx in range(num_frames):
435
- t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
436
-
437
- # μƒˆ 씬 생성
438
- scene = trimesh.Scene()
439
-
440
- # 각 μ»΄ν¬λ„ŒνŠΈλ₯Ό μ€‘μ‹¬μ—μ„œ λ°”κΉ₯μͺ½μœΌλ‘œ 이동
441
- for i, component in enumerate(components):
442
- # μ»΄ν¬λ„ŒνŠΈ 볡사
443
- animated_component = component.copy()
444
-
445
- # μ»΄ν¬λ„ŒνŠΈ μ€‘μ‹¬μ μ—μ„œ λ°©ν–₯ 계산
446
- direction = animated_component.centroid
447
- if np.linalg.norm(direction) < 1e-10:
448
- # 쀑심점이 원점과 λ„ˆλ¬΄ κ°€κΉŒμš°λ©΄ 랜덀 λ°©ν–₯ 선택
449
- direction = np.random.rand(3) - 0.5
450
-
451
- direction = direction / np.linalg.norm(direction)
452
-
453
- # 폭발 이동 적용
454
- translation = direction * t * 0.5
455
- translation_matrix = tf.translation_matrix(translation)
456
- animated_component.apply_transform(translation_matrix)
457
-
458
- # 씬에 μΆ”κ°€
459
- scene.add_geometry(animated_component, node_name=f"component_{i}")
460
-
461
- # 씬을 단일 λ©”μ‹œλ‘œ λ³€ν™˜ (κ·Όμ‚¬μΉ˜)
462
- animated_mesh = trimesh.util.concatenate(scene.dump())
463
- frames.append(animated_mesh)
464
-
465
- return frames
466
-
467
- def create_assemble_animation(mesh, duration=3.0, fps=30):
468
- """Create an assembly animation (reverse of explode)"""
469
- # Get explode animation and reverse it
470
- explode_frames = create_explode_animation(mesh, duration, fps)
471
- return list(reversed(explode_frames))
472
-
473
- def create_pulse_animation(mesh, duration=3.0, fps=30, min_scale=0.8, max_scale=1.2):
474
- """Create a pulsing animation where the mesh scales up and down"""
475
- num_frames = int(duration * fps)
476
- frames = []
477
-
478
- # Normalize the mesh for consistent animation
479
- mesh, original_center, original_scale = normalize_mesh(mesh)
480
-
481
- for frame_idx in range(num_frames):
482
- t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
483
-
484
- # Create a copy of the mesh to animate
485
- animated_mesh = mesh.copy()
486
-
487
- # Apply pulsing motion (sinusoidal scale)
488
- scale_factor = min_scale + (max_scale - min_scale) * (0.5 + 0.5 * math.sin(2 * math.pi * t))
489
- scale_matrix = tf.scale_matrix(scale_factor)
490
- animated_mesh.apply_transform(scale_matrix)
491
-
492
- # Add to frames
493
- frames.append(animated_mesh)
494
-
495
- return frames
496
-
497
- def create_swing_animation(mesh, duration=3.0, fps=30, max_angle=math.pi/6):
498
- """Create a swinging animation where the mesh rotates back and forth"""
499
- num_frames = int(duration * fps)
500
- frames = []
501
-
502
- # Normalize the mesh for consistent animation
503
- mesh, original_center, original_scale = normalize_mesh(mesh)
504
-
505
- for frame_idx in range(num_frames):
506
- t = frame_idx / (num_frames - 1) # Normalized time [0, 1]
507
-
508
- # Create a copy of the mesh to animate
509
- animated_mesh = mesh.copy()
510
-
511
- # Apply swinging motion (sinusoidal rotation)
512
- angle = max_angle * math.sin(2 * math.pi * t)
513
- rotation_matrix = tf.rotation_matrix(angle, [0, 1, 0])
514
- animated_mesh.apply_transform(rotation_matrix)
515
-
516
- # Add to frames
517
- frames.append(animated_mesh)
518
-
519
- return frames
520
-
521
- def generate_gif_from_frames(frames, output_path, fps=30, resolution=(640, 480), background_color=(255, 255, 255, 255)):
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
-
728
- # Save animated mesh as GLB
729
- try:
730
- animated_glb_path = os.path.join(LOG_PATH, f'animated_{base_filename}.glb')
731
-
732
- # For GLB output, we'll use the first frame for now
733
- # In a production environment, you'd want to use proper animation keyframes
734
- if frames and len(frames) > 0:
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:
747
- print(f"Error exporting GLB: {str(e)}")
748
- animated_glb_path = None
749
-
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)}")
757
- animated_gif_path = None
758
-
759
- return animated_glb_path, animated_gif_path
760
-
761
  @spaces.GPU
762
  def process_3d_model(input_3d, animation_type, animation_duration, fps):
763
  """Process a 3D model and apply animation"""
764
  print(f"Processing: {input_3d} with animation type: {animation_type}")
765
 
766
  try:
767
- # Create animation
768
- animated_glb_path, animated_gif_path = create_animation_mesh(
769
- input_3d,
770
- animation_type=animation_type,
771
- duration=animation_duration,
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 = {
823
  "animation_type": animation_type,
824
  "duration": animation_duration,
@@ -827,206 +144,98 @@ def process_3d_model(input_3d, animation_type, animation_duration, fps):
827
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S")
828
  }
829
 
830
- json_path = os.path.join(LOG_PATH, f'metadata_{os.path.basename(input_3d).rsplit(".", 1)[0]}.json')
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)}"
936
  print(error_msg)
937
  return error_msg, None, None
938
 
939
- _HEADER_ = '''
940
- <h2><b>GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 - 3D λͺ¨λΈ μ›€μ§μž„ 효과</b></h2>
941
-
942
- 이 데λͺ¨λ₯Ό 톡해 정적인 3D λͺ¨λΈ(GLB 파일)에 λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
943
-
944
- ❗️❗️❗️**μ€‘μš”μ‚¬ν•­:**
945
- - 이 데λͺ¨λŠ” μ—…λ‘œλ“œλœ GLB νŒŒμΌμ— μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•©λ‹ˆλ‹€.
946
- - λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ μŠ€νƒ€μΌ μ€‘μ—μ„œ μ„ νƒν•˜μ„Έμš”: νšŒμ „, λΆ€μœ , 폭발, 쑰립, νŽ„μŠ€, μŠ€μœ™.
947
- - κ²°κ³ΌλŠ” μ• λ‹ˆλ©”μ΄μ…˜λœ GLB 파일과 미리보기용 GIF 파일둜 μ œκ³΅λ©λ‹ˆλ‹€.
948
- '''
949
-
950
- _INFO_ = r"""
951
- ### μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜• μ„€λͺ…
952
- - **νšŒμ „(rotate)**: λͺ¨λΈμ΄ Y좕을 μ€‘μ‹¬μœΌλ‘œ νšŒμ „ν•©λ‹ˆλ‹€.
953
- - **λΆ€μœ (float)**: λͺ¨λΈμ΄ μœ„μ•„λž˜λ‘œ λΆ€λ“œλŸ½κ²Œ λ– λ‹€λ‹™λ‹ˆλ‹€.
954
- - **폭발(explode)**: λͺ¨λΈμ˜ 각 뢀뢄이 μ€‘μ‹¬μ—μ„œ λ°”κΉ₯μͺ½μœΌλ‘œ νΌμ Έλ‚˜κ°‘λ‹ˆλ‹€.
955
- - **쑰립(assemble)**: 폭발 μ• λ‹ˆλ©”μ΄μ…˜μ˜ λ°˜λŒ€ - λΆ€ν’ˆλ“€μ΄ ν•¨κ»˜ λͺ¨μž…λ‹ˆλ‹€.
956
- - **νŽ„μŠ€(pulse)**: λͺ¨λΈμ΄ 크기가 μ»€μ‘Œλ‹€ μž‘μ•„μ‘Œλ‹€λ₯Ό λ°˜λ³΅ν•©λ‹ˆλ‹€.
957
- - **μŠ€μœ™(swing)**: λͺ¨λΈμ΄ 쒌우둜 λΆ€λ“œλŸ½κ²Œ ν”λ“€λ¦½λ‹ˆλ‹€.
958
-
959
- ### 팁
960
- - μ• λ‹ˆλ©”μ΄μ…˜ 길이와 FPSλ₯Ό μ‘°μ ˆν•˜μ—¬ μ›€μ§μž„μ˜ 속도와 λΆ€λ“œλŸ¬μ›€μ„ μ‘°μ ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€.
961
- - λ³΅μž‘ν•œ λͺ¨λΈμ€ 처리 μ‹œκ°„μ΄ 더 였래 걸릴 수 μžˆμŠ΅λ‹ˆλ‹€.
962
- - GIF λ―Έλ¦¬λ³΄κΈ°λŠ” λΉ λ₯Έ 참쑰용이며, κ³ ν’ˆμ§ˆ κ²°κ³Όλ₯Ό μœ„ν•΄μ„œλŠ” μ• λ‹ˆλ©”μ΄μ…˜λœ GLB νŒŒμΌμ„ λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”.
963
- """
964
-
965
  # Gradio μΈν„°νŽ˜μ΄μŠ€ μ„€μ •
966
- def create_gradio_interface():
967
- with gr.Blocks(title="GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기") as demo:
968
- # 제λͺ© μ„Ήμ…˜
969
- gr.Markdown(_HEADER_)
970
-
971
- with gr.Row():
972
- with gr.Column():
973
- # μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ
974
- input_3d = gr.Model3D(label="3D λͺ¨λΈ 파일 μ—…λ‘œλ“œ (GLB 포맷)")
975
-
976
- with gr.Row():
977
- animation_type = gr.Dropdown(
978
- label="μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•",
979
- choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
980
- value="rotate"
981
- )
982
-
983
- with gr.Row():
984
- animation_duration = gr.Slider(
985
- label="μ• λ‹ˆλ©”μ΄μ…˜ 길이 (초)",
986
- minimum=1.0,
987
- maximum=10.0,
988
- value=3.0,
989
- step=0.5
990
- )
991
- fps = gr.Slider(
992
- label="μ΄ˆλ‹Ή ν”„λ ˆμž„ 수",
993
- minimum=15,
994
- maximum=60,
995
- value=30,
996
- step=1
997
- )
998
-
999
- submit_btn = gr.Button("λͺ¨λΈ 처리 및 μ• λ‹ˆλ©”μ΄μ…˜ 생성")
1000
-
1001
- with gr.Column():
1002
- # 좜λ ₯ μ»΄ν¬λ„ŒνŠΈ
1003
- output_3d = gr.Model3D(label="μ• λ‹ˆλ©”μ΄μ…˜ 적용된 3D λͺ¨λΈ")
1004
- output_gif = gr.Image(label="μ• λ‹ˆλ©”μ΄μ…˜ 미리보기 (GIF)")
1005
- output_json = gr.File(label="메타데이터 파일 λ‹€μš΄λ‘œλ“œ")
1006
-
1007
- # μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜• μ„€λͺ…
1008
- gr.Markdown(_INFO_)
1009
-
1010
- # λ²„νŠΌ λ™μž‘ μ„€μ •
1011
- submit_btn.click(
1012
- fn=process_3d_model,
1013
- inputs=[input_3d, animation_type, animation_duration, fps],
1014
- outputs=[output_3d, output_gif, output_json]
1015
- )
1016
-
1017
- # 예제 μ€€λΉ„
1018
- example_files = [ [f] for f in glob.glob('./data/demo_glb/*.glb') ]
1019
-
1020
- if example_files:
1021
- example = gr.Examples(
1022
- examples=example_files,
1023
- inputs=[input_3d],
1024
- examples_per_page=10,
1025
- )
 
 
 
 
 
 
 
 
 
 
 
1026
 
1027
- return demo
 
 
 
 
 
 
 
1028
 
1029
- # 메인 μ‹€ν–‰ λΆ€λΆ„
1030
  if __name__ == "__main__":
1031
- demo = create_gradio_interface()
1032
- demo.launch(share=True, server_name="0.0.0.0", server_port=7860)
 
1
+ import os
2
+ import time
3
+ import glob
4
+ import json
5
+ import numpy as np
6
+ import trimesh
7
+ import argparse
8
+ from scipy.spatial.transform import Rotation
9
+ from PIL import Image, ImageDraw
10
+ import math
11
+ import trimesh.transformations as tf
12
+
13
+ os.environ['PYOPENGL_PLATFORM'] = 'egl'
14
+
15
+ import gradio as gr
16
+ import spaces
17
+
18
+ # κ²°κ³Ό μ €μž₯ 경둜
19
+ LOG_PATH = './results/demo'
20
+ os.makedirs(LOG_PATH, exist_ok=True)
21
+
22
  def create_textual_animation_gif(output_path, model_name, animation_type, duration=3.0, fps=30):
23
  """ν…μŠ€νŠΈ 기반의 κ°„λ‹¨ν•œ μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성 - λ Œλ”λ§ μ‹€νŒ¨ μ‹œ λŒ€μ²΄μš©"""
24
  try:
 
105
  return output_path
106
  except Exception as e:
107
  print(f"Error creating textual animation: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return None
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  @spaces.GPU
111
  def process_3d_model(input_3d, animation_type, animation_duration, fps):
112
  """Process a 3D model and apply animation"""
113
  print(f"Processing: {input_3d} with animation type: {animation_type}")
114
 
115
  try:
116
+ # ν…μŠ€νŠΈ 기반 μ• λ‹ˆλ©”μ΄μ…˜ GIF 생성 (λ Œλ”λ§ μ‹€νŒ¨λ₯Ό μš°λ €ν•˜μ—¬ 항상 생성)
117
+ base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
118
+ text_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
119
+ animated_gif_path = create_textual_animation_gif(
120
+ text_gif_path,
121
+ os.path.basename(input_3d),
122
+ animation_type,
123
+ animation_duration,
124
+ fps
125
  )
126
 
127
+ # 원본 GLB 파일 볡사
128
+ copy_glb_path = os.path.join(LOG_PATH, f'copy_{base_filename}.glb')
129
+ import shutil
130
+ try:
131
+ shutil.copy(input_3d, copy_glb_path)
132
+ animated_glb_path = copy_glb_path
133
+ print(f"Copied original GLB to {copy_glb_path}")
134
+ except Exception as e:
135
+ print(f"Error copying GLB: {e}")
136
+ animated_glb_path = input_3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ # 메타데이터 생성
139
  metadata = {
140
  "animation_type": animation_type,
141
  "duration": animation_duration,
 
144
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S")
145
  }
146
 
147
+ json_path = os.path.join(LOG_PATH, f'metadata_{base_filename}.json')
148
  with open(json_path, 'w') as f:
149
  json.dump(metadata, f, indent=4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  return animated_glb_path, animated_gif_path, json_path
152
  except Exception as e:
153
  error_msg = f"Error processing file: {str(e)}"
154
  print(error_msg)
155
  return error_msg, None, None
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  # Gradio μΈν„°νŽ˜μ΄μŠ€ μ„€μ •
158
+ with gr.Blocks(title="GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기") as demo:
159
+ # 제λͺ© μ„Ήμ…˜
160
+ gr.Markdown("""
161
+ <h2><b>GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 - 3D λͺ¨λΈ μ›€μ§μž„ 효과</b></h2>
162
+
163
+ 이 데λͺ¨λ₯Ό 톡해 정적인 3D λͺ¨λΈ(GLB 파일)에 λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
164
+
165
+ ❗️❗️❗️**μ€‘μš”μ‚¬ν•­:**
166
+ - 이 데λͺ¨λŠ” μ—…λ‘œλ“œλœ GLB νŒŒμΌμ— μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•©λ‹ˆλ‹€.
167
+ - λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ μŠ€νƒ€μΌ μ€‘μ—μ„œ μ„ νƒν•˜μ„Έμš”: νšŒμ „, λΆ€μœ , 폭발, 쑰립, νŽ„μŠ€, μŠ€μœ™.
168
+ - κ²°κ³ΌλŠ” μ• λ‹ˆλ©”μ΄μ…˜λœ GLB 파일과 미리보기용 GIF 파일둜 μ œκ³΅λ©λ‹ˆλ‹€.
169
+ """)
170
+
171
+ with gr.Row():
172
+ with gr.Column():
173
+ # μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ
174
+ input_3d = gr.Model3D(label="3D λͺ¨λΈ 파일 μ—…λ‘œλ“œ (GLB 포맷)")
175
+
176
+ with gr.Row():
177
+ animation_type = gr.Dropdown(
178
+ label="μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•",
179
+ choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
180
+ value="rotate"
181
+ )
182
+
183
+ with gr.Row():
184
+ animation_duration = gr.Slider(
185
+ label="μ• λ‹ˆλ©”μ΄μ…˜ 길이 (초)",
186
+ minimum=1.0,
187
+ maximum=10.0,
188
+ value=3.0,
189
+ step=0.5
190
+ )
191
+ fps = gr.Slider(
192
+ label="μ΄ˆλ‹Ή ν”„λ ˆμž„ 수",
193
+ minimum=15,
194
+ maximum=60,
195
+ value=30,
196
+ step=1
197
+ )
198
+
199
+ submit_btn = gr.Button("λͺ¨λΈ 처리 및 μ• λ‹ˆλ©”μ΄μ…˜ 생성")
200
+
201
+ with gr.Column():
202
+ # 좜λ ₯ μ»΄ν¬λ„ŒνŠΈ
203
+ output_3d = gr.Model3D(label="μ• λ‹ˆλ©”μ΄μ…˜ 적용된 3D λͺ¨λΈ")
204
+ output_gif = gr.Image(label="μ• λ‹ˆλ©”μ΄μ…˜ 미리보기 (GIF)")
205
+ output_json = gr.File(label="메타데이터 파일 λ‹€μš΄λ‘œλ“œ")
206
+
207
+ # μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜• μ„€λͺ…
208
+ gr.Markdown("""
209
+ ### μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜• μ„€λͺ…
210
+ - **νšŒμ „(rotate)**: λͺ¨λΈμ΄ Y좕을 μ€‘μ‹¬μœΌλ‘œ νšŒμ „ν•©λ‹ˆλ‹€.
211
+ - **λΆ€μœ (float)**: λͺ¨λΈμ΄ μœ„μ•„λž˜λ‘œ λΆ€λ“œλŸ½κ²Œ λ– λ‹€λ‹™λ‹ˆλ‹€.
212
+ - **폭발(explode)**: λͺ¨λΈμ˜ 각 뢀뢄이 μ€‘μ‹¬μ—μ„œ λ°”κΉ₯μͺ½μœΌλ‘œ νΌμ Έλ‚˜κ°‘λ‹ˆλ‹€.
213
+ - **쑰립(assemble)**: 폭발 μ• λ‹ˆλ©”μ΄μ…˜μ˜ λ°˜λŒ€ - λΆ€ν’ˆλ“€μ΄ ν•¨κ»˜ λͺ¨μž…λ‹ˆλ‹€.
214
+ - **νŽ„μŠ€(pulse)**: λͺ¨λΈμ΄ 크기가 μ»€μ‘Œλ‹€ μž‘μ•„μ‘Œλ‹€λ₯Ό λ°˜λ³΅ν•©λ‹ˆλ‹€.
215
+ - **μŠ€μœ™(swing)**: λͺ¨λΈμ΄ 쒌우둜 λΆ€λ“œλŸ½κ²Œ ν”λ“€λ¦½λ‹ˆλ‹€.
216
+
217
+ ### 팁
218
+ - μ• λ‹ˆλ©”μ΄μ…˜ 길이와 FPSλ₯Ό μ‘°μ ˆν•˜μ—¬ μ›€μ§μž„μ˜ 속도와 λΆ€λ“œλŸ¬μ›€μ„ μ‘°μ ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€.
219
+ - λ³΅μž‘ν•œ λͺ¨λΈμ€ 처리 μ‹œκ°„μ΄ 더 였래 걸릴 수 μžˆμŠ΅λ‹ˆλ‹€.
220
+ - GIF λ―Έλ¦¬λ³΄κΈ°λŠ” λΉ λ₯Έ 참쑰용이며, κ³ ν’ˆμ§ˆ κ²°κ³Όλ₯Ό μœ„ν•΄μ„œλŠ” μ• λ‹ˆλ©”μ΄μ…˜λœ GLB νŒŒμΌμ„ λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”.
221
+ """)
222
+
223
+ # λ²„νŠΌ λ™μž‘ μ„€μ •
224
+ submit_btn.click(
225
+ fn=process_3d_model,
226
+ inputs=[input_3d, animation_type, animation_duration, fps],
227
+ outputs=[output_3d, output_gif, output_json]
228
+ )
229
 
230
+ # 예제 μ€€λΉ„
231
+ example_files = [[f] for f in glob.glob('./data/demo_glb/*.glb')]
232
+ if example_files:
233
+ gr.Examples(
234
+ examples=example_files,
235
+ inputs=[input_3d],
236
+ examples_per_page=10,
237
+ )
238
 
239
+ # μ•± μ‹€ν–‰
240
  if __name__ == "__main__":
241
+ demo.launch(server_name="0.0.0.0", server_port=7860)