Files
animation/tools/trim_clip.py
T

90 lines
3.4 KiB
Python
Raw Normal View History

"""trim_clip.py — keep a frame window of a clip and re-base it to frame 0.
Samples every fcurve across [--start .. --end] (inclusive, source frames) and rewrites
the action to hold just that window at frames 0..(end-start). Use to lift a sub-beat out
of a longer take — e.g. keep only the dismount tail of a ladder clip so the descent loop
owns the vertical travel and the trimmed clip is purely the step-off.
Usage:
blender --background --python tools/trim_clip.py -- \
--src <in.glb> --out <out.glb> --name <NewClipName> --start <f> --end <f>
"""
import bpy, sys, argparse
def parse():
argv = sys.argv[sys.argv.index("--")+1:] if "--" in sys.argv else []
p = argparse.ArgumentParser()
p.add_argument("--src", required=True)
p.add_argument("--out", required=True)
p.add_argument("--name", required=True, help="clip name baked into the output")
p.add_argument("--start", type=int, required=True, help="first source frame to keep")
p.add_argument("--end", type=int, required=True, help="last source frame to keep (inclusive)")
return p.parse_args(argv)
def action_fcurves(a):
"""All fcurves across Blender 5 slotted-action layers/strips (legacy fallback)."""
if hasattr(a, "fcurves") and getattr(a, "fcurves", None) is not None:
return list(a.fcurves)
fcs = []
for layer in getattr(a, "layers", []):
for strip in layer.strips:
for bag in strip.channelbags:
fcs.extend(bag.fcurves)
return fcs
def main():
a = parse()
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.scene.render.fps = 30 # pipeline is 30fps (see reverse_clip.py)
bpy.ops.import_scene.gltf(filepath=a.src)
arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE')
act = arm.animation_data.action
if getattr(act, "slots", None) and arm.animation_data.action_slot is None:
arm.animation_data.action_slot = act.slots[0]
s, e = a.start, a.end
n = e - s
if n <= 0:
raise SystemExit(f"[trim] bad window {s}..{e}")
print(f"[trim] src '{act.name}' keep frames {s}..{e} -> 0..{n}")
for fc in action_fcurves(act):
vals = [fc.evaluate(s + k) for k in range(n + 1)]
need = n + 1
have = len(fc.keyframe_points)
if have < need:
fc.keyframe_points.add(need - have)
elif have > need:
for _ in range(have - need):
fc.keyframe_points.remove(fc.keyframe_points[-1], fast=True)
for k in range(need):
kp = fc.keyframe_points[k]
kp.co = (k, vals[k])
kp.interpolation = 'LINEAR'
fc.update()
act.name = a.name
for t in list(arm.animation_data.nla_tracks):
arm.animation_data.nla_tracks.remove(t)
track = arm.animation_data.nla_tracks.new()
track.name = a.name
strip = track.strips.new(a.name, 0, act)
strip.name = a.name
arm.animation_data.action = None
for o in [o for o in bpy.data.objects if o.type == 'MESH']:
bpy.data.objects.remove(o, do_unlink=True)
bpy.context.scene.frame_start = 0
bpy.context.scene.frame_end = n
bpy.ops.export_scene.gltf(
filepath=a.out,
export_animation_mode="NLA_TRACKS",
export_force_sampling=True,
export_optimize_animation_size=False,
)
print(f"[trim] EXPORTED {a.out} clip '{a.name}' ({n+1} frames)")
if __name__ == "__main__":
main()