@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