extends SceneTree ## Which way does each character's REST pose point their limbs? ## ## The retarget bakes every clip as a rest-relative delta: it works out what the ## clip does to the LIBRARY's rest pose and applies that rotation to THIS rig's ## rest pose. That is what stops a foreign bone roll twisting a limb, and it is ## right — but it carries an assumption nobody has checked, which is that the two ## rest poses are broadly alike. If a character's arms rest overhead and the ## library's rest at its sides, then "what the clip does relative to the sides" ## applied to overhead leaves the arms overhead, in every clip, forever. ## ## So: measure it. For each character, the world-space direction each limb points ## in its BIND pose, and the angle between that and the library's. ## ## godot --headless --path . -s res://debug/rest_pose_check.gd ## limb role -> the role of the bone it points AT. Not "its first child": a ## thigh's first child is as likely to be a skirt bone as a shin, and taking it ## reported kiyoko's and aria's legs as 71° off the library when their legs are ## fine — it was measuring the hang of a skirt panel. const LIMBS := { "upper_arm.L": "forearm.L", "upper_arm.R": "forearm.R", "forearm.L": "hand.L", "forearm.R": "hand.R", "thigh.L": "shin.L", "thigh.R": "shin.R", "shin.L": "foot.L", "shin.R": "foot.R", } ## The reference is the mannequin: it comes from the animation library itself, so ## its rest pose IS the pose every clip was authored against. const REFERENCE := "mannequin" func _init() -> void: await process_frame var data = JSON.parse_string(FileAccess.get_file_as_string( "res://assets/characters/skins/skins.json")) var dirs := {} for entry in data["skins"]: var d := _limb_directions(entry.get("model", "")) if not d.is_empty(): dirs[entry["id"]] = d var ref: Dictionary = dirs.get(REFERENCE, {}) print("Rest-pose limb directions, and the angle from the animation library's own rig\n") for id in dirs: var parts: PackedStringArray = [] var worst := 0.0 for limb in LIMBS: if not dirs[id].has(limb) or not ref.has(limb): continue var v: Vector3 = dirs[id][limb] var deg := rad_to_deg(v.angle_to(ref[limb])) worst = maxf(worst, deg) parts.append("%s %3.0f°" % [limb, deg]) parts.sort() print("%-10s worst %5.1f° %s" % [id, worst, " ".join(parts)]) print("\nA limb tens of degrees from the reference will play every clip with") print("that offset baked in, because the clip only ever supplied the DELTA.") quit() ## limb role -> the unit vector the bone points along, in bind pose, in the ## model's own space. ## ## Read from the sidecar's roles, never by bone name — a rig whose bones are ## called `Unused_Noname_010` is exactly the case this has to survive. func _limb_directions(path: String) -> Dictionary: if path == "" or not ResourceLoader.exists(path): return {} var rig = JSON.parse_string(FileAccess.get_file_as_string( path.get_basename() + ".rig.json")) if typeof(rig) != TYPE_DICTIONARY: return {} var scene: Node = load(path).instantiate() var skel: Skeleton3D = _first_skeleton(scene) var out := {} if skel: for limb in LIMBS: var roles: Dictionary = rig.get("roles", {}) var from_name: String = roles.get(limb, "") var to_name: String = roles.get(LIMBS[limb], "") if from_name == "" or to_name == "": continue var bi := skel.find_bone(from_name) var ti := skel.find_bone(to_name) if bi < 0 or ti < 0: continue # Where the limb points: at the NEXT limb along, by role. A bone's # own axes say nothing on their own — every rig rolls them # differently, which is the whole reason the retarget works in # deltas — so the direction has to come from the geometry. var here := skel.get_bone_global_rest(bi).origin var there := skel.get_bone_global_rest(ti).origin var v := there - here if v.length() > 1e-5: out[limb] = v.normalized() scene.free() return out func _first_skeleton(node: Node) -> Skeleton3D: if node is Skeleton3D: return node for c in node.get_children(): var f := _first_skeleton(c) if f: return f return null