95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
|
|
"""pingpong_bake.py — bake a clip into a palindrome (forward + reversed) loop.
|
||
|
|
|
||
|
|
Plays the clip forward, then backward, as ONE animation: frames [0..N] followed by
|
||
|
|
[N-1..1] mirrored (total 2N). The final frame equals frame 0 exactly, so Godot's
|
||
|
|
LoopMode.Linear wraps with zero pose gap and zero root drift — the character returns
|
||
|
|
to its start point by construction. The trade-off: motion direction reverses at the
|
||
|
|
turn point and at the wrap (inherent to the tactic).
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
blender --background --python tools/pingpong_bake.py -- \
|
||
|
|
--src <in.glb> --out <out.glb> --name <NewClipName>
|
||
|
|
"""
|
||
|
|
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")
|
||
|
|
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.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]
|
||
|
|
f0, f1 = int(act.frame_range[0]), int(act.frame_range[1])
|
||
|
|
n = f1 - f0
|
||
|
|
print(f"[pingpong] src clip '{act.name}' frames {f0}..{f1} -> palindrome 0..{2*n}")
|
||
|
|
|
||
|
|
# extend every fcurve in place: append mirrored keys [N-1..0] after frame N
|
||
|
|
for fc in action_fcurves(act):
|
||
|
|
vals = [fc.evaluate(f0 + k) for k in range(n + 1)]
|
||
|
|
base = len(fc.keyframe_points)
|
||
|
|
fc.keyframe_points.add(n) # frames N+1 .. 2N
|
||
|
|
# rewrite ALL points to a clean dense 0..2N ramp (handles f0 offset too)
|
||
|
|
total = 2 * n
|
||
|
|
need = total + 1
|
||
|
|
have = base + n
|
||
|
|
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):
|
||
|
|
v = vals[k] if k <= n else vals[2 * n - k]
|
||
|
|
kp = fc.keyframe_points[k]
|
||
|
|
kp.co = (k, v)
|
||
|
|
kp.interpolation = 'LINEAR'
|
||
|
|
fc.update()
|
||
|
|
|
||
|
|
act.name = a.name
|
||
|
|
|
||
|
|
# drop NLA tracks that came in with the import — only our palindrome track may export
|
||
|
|
for t in list(arm.animation_data.nla_tracks):
|
||
|
|
arm.animation_data.nla_tracks.remove(t)
|
||
|
|
|
||
|
|
# single NLA track named = clip name (same convention as cc_retarget.py)
|
||
|
|
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 = 2 * 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"[pingpong] EXPORTED {a.out} clip '{a.name}' ({2*n+1} frames)")
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|