extends SceneTree ## Build animation-pack.res from animation-0.glb (original Blender bone names). ## ## Usage: ## godot --headless --path --script tools/build_animation_pack.gd ## ## No rest-pose correction — animations are copied verbatim from the source GLB. ## All characters share the same rig structure (identical 25 bones), so the same ## animation works on all of them. const SOURCE_GLB := "res://assets/characters/animation-0.glb" const OUTPUT_PATH := "res://assets/characters/animations/animation-pack.res" func _init() -> void: print("[build_animation_pack] Starting build...") # Step 1: Load source animations var src_doc := GLTFDocument.new() var src_state := GLTFState.new() var err := src_doc.append_from_file( ProjectSettings.globalize_path(SOURCE_GLB), src_state ) if err != OK: push_error("[build_animation_pack] Failed to load source GLB: error %d" % err) quit(1) return var src_scene: Node = src_doc.generate_scene(src_state) if not src_scene: push_error("[build_animation_pack] Failed to generate source scene") quit(1) return # Step 2: Extract animations var anim_player: AnimationPlayer = src_scene.find_child("AnimationPlayer", true, false) if not anim_player: push_error("[build_animation_pack] No AnimationPlayer in source") src_scene.queue_free() quit(1) return var out_lib := AnimationLibrary.new() var count := 0 var total_tracks := 0 for lib_name in anim_player.get_animation_library_list(): var src_lib := anim_player.get_animation_library(lib_name) for anim_name in src_lib.get_animation_list(): var src_anim: Animation = src_lib.get_animation(anim_name) if not src_anim: continue var new_anim := Animation.new() new_anim.loop_mode = src_anim.loop_mode # holding_1 must loop while player carries tekton if anim_name == "holding_1": new_anim.loop_mode = Animation.LOOP_LINEAR var max_time := 0.0 for i in src_anim.get_track_count(): var path_str: String = str(src_anim.track_get_path(i)) var track_type: int = src_anim.track_get_type(i) # Skip the GLB root "retarget" node if "retarget" in path_str and ":" not in path_str: continue # Convert track path: "retarget/Skeleton3D:bone" -> "Character/Skeleton3D:bone" if "retarget/Skeleton3D:" in path_str: path_str = path_str.replace("retarget/Skeleton3D:", "Character/Skeleton3D:") var track_last := _copy_track(src_anim, i, new_anim, path_str, track_type) if track_last > max_time: max_time = track_last total_tracks += 1 new_anim.length = max_time if out_lib.has_animation(anim_name): push_warning("[build_animation_pack] Duplicate: %s" % anim_name) continue out_lib.add_animation(anim_name, new_anim) count += 1 print(" [%d] %s (%d tracks, %.3fs)" % [count, anim_name, new_anim.get_track_count(), max_time]) # Step 4: Build holding_walk (walk legs + holding arms) if out_lib.has_animation("walk_forward") and out_lib.has_animation("holding_1"): var holding_walk := _build_holding_walk( out_lib.get_animation("walk_forward"), out_lib.get_animation("holding_1") ) out_lib.add_animation("holding_walk", holding_walk) var hw_path := "res://assets/characters/animations/holding_walk.res" var err_hw := ResourceSaver.save(holding_walk, ProjectSettings.globalize_path(hw_path)) if err_hw != OK: push_warning("[build_animation_pack] Failed to save holding_walk: %d" % err_hw) else: print(" [+] holding_walk (%d tracks, %.3fs, looped)" % [holding_walk.get_track_count(), holding_walk.length]) # Step 5: Save animation-pack.res var err_save := ResourceSaver.save(out_lib, ProjectSettings.globalize_path(OUTPUT_PATH)) if err_save != OK: push_error("[build_animation_pack] Failed to save: error %d" % err_save) src_scene.queue_free() quit(1) return # Step 4: Save individual clip .res files alongside animation-pack.res for anim_name in out_lib.get_animation_list(): var clip_path := "res://assets/characters/animations/%s.res" % anim_name var clip_res: Animation = out_lib.get_animation(anim_name) var err_clip := ResourceSaver.save(clip_res, ProjectSettings.globalize_path(clip_path)) if err_clip != OK: push_warning("[build_animation_pack] Failed to save clip %s: %d" % [anim_name, err_clip]) # Step 5: Build holding_walk (walk legs + holding arms) if out_lib.has_animation("walk_forward") and out_lib.has_animation("holding_1"): var holding_walk := _build_holding_walk( out_lib.get_animation("walk_forward"), out_lib.get_animation("holding_1") ) out_lib.add_animation("holding_walk", holding_walk) var hw_path := "res://assets/characters/animations/holding_walk.res" var err_hw := ResourceSaver.save(holding_walk, ProjectSettings.globalize_path(hw_path)) if err_hw != OK: push_warning("[build_animation_pack] Failed to save holding_walk: %d" % err_hw) else: print(" [+] holding_walk (%d tracks, %.3fs, looped)" % [holding_walk.get_track_count(), holding_walk.length]) src_scene.queue_free() print("[build_animation_pack] Extracted %d animations, %d tracks" % [count, total_tracks]) print("[build_animation_pack] Saved to %s + %d clips" % [OUTPUT_PATH, count]) quit(0) func _copy_track(src: Animation, src_idx: int, dst: Animation, new_path: String, track_type: int) -> float: var track_idx := dst.add_track(track_type) dst.track_set_path(track_idx, new_path) var key_count := src.track_get_key_count(src_idx) var last_time := 0.0 for k in key_count: var t := src.track_get_key_time(src_idx, k) dst.track_insert_key(track_idx, t, src.track_get_key_value(src_idx, k)) if t > last_time: last_time = t return last_time func _build_holding_walk(walk: Animation, hold: Animation) -> Animation: """Merge walk_forward (full body) with holding_1 (upper body override). Lower body bones (spine, thighs, legs) come from walk_forward. Upper body bones (spine.001, head, shoulders, arms) come from holding_1. """ # Upper body bone prefixes — holding_1 overrides these var upper_bones := [ "spine.001", "head", "shoulder.L", "upper_arm.L", "forearm.L", "hand.L", "shoulder.R", "upper_arm.R", "forearm.R", "hand.R", ] var result := Animation.new() result.loop_mode = Animation.LOOP_LINEAR result.length = walk.length # Collect all track bone names from both animations var walk_tracks: Dictionary = {} # bone_name -> [track_idx, ...] for i in walk.get_track_count(): var path := str(walk.track_get_path(i)) if "Skeleton3D:" in path: var bone := path.split(":")[1] if not walk_tracks.has(bone): walk_tracks[bone] = [] walk_tracks[bone].append(i) var hold_tracks: Dictionary = {} for i in hold.get_track_count(): var path := str(hold.track_get_path(i)) if "Skeleton3D:" in path: var bone := path.split(":")[1] if not hold_tracks.has(bone): hold_tracks[bone] = [] hold_tracks[bone].append(i) # Add all walk_forward tracks as base for bone in walk_tracks: for idx in walk_tracks[bone]: var path := str(walk.track_get_path(idx)) var track_type := walk.track_get_type(idx) _copy_track(walk, idx, result, path, track_type) # Override upper body tracks with holding_1 values for bone in upper_bones: if hold_tracks.has(bone) and walk_tracks.has(bone): # Replace matching walk tracks with hold values var h_indices: Array = hold_tracks[bone] var w_indices: Array = walk_tracks[bone] for hi in h_indices: var hold_path := str(hold.track_get_path(hi)) var hold_type := hold.track_get_type(hi) # Find matching walk track (same type) for wi in w_indices: if walk.track_get_type(wi) == hold_type: # Remove the walk track from result, add hold track var result_path := str(result.track_get_path(wi)) # Find this track in result and remove it for ri in result.get_track_count(): if str(result.track_get_path(ri)) == result_path and result.track_get_type(ri) == hold_type: result.remove_track(ri) break # Copy hold track _copy_track(hold, hi, result, hold_path, hold_type) w_indices.erase(wi) break return result