OverviewFilesCommitsIssuesDocsReleasesPeers

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

112 lines · gd ref:
@tool
class_name PoseLibrary extends Resource
## Stores named poses as bone transform dictionaries.
## Each pose is a Dictionary of { bone_name: String -> Transform3D }.
## Designed for Overgrowth-style procedural animation with ~13 key poses.

@export var poses: Dictionary = {}


func get_pose(pose_name: String) -> Dictionary:
	return poses.get(pose_name, {})


func get_bone_transform(pose_name: String, bone_name: String) -> Transform3D:
	var pose: Dictionary = poses.get(pose_name, {})
	return pose.get(bone_name, Transform3D.IDENTITY)


func has_pose(pose_name: String) -> bool:
	return poses.has(pose_name)


func get_pose_names() -> Array:
	return poses.keys()


## Mirror a pose on X axis and swap L/R bone names.
## Overgrowth uses: Quaternion(-q.x, q.y, q.z, -q.w) + name swap.
func mirror_pose(pose_name: String) -> Dictionary:
	var source: Dictionary = poses.get(pose_name, {})
	var mirrored: Dictionary = {}
	for bone_name: String in source:
		var xform: Transform3D = source[bone_name]
		var mirrored_name: String = _swap_lr(bone_name)
		# Mirror position on X
		var pos: Vector3 = xform.origin
		pos.x = -pos.x
		# Mirror quaternion on X: negate x and w
		var basis: Basis = xform.basis
		var q: Quaternion = basis.get_rotation_quaternion()
		q = Quaternion(-q.x, q.y, q.z, -q.w)
		var scale: Vector3 = basis.get_scale()
		mirrored[mirrored_name] = Transform3D(Basis(q).scaled(scale), pos)
	return mirrored


func _swap_lr(bone_name: String) -> String:
	if bone_name.ends_with(".L"):
		return bone_name.substr(0, bone_name.length() - 2) + ".R"
	elif bone_name.ends_with(".R"):
		return bone_name.substr(0, bone_name.length() - 2) + ".L"
	return bone_name


## Linearly interpolate between two pose dictionaries.
## Returns a new Dictionary { bone_name -> Transform3D }.
static func lerp_pose(a: Dictionary, b: Dictionary, t: float) -> Dictionary:
	var result: Dictionary = {}
	# Union of all bone names
	for bone_name: String in a:
		if b.has(bone_name):
			result[bone_name] = a[bone_name].interpolate_with(b[bone_name], t)
		else:
			result[bone_name] = a[bone_name]
	for bone_name: String in b:
		if not a.has(bone_name):
			result[bone_name] = b[bone_name]
	return result


## Catmull-Rom interpolation between 4 poses.
## p0, p1, p2, p3 are pose dictionaries. t is 0-1 between p1 and p2.
## This matches Overgrowth's GetCubicSplineWeights() approach.
static func catmull_rom_pose(p0: Dictionary, p1: Dictionary, p2: Dictionary,
		p3: Dictionary, t: float) -> Dictionary:
	var t2: float = t * t
	var t3: float = t2 * t
	# Catmull-Rom weights
	var w0: float = -0.5 * t3 + t2 - 0.5 * t
	var w1: float = 1.5 * t3 - 2.5 * t2 + 1.0
	var w2: float = -1.5 * t3 + 2.0 * t2 + 0.5 * t
	var w3: float = 0.5 * t3 - 0.5 * t2
	var result: Dictionary = {}
	# Collect all bone names
	var all_bones: Dictionary = {}
	for b: String in p0:
		all_bones[b] = true
	for b: String in p1:
		all_bones[b] = true
	for b: String in p2:
		all_bones[b] = true
	for b: String in p3:
		all_bones[b] = true
	for bone_name: String in all_bones:
		var x0: Transform3D = p0.get(bone_name, Transform3D.IDENTITY)
		var x1: Transform3D = p1.get(bone_name, Transform3D.IDENTITY)
		var x2: Transform3D = p2.get(bone_name, Transform3D.IDENTITY)
		var x3: Transform3D = p3.get(bone_name, Transform3D.IDENTITY)
		# Position: direct Catmull-Rom
		var pos: Vector3 = x0.origin * w0 + x1.origin * w1 + x2.origin * w2 + x3.origin * w3
		# Rotation: use slerp chain (cubic slerp approximation)
		var q1: Quaternion = x1.basis.get_rotation_quaternion()
		var q2: Quaternion = x2.basis.get_rotation_quaternion()
		# For quaternion, use basic slerp as first pass (upgrade to full cubic later)
		var rot: Quaternion = q1.slerp(q2, t)
		# Scale: linear blend of p1/p2
		var s1: Vector3 = x1.basis.get_scale()
		var s2: Vector3 = x2.basis.get_scale()
		var scl: Vector3 = s1.lerp(s2, t)
		result[bone_name] = Transform3D(Basis(rot).scaled(scl), pos)
	return result