|
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 Nonedef create_simple_animation_frames(input_mesh_path, animation_type='rotate', num_frames=30): |
|
"""κ°λ¨ν λ°©μμΌλ‘ μ λλ©μ΄μ
νλ μ μμ± - λ€λ₯Έ λ°©λ²λ€μ΄ μ€ν¨ν κ²½μ° λ체μ©""" |
|
try: |
|
|
|
mesh = trimesh.load(input_mesh_path) |
|
|
|
|
|
if isinstance(mesh, trimesh.Scene): |
|
|
|
geometries = list(mesh.geometry.values()) |
|
if geometries: |
|
base_mesh = geometries[0] |
|
else: |
|
print("No geometries found in scene") |
|
return None |
|
else: |
|
base_mesh = mesh |
|
|
|
|
|
frames = [] |
|
|
|
|
|
for i in range(num_frames): |
|
angle = i * 2 * math.pi / num_frames |
|
|
|
|
|
scene = trimesh.Scene() |
|
|
|
|
|
rotated_mesh = base_mesh.copy() |
|
rotation = tf.rotation_matrix(angle, [0, 1, 0]) |
|
rotated_mesh.apply_transform(rotation) |
|
|
|
|
|
scene.add_geometry(rotated_mesh) |
|
|
|
|
|
scene.set_camera() |
|
|
|
frames.append(scene) |
|
|
|
return frames |
|
except Exception as e: |
|
print(f"Error in simple animation: {str(e)}") |
|
return Noneimport os |
|
import time |
|
import glob |
|
import json |
|
import numpy as np |
|
import trimesh |
|
import argparse |
|
from scipy.spatial.transform import Rotation |
|
import PIL.Image |
|
from PIL import Image |
|
import math |
|
import trimesh.transformations as tf |
|
from trimesh.exchange.gltf import export_glb |
|
|
|
os.environ['PYOPENGL_PLATFORM'] = 'egl' |
|
|
|
import gradio as gr |
|
import spaces |
|
|
|
def parse_args(): |
|
parser = argparse.ArgumentParser(description='Create animations for 3D models') |
|
|
|
parser.add_argument( |
|
'--input', |
|
type=str, |
|
default='./data/demo_glb/', |
|
help='Input file or directory path (default: ./data/demo_glb/)' |
|
) |
|
|
|
parser.add_argument( |
|
'--log_path', |
|
type=str, |
|
default='./results/demo', |
|
help='Output directory path (default: results/demo)' |
|
) |
|
|
|
parser.add_argument( |
|
'--animation_type', |
|
type=str, |
|
default='rotate', |
|
choices=['rotate', 'float', 'explode', 'assemble', 'pulse', 'swing'], |
|
help='Type of animation to apply' |
|
) |
|
|
|
parser.add_argument( |
|
'--animation_duration', |
|
type=float, |
|
default=3.0, |
|
help='Duration of animation in seconds' |
|
) |
|
|
|
parser.add_argument( |
|
'--fps', |
|
type=int, |
|
default=30, |
|
help='Frames per second for animation' |
|
) |
|
|
|
return parser.parse_args() |
|
|
|
def get_input_files(input_path): |
|
if os.path.isfile(input_path): |
|
return [input_path] |
|
elif os.path.isdir(input_path): |
|
return glob.glob(os.path.join(input_path, '*')) |
|
else: |
|
raise ValueError(f"Input path {input_path} is neither a file nor a directory") |
|
|
|
args = parse_args() |
|
|
|
LOG_PATH = args.log_path |
|
os.makedirs(LOG_PATH, exist_ok=True) |
|
|
|
print(f"Output directory: {LOG_PATH}") |
|
|
|
def normalize_mesh(mesh): |
|
"""Normalize mesh to fit in a unit cube centered at origin""" |
|
try: |
|
if isinstance(mesh, trimesh.Scene): |
|
|
|
|
|
meshes = [] |
|
for geometry in mesh.geometry.values(): |
|
if isinstance(geometry, trimesh.Trimesh): |
|
meshes.append(geometry) |
|
|
|
if not meshes: |
|
print("Warning: No meshes found in scene during normalization") |
|
return mesh, np.zeros(3), 1.0 |
|
|
|
|
|
try: |
|
all_vertices = np.vstack([m.vertices for m in meshes if hasattr(m, 'vertices') and m.vertices.shape[0] > 0]) |
|
if len(all_vertices) == 0: |
|
print("Warning: No vertices found in meshes during normalization") |
|
return mesh, np.zeros(3), 1.0 |
|
|
|
bounds = np.array([all_vertices.min(axis=0), all_vertices.max(axis=0)]) |
|
center = (bounds[0] + bounds[1]) / 2 |
|
scale_value = (bounds[1] - bounds[0]).max() |
|
if scale_value < 1e-10: |
|
print("Warning: Mesh is too small, using default scale") |
|
scale = 1.0 |
|
else: |
|
scale = 1.0 / scale_value |
|
|
|
|
|
normalized_scene = trimesh.Scene() |
|
for mesh_idx, mesh_obj in enumerate(meshes): |
|
normalized_mesh = mesh_obj.copy() |
|
try: |
|
normalized_mesh.vertices = (normalized_mesh.vertices - center) * scale |
|
normalized_scene.add_geometry(normalized_mesh, node_name=f"normalized_mesh_{mesh_idx}") |
|
except Exception as e: |
|
print(f"Error normalizing mesh {mesh_idx}: {str(e)}") |
|
|
|
normalized_scene.add_geometry(mesh_obj, node_name=f"original_mesh_{mesh_idx}") |
|
|
|
return normalized_scene, center, scale |
|
except Exception as e: |
|
print(f"Error during scene normalization: {str(e)}") |
|
return mesh, np.zeros(3), 1.0 |
|
else: |
|
|
|
if not hasattr(mesh, 'vertices') or mesh.vertices.shape[0] == 0: |
|
print("Warning: Mesh has no vertices") |
|
return mesh, np.zeros(3), 1.0 |
|
|
|
vertices = mesh.vertices |
|
bounds = np.array([vertices.min(axis=0), vertices.max(axis=0)]) |
|
center = (bounds[0] + bounds[1]) / 2 |
|
scale_value = (bounds[1] - bounds[0]).max() |
|
if scale_value < 1e-10: |
|
print("Warning: Mesh is too small, using default scale") |
|
scale = 1.0 |
|
else: |
|
scale = 1.0 / scale_value |
|
|
|
|
|
normalized_mesh = mesh.copy() |
|
normalized_mesh.vertices = (vertices - center) * scale |
|
|
|
return normalized_mesh, center, scale |
|
except Exception as e: |
|
print(f"Unexpected error in normalize_mesh: {str(e)}") |
|
|
|
return mesh, np.zeros(3), 1.0 |
|
|
|
def create_rotation_animation(mesh, duration=3.0, fps=30): |
|
"""Create a rotation animation around the Y axis""" |
|
num_frames = int(duration * fps) |
|
frames = [] |
|
|
|
|
|
try: |
|
mesh, original_center, original_scale = normalize_mesh(mesh) |
|
print(f"Normalized mesh center: {original_center}, scale: {original_scale}") |
|
except Exception as e: |
|
print(f"Error normalizing mesh: {str(e)}") |
|
|
|
pass |
|
|
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
angle = t * 2 * math.pi |
|
|
|
if isinstance(mesh, trimesh.Scene): |
|
|
|
frame_scene = trimesh.Scene() |
|
|
|
for node_name, transform, geometry_name in mesh.graph.nodes_geometry: |
|
|
|
mesh_copy = mesh.geometry[geometry_name].copy() |
|
|
|
|
|
center_point = mesh_copy.centroid if hasattr(mesh_copy, 'centroid') else np.zeros(3) |
|
|
|
|
|
rotation_matrix = tf.rotation_matrix(angle, [0, 1, 0], center_point) |
|
|
|
|
|
mesh_copy.apply_transform(rotation_matrix) |
|
|
|
|
|
frame_scene.add_geometry(mesh_copy, node_name=node_name) |
|
|
|
frames.append(frame_scene) |
|
else: |
|
|
|
animated_mesh = mesh.copy() |
|
rotation_matrix = tf.rotation_matrix(angle, [0, 1, 0]) |
|
animated_mesh.apply_transform(rotation_matrix) |
|
frames.append(animated_mesh) |
|
|
|
return frames |
|
|
|
def create_float_animation(mesh, duration=3.0, fps=30, amplitude=0.2): |
|
"""Create a floating animation where the mesh moves up and down""" |
|
num_frames = int(duration * fps) |
|
frames = [] |
|
|
|
|
|
mesh, original_center, original_scale = normalize_mesh(mesh) |
|
|
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
y_offset = amplitude * math.sin(2 * math.pi * t) |
|
|
|
if isinstance(mesh, trimesh.Scene): |
|
|
|
frame_scene = trimesh.Scene() |
|
|
|
for node_name, transform, geometry_name in mesh.graph.nodes_geometry: |
|
|
|
mesh_copy = mesh.geometry[geometry_name].copy() |
|
|
|
|
|
translation_matrix = tf.translation_matrix([0, y_offset, 0]) |
|
mesh_copy.apply_transform(translation_matrix) |
|
|
|
|
|
frame_scene.add_geometry(mesh_copy, node_name=node_name) |
|
|
|
frames.append(frame_scene) |
|
else: |
|
|
|
animated_mesh = mesh.copy() |
|
translation_matrix = tf.translation_matrix([0, y_offset, 0]) |
|
animated_mesh.apply_transform(translation_matrix) |
|
frames.append(animated_mesh) |
|
|
|
return frames |
|
|
|
def create_explode_animation(mesh, duration=3.0, fps=30): |
|
"""Create an explode animation where parts of the mesh move outward""" |
|
num_frames = int(duration * fps) |
|
frames = [] |
|
|
|
|
|
mesh, original_center, original_scale = normalize_mesh(mesh) |
|
|
|
|
|
if isinstance(mesh, trimesh.Scene): |
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
|
|
|
|
frame_scene = trimesh.Scene() |
|
|
|
|
|
for node_name, transform, geometry_name in mesh.graph.nodes_geometry: |
|
mesh_copy = mesh.geometry[geometry_name].copy() |
|
|
|
|
|
center_point = mesh_copy.centroid if hasattr(mesh_copy, 'centroid') else np.zeros(3) |
|
|
|
|
|
direction = center_point |
|
if np.linalg.norm(direction) < 1e-10: |
|
|
|
direction = np.random.rand(3) - 0.5 |
|
|
|
direction = direction / np.linalg.norm(direction) |
|
|
|
|
|
translation = direction * t * 0.5 |
|
translation_matrix = tf.translation_matrix(translation) |
|
mesh_copy.apply_transform(translation_matrix) |
|
|
|
|
|
frame_scene.add_geometry(mesh_copy, node_name=f"{node_name}_{frame_idx}") |
|
|
|
frames.append(frame_scene) |
|
else: |
|
|
|
try: |
|
components = mesh.split(only_watertight=False) |
|
if len(components) <= 1: |
|
|
|
raise ValueError("Mesh cannot be split into components") |
|
except: |
|
|
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
|
|
|
|
animated_mesh = mesh.copy() |
|
vertices = animated_mesh.vertices.copy() |
|
|
|
|
|
directions = vertices.copy() |
|
norms = np.linalg.norm(directions, axis=1, keepdims=True) |
|
mask = norms > 1e-10 |
|
directions[mask] = directions[mask] / norms[mask] |
|
directions[~mask] = np.random.rand(np.sum(~mask), 3) - 0.5 |
|
|
|
|
|
vertices += directions * t * 0.3 |
|
animated_mesh.vertices = vertices |
|
|
|
frames.append(animated_mesh) |
|
else: |
|
|
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
|
|
|
|
scene = trimesh.Scene() |
|
|
|
|
|
for i, component in enumerate(components): |
|
|
|
animated_component = component.copy() |
|
|
|
|
|
direction = animated_component.centroid |
|
if np.linalg.norm(direction) < 1e-10: |
|
|
|
direction = np.random.rand(3) - 0.5 |
|
|
|
direction = direction / np.linalg.norm(direction) |
|
|
|
|
|
translation = direction * t * 0.5 |
|
translation_matrix = tf.translation_matrix(translation) |
|
animated_component.apply_transform(translation_matrix) |
|
|
|
|
|
scene.add_geometry(animated_component, node_name=f"component_{i}") |
|
|
|
|
|
animated_mesh = trimesh.util.concatenate(scene.dump()) |
|
frames.append(animated_mesh) |
|
|
|
return frames |
|
|
|
def create_assemble_animation(mesh, duration=3.0, fps=30): |
|
"""Create an assembly animation (reverse of explode)""" |
|
|
|
explode_frames = create_explode_animation(mesh, duration, fps) |
|
return list(reversed(explode_frames)) |
|
|
|
def create_pulse_animation(mesh, duration=3.0, fps=30, min_scale=0.8, max_scale=1.2): |
|
"""Create a pulsing animation where the mesh scales up and down""" |
|
num_frames = int(duration * fps) |
|
frames = [] |
|
|
|
|
|
mesh, original_center, original_scale = normalize_mesh(mesh) |
|
|
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
|
|
|
|
animated_mesh = mesh.copy() |
|
|
|
|
|
scale_factor = min_scale + (max_scale - min_scale) * (0.5 + 0.5 * math.sin(2 * math.pi * t)) |
|
scale_matrix = tf.scale_matrix(scale_factor) |
|
animated_mesh.apply_transform(scale_matrix) |
|
|
|
|
|
frames.append(animated_mesh) |
|
|
|
return frames |
|
|
|
def create_swing_animation(mesh, duration=3.0, fps=30, max_angle=math.pi/6): |
|
"""Create a swinging animation where the mesh rotates back and forth""" |
|
num_frames = int(duration * fps) |
|
frames = [] |
|
|
|
|
|
mesh, original_center, original_scale = normalize_mesh(mesh) |
|
|
|
for frame_idx in range(num_frames): |
|
t = frame_idx / (num_frames - 1) |
|
|
|
|
|
animated_mesh = mesh.copy() |
|
|
|
|
|
angle = max_angle * math.sin(2 * math.pi * t) |
|
rotation_matrix = tf.rotation_matrix(angle, [0, 1, 0]) |
|
animated_mesh.apply_transform(rotation_matrix) |
|
|
|
|
|
frames.append(animated_mesh) |
|
|
|
return frames |
|
|
|
def generate_gif_from_frames(frames, output_path, fps=30, resolution=(640, 480), background_color=(255, 255, 255, 255)): |
|
"""Generate a GIF from animation frames""" |
|
gif_frames = [] |
|
|
|
|
|
all_bounds = [] |
|
for frame in frames: |
|
if isinstance(frame, trimesh.Scene): |
|
|
|
all_points = [] |
|
for geom in frame.geometry.values(): |
|
if hasattr(geom, 'vertices') and geom.vertices.shape[0] > 0: |
|
all_points.append(geom.vertices) |
|
|
|
if all_points: |
|
all_vertices = np.vstack(all_points) |
|
bounds = np.array([all_vertices.min(axis=0), all_vertices.max(axis=0)]) |
|
all_bounds.append(bounds) |
|
elif hasattr(frame, 'vertices') and frame.vertices.shape[0] > 0: |
|
|
|
bounds = np.array([frame.vertices.min(axis=0), frame.vertices.max(axis=0)]) |
|
all_bounds.append(bounds) |
|
|
|
|
|
if all_bounds: |
|
min_corner = np.min(np.array([b[0] for b in all_bounds]), axis=0) |
|
max_corner = np.max(np.array([b[1] for b in all_bounds]), axis=0) |
|
total_bounds = np.array([min_corner, max_corner]) |
|
|
|
|
|
center = (total_bounds[0] + total_bounds[1]) / 2 |
|
size = np.max(total_bounds[1] - total_bounds[0]) |
|
|
|
|
|
camera_distance = size * 2.5 |
|
else: |
|
|
|
center = np.zeros(3) |
|
camera_distance = 2.0 |
|
|
|
|
|
for i, frame in enumerate(frames): |
|
try: |
|
|
|
if not isinstance(frame, trimesh.Scene): |
|
scene = trimesh.Scene(frame) |
|
else: |
|
scene = frame |
|
|
|
|
|
try: |
|
|
|
camera_fov = 60.0 |
|
camera_pos = np.array([0, 0, camera_distance]) |
|
|
|
|
|
camera_transform = np.eye(4) |
|
camera_transform[:3, 3] = camera_pos |
|
|
|
|
|
scene.camera = trimesh.scene.Camera( |
|
resolution=resolution, |
|
fov=[camera_fov, camera_fov * (resolution[1] / resolution[0])] |
|
) |
|
scene.camera_transform = camera_transform |
|
except Exception as e: |
|
print(f"Error setting camera: {str(e)}") |
|
|
|
scene.set_camera(angles=(0, 0, 0), distance=camera_distance) |
|
|
|
|
|
try: |
|
|
|
import warnings |
|
warnings.filterwarnings("ignore", category=UserWarning) |
|
|
|
|
|
rendered_img = scene.save_image(resolution=resolution, background=background_color) |
|
pil_img = Image.open(rendered_img) |
|
|
|
|
|
if np.array(pil_img).std() < 1.0: |
|
print(f"Warning: Frame {i} seems to be empty, trying different angle") |
|
|
|
scene.set_camera(angles=(np.pi/4, np.pi/4, 0), distance=camera_distance*1.2) |
|
rendered_img = scene.save_image(resolution=resolution, background=background_color) |
|
pil_img = Image.open(rendered_img) |
|
|
|
gif_frames.append(pil_img) |
|
except Exception as e: |
|
print(f"Error in main rendering: {str(e)}") |
|
|
|
try: |
|
|
|
from PIL import Image, ImageDraw |
|
img = Image.new('RGB', resolution, color=(255, 255, 255)) |
|
draw = ImageDraw.Draw(img) |
|
|
|
|
|
draw.text((resolution[0]//2, resolution[1]//2), f"Frame {i}", fill=(0, 0, 0)) |
|
gif_frames.append(img) |
|
except Exception as e2: |
|
print(f"Error in fallback rendering: {str(e2)}") |
|
gif_frames.append(Image.new('RGB', resolution, (255, 255, 255))) |
|
except Exception as e: |
|
print(f"Unexpected error in frame processing: {str(e)}") |
|
gif_frames.append(Image.new('RGB', resolution, (255, 255, 255))) |
|
|
|
|
|
if gif_frames: |
|
try: |
|
gif_frames[0].save( |
|
output_path, |
|
save_all=True, |
|
append_images=gif_frames[1:], |
|
optimize=False, |
|
duration=int(1000 / fps), |
|
loop=0 |
|
) |
|
print(f"GIF saved to {output_path}") |
|
|
|
first_frame_path = output_path.replace('.gif', '_first_frame.png') |
|
gif_frames[0].save(first_frame_path) |
|
print(f"First frame saved to {first_frame_path}") |
|
return output_path |
|
except Exception as e: |
|
print(f"Error saving GIF: {str(e)}") |
|
return None |
|
else: |
|
print("No frames to save") |
|
return None |
|
|
|
def create_animation_mesh(input_mesh_path, animation_type='rotate', duration=3.0, fps=30): |
|
"""Create animation from input mesh based on animation type""" |
|
|
|
try: |
|
print(f"Loading mesh from {input_mesh_path}") |
|
|
|
loaded_obj = trimesh.load(input_mesh_path) |
|
print(f"Loaded object type: {type(loaded_obj)}") |
|
|
|
|
|
if isinstance(loaded_obj, trimesh.Scene): |
|
print("Loaded a scene, extracting meshes...") |
|
|
|
meshes = [] |
|
for geometry_name, geometry in loaded_obj.geometry.items(): |
|
if isinstance(geometry, trimesh.Trimesh): |
|
print(f"Found mesh: {geometry_name} with {len(geometry.vertices)} vertices") |
|
meshes.append(geometry) |
|
|
|
if not meshes: |
|
print("No meshes found in scene, trying simple animation...") |
|
frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps)) |
|
if not frames: |
|
print("Simple animation failed too") |
|
return None, None |
|
else: |
|
|
|
mesh = loaded_obj |
|
print(f"Using original scene with {len(meshes)} meshes") |
|
elif isinstance(loaded_obj, trimesh.Trimesh): |
|
mesh = loaded_obj |
|
print(f"Loaded a single mesh with {len(mesh.vertices)} vertices") |
|
else: |
|
print(f"Unsupported object type: {type(loaded_obj)}") |
|
|
|
frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps)) |
|
if not frames: |
|
return None, None |
|
except Exception as e: |
|
print(f"Error loading mesh: {str(e)}") |
|
|
|
frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps)) |
|
if not frames: |
|
return None, None |
|
|
|
|
|
try: |
|
if 'frames' not in locals(): |
|
print(f"Generating {animation_type} animation...") |
|
if animation_type == 'rotate': |
|
frames = create_rotation_animation(mesh, duration, fps) |
|
elif animation_type == 'float': |
|
frames = create_float_animation(mesh, duration, fps) |
|
elif animation_type == 'explode': |
|
frames = create_explode_animation(mesh, duration, fps) |
|
elif animation_type == 'assemble': |
|
frames = create_assemble_animation(mesh, duration, fps) |
|
elif animation_type == 'pulse': |
|
frames = create_pulse_animation(mesh, duration, fps) |
|
elif animation_type == 'swing': |
|
frames = create_swing_animation(mesh, duration, fps) |
|
else: |
|
print(f"Unknown animation type: {animation_type}, using default rotate") |
|
frames = create_rotation_animation(mesh, duration, fps) |
|
except Exception as e: |
|
print(f"Error generating animation: {str(e)}") |
|
|
|
frames = create_simple_animation_frames(input_mesh_path, animation_type, int(duration * fps)) |
|
if not frames: |
|
return None, None |
|
|
|
print(f"Generated {len(frames)} animation frames") |
|
|
|
base_filename = os.path.basename(input_mesh_path).rsplit('.', 1)[0] |
|
|
|
|
|
try: |
|
animated_glb_path = os.path.join(LOG_PATH, f'animated_{base_filename}.glb') |
|
|
|
|
|
|
|
if frames and len(frames) > 0: |
|
|
|
first_frame = frames[0] |
|
|
|
if not isinstance(first_frame, trimesh.Scene): |
|
scene = trimesh.Scene(first_frame) |
|
else: |
|
scene = first_frame |
|
scene.export(animated_glb_path) |
|
print(f"Exported GLB to {animated_glb_path}") |
|
else: |
|
return None, None |
|
except Exception as e: |
|
print(f"Error exporting GLB: {str(e)}") |
|
animated_glb_path = None |
|
|
|
|
|
try: |
|
animated_gif_path = os.path.join(LOG_PATH, f'animated_{base_filename}.gif') |
|
print(f"Creating GIF at {animated_gif_path}") |
|
generate_gif_from_frames(frames, animated_gif_path, fps) |
|
except Exception as e: |
|
print(f"Error creating GIF: {str(e)}") |
|
animated_gif_path = None |
|
|
|
return animated_glb_path, animated_gif_path |
|
|
|
@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: |
|
|
|
animated_glb_path, animated_gif_path = create_animation_mesh( |
|
input_3d, |
|
animation_type=animation_type, |
|
duration=animation_duration, |
|
fps=fps |
|
) |
|
|
|
if not animated_glb_path: |
|
|
|
print("Primary animation method failed, trying simple animation...") |
|
frames = create_simple_animation_frames( |
|
input_3d, |
|
animation_type, |
|
int(animation_duration * fps) |
|
) |
|
|
|
if frames: |
|
base_filename = os.path.basename(input_3d).rsplit('.', 1)[0] |
|
|
|
|
|
animated_glb_path = os.path.join(LOG_PATH, f'simple_animated_{base_filename}.glb') |
|
if isinstance(frames[0], trimesh.Scene): |
|
scene = frames[0] |
|
else: |
|
scene = trimesh.Scene(frames[0]) |
|
scene.export(animated_glb_path) |
|
|
|
|
|
animated_gif_path = os.path.join(LOG_PATH, f'simple_animated_{base_filename}.gif') |
|
generate_gif_from_frames(frames, animated_gif_path, fps) |
|
else: |
|
|
|
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: |
|
animated_glb_path = input_3d |
|
print("Could not copy original GLB, using input path") |
|
|
|
|
|
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_{os.path.basename(input_3d).rsplit(".", 1)[0]}.json') |
|
with open(json_path, 'w') as f: |
|
json.dump(metadata, f, indent=4) |
|
|
|
|
|
if not animated_gif_path: |
|
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 |
|
) |
|
|
|
return animated_glb_path, animated_gif_path, json_path |
|
except Exception as e: |
|
error_msg = f"Error processing file: {str(e)}" |
|
print(error_msg) |
|
|
|
|
|
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 |
|
except: |
|
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"), |
|
"error": str(e) |
|
} |
|
|
|
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: |
|
|
|
return error_msg, None, None, 50), f"Model: {os.path.basename(input_3d)}", fill=(0, 0, 0)) |
|
draw.text((50, 100), f"Animation: {animation_type}", fill=(0, 0, 0)) |
|
draw.text((50, 150), f"Angle: {angle}Β°", fill=(0, 0, 0)) |
|
|
|
center_x, center_y = 200, 180 |
|
radius = 50 |
|
x = center_x + radius * math.cos(math.radians(angle)) |
|
y = center_y + radius * math.sin(math.radians(angle)) |
|
draw.ellipse((x-20, y-20, x+20, y+20), fill=(255, 0, 0)) |
|
simple_frames.append(img) |
|
|
|
|
|
simple_frames[0].save( |
|
backup_gif_path, |
|
save_all=True, |
|
append_images=simple_frames[1:], |
|
optimize=False, |
|
duration=int(1000 / fps), |
|
loop=0 |
|
) |
|
print(f"Backup GIF created at {backup_gif_path}") |
|
|
|
|
|
try: |
|
|
|
original_gif = Image.open(animated_gif_path) |
|
original_frames = [] |
|
try: |
|
for i in range(100): |
|
original_gif.seek(i) |
|
frame = original_gif.copy() |
|
original_frames.append(frame) |
|
except EOFError: |
|
pass |
|
|
|
if not original_frames: |
|
print("Original GIF is empty, using backup") |
|
animated_gif_path = backup_gif_path |
|
except Exception as e: |
|
print(f"Error checking original GIF: {str(e)}, using backup") |
|
animated_gif_path = backup_gif_path |
|
except Exception as e: |
|
print(f"Error creating backup GIF: {str(e)}") |
|
|
|
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 |
|
|
|
_HEADER_ = ''' |
|
<h2><b>GLB μ λλ©μ΄μ
μμ±κΈ° - 3D λͺ¨λΈ μμ§μ ν¨κ³Ό</b></h2> |
|
|
|
μ΄ λ°λͺ¨λ₯Ό ν΅ν΄ μ μ μΈ 3D λͺ¨λΈ(GLB νμΌ)μ λ€μν μ λλ©μ΄μ
ν¨κ³Όλ₯Ό μ μ©ν μ μμ΅λλ€. |
|
|
|
βοΈβοΈβοΈ**μ€μμ¬ν:** |
|
- μ΄ λ°λͺ¨λ μ
λ‘λλ GLB νμΌμ μ λλ©μ΄μ
μ μ μ©ν©λλ€. |
|
- λ€μν μ λλ©μ΄μ
μ€νμΌ μ€μμ μ ννμΈμ: νμ , λΆμ , νλ°, 쑰립, νμ€, μ€μ. |
|
- κ²°κ³Όλ μ λλ©μ΄μ
λ GLB νμΌκ³Ό λ―Έλ¦¬λ³΄κΈ°μ© GIF νμΌλ‘ μ 곡λ©λλ€. |
|
''' |
|
|
|
_INFO_ = r""" |
|
### μ λλ©μ΄μ
μ ν μ€λͺ
|
|
- **νμ (rotate)**: λͺ¨λΈμ΄ YμΆμ μ€μ¬μΌλ‘ νμ ν©λλ€. |
|
- **λΆμ (float)**: λͺ¨λΈμ΄ μμλλ‘ λΆλλ½κ² λ λ€λλλ€. |
|
- **νλ°(explode)**: λͺ¨λΈμ κ° λΆλΆμ΄ μ€μ¬μμ λ°κΉ₯μͺ½μΌλ‘ νΌμ Έλκ°λλ€. |
|
- **쑰립(assemble)**: νλ° μ λλ©μ΄μ
μ λ°λ - λΆνλ€μ΄ ν¨κ» λͺ¨μ
λλ€. |
|
- **νμ€(pulse)**: λͺ¨λΈμ΄ ν¬κΈ°κ° 컀μ‘λ€ μμμ‘λ€λ₯Ό λ°λ³΅ν©λλ€. |
|
- **μ€μ(swing)**: λͺ¨λΈμ΄ μ’μ°λ‘ λΆλλ½κ² νλ€λ¦½λλ€. |
|
|
|
### ν |
|
- μ λλ©μ΄μ
κΈΈμ΄μ FPSλ₯Ό μ‘°μ νμ¬ μμ§μμ μλμ λΆλλ¬μμ μ‘°μ ν μ μμ΅λλ€. |
|
- 볡μ‘ν λͺ¨λΈμ μ²λ¦¬ μκ°μ΄ λ μ€λ 걸릴 μ μμ΅λλ€. |
|
- GIF 미리보기λ λΉ λ₯Έ μ°Έμ‘°μ©μ΄λ©°, κ³ νμ§ κ²°κ³Όλ₯Ό μν΄μλ μ λλ©μ΄μ
λ GLB νμΌμ λ€μ΄λ‘λνμΈμ. |
|
""" |
|
|
|
|
|
def create_gradio_interface(): |
|
with gr.Blocks(title="GLB μ λλ©μ΄μ
μμ±κΈ°") as demo: |
|
|
|
gr.Markdown(_HEADER_) |
|
|
|
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(_INFO_) |
|
|
|
|
|
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: |
|
example = gr.Examples( |
|
examples=example_files, |
|
inputs=[input_3d], |
|
examples_per_page=10, |
|
) |
|
|
|
return demo |
|
|
|
|
|
if __name__ == "__main__": |
|
demo = create_gradio_interface() |
|
demo.launch(share=True, server_name="0.0.0.0", server_port=7860) |