This commit is contained in:
Nicholas Butzke
2026-08-02 02:20:02 -04:00
parent 61669627db
commit 922983429e
226 changed files with 34032 additions and 18521 deletions
+125
View File
@@ -0,0 +1,125 @@
"""Silhouette-preserving helpers shared by the animation retargeters.
The source animation owns the direction of each major body segment. Copying
only a bone's rest-relative rotation preserves a target rig's modelling pose;
characters authored with crossed or rearward arms then keep those directions
in every idle, dance, and locomotion clip.
These helpers transfer animated joint-to-joint directions for the torso and
limbs while retaining the target bone's own roll. This is an offline bake of
the source performance, not a runtime procedural animation layer.
"""
from mathutils import Quaternion, Vector
def build_segment_pairs(mapping, source_roles, target_roles):
"""Return target bone -> (target child, source bone, source child)."""
chains = [
[target_roles.hips] + list(target_roles.spine),
]
for side in ("L", "R"):
chains.append([
target_roles.limb.get((role, side))
for role in ("shoulder", "upper_arm", "forearm", "hand")
])
chains.append([
target_roles.limb.get((role, side))
for role in ("thigh", "shin", "foot", "toe")
])
pairs = {}
for chain in chains:
chain = [name for name in chain if name]
for index, target_name in enumerate(chain[:-1]):
source_name = mapping.get(target_name)
if not source_name:
continue
# Chain resampling can map neighbouring target spine bones to the
# same source joint. Look ahead to the first genuinely new joint.
for target_child in chain[index + 1:]:
source_child = mapping.get(target_child)
if source_child and source_child != source_name:
pairs[target_name] = (
target_child, source_name, source_child
)
break
return pairs
def _twist_about(rotation, axis):
"""Extract the twist component of a world-space quaternion around axis."""
axis = axis.normalized()
vector = Vector((rotation.x, rotation.y, rotation.z))
projected = axis * vector.dot(axis)
twist = Quaternion((rotation.w, projected.x, projected.y, projected.z))
if twist.magnitude < 1e-8:
return Quaternion()
twist.normalize()
return twist
def heading_inverse(pose_rotation, rest_rotation, up):
"""World-up heading that removes a capture's initial stage orientation."""
return _twist_about(
pose_rotation @ rest_rotation.inverted(), up
).inverted()
def authored_world_rotation(
target_name,
source_name,
source_eval,
source_world,
source_rest_rot,
target_rest,
target_rest_rot,
yaw,
yaw_inverse,
segment_pairs,
source_heading=Quaternion(),
):
"""Retarget one bone, using authored segment direction where available."""
pose_rotation = source_heading @ (
source_world @ source_eval.pose.bones[source_name].matrix
).to_quaternion()
delta = pose_rotation @ source_rest_rot[source_name].inverted()
full_rotation = (
yaw @ delta @ yaw_inverse
) @ target_rest_rot[target_name]
pair = segment_pairs.get(target_name)
if pair is None:
return full_rotation
target_child, pair_source, source_child = pair
if (
pair_source not in source_eval.pose.bones
or source_child not in source_eval.pose.bones
or target_child not in target_rest
):
return full_rotation
source_head = (
source_world @ source_eval.pose.bones[pair_source].matrix
).translation
source_child_head = (
source_world @ source_eval.pose.bones[source_child].matrix
).translation
desired_direction = yaw @ (
source_heading @ (source_child_head - source_head)
)
target_direction = (
target_rest[target_child].translation
- target_rest[target_name].translation
)
if desired_direction.length < 1e-7 or target_direction.length < 1e-7:
return full_rotation
desired_direction.normalize()
target_direction.normalize()
swing = target_direction.rotation_difference(desired_direction)
swung_rotation = swing @ target_rest_rot[target_name]
residual = full_rotation @ swung_rotation.inverted()
return (
_twist_about(residual, desired_direction) @ swung_rotation
).normalized()