|
""" |
|
GLB ์ ๋๋ฉ์ด์
์์ฑ๊ธฐ โ ์
๋ก๋ํ 3D ๋ชจ๋ธ์ ์ง์ ๋ ๋๋งํด GIFยทGLB๋ก ๋ด๋ณด๋
๋๋ค. |
|
(Gradio + Trimesh ์คํ์คํฌ๋ฆฐ ๋ ๋๋ง) |
|
|
|
โป ์ฃผ์ ๋ณ๊ฒฝ |
|
1. create_model_animation_gif : ๋ชจ๋ธ์ ์ค์ ๋ ๋๋งํด ํ๋ ์ ์์ฑ |
|
2. process_3d_model ๋ด๋ถ์์ ์ GIF ํจ์ ์ฌ์ฉ |
|
3. ์์ธ ์ฒ๋ฆฌยท์ฃผ์ ์ผ๋ถ ๋ณด๊ฐ |
|
""" |
|
|
|
import os, io, time, glob, json, math |
|
import numpy as np |
|
import trimesh |
|
from PIL import Image |
|
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_model_animation_gif( |
|
output_path: str, |
|
input_glb_path: str, |
|
animation_type: str, |
|
duration: float = 3.0, |
|
fps: int = 30, |
|
resolution=(640, 480), |
|
): |
|
""" |
|
์
๋ก๋๋ GLB ๋ชจ๋ธ์ ์คํ์คํฌ๋ฆฐ์ผ๋ก ๋ ๋๋งํด ์ ๋๋ฉ์ด์
GIF ์์ฑ |
|
""" |
|
|
|
scene = trimesh.load(input_glb_path) |
|
if isinstance(scene, trimesh.Trimesh): |
|
scene = trimesh.Scene(scene) |
|
|
|
frames, num_frames = [], min(int(duration * fps), 60) |
|
for i in range(num_frames): |
|
t = i / (num_frames - 1) |
|
|
|
|
|
scene_i = scene.copy() |
|
if animation_type == "rotate": |
|
M = tf.rotation_matrix(2 * math.pi * t, [0, 1, 0]) |
|
elif animation_type == "float": |
|
M = tf.translation_matrix([0, 0.5 * math.sin(2 * math.pi * t), 0]) |
|
elif animation_type == "pulse": |
|
M = tf.scale_matrix(0.8 + 0.4 * math.sin(2 * math.pi * t)) |
|
elif animation_type == "explode": |
|
M = tf.translation_matrix([0.5 * t, 0, 0]) |
|
elif animation_type == "assemble": |
|
M = tf.translation_matrix([0.5 * (1 - t), 0, 0]) |
|
elif animation_type == "swing": |
|
M = tf.rotation_matrix(math.pi / 6 * math.sin(2 * math.pi * t), [0, 0, 1]) |
|
else: |
|
M = np.eye(4) |
|
|
|
scene_i.apply_transform(M) |
|
|
|
|
|
png_bytes = scene_i.save_image(resolution=resolution, visible=True) |
|
frame = Image.open(io.BytesIO(png_bytes)).convert("RGB") |
|
frames.append(frame) |
|
|
|
|
|
frames[0].save( |
|
output_path, |
|
save_all=True, |
|
append_images=frames[1:], |
|
optimize=False, |
|
duration=int(1000 / fps), |
|
loop=0, |
|
) |
|
print(f"Created model animation GIF at {output_path}") |
|
return output_path |
|
|
|
|
|
|
|
|
|
|
|
def modify_glb_file(input_glb_path, output_glb_path, animation_type="rotate"): |
|
""" |
|
์
๋ก๋๋ GLB ํ์ผ์ ๋จ์ผ ๋ณํ(ํ์ ยท์ด๋ยท์ค์ผ์ผ)์ ์ ์ฉํด ์ GLB๋ก ์ ์ฅ |
|
""" |
|
try: |
|
scene = trimesh.load(input_glb_path) |
|
if not isinstance(scene, trimesh.Scene): |
|
scene = trimesh.Scene(scene) |
|
|
|
|
|
if animation_type == "rotate": |
|
transform = tf.rotation_matrix(math.pi / 4, [0, 1, 0]) |
|
elif animation_type == "float": |
|
transform = tf.translation_matrix([0, 0.5, 0]) |
|
elif animation_type == "pulse": |
|
transform = tf.scale_matrix(1.2) |
|
elif animation_type == "explode": |
|
transform = tf.translation_matrix([0.5, 0, 0]) |
|
elif animation_type == "assemble": |
|
transform = tf.translation_matrix([-0.5, 0, 0]) |
|
elif animation_type == "swing": |
|
transform = tf.rotation_matrix(math.pi / 8, [0, 0, 1]) |
|
else: |
|
transform = np.eye(4) |
|
|
|
scene.apply_transform(transform) |
|
scene.export(output_glb_path) |
|
print(f"Exported modified GLB to {output_glb_path}") |
|
return output_glb_path |
|
|
|
except Exception as e: |
|
print("Error modifying GLB:", e) |
|
|
|
import shutil |
|
|
|
shutil.copy(input_glb_path, output_glb_path) |
|
print(f"Copied original GLB to {output_glb_path}") |
|
return output_glb_path |
|
|
|
|
|
|
|
|
|
|
|
@spaces.GPU |
|
def process_3d_model(input_3d, animation_type, animation_duration, fps): |
|
""" |
|
โ GLB ๋ณํ โ โก GIF ๋ ๋ โ โข ๋ฉํ๋ฐ์ดํฐ JSON ์ ์ฅ |
|
""" |
|
try: |
|
base = os.path.splitext(os.path.basename(input_3d))[0] |
|
animated_glb = os.path.join(LOG_PATH, f"animated_{base}.glb") |
|
animated_gif = os.path.join(LOG_PATH, f"preview_{base}.gif") |
|
json_path = os.path.join(LOG_PATH, f"metadata_{base}.json") |
|
|
|
|
|
modify_glb_file(input_3d, animated_glb, animation_type) |
|
|
|
|
|
create_model_animation_gif( |
|
animated_gif, |
|
input_3d, |
|
animation_type, |
|
animation_duration, |
|
fps, |
|
) |
|
|
|
|
|
metadata = dict( |
|
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"), |
|
) |
|
with open(json_path, "w") as f: |
|
json.dump(metadata, f, indent=4) |
|
|
|
return animated_glb, animated_gif, json_path |
|
|
|
except Exception as e: |
|
|
|
print("Error in process_3d_model:", e) |
|
fallback_gif = os.path.join(LOG_PATH, "error.gif") |
|
Image.new("RGB", (640, 480), (255, 0, 0)).save(fallback_gif) |
|
return input_3d, fallback_gif, None |
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="GLB ์ ๋๋ฉ์ด์
์์ฑ๊ธฐ") as demo: |
|
gr.Markdown( |
|
""" |
|
<h2><b>GLB ์ ๋๋ฉ์ด์
์์ฑ๊ธฐ - 3D ๋ชจ๋ธ ์์ง์ ํจ๊ณผ</b></h2> |
|
์ ์ ์ธ 3D ๋ชจ๋ธ(GLB)์ ํ์ ยท๋ถ์ ยทํญ๋ฐ ๋ฑ ์ ๋๋ฉ์ด์
์ ์ ์ฉํฉ๋๋ค. |
|
""" |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
input_3d = gr.Model3D(label="3D ๋ชจ๋ธ ์
๋ก๋ (GLB)") |
|
animation_type = gr.Dropdown( |
|
label="์ ๋๋ฉ์ด์
์ ํ", |
|
choices=["rotate", "float", "explode", "assemble", "pulse", "swing"], |
|
value="rotate", |
|
) |
|
animation_duration = gr.Slider( |
|
label="์ ๋๋ฉ์ด์
๊ธธ์ด (์ด)", minimum=1.0, maximum=10.0, value=3.0, step=0.5 |
|
) |
|
fps = gr.Slider(label="FPS", minimum=15, maximum=60, value=30, step=1) |
|
submit_btn = gr.Button("์ ๋๋ฉ์ด์
์์ฑ") |
|
|
|
with gr.Column(): |
|
output_3d = gr.Model3D(label="์ ๋๋ฉ์ด์
๋ GLB") |
|
output_gif = gr.Image(label="๋ฏธ๋ฆฌ๋ณด๊ธฐ GIF") |
|
output_json = gr.File(label="๋ฉํ๋ฐ์ดํฐ JSON") |
|
|
|
submit_btn.click( |
|
fn=process_3d_model, |
|
inputs=[input_3d, animation_type, animation_duration, fps], |
|
outputs=[output_3d, output_gif, output_json], |
|
) |
|
|
|
|
|
example_glbs = [[f] for f in glob.glob("./data/demo_glb/*.glb")] |
|
if example_glbs: |
|
gr.Examples(examples=example_glbs, inputs=[input_3d]) |
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch(server_name="0.0.0.0", server_port=7860) |
|
|