ginipick commited on
Commit
a300b36
ยท
verified ยท
1 Parent(s): 3aff1e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +181 -257
app.py CHANGED
@@ -1,298 +1,222 @@
1
- import os
2
- import time
3
- import glob
4
- import json
 
 
 
 
 
 
 
5
  import numpy as np
6
  import trimesh
7
- from PIL import Image, ImageDraw
8
- import math
9
  import trimesh.transformations as tf
10
 
11
- os.environ['PYOPENGL_PLATFORM'] = 'egl'
12
 
13
  import gradio as gr
14
  import spaces
15
 
16
- # ๊ฒฐ๊ณผ ์ €์žฅ ๊ฒฝ๋กœ
17
- LOG_PATH = './results/demo'
18
  os.makedirs(LOG_PATH, exist_ok=True)
19
 
20
- def create_textual_animation_gif(output_path, model_name, animation_type, duration=3.0, fps=30):
21
- """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์˜ ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ GIF ์ƒ์„ฑ"""
22
- try:
23
- frames = []
24
- num_frames = int(duration * fps)
25
- if num_frames > 60:
26
- num_frames = 60
27
-
28
- for i in range(num_frames):
29
- t = i / (num_frames - 1)
30
- angle = t * 360
31
-
32
- img = Image.new('RGB', (640, 480), color=(240, 240, 240))
33
- draw = ImageDraw.Draw(img)
34
-
35
- draw.text((50, 50), f"Model: {os.path.basename(model_name)}", fill=(0, 0, 0))
36
- draw.text((50, 100), f"Animation Type: {animation_type}", fill=(0, 0, 0))
37
- draw.text((50, 150), f"Frame: {i+1}/{num_frames}", fill=(0, 0, 0))
38
-
39
- # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜•๋ณ„ ์‹œ๊ฐํ™”
40
- center_x, center_y = 320, 240
41
- if animation_type == 'rotate':
42
- radius = 100
43
- x = center_x + radius * math.cos(math.radians(angle))
44
- y = center_y + radius * math.sin(math.radians(angle))
45
- draw.rectangle((x-40, y-40, x+40, y+40), outline=(0, 0, 0), fill=(255, 0, 0))
46
- elif animation_type == 'float':
47
- offset_y = 50 * math.sin(2 * math.pi * t)
48
- draw.ellipse((center_x-50, center_y-50+offset_y, center_x+50, center_y+50+offset_y),
49
- outline=(0, 0, 0), fill=(0, 0, 255))
50
- elif animation_type in ['explode', 'assemble']:
51
- scale = t if animation_type == 'explode' else 1 - t
52
- for j in range(8):
53
- angle_j = j * 45
54
- dist = 120 * scale
55
- x = center_x + dist * math.cos(math.radians(angle_j))
56
- y = center_y + dist * math.sin(math.radians(angle_j))
57
-
58
- if j % 3 == 0:
59
- draw.rectangle((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(255, 0, 0))
60
- elif j % 3 == 1:
61
- draw.ellipse((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(0, 255, 0))
62
- else:
63
- draw.polygon([(x, y-20), (x+20, y+20), (x-20, y+20)], outline=(0, 0, 0), fill=(0, 0, 255))
64
- elif animation_type == 'pulse':
65
- scale = 0.5 + 0.5 * math.sin(2 * math.pi * t)
66
- radius = 100 * scale
67
- draw.ellipse((center_x-radius, center_y-radius, center_x+radius, center_y+radius),
68
- outline=(0, 0, 0), fill=(0, 255, 0))
69
- elif animation_type == 'swing':
70
- angle_offset = 30 * math.sin(2 * math.pi * t)
71
- points = [
72
- (center_x + 100 * math.cos(math.radians(angle_offset)), center_y - 80),
73
- (center_x + 100 * math.cos(math.radians(120 + angle_offset)), center_y + 40),
74
- (center_x + 100 * math.cos(math.radians(240 + angle_offset)), center_y + 40)
75
- ]
76
- draw.polygon(points, outline=(0, 0, 0), fill=(255, 165, 0))
77
-
78
- frames.append(img)
79
-
80
- frames[0].save(
81
- output_path,
82
- save_all=True,
83
- append_images=frames[1:],
84
- optimize=False,
85
- duration=int(1000 / fps),
86
- loop=0
87
- )
88
- print(f"Created textual animation GIF at {output_path}")
89
- return output_path
90
- except Exception as e:
91
- print(f"Error creating textual animation: {str(e)}")
92
- return None
93
 
94
- def modify_glb_file(input_glb_path, output_glb_path, animation_type='rotate'):
95
- """์—…๋กœ๋“œ๋œ GLB ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ฃผ๋Š” ํ•จ์ˆ˜"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  try:
97
- # ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•: ์›๋ณธ ํŒŒ์ผ์„ ๊ทธ๋Œ€๋กœ ๋กœ๋“œํ•˜์—ฌ ๋‹จ์ผ ๋ณ€ํ™˜ ์ ์šฉ ํ›„ ์ €์žฅ
98
- # ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋กœ ๋‚˜๋ˆ„์–ด ์ฒ˜๋ฆฌํ•˜์—ฌ ๊ฐ ๋‹จ๊ณ„์—์„œ ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ
99
-
100
- print(f"Step 1: Loading GLB file '{input_glb_path}'")
101
  scene = trimesh.load(input_glb_path)
102
- print(f"Loaded GLB file, type: {type(scene)}")
103
-
104
- print(f"Step 2: Preparing transformation for animation type: {animation_type}")
105
- # ๋‹จ์ˆœํ™”๋œ ๋ณ€ํ™˜ ์ค€๋น„ - ์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜•๋ณ„๋กœ ๋‹ค๋ฅธ ๋ณ€ํ™˜ ์ ์šฉ
106
- transform = np.eye(4) # ๊ธฐ๋ณธ ํ•ญ๋“ฑ ๋ณ€ํ™˜ (์•„๋ฌด ๋ณ€ํ™” ์—†์Œ)
107
-
108
- if animation_type == 'rotate':
109
- # Y์ถ• ๊ธฐ์ค€ 45๋„ ํšŒ์ „
110
- transform = tf.rotation_matrix(math.pi/4, [0, 1, 0])
111
- elif animation_type == 'float':
112
- # ์œ„๋กœ ์ด๋™
113
  transform = tf.translation_matrix([0, 0.5, 0])
114
- elif animation_type == 'pulse':
115
- # ํฌ๊ธฐ ํ™•๋Œ€
116
  transform = tf.scale_matrix(1.2)
117
- elif animation_type == 'explode':
118
- # X์ถ• ๋ฐฉํ–ฅ์œผ๋กœ ์•ฝ๊ฐ„ ์ด๋™
119
  transform = tf.translation_matrix([0.5, 0, 0])
120
- elif animation_type == 'assemble':
121
- # X์ถ• ๋ฐฉํ–ฅ์œผ๋กœ ์•ฝ๊ฐ„ ์ด๋™ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ)
122
  transform = tf.translation_matrix([-0.5, 0, 0])
123
- elif animation_type == 'swing':
124
- # Z์ถ• ๊ธฐ์ค€ ์•ฝ๊ฐ„ ํšŒ์ „
125
- transform = tf.rotation_matrix(math.pi/8, [0, 0, 1])
126
-
127
- print(f"Step 3: Applying transformation to scene")
128
- if isinstance(scene, trimesh.Scene):
129
- # Scene์ธ ๊ฒฝ์šฐ ์ง์ ‘ ๋ณ€ํ™˜ ์ ์šฉ
130
- scene.apply_transform(transform)
131
- print(f"Applied transform to scene")
132
  else:
133
- print(f"Scene is not trimesh.Scene, but {type(scene)}")
134
-
135
- print(f"Step 4: Exporting modified scene to '{output_glb_path}'")
136
  scene.export(output_glb_path)
137
- print(f"Successfully exported modified GLB to '{output_glb_path}'")
138
-
139
  return output_glb_path
140
-
141
  except Exception as e:
142
- print(f"Error modifying GLB file: {str(e)}")
143
-
144
- # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›๋ณธ ํŒŒ์ผ ๋ณต์‚ฌ
145
- try:
146
- import shutil
147
- print(f"Copying original GLB file as fallback")
148
- shutil.copy(input_glb_path, output_glb_path)
149
- print(f"Successfully copied original GLB to '{output_glb_path}'")
150
- return output_glb_path
151
- except Exception as copy_error:
152
- print(f"Error copying GLB: {copy_error}")
153
- return input_glb_path # ์›๋ณธ ํŒŒ์ผ ๊ฒฝ๋กœ ๋ฐ˜ํ™˜
154
 
 
 
 
155
  @spaces.GPU
156
  def process_3d_model(input_3d, animation_type, animation_duration, fps):
157
- """Process a 3D model and apply animation"""
158
- print(f"Processing: {input_3d} with animation type: {animation_type}")
159
-
160
  try:
161
- # ํŒŒ์ผ๋ช… ์ค€๋น„
162
- base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
163
- animated_glb_path = os.path.join(LOG_PATH, f'animated_{base_filename}.glb')
164
- animated_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
165
- json_path = os.path.join(LOG_PATH, f'metadata_{base_filename}.json')
166
-
167
- # 1. GLB ํŒŒ์ผ ์ˆ˜์ •
168
- print(f"Modifying GLB file")
169
- modified_glb = modify_glb_file(input_3d, animated_glb_path, animation_type)
170
-
171
- # 2. ์• ๋‹ˆ๋ฉ”์ด์…˜ GIF ์ƒ์„ฑ (์–ด๋–ค ๊ฒฝ์šฐ์—๋„ ํ•ญ์ƒ ์ƒ์„ฑ)
172
- print(f"Creating animation GIF")
173
- animated_gif = create_textual_animation_gif(
174
- animated_gif_path,
175
- os.path.basename(input_3d),
176
  animation_type,
177
  animation_duration,
178
- fps
179
  )
180
-
181
- # 3. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ
182
- print(f"Creating metadata")
183
- metadata = {
184
- "animation_type": animation_type,
185
- "duration": animation_duration,
186
- "fps": fps,
187
- "original_model": os.path.basename(input_3d),
188
- "created_at": time.strftime("%Y-%m-%d %H:%M:%S")
189
- }
190
-
191
- with open(json_path, 'w') as f:
192
  json.dump(metadata, f, indent=4)
193
-
194
- print(f"Processing complete, returning results")
195
- return modified_glb, animated_gif, json_path
196
-
197
  except Exception as e:
198
- print(f"Error in process_3d_model: {str(e)}")
199
- # ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›๋ณธ ํŒŒ์ผ๊ณผ ๊ธฐ๋ณธ GIF ๋ฐ˜ํ™˜
200
- try:
201
- base_filename = os.path.basename(input_3d).rsplit('.', 1)[0]
202
- animated_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif')
203
- create_textual_animation_gif(
204
- animated_gif_path,
205
- os.path.basename(input_3d),
206
- animation_type,
207
- animation_duration,
208
- fps
209
- )
210
- return input_3d, animated_gif_path, None
211
- except:
212
- return f"Error processing file: {str(e)}", None, None
213
 
214
- # Gradio ์ธํ„ฐํŽ˜์ด์Šค ์„ค์ •
 
 
 
215
  with gr.Blocks(title="GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ") as demo:
216
- # ์ œ๋ชฉ ์„น์…˜
217
- gr.Markdown("""
218
- <h2><b>GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ - 3D ๋ชจ๋ธ ์›€์ง์ž„ ํšจ๊ณผ</b></h2>
219
-
220
- ์ด ๋ฐ๋ชจ๋ฅผ ํ†ตํ•ด ์ •์ ์ธ 3D ๋ชจ๋ธ(GLB ํŒŒ์ผ)์— ๋‹ค์–‘ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
221
-
222
- โ—๏ธโ—๏ธโ—๏ธ**์ค‘์š”์‚ฌํ•ญ:**
223
- - ์ด ๋ฐ๋ชจ๋Š” ์—…๋กœ๋“œ๋œ GLB ํŒŒ์ผ์— ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
224
- - ๋‹ค์–‘ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ ์ค‘์—์„œ ์„ ํƒํ•˜์„ธ์š”: ํšŒ์ „, ๋ถ€์œ , ํญ๋ฐœ, ์กฐ๋ฆฝ, ํŽ„์Šค, ์Šค์œ™.
225
- - ๊ฒฐ๊ณผ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜๋œ GLB ํŒŒ์ผ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ GIF ํŒŒ์ผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.
226
- """)
227
-
228
  with gr.Row():
229
  with gr.Column():
230
- # ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ
231
- input_3d = gr.Model3D(label="3D ๋ชจ๋ธ ํŒŒ์ผ ์—…๋กœ๋“œ (GLB ํฌ๋งท)")
232
-
233
- with gr.Row():
234
- animation_type = gr.Dropdown(
235
- label="์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜•",
236
- choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
237
- value="rotate"
238
- )
239
-
240
- with gr.Row():
241
- animation_duration = gr.Slider(
242
- label="์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธธ์ด (์ดˆ)",
243
- minimum=1.0,
244
- maximum=10.0,
245
- value=3.0,
246
- step=0.5
247
- )
248
- fps = gr.Slider(
249
- label="์ดˆ๋‹น ํ”„๋ ˆ์ž„ ์ˆ˜",
250
- minimum=15,
251
- maximum=60,
252
- value=30,
253
- step=1
254
- )
255
-
256
- submit_btn = gr.Button("๋ชจ๋ธ ์ฒ˜๋ฆฌ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ")
257
-
258
  with gr.Column():
259
- # ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ
260
- output_3d = gr.Model3D(label="์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ๋œ 3D ๋ชจ๋ธ")
261
- output_gif = gr.Image(label="์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (GIF)")
262
- output_json = gr.File(label="๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ")
263
-
264
- # ์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜• ์„ค๋ช…
265
- gr.Markdown("""
266
- ### ์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜• ์„ค๋ช…
267
- - **ํšŒ์ „(rotate)**: ๋ชจ๋ธ์ด Y์ถ•์„ ์ค‘์‹ฌ์œผ๋กœ ํšŒ์ „ํ•ฉ๋‹ˆ๋‹ค.
268
- - **๋ถ€์œ (float)**: ๋ชจ๋ธ์ด ์œ„์•„๋ž˜๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋– ๋‹ค๋‹™๋‹ˆ๋‹ค.
269
- - **ํญ๋ฐœ(explode)**: ๋ชจ๋ธ์˜ ๊ฐ ๋ถ€๋ถ„์ด ์ค‘์‹ฌ์—์„œ ๋ฐ”๊นฅ์ชฝ์œผ๋กœ ํผ์ ธ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.
270
- - **์กฐ๋ฆฝ(assemble)**: ํญ๋ฐœ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ๋ฐ˜๋Œ€ - ๋ถ€ํ’ˆ๋“ค์ด ํ•จ๊ป˜ ๋ชจ์ž…๋‹ˆ๋‹ค.
271
- - **ํŽ„์Šค(pulse)**: ๋ชจ๋ธ์ด ํฌ๊ธฐ๊ฐ€ ์ปค์กŒ๋‹ค ์ž‘์•„์กŒ๋‹ค๋ฅผ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.
272
- - **์Šค์œ™(swing)**: ๋ชจ๋ธ์ด ์ขŒ์šฐ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ํ”๋“ค๋ฆฝ๋‹ˆ๋‹ค.
273
-
274
- ### ํŒ
275
- - ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธธ์ด์™€ FPS๋ฅผ ์กฐ์ ˆํ•˜์—ฌ ์›€์ง์ž„์˜ ์†๋„์™€ ๋ถ€๋“œ๋Ÿฌ์›€์„ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
276
- - ๋ณต์žกํ•œ ๋ชจ๋ธ์€ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ๋” ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
277
- - GIF ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ๋น ๋ฅธ ์ฐธ์กฐ์šฉ์ด๋ฉฐ, ๊ณ ํ’ˆ์งˆ ๊ฒฐ๊ณผ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜๋œ GLB ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”.
278
- """)
279
-
280
- # ๋ฒ„ํŠผ ๋™์ž‘ ์„ค์ •
281
  submit_btn.click(
282
  fn=process_3d_model,
283
  inputs=[input_3d, animation_type, animation_duration, fps],
284
- outputs=[output_3d, output_gif, output_json]
285
  )
286
-
287
- # ์˜ˆ์ œ ์ค€๋น„
288
- example_files = [[f] for f in glob.glob('./data/demo_glb/*.glb')]
289
- if example_files:
290
- gr.Examples(
291
- examples=example_files,
292
- inputs=[input_3d],
293
- examples_per_page=10,
294
- )
295
 
296
- # ์•ฑ ์‹คํ–‰
 
 
 
 
 
 
 
297
  if __name__ == "__main__":
298
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ """
2
+ GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ โ€“ ์—…๋กœ๋“œํ•œ 3D ๋ชจ๋ธ์„ ์ง์ ‘ ๋ Œ๋”๋งํ•ด GIFยทGLB๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.
3
+ (Gradio + Trimesh ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋”๋ง)
4
+
5
+ โ€ป ์ฃผ์š” ๋ณ€๊ฒฝ
6
+ 1. create_model_animation_gif : ๋ชจ๋ธ์„ ์‹ค์ œ ๋ Œ๋”๋งํ•ด ํ”„๋ ˆ์ž„ ์ƒ์„ฑ
7
+ 2. process_3d_model ๋‚ด๋ถ€์—์„œ ์ƒˆ GIF ํ•จ์ˆ˜ ์‚ฌ์šฉ
8
+ 3. ์˜ˆ์™ธ ์ฒ˜๋ฆฌยท์ฃผ์„ ์ผ๋ถ€ ๋ณด๊ฐ•
9
+ """
10
+
11
+ import os, io, time, glob, json, math
12
  import numpy as np
13
  import trimesh
14
+ from PIL import Image
 
15
  import trimesh.transformations as tf
16
 
17
+ os.environ["PYOPENGL_PLATFORM"] = "egl" # ๋ชจ๋‹ˆํ„ฐ ์—†์ด ๋ Œ๋”๋ง
18
 
19
  import gradio as gr
20
  import spaces
21
 
22
+ # ๊ฒฐ๊ณผ ์ €์žฅ ๋””๋ ‰ํ„ฐ๋ฆฌ
23
+ LOG_PATH = "./results/demo"
24
  os.makedirs(LOG_PATH, exist_ok=True)
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # ------------------------------------------------------------------
28
+ # 1) ์‹ค์ œ GLB ๋ชจ๋ธ์„ ๋ Œ๋”๋งํ•ด GIF ์ƒ์„ฑ
29
+ # ------------------------------------------------------------------
30
+ def create_model_animation_gif(
31
+ output_path: str,
32
+ input_glb_path: str,
33
+ animation_type: str,
34
+ duration: float = 3.0,
35
+ fps: int = 30,
36
+ resolution=(640, 480),
37
+ ):
38
+ """
39
+ ์—…๋กœ๋“œ๋œ GLB ๋ชจ๋ธ์„ ์˜คํ”„์Šคํฌ๋ฆฐ์œผ๋กœ ๋ Œ๋”๋งํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜ GIF ์ƒ์„ฑ
40
+ """
41
+ # (1) ๋ชจ๋ธ ๋กœ๋“œ
42
+ scene = trimesh.load(input_glb_path)
43
+ if isinstance(scene, trimesh.Trimesh):
44
+ scene = trimesh.Scene(scene)
45
+
46
+ frames, num_frames = [], min(int(duration * fps), 60)
47
+ for i in range(num_frames):
48
+ t = i / (num_frames - 1)
49
+
50
+ # (2) ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋ณ€ํ™˜ ์ ์šฉ
51
+ scene_i = scene.copy()
52
+ if animation_type == "rotate":
53
+ M = tf.rotation_matrix(2 * math.pi * t, [0, 1, 0])
54
+ elif animation_type == "float":
55
+ M = tf.translation_matrix([0, 0.5 * math.sin(2 * math.pi * t), 0])
56
+ elif animation_type == "pulse":
57
+ M = tf.scale_matrix(0.8 + 0.4 * math.sin(2 * math.pi * t))
58
+ elif animation_type == "explode":
59
+ M = tf.translation_matrix([0.5 * t, 0, 0])
60
+ elif animation_type == "assemble":
61
+ M = tf.translation_matrix([0.5 * (1 - t), 0, 0])
62
+ elif animation_type == "swing":
63
+ M = tf.rotation_matrix(math.pi / 6 * math.sin(2 * math.pi * t), [0, 0, 1])
64
+ else:
65
+ M = np.eye(4)
66
+
67
+ scene_i.apply_transform(M)
68
+
69
+ # (3) ์˜คํ”„์Šคํฌ๋ฆฐ ๋ Œ๋” โ†’ PNG ๋ฐ”์ดํŠธ
70
+ png_bytes = scene_i.save_image(resolution=resolution, visible=True)
71
+ frame = Image.open(io.BytesIO(png_bytes)).convert("RGB")
72
+ frames.append(frame)
73
+
74
+ # (4) 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 model animation GIF at {output_path}")
84
+ return output_path
85
+
86
+
87
+ # ------------------------------------------------------------------
88
+ # 2) GLB ํŒŒ์ผ์— ๋‹จ์ˆœ ๋ณ€ํ™˜ ์ ์šฉํ•ด ์ €์žฅ
89
+ # ------------------------------------------------------------------
90
+ def modify_glb_file(input_glb_path, output_glb_path, animation_type="rotate"):
91
+ """
92
+ ์—…๋กœ๋“œ๋œ GLB ํŒŒ์ผ์— ๋‹จ์ผ ๋ณ€ํ™˜(ํšŒ์ „ยท์ด๋™ยท์Šค์ผ€์ผ)์„ ์ ์šฉํ•ด ์ƒˆ GLB๋กœ ์ €์žฅ
93
+ """
94
  try:
 
 
 
 
95
  scene = trimesh.load(input_glb_path)
96
+ if not isinstance(scene, trimesh.Scene):
97
+ scene = trimesh.Scene(scene)
98
+
99
+ # ๋ณ€ํ™˜ ๋งคํŠธ๋ฆญ์Šค ๊ฒฐ์ •
100
+ if animation_type == "rotate":
101
+ transform = tf.rotation_matrix(math.pi / 4, [0, 1, 0])
102
+ elif animation_type == "float":
 
 
 
 
103
  transform = tf.translation_matrix([0, 0.5, 0])
104
+ elif animation_type == "pulse":
 
105
  transform = tf.scale_matrix(1.2)
106
+ elif animation_type == "explode":
 
107
  transform = tf.translation_matrix([0.5, 0, 0])
108
+ elif animation_type == "assemble":
 
109
  transform = tf.translation_matrix([-0.5, 0, 0])
110
+ elif animation_type == "swing":
111
+ transform = tf.rotation_matrix(math.pi / 8, [0, 0, 1])
 
 
 
 
 
 
 
112
  else:
113
+ transform = np.eye(4)
114
+
115
+ scene.apply_transform(transform)
116
  scene.export(output_glb_path)
117
+ print(f"Exported modified GLB to {output_glb_path}")
 
118
  return output_glb_path
119
+
120
  except Exception as e:
121
+ print("Error modifying GLB:", e)
122
+ # ์‹คํŒจ ์‹œ ์›๋ณธ ๋ณต์‚ฌ
123
+ import shutil
124
+
125
+ shutil.copy(input_glb_path, output_glb_path)
126
+ print(f"Copied original GLB to {output_glb_path}")
127
+ return output_glb_path
128
+
 
 
 
 
129
 
130
+ # ------------------------------------------------------------------
131
+ # 3) Gradio์—์„œ ํ˜ธ์ถœํ•  ๋ฉ”์ธ ํŒŒ์ดํ”„๋ผ์ธ
132
+ # ------------------------------------------------------------------
133
  @spaces.GPU
134
  def process_3d_model(input_3d, animation_type, animation_duration, fps):
135
+ """
136
+ โ‘  GLB ๋ณ€ํ™˜ โ†’ โ‘ก GIF ๋ Œ๋” โ†’ โ‘ข ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON ์ €์žฅ
137
+ """
138
  try:
139
+ base = os.path.splitext(os.path.basename(input_3d))[0]
140
+ animated_glb = os.path.join(LOG_PATH, f"animated_{base}.glb")
141
+ animated_gif = os.path.join(LOG_PATH, f"preview_{base}.gif")
142
+ json_path = os.path.join(LOG_PATH, f"metadata_{base}.json")
143
+
144
+ # 1) GLB ๋ณ€ํ˜•
145
+ modify_glb_file(input_3d, animated_glb, animation_type)
146
+
147
+ # 2) ์‹ค์ œ ๋ชจ๋ธ ๊ธฐ๋ฐ˜ GIF
148
+ create_model_animation_gif(
149
+ animated_gif,
150
+ input_3d,
 
 
 
151
  animation_type,
152
  animation_duration,
153
+ fps,
154
  )
155
+
156
+ # 3) ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ธฐ๋ก
157
+ metadata = dict(
158
+ animation_type=animation_type,
159
+ duration=animation_duration,
160
+ fps=fps,
161
+ original_model=os.path.basename(input_3d),
162
+ created_at=time.strftime("%Y-%m-%d %H:%M:%S"),
163
+ )
164
+ with open(json_path, "w") as f:
 
 
165
  json.dump(metadata, f, indent=4)
166
+
167
+ return animated_glb, animated_gif, json_path
168
+
 
169
  except Exception as e:
170
+ # ์น˜๋ช…์  ์˜ค๋ฅ˜ ์‹œ: ์›๋ณธ ๋ชจ๋ธ + ๋นˆ GIF
171
+ print("Error in process_3d_model:", e)
172
+ fallback_gif = os.path.join(LOG_PATH, "error.gif")
173
+ Image.new("RGB", (640, 480), (255, 0, 0)).save(fallback_gif)
174
+ return input_3d, fallback_gif, None
 
 
 
 
 
 
 
 
 
 
175
 
176
+
177
+ # ------------------------------------------------------------------
178
+ # 4) Gradio UI
179
+ # ------------------------------------------------------------------
180
  with gr.Blocks(title="GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ") as demo:
181
+ gr.Markdown(
182
+ """
183
+ <h2><b>GLB ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ๊ธฐ - 3D ๋ชจ๋ธ ์›€์ง์ž„ ํšจ๊ณผ</b></h2>
184
+ ์ •์ ์ธ 3D ๋ชจ๋ธ(GLB)์— ํšŒ์ „ยท๋ถ€์œ ยทํญ๋ฐœ ๋“ฑ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
185
+ """
186
+ )
187
+
 
 
 
 
 
188
  with gr.Row():
189
  with gr.Column():
190
+ input_3d = gr.Model3D(label="3D ๋ชจ๋ธ ์—…๋กœ๋“œ (GLB)")
191
+ animation_type = gr.Dropdown(
192
+ label="์• ๋‹ˆ๋ฉ”์ด์…˜ ์œ ํ˜•",
193
+ choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
194
+ value="rotate",
195
+ )
196
+ animation_duration = gr.Slider(
197
+ label="์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธธ์ด (์ดˆ)", minimum=1.0, maximum=10.0, value=3.0, step=0.5
198
+ )
199
+ fps = gr.Slider(label="FPS", minimum=15, maximum=60, value=30, step=1)
200
+ submit_btn = gr.Button("์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ")
201
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  with gr.Column():
203
+ output_3d = gr.Model3D(label="์• ๋‹ˆ๋ฉ”์ด์…˜๋œ GLB")
204
+ output_gif = gr.Image(label="๋ฏธ๋ฆฌ๋ณด๊ธฐ GIF")
205
+ output_json = gr.File(label="๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON")
206
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  submit_btn.click(
208
  fn=process_3d_model,
209
  inputs=[input_3d, animation_type, animation_duration, fps],
210
+ outputs=[output_3d, output_gif, output_json],
211
  )
 
 
 
 
 
 
 
 
 
212
 
213
+ # ์˜ˆ์ œ GLB ํด๋”๊ฐ€ ์žˆ๋‹ค๋ฉด ์ž๋™ ๋“ฑ๋ก
214
+ example_glbs = [[f] for f in glob.glob("./data/demo_glb/*.glb")]
215
+ if example_glbs:
216
+ gr.Examples(examples=example_glbs, inputs=[input_3d])
217
+
218
+ # ------------------------------------------------------------------
219
+ # 5) ์•ฑ ์‹คํ–‰
220
+ # ------------------------------------------------------------------
221
  if __name__ == "__main__":
222
+ demo.launch(server_name="0.0.0.0", server_port=7860)