126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
"""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()
|