OverviewFilesCommitsIssuesDocsReleasesPeers

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

100 lines · gd ref:
extends SceneTree
## Headless pose extraction. Run with:
## godot4 --headless --script scenes/player/procedural/extract_poses_headless.gd

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

const EXTRACTION_MAP: Dictionary = {
	"idle": { "anim": "default", "time": 0.042 },
	"walk_a": { "anim": "walk-forward", "time": 0.042 },
	"walk_b": { "anim": "walk-forward", "time": 0.458 },
	"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 _init() -> void:
	# Need to wait for tree to be ready
	call_deferred("_extract")


func _extract() -> void:
	var tumble_scene: PackedScene = load("res://assets/models/tumble/tumble.glb")
	if not tumble_scene:
		print("ERROR: Could not load tumble.glb")
		quit()
		return

	var tumble: Node3D = tumble_scene.instantiate()
	root.add_child(tumble)

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

	if not anim_player or not skeleton:
		print("ERROR: Missing AnimationPlayer or Skeleton3D")
		quit()
		return

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

	# Extract
	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.queue_free()

	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)

	quit()


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