|
import os |
|
import time |
|
import glob |
|
import json |
|
import numpy as np |
|
import trimesh |
|
import argparse |
|
from scipy.spatial.transform import Rotation |
|
from PIL import Image, ImageDraw |
|
import math |
|
import trimesh.transformations as tf |
|
|
|
os.environ['PYOPENGL_PLATFORM'] = 'egl' |
|
|
|
import gradio as gr |
|
import spaces |
|
|
|
|
|
LOG_PATH = './results/demo' |
|
os.makedirs(LOG_PATH, exist_ok=True) |
|
|
|
def create_textual_animation_gif(output_path, model_name, animation_type, duration=3.0, fps=30): |
|
"""ν
μ€νΈ κΈ°λ°μ κ°λ¨ν μ λλ©μ΄μ
GIF μμ± - λ λλ§ μ€ν¨ μ λ체μ©""" |
|
try: |
|
|
|
frames = [] |
|
num_frames = int(duration * fps) |
|
if num_frames > 60: |
|
num_frames = 60 |
|
|
|
for i in range(num_frames): |
|
t = i / (num_frames - 1) |
|
angle = t * 360 |
|
|
|
|
|
img = Image.new('RGB', (640, 480), color=(240, 240, 240)) |
|
draw = ImageDraw.Draw(img) |
|
|
|
|
|
draw.text((50, 50), f"Model: {os.path.basename(model_name)}", fill=(0, 0, 0)) |
|
draw.text((50, 100), f"Animation Type: {animation_type}", fill=(0, 0, 0)) |
|
draw.text((50, 150), f"Frame: {i+1}/{num_frames}", fill=(0, 0, 0)) |
|
|
|
|
|
center_x, center_y = 320, 240 |
|
if animation_type == 'rotate': |
|
|
|
radius = 100 |
|
x = center_x + radius * math.cos(math.radians(angle)) |
|
y = center_y + radius * math.sin(math.radians(angle)) |
|
draw.rectangle((x-40, y-40, x+40, y+40), outline=(0, 0, 0), fill=(255, 0, 0)) |
|
|
|
elif animation_type == 'float': |
|
|
|
offset_y = 50 * math.sin(2 * math.pi * t) |
|
draw.ellipse((center_x-50, center_y-50+offset_y, center_x+50, center_y+50+offset_y), |
|
outline=(0, 0, 0), fill=(0, 0, 255)) |
|
|
|
elif animation_type == 'explode' or animation_type == 'assemble': |
|
|
|
scale = t if animation_type == 'explode' else 1 - t |
|
for j in range(8): |
|
angle_j = j * 45 |
|
dist = 120 * scale |
|
x = center_x + dist * math.cos(math.radians(angle_j)) |
|
y = center_y + dist * math.sin(math.radians(angle_j)) |
|
|
|
if j % 3 == 0: |
|
draw.rectangle((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(255, 0, 0)) |
|
elif j % 3 == 1: |
|
draw.ellipse((x-20, y-20, x+20, y+20), outline=(0, 0, 0), fill=(0, 255, 0)) |
|
else: |
|
draw.polygon([(x, y-20), (x+20, y+20), (x-20, y+20)], outline=(0, 0, 0), fill=(0, 0, 255)) |
|
|
|
elif animation_type == 'pulse': |
|
|
|
scale = 0.5 + 0.5 * math.sin(2 * math.pi * t) |
|
radius = 100 * scale |
|
draw.ellipse((center_x-radius, center_y-radius, center_x+radius, center_y+radius), |
|
outline=(0, 0, 0), fill=(0, 255, 0)) |
|
|
|
elif animation_type == 'swing': |
|
|
|
angle_offset = 30 * math.sin(2 * math.pi * t) |
|
points = [ |
|
(center_x + 100 * math.cos(math.radians(angle_offset)), center_y - 80), |
|
(center_x + 100 * math.cos(math.radians(120 + angle_offset)), center_y + 40), |
|
(center_x + 100 * math.cos(math.radians(240 + angle_offset)), center_y + 40) |
|
] |
|
draw.polygon(points, outline=(0, 0, 0), fill=(255, 165, 0)) |
|
|
|
|
|
frames.append(img) |
|
|
|
|
|
frames[0].save( |
|
output_path, |
|
save_all=True, |
|
append_images=frames[1:], |
|
optimize=False, |
|
duration=int(1000 / fps), |
|
loop=0 |
|
) |
|
print(f"Created textual animation GIF at {output_path}") |
|
return output_path |
|
except Exception as e: |
|
print(f"Error creating textual animation: {str(e)}") |
|
return None |
|
|
|
@spaces.GPU |
|
def process_3d_model(input_3d, animation_type, animation_duration, fps): |
|
"""Process a 3D model and apply animation""" |
|
print(f"Processing: {input_3d} with animation type: {animation_type}") |
|
|
|
try: |
|
|
|
base_filename = os.path.basename(input_3d).rsplit('.', 1)[0] |
|
text_gif_path = os.path.join(LOG_PATH, f'text_animated_{base_filename}.gif') |
|
animated_gif_path = create_textual_animation_gif( |
|
text_gif_path, |
|
os.path.basename(input_3d), |
|
animation_type, |
|
animation_duration, |
|
fps |
|
) |
|
|
|
|
|
copy_glb_path = os.path.join(LOG_PATH, f'copy_{base_filename}.glb') |
|
import shutil |
|
try: |
|
shutil.copy(input_3d, copy_glb_path) |
|
animated_glb_path = copy_glb_path |
|
print(f"Copied original GLB to {copy_glb_path}") |
|
except Exception as e: |
|
print(f"Error copying GLB: {e}") |
|
animated_glb_path = input_3d |
|
|
|
|
|
metadata = { |
|
"animation_type": animation_type, |
|
"duration": animation_duration, |
|
"fps": fps, |
|
"original_model": os.path.basename(input_3d), |
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S") |
|
} |
|
|
|
json_path = os.path.join(LOG_PATH, f'metadata_{base_filename}.json') |
|
with open(json_path, 'w') as f: |
|
json.dump(metadata, f, indent=4) |
|
|
|
return animated_glb_path, animated_gif_path, json_path |
|
except Exception as e: |
|
error_msg = f"Error processing file: {str(e)}" |
|
print(error_msg) |
|
return error_msg, None, None |
|
|
|
|
|
with gr.Blocks(title="GLB μ λλ©μ΄μ
μμ±κΈ°") as demo: |
|
|
|
gr.Markdown(""" |
|
<h2><b>GLB μ λλ©μ΄μ
μμ±κΈ° - 3D λͺ¨λΈ μμ§μ ν¨κ³Ό</b></h2> |
|
|
|
μ΄ λ°λͺ¨λ₯Ό ν΅ν΄ μ μ μΈ 3D λͺ¨λΈ(GLB νμΌ)μ λ€μν μ λλ©μ΄μ
ν¨κ³Όλ₯Ό μ μ©ν μ μμ΅λλ€. |
|
|
|
βοΈβοΈβοΈ**μ€μμ¬ν:** |
|
- μ΄ λ°λͺ¨λ μ
λ‘λλ GLB νμΌμ μ λλ©μ΄μ
μ μ μ©ν©λλ€. |
|
- λ€μν μ λλ©μ΄μ
μ€νμΌ μ€μμ μ ννμΈμ: νμ , λΆμ , νλ°, 쑰립, νμ€, μ€μ. |
|
- κ²°κ³Όλ μ λλ©μ΄μ
λ GLB νμΌκ³Ό λ―Έλ¦¬λ³΄κΈ°μ© GIF νμΌλ‘ μ 곡λ©λλ€. |
|
""") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
|
|
input_3d = gr.Model3D(label="3D λͺ¨λΈ νμΌ μ
λ‘λ (GLB ν¬λ§·)") |
|
|
|
with gr.Row(): |
|
animation_type = gr.Dropdown( |
|
label="μ λλ©μ΄μ
μ ν", |
|
choices=["rotate", "float", "explode", "assemble", "pulse", "swing"], |
|
value="rotate" |
|
) |
|
|
|
with gr.Row(): |
|
animation_duration = gr.Slider( |
|
label="μ λλ©μ΄μ
κΈΈμ΄ (μ΄)", |
|
minimum=1.0, |
|
maximum=10.0, |
|
value=3.0, |
|
step=0.5 |
|
) |
|
fps = gr.Slider( |
|
label="μ΄λΉ νλ μ μ", |
|
minimum=15, |
|
maximum=60, |
|
value=30, |
|
step=1 |
|
) |
|
|
|
submit_btn = gr.Button("λͺ¨λΈ μ²λ¦¬ λ° μ λλ©μ΄μ
μμ±") |
|
|
|
with gr.Column(): |
|
|
|
output_3d = gr.Model3D(label="μ λλ©μ΄μ
μ μ©λ 3D λͺ¨λΈ") |
|
output_gif = gr.Image(label="μ λλ©μ΄μ
미리보기 (GIF)") |
|
output_json = gr.File(label="λ©νλ°μ΄ν° νμΌ λ€μ΄λ‘λ") |
|
|
|
|
|
gr.Markdown(""" |
|
### μ λλ©μ΄μ
μ ν μ€λͺ
|
|
- **νμ (rotate)**: λͺ¨λΈμ΄ YμΆμ μ€μ¬μΌλ‘ νμ ν©λλ€. |
|
- **λΆμ (float)**: λͺ¨λΈμ΄ μμλλ‘ λΆλλ½κ² λ λ€λλλ€. |
|
- **νλ°(explode)**: λͺ¨λΈμ κ° λΆλΆμ΄ μ€μ¬μμ λ°κΉ₯μͺ½μΌλ‘ νΌμ Έλκ°λλ€. |
|
- **쑰립(assemble)**: νλ° μ λλ©μ΄μ
μ λ°λ - λΆνλ€μ΄ ν¨κ» λͺ¨μ
λλ€. |
|
- **νμ€(pulse)**: λͺ¨λΈμ΄ ν¬κΈ°κ° 컀μ‘λ€ μμμ‘λ€λ₯Ό λ°λ³΅ν©λλ€. |
|
- **μ€μ(swing)**: λͺ¨λΈμ΄ μ’μ°λ‘ λΆλλ½κ² νλ€λ¦½λλ€. |
|
|
|
### ν |
|
- μ λλ©μ΄μ
κΈΈμ΄μ FPSλ₯Ό μ‘°μ νμ¬ μμ§μμ μλμ λΆλλ¬μμ μ‘°μ ν μ μμ΅λλ€. |
|
- 볡μ‘ν λͺ¨λΈμ μ²λ¦¬ μκ°μ΄ λ μ€λ 걸릴 μ μμ΅λλ€. |
|
- GIF 미리보기λ λΉ λ₯Έ μ°Έμ‘°μ©μ΄λ©°, κ³ νμ§ κ²°κ³Όλ₯Ό μν΄μλ μ λλ©μ΄μ
λ GLB νμΌμ λ€μ΄λ‘λνμΈμ. |
|
""") |
|
|
|
|
|
submit_btn.click( |
|
fn=process_3d_model, |
|
inputs=[input_3d, animation_type, animation_duration, fps], |
|
outputs=[output_3d, output_gif, output_json] |
|
) |
|
|
|
|
|
example_files = [[f] for f in glob.glob('./data/demo_glb/*.glb')] |
|
if example_files: |
|
gr.Examples( |
|
examples=example_files, |
|
inputs=[input_3d], |
|
examples_per_page=10, |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch(server_name="0.0.0.0", server_port=7860) |