Files

185 lines
6.8 KiB
Python

import os, sys, json, argparse
import traceback
import shutil
from pathlib import Path
WRAPPER_ROOT = os.path.dirname(__file__)
if WRAPPER_ROOT not in sys.path:
sys.path.insert(0, WRAPPER_ROOT)
from Make_It_Animatable import app as mia
#app.py crashes if it tries to log to gradio
def _headless_log_message(message: str, level="info", duration=None, visible=True, **kwargs):
if level in ("info", "success"):
print(f"[Make-It-Animatable] {message}")
elif level == "warning":
print(f"[Make-It-Animatable WARNING] {message}")
import gradio.helpers
gradio.helpers.log_message = _headless_log_message
mia.init_models()
#app.py crashes if these files don't exist
data_dir = Path(WRAPPER_ROOT) / "Make_It_Animatable" / "data"
(data_dir / "Standard Run.fbx").touch()
examples_dir = data_dir / "examples"
examples_dir.mkdir(parents=True, exist_ok=True)
(examples_dir / "log.csv").touch()
mia.init_blocks()
import bpy
def fbx2glb(input_fbx: str, output_glb: str, original_glb: str):
input_fbx = os.path.abspath(input_fbx)
output_glb = os.path.abspath(output_glb)
bpy.ops.wm.read_factory_settings(use_empty=True)
# Get PBR from original GLB
original_glb = os.path.abspath(original_glb)
bpy.ops.import_scene.gltf(filepath=original_glb)
original_meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH']
original_materials = {}
for mesh in original_meshes:
if mesh.data and hasattr(mesh.data, 'materials'):
for mat in mesh.data.materials:
if mat:
original_materials[mat.name] = mat
# Clear the scene to import FBX
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
bpy.ops.import_scene.fbx(filepath=input_fbx, ignore_leaf_bones=False)
# Get the auto-rigged mesh from FBX
fbx_meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH']
# Apply the original GLB materials to the FBX meshes
for fbx_mesh in fbx_meshes:
if fbx_mesh.data and hasattr(fbx_mesh.data, 'materials'):
fbx_mesh.data.materials.clear()
applied_any_material = False
for _, orig_mat in original_materials.items():
fbx_mesh.data.materials.append(orig_mat)
applied_any_material = True
# If no materials were applied, keep existing ones
if not applied_any_material and len(fbx_mesh.data.materials) == 0:
if original_materials:
first_orig_mat = next(iter(original_materials.values()))
fbx_mesh.data.materials.append(first_orig_mat)
# When animation is present, object-level transforms can be keyframed.
# We must remove these object-level animation tracks to preserve the pose-bone animations.
# We clean all actions in bpy.data.actions to ensure NLA tracks and multiple actions are covered.
for action in bpy.data.actions:
fcurves_to_remove = [
fcurve for fcurve in action.fcurves
if not fcurve.data_path.startswith("pose.bones")
]
for fcurve in fcurves_to_remove:
action.fcurves.remove(fcurve)
# Scale all bone location keyframes by 0.01 to match the baked armature scale
for fcurve in action.fcurves:
if fcurve.data_path.endswith(".location") or "location" in fcurve.data_path:
for kp in fcurve.keyframe_points:
kp.co[1] *= 0.01
kp.handle_left[1] *= 0.01
kp.handle_right[1] *= 0.01
# Stash all actions onto the armature's NLA tracks so they are exported to glTF/GLB
armatures = [obj for obj in bpy.context.scene.objects if obj.type == 'ARMATURE']
if armatures:
arm = armatures[0]
if not arm.animation_data:
arm.animation_data_create()
for track in list(arm.animation_data.nla_tracks):
arm.animation_data.nla_tracks.remove(track)
for action in list(bpy.data.actions):
track = arm.animation_data.nla_tracks.new()
track.name = action.name
track.mute = True
track.strips.new(action.name, int(action.frame_range[0]), action)
if bpy.data.actions:
arm.animation_data.action = list(bpy.data.actions)[0]
# Apply location, rotation, and scale transforms to meshes and armatures
meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH']
armatures = [obj for obj in bpy.context.scene.objects if obj.type == 'ARMATURE']
bpy.ops.object.select_all(action='DESELECT')
for obj in meshes + armatures:
obj.select_set(True)
if bpy.context.selected_objects:
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Delete unused materials and meshes to keep output GLB clean and avoid name collisions
for mat in list(bpy.data.materials):
if mat.users == 0:
bpy.data.materials.remove(mat)
for mesh in list(bpy.data.meshes):
if mesh.users == 0:
bpy.data.meshes.remove(mesh)
bpy.ops.export_scene.gltf(
filepath=output_glb,
export_format='GLB',
export_extras=False
)
print(f"Converted:\n {input_fbx}\n-> {output_glb}")
def run_once(input_path, db, **kwargs):
list(mia._pipeline(input_path=str(input_path), db=db, **kwargs))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--kwargs", default="{}")
args = parser.parse_args()
try:
kwargs = json.loads(args.kwargs)
db = mia.DB()
run_once(Path(args.input), db, **kwargs)
output_dir = Path(os.path.join(os.path.dirname(args.input), os.path.splitext(os.path.basename(args.input))[0]))
temp_files_to_delete = [
db.joints_coarse_path,
db.normed_path,
db.sample_path,
db.bw_path,
db.joints_path,
db.rest_lbs_path,
db.rest_vis_path,
db.anim_vis_path,
]
for path in temp_files_to_delete:
if path:
try:
Path(path).unlink(missing_ok=True)
except Exception:
pass
input_path = Path(db.anim_path)
output_path = Path(args.output)
if input_path.suffix.lower() == '.fbx':
# Pass the original input path to preserve original GLB data
fbx2glb(db.anim_path, output_path, original_glb=args.input)
else:
output_path.parent.mkdir(parents=True, exist_ok=True)
shutil.move(input_path, output_path)
except Exception as e:
tb = traceback.format_exc()
print(tb, file=sys.stderr)
sys.exit(1)