OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / player / procedural / pose_extraction_tool.gd

123 lines · gd ref:
@tool
extends EditorScript
## Run this from the Godot editor: Script Editor -> File -> Run (Ctrl+Shift+X)
## It opens the tumble.glb, lists all animations, and extracts poses.
##
## BEFORE RUNNING: Edit the EXTRACTION_MAP below to map pose names to
## animation names + seek times. Run once with empty map to see available
## animations and their durations.

const PoseLibraryClass := preload("res://scenes/player/procedural/pose_library.gd")

## Edit this map! Format: "pose_name": { "anim": "animation_name", "time": seconds }
## Run with empty map first to see available animations.
const EXTRACTION_MAP: Dictionary = {
	"idle": { "anim": "default", "time": 0.042 },
	"walk_a": { "anim": "walk-forward", "time": 0.042 },   # contact: left foot forward
	"walk_b": { "anim": "walk-forward", "time": 0.458 },   # contact: right foot forward
	"run_a": { "anim": "run", "time": 0.042 },
	"run_b": { "anim": "run", "time": 0.458 },
	"jump_launch": { "anim": "jump", "time": 0.042 },
	"jump_air": { "anim": "jump", "time": 0.5 },
	"fall": { "anim": "falling", "time": 0.042 },
	"land": { "anim": "landing", "time": 0.042 },
}

const OUTPUT_PATH: String = "res://scenes/player/procedural/tumble_poses.tres"


func _run() -> void:
	# Load the tumble model
	var tumble_scene: PackedScene = load("res://assets/models/tumble/tumble.glb")
	if not tumble_scene:
		print("ERROR: Could not load tumble.glb")
		return

	var tumble: Node3D = tumble_scene.instantiate()

	# Find AnimationPlayer and Skeleton3D
	var anim_player: AnimationPlayer = _find_child(tumble, "AnimationPlayer") as AnimationPlayer
	var skeleton: Skeleton3D = _find_child(tumble, "Skeleton3D") as Skeleton3D

	if not anim_player:
		print("ERROR: No AnimationPlayer found in tumble.glb")
		tumble.queue_free()
		return
	if not skeleton:
		print("ERROR: No Skeleton3D found in tumble.glb")
		tumble.queue_free()
		return

	# List all animations
	print("=== Available Animations ===")
	var anims: PackedStringArray = anim_player.get_animation_list()
	for anim_name in anims:
		var anim: Animation = anim_player.get_animation(anim_name)
		print("  '", anim_name, "': length=", snapped(anim.length, 0.001), "s, tracks=", anim.get_track_count(), ", loop=", anim.loop_mode)
	print("")

	# List all bones
	print("=== Skeleton Bones (", skeleton.get_bone_count(), ") ===")
	for i in skeleton.get_bone_count():
		var parent_idx: int = skeleton.get_bone_parent(i)
		var parent_name: String = skeleton.get_bone_name(parent_idx) if parent_idx >= 0 else "(root)"
		print("  [", i, "] ", skeleton.get_bone_name(i), " <- ", parent_name)
	print("")

	if EXTRACTION_MAP.is_empty():
		print("EXTRACTION_MAP is empty. Edit this script, fill in the map above, and run again.")
		tumble.queue_free()
		return

	# We need to add to scene tree for AnimationPlayer to work
	get_editor_interface().get_edited_scene_root().add_child(tumble)

	# Extract poses
	var lib: Resource = PoseLibraryClass.new()

	for pose_name: String in EXTRACTION_MAP:
		var entry: Dictionary = EXTRACTION_MAP[pose_name]
		var anim_name: String = entry.get("anim", "")
		var seek_time: float = entry.get("time", 0.0)

		if not anim_player.has_animation(anim_name):
			print("WARNING: Animation '", anim_name, "' not found, skipping '", pose_name, "'")
			continue

		anim_player.play(anim_name)
		anim_player.seek(seek_time, true)
		anim_player.advance(0.0)

		var pose: Dictionary = {}
		for bone_idx in skeleton.get_bone_count():
			var bone_name: String = skeleton.get_bone_name(bone_idx)
			var pos: Vector3 = skeleton.get_bone_pose_position(bone_idx)
			var rot: Quaternion = skeleton.get_bone_pose_rotation(bone_idx)
			var scl: Vector3 = skeleton.get_bone_pose_scale(bone_idx)
			pose[bone_name] = Transform3D(Basis(rot).scaled(scl), pos)

		lib.poses[pose_name] = pose
		print("Captured '", pose_name, "' from '", anim_name, "' @ t=", seek_time, " (", pose.size(), " bones)")

	anim_player.stop()
	tumble.get_parent().remove_child(tumble)
	tumble.queue_free()

	# Save
	var err: int = ResourceSaver.save(lib, OUTPUT_PATH)
	if err == OK:
		print("\nSaved PoseLibrary to ", OUTPUT_PATH, " with ", lib.poses.size(), " poses")
		print("Poses: ", lib.poses.keys())
	else:
		print("ERROR: Failed to save, error code ", err)


func _find_child(node: Node, type_name: String) -> Node:
	for child in node.get_children():
		if child.get_class() == type_name:
			return child
		var found: Node = _find_child(child, type_name)
		if found:
			return found
	return null