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)