3D-VIDEO / app.py
ginipick's picture
Update app.py
8503628 verified
raw
history blame
8.26 kB
"""
GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기
────────────────────────────────────────────────────────
μ—…λ‘œλ“œν•œ GLB 3D λͺ¨λΈμ— νšŒμ „Β·λΆ€μœ Β·ν­λ°œ λ“± μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•΄
β‘  λ³€ν˜•λœ GLB β‘‘ μ‹€μ œ λͺ¨λΈμ„ λ Œλ”λ§ν•œ GIF β‘’ 메타데이터 JSON 을 λŒλ €μ€λ‹ˆλ‹€.
β€’ headless μ„œλ²„ λŒ€μ‘: EGL + pyglet.headless + trimesh β†’ μ‹€νŒ¨ μ‹œ pyrender 폴백
β€’ μ΅œλŒ€ 60 fpsΒ·60 ν”„λ ˆμž„λ‘œ μ œν•œ (데λͺ¨ λͺ©μ )
"""
# ──────────────────── 1. 곡톡 λͺ¨λ“ˆ ────────────────────
import os, io, time, glob, json, math, shutil
import numpy as np
from PIL import Image
import pyglet
pyglet.options["headless"] = True # λ°˜λ“œμ‹œ trimesh 전에!
os.environ["PYOPENGL_PLATFORM"] = "egl" # Off-screen GL
import trimesh
import trimesh.transformations as tf
import gradio as gr
import spaces
LOG_PATH = "./results/demo"
os.makedirs(LOG_PATH, exist_ok=True)
# ──────────────────── 2. λ Œλ” μœ ν‹Έ ────────────────────
def _render_with_trimesh(scene: trimesh.Scene, res):
png = scene.save_image(resolution=res, visible=True)
if png is None:
raise RuntimeError("trimesh.save_image returned None")
return Image.open(io.BytesIO(png)).convert("RGB")
def _render_with_pyrender(mesh_or_scene, res):
import pyrender # μ§€μ—° μž„ν¬νŠΈ
if isinstance(mesh_or_scene, trimesh.Scene):
mesh = trimesh.util.concatenate(mesh_or_scene.dump())
else:
mesh = mesh_or_scene
mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False)
scn = pyrender.Scene()
scn.add(mesh)
cam = pyrender.PerspectiveCamera(yfov=np.pi / 3)
scn.add(cam, pose=tf.translation_matrix([0, 0, 3]))
light = pyrender.DirectionalLight(intensity=3.0)
scn.add(light, pose=tf.translation_matrix([0, 5, 5]))
r = pyrender.OffscreenRenderer(*res)
color, _ = r.render(scn, flags=pyrender.RenderFlags.RGBA)
r.delete()
return Image.fromarray(color[..., :3])
# ──────────────────── 3. GIF 생성 ────────────────────
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 생성"""
base = trimesh.load(input_glb_path)
if isinstance(base, trimesh.Trimesh):
base = trimesh.Scene(base)
num_frames = min(int(duration * fps), 60)
frames = []
for i in range(num_frames):
t = i / (num_frames - 1)
scene = base.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.apply_transform(M)
# β‘  trimesh β†’ β‘‘ pyrender 폴백
try:
frame = _render_with_trimesh(scene, resolution)
except Exception as e:
print("trimesh λ Œλ” μ‹€νŒ¨, pyrender 폴백:", e)
frame = _render_with_pyrender(scene, resolution)
frames.append(frame)
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=int(1000 / fps),
loop=0,
)
print("GIF saved:", output_path)
return output_path
# ──────────────────── 4. GLB λ³€ν˜• ────────────────────
def modify_glb_file(input_glb_path, output_glb_path, animation_type="rotate"):
"""단일 λ³€ν™˜μ„ μ μš©ν•œ μƒˆ GLB μ €μž₯ (μ‹€μ œ μ• λ‹ˆλ©”μ΄μ…˜μ€ μ•„λ‹˜)"""
try:
scn = trimesh.load(input_glb_path)
if not isinstance(scn, trimesh.Scene):
scn = trimesh.Scene(scn)
if animation_type == "rotate":
T = tf.rotation_matrix(math.pi / 4, [0, 1, 0])
elif animation_type == "float":
T = tf.translation_matrix([0, 0.5, 0])
elif animation_type == "pulse":
T = tf.scale_matrix(1.2)
elif animation_type == "explode":
T = tf.translation_matrix([0.5, 0, 0])
elif animation_type == "assemble":
T = tf.translation_matrix([-0.5, 0, 0])
elif animation_type == "swing":
T = tf.rotation_matrix(math.pi / 8, [0, 0, 1])
else:
T = np.eye(4)
scn.apply_transform(T)
scn.export(output_glb_path)
return output_glb_path
except Exception as e:
print("GLB λ³€ν˜• μ‹€νŒ¨, 원본 볡사:", e)
shutil.copy(input_glb_path, output_glb_path)
return output_glb_path
# ──────────────────── 5. Gradio νŒŒμ΄ν”„λΌμΈ ────────────────────
@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]
glb_out = os.path.join(LOG_PATH, f"animated_{base}.glb")
gif_out = os.path.join(LOG_PATH, f"preview_{base}.gif")
json_out = os.path.join(LOG_PATH, f"metadata_{base}.json")
modify_glb_file(input_3d, glb_out, animation_type)
create_model_animation_gif(
gif_out, input_3d, animation_type, animation_duration, fps
)
meta = 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_out, "w") as f:
json.dump(meta, f, indent=4)
return glb_out, gif_out, json_out
except Exception as e:
print("process_3d_model μ‹€νŒ¨:", e)
err_gif = os.path.join(LOG_PATH, "error.gif")
Image.new("RGB", (640, 480), (255, 0, 0)).save(err_gif)
return input_3d, err_gif, None
# ──────────────────── 6. Gradio UI ────────────────────
with gr.Blocks(title="GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기") as demo:
gr.Markdown(
"""
<h2><b>GLB μ• λ‹ˆλ©”μ΄μ…˜ 생성기 - 3D λͺ¨λΈ μ›€μ§μž„ 효과</b></h2>
μ—…λ‘œλ“œν•œ GLB λͺ¨λΈμ— μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•΄ λ³€ν˜•λœ GLBΒ·GIFΒ·JSON을 μ œκ³΅ν•©λ‹ˆλ‹€.
"""
)
with gr.Row():
with gr.Column():
inp = gr.Model3D(label="3D λͺ¨λΈ μ—…λ‘œλ“œ (GLB)")
typ = gr.Dropdown(
label="μ• λ‹ˆλ©”μ΄μ…˜ μœ ν˜•",
choices=["rotate", "float", "explode", "assemble", "pulse", "swing"],
value="rotate",
)
dur = gr.Slider("μ• λ‹ˆλ©”μ΄μ…˜ 길이 (초)", 1.0, 10.0, 3.0, 0.5)
fps = gr.Slider("FPS", 15, 60, 30, 1)
btn = gr.Button("μ• λ‹ˆλ©”μ΄μ…˜ 생성")
with gr.Column():
out_glb = gr.Model3D(label="μ• λ‹ˆλ©”μ΄μ…˜λœ GLB")
out_gif = gr.Image(label="미리보기 GIF")
out_json = gr.File(label="메타데이터 JSON")
btn.click(
fn=process_3d_model,
inputs=[inp, typ, dur, fps],
outputs=[out_glb, out_gif, out_json],
)
ex = [[f] for f in glob.glob("./data/demo_glb/*.glb")]
if ex:
gr.Examples(examples=ex, inputs=[inp])
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)