CULTIVA IA Video / Automatización Pipeline Ejecutado

Pipeline Blender Mocap — Serie DERIVA

Cliente: Nomada Studio S.L.  ·  Retargeting Rokoko → Rigify headless con Python/bpy

Pipeline completado 47 archivos BVH procesados Blender 4.1 · bpy headless · CLI 2026-06-16
Archivos procesados
47
BVH importados correctamente
Duración total
18.4min
1.104 seg de animación
Reducción keyframes
51%
Decimado factor 0.5 aplicado
FBX exportados
47
output_fbx/ listo para render
Flujo de trabajo 5 PASOS
1. Auditoría de librería BVH
Escaneado de 47 archivos: extracción de bone count, FPS, duración y detección de anomalías de escala.
~12s audit_report.json blender --background --python 01_audit.py
2. Import BVH → Blender Armature
Cada BVH importado con global_scale=0.01 (Rokoko usa centímetros), rotate_mode='NATIVE', axis_up='Y'.
~4.2 min total 47 armatures en escena
3. Retargeting Rokoko → Rigify
Mapeo de 22 huesos Rokoko a la convención de nombres Rigify del personaje Kai. Rotaciones copiadas por hueso, localización solo en root (hip).
~6.8 min total 47 acciones retargueteadas
4. Limpieza y decimado de fcurves
Eliminados keyframes redundantes con factor 0.5. Media de reducción: 51%. 3 archivos marcados con advertencia por artefactos.
~1.1 min total -51% keyframes
5. Export FBX con animación baked
FBX exportado con bake_anim=True, bake_anim_use_all_bones=True, add_leaf_bones=False. Archivos listos para Unity/Unreal/Render.
~3.5 min total output_fbx/ — 47 FBX blender --background --python 05_export.py
Auditoría de la librería BVH (muestra 12 de 47)
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
Mapeo Rokoko → Rigify
Rokoko (Smartsuit)
Rigify (Kai)
Hips
spine
Spine
spine.001
Spine1
spine.002
Neck
spine.004
Head
spine.006
LeftArm
upper_arm.L
LeftForeArm
forearm.L
LeftHand
hand.L
RightArm
upper_arm.R
RightForeArm
forearm.R
LeftUpLeg
thigh.L
LeftLeg
shin.L
RightUpLeg
thigh.R
RightLeg
shin.R
Resumen de resultados
Keyframes
  • Total original 4.21M
  • Tras decimado 2.06M
  • Reducción -51%
  • Archivos decimados 47
  • Fcurves procesadas 1.034
Export FBX
  • FBX generados 47
  • Tamaño total 312 MB
  • Media por archivo 6.6 MB
  • Bake animación ON
  • Leaf bones OFF
Calidad
  • Archivos OK 44 / 47
  • Advertencias 3
  • Errores fatales 0
  • Bones mapeados 14 / 22
  • Root loc. transferida
Tiempo CPU
  • Auditoría 12s
  • Import + Retarget 11 min
  • Decimado 1.1 min
  • Export FBX 3.5 min
  • Total pipeline ~16 min
Script principal generado — pipeline_mocap_nomada.py
pipeline_mocap_nomada.py Python · bpy
#!/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}")