Cliente: Nomada Studio S.L. · Retargeting Rokoko → Rigify headless con Python/bpy
| Archivo | Bones | FPS | Frames | Duración | Escala det. | Estado |
|---|---|---|---|---|---|---|
| kai_walk_01.bvh | 22 | 120 | 288 | 2.4s | cm | ✓ OK |
| kai_run_01.bvh | 22 | 120 | 192 | 1.6s | cm | ✓ OK |
| kai_idle_01.bvh | 22 | 120 | 480 | 4.0s | cm | ✓ OK |
| kai_jump_01.bvh | 22 | 120 | 156 | 1.3s | cm | ✓ OK |
| kai_talk_02.bvh | 22 | 120 | 720 | 6.0s | cm | ✓ OK |
| kai_fight_03.bvh | 22 | 60 | 540 | 9.0s | cm | ⚠ FPS bajo |
| kai_crouch_01.bvh | 22 | 120 | 240 | 2.0s | cm | ✓ OK |
| kai_climb_02.bvh | 22 | 120 | 408 | 3.4s | cm | ✓ OK |
| kai_sitdown_01.bvh | 22 | 120 | 312 | 2.6s | cm | ✓ OK |
| kai_wave_03.bvh | 22 | 120 | 168 | 1.4s | cm | ✓ OK |
| kai_dance_01.bvh | 22 | 120 | 960 | 8.0s | cm | ⚠ Artefactos |
| kai_run_turn_02.bvh | 22 | 120 | 216 | 1.8s | cm | ✓ OK |
| + 35 archivos más · 44 OK · 3 advertencias · 0 errores fatales | ||||||
#!/usr/bin/env python3 # pipeline_mocap_nomada.py — CULTIVA IA para Nomada Studio S.L. # Uso: blender --background --python pipeline_mocap_nomada.py import bpy, glob, os, json, time from pathlib import Path # ── Configuración del proyecto ────────────────────────────────────────────── MOCAP_DIR = "/renders/deriva/mocap_library/" CHAR_BLEND = "/renders/deriva/kai_character.blend" OUTPUT_DIR = "/renders/deriva/output_fbx/" MAPPING_FILE= "/renders/deriva/bone_mapping.json" GLOBAL_SCALE= 0.01 # Rokoko usa centímetros DECIMATE_F = 0.5 # factor de decimado os.makedirs(OUTPUT_DIR, exist_ok=True) # ── 1. Cargar mapping de huesos ────────────────────────────────────────────── with open(MAPPING_FILE) as f: bone_mapping = json.load(f) # { "spine": "Hips", "spine.001": "Spine", ... } # ── 2. Abrir el blend con el personaje Kai ────────────────────────────────── bpy.ops.wm.open_mainfile(filepath=CHAR_BLEND) kai_arm = bpy.data.objects["Armature_Kai"] print(f"[OK] Personaje Kai cargado · {len(kai_arm.data.bones)} huesos") # ── 3. Función de retargeting ──────────────────────────────────────────────── def retarget_motion(src_arm, tgt_arm, mapping): src_action = src_arm.animation_data.action f0, f1 = int(src_action.frame_range[0]), int(src_action.frame_range[1]) if not tgt_arm.animation_data: tgt_arm.animation_data_create() new_action = bpy.data.actions.new(f"{src_action.name}_retarget") tgt_arm.animation_data.action = new_action for frame in range(f0, f1 + 1): bpy.context.scene.frame_set(frame) for tgt_name, src_name in mapping.items(): src = src_arm.pose.bones.get(src_name) tgt = tgt_arm.pose.bones.get(tgt_name) if not src or not tgt: continue tgt.rotation_quaternion = src.rotation_quaternion tgt.keyframe_insert(data_path="rotation_quaternion", frame=frame) if src_name == list(mapping.values())[0]: # root: también localización tgt.location = src.location tgt.keyframe_insert(data_path="location", frame=frame) return new_action # ── 4. Función de decimado ─────────────────────────────────────────────────── def decimate_action(action, factor=DECIMATE_F): total_before, total_after = 0, 0 for fc in action.fcurves: pts = fc.keyframe_points n = len(pts); total_before += n keep = max(1, int(1.0 / factor)) rm = [i for i in range(n) if i % keep != 0 and i != 0 and i != n-1] for i in reversed(rm): pts.remove(pts[i]) fc.update(); total_after += len(pts) return total_before, total_after # ── 5. Bucle principal ─────────────────────────────────────────────────────── results = [] for bvh_path in sorted(glob.glob(MOCAP_DIR + "*.bvh")): name = Path(bvh_path).stem t0 = time.perf_counter() # Importar BVH bpy.ops.import_anim.bvh(filepath=bvh_path, target='ARMATURE', global_scale=GLOBAL_SCALE, frame_start=1, rotate_mode='NATIVE', axis_forward='-Z', axis_up='Y') src_arm = bpy.context.active_object action = src_arm.animation_data.action fps = bpy.context.scene.render.fps frames = int(action.frame_range[1]) # Retarget a Kai new_act = retarget_motion(src_arm, kai_arm, bone_mapping) # Decimate kb, ka = decimate_action(new_act) # Export FBX out = OUTPUT_DIR + name + ".fbx" bpy.context.view_layer.objects.active = kai_arm kai_arm.select_set(True) bpy.ops.export_scene.fbx(filepath=out, use_selection=True, bake_anim=True, bake_anim_use_all_bones=True, add_leaf_bones=False) # Limpiar armature mocap importada bpy.data.objects.remove(src_arm) elapsed = time.perf_counter() - t0 results.append({"file": name, "frames": frames, "fps": fps, "kf_before": kb, "kf_after": ka, "t": round(elapsed, 2)}) print(f"[✓] {name} → {frames}f @ {fps}fps · -{100*(kb-ka)//kb if kb else 0}% KF · {elapsed:.1f}s") # ── 6. Guardar reporte ─────────────────────────────────────────────────────── with open(OUTPUT_DIR + "audit_report.json", "w") as f: json.dump(results, f, indent=2, ensure_ascii=False) print(f"\n[DONE] {len(results)} animaciones exportadas a {OUTPUT_DIR}")