Spaces:
Sleeping
Sleeping
| """ | |
| retarget.py | |
| Pure-Python port of KeeMapBoneOperators.py core math. | |
| Replaces bpy / mathutils with numpy. No Blender dependency. | |
| Public API mirrors the Blender operator flow: | |
| get_bone_position_ws(bone, arm) → np.ndarray(3) | |
| get_bone_ws_quat(bone, arm) → np.ndarray(4) w,x,y,z | |
| set_bone_position_ws(bone, arm, pos) | |
| set_bone_rotation(...) | |
| set_bone_position(...) | |
| set_bone_position_pole(...) | |
| set_bone_scale(...) | |
| calc_rotation_offset(bone_item, src_arm, dst_arm, settings) | |
| calc_location_offset(bone_item, src_arm, dst_arm) | |
| transfer_frame(src_arm, dst_arm, bone_items, settings, do_keyframe) | |
| → Dict[bone_name → (pose_loc, pose_rot, pose_scale)] | |
| transfer_animation(src_anim, dst_arm, bone_items, settings) | |
| → List[Dict[bone_name → (pose_loc, pose_rot, pose_scale)]] | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import sys | |
| from typing import Dict, List, Optional, Tuple | |
| import numpy as np | |
| from .skeleton import Armature, PoseBone | |
| from .math3d import ( | |
| quat_identity, quat_normalize, quat_mul, quat_conjugate, | |
| quat_rotation_difference, quat_dot, | |
| quat_to_matrix4, matrix4_to_quat, | |
| euler_to_quat, quat_to_euler, | |
| translation_matrix, vec3, get_point_on_vector, | |
| ) | |
| from .io.mapping import BoneMappingItem, KeeMapSettings | |
| # --------------------------------------------------------------------------- | |
| # Progress bar (console) | |
| # --------------------------------------------------------------------------- | |
| def _update_progress(job: str, progress: float) -> None: | |
| length = 40 | |
| block = int(round(length * progress)) | |
| msg = f"\r{job}: [{'#'*block}{'-'*(length-block)}] {round(progress*100, 1)}%" | |
| if progress >= 1: | |
| msg += " DONE\r\n" | |
| sys.stdout.write(msg) | |
| sys.stdout.flush() | |
| # --------------------------------------------------------------------------- | |
| # World-space position / quaternion getters | |
| # --------------------------------------------------------------------------- | |
| def get_bone_position_ws(bone: PoseBone, arm: Armature) -> np.ndarray: | |
| """ | |
| Return world-space position of bone head. | |
| Equivalent to Blender's GetBonePositionWS(). | |
| """ | |
| ws_matrix = arm.world_matrix @ bone.matrix_armature | |
| return ws_matrix[:3, 3].copy() | |
| def get_bone_ws_quat(bone: PoseBone, arm: Armature) -> np.ndarray: | |
| """ | |
| Return world-space rotation as quaternion (w,x,y,z). | |
| Equivalent to Blender's GetBoneWSQuat(). | |
| """ | |
| ws_matrix = arm.world_matrix @ bone.matrix_armature | |
| return matrix4_to_quat(ws_matrix) | |
| # --------------------------------------------------------------------------- | |
| # World-space position setter | |
| # --------------------------------------------------------------------------- | |
| def set_bone_position_ws(bone: PoseBone, arm: Armature, position: np.ndarray) -> None: | |
| """ | |
| Move bone so its world-space head = position. | |
| Equivalent to Blender's SetBonePositionWS(). | |
| Strategy: | |
| 1. Build new armature-space matrix = old rotation + new translation | |
| 2. Strip parent transform to get new local translation | |
| 3. Update pose_location so FK matches | |
| """ | |
| # Current armature-space matrix (rotation/scale part preserved) | |
| arm_mat = bone.matrix_armature.copy() | |
| # Target armature-space position | |
| arm_world_inv = np.linalg.inv(arm.world_matrix) | |
| target_arm_pos = (arm_world_inv @ np.append(position, 1.0))[:3] | |
| # New armature-space matrix with replaced translation | |
| new_arm_mat = arm_mat.copy() | |
| new_arm_mat[:3, 3] = target_arm_pos | |
| # Convert to local (parent-relative) space | |
| if bone.parent is not None: | |
| parent_arm_mat = bone.parent.matrix_armature | |
| new_local = np.linalg.inv(parent_arm_mat) @ new_arm_mat | |
| else: | |
| new_local = new_arm_mat | |
| # Extract translation from new_local = rest_local @ T(pose_loc) @ ... | |
| # Approximate: strip rest_local rotation contribution to isolate pose_location | |
| rest_inv = np.linalg.inv(bone.rest_matrix_local) | |
| pose_delta = rest_inv @ new_local | |
| bone.pose_location = pose_delta[:3, 3].copy() | |
| # Recompute FK for this bone and its subtree | |
| if bone.parent is not None: | |
| bone._fk(bone.parent.matrix_armature) | |
| else: | |
| bone._fk(np.eye(4)) | |
| # --------------------------------------------------------------------------- | |
| # Rotation setter (core retargeting math) | |
| # --------------------------------------------------------------------------- | |
| def set_bone_rotation( | |
| src_arm: Armature, src_name: str, | |
| dst_arm: Armature, dst_name: str, | |
| dst_twist_name: str, | |
| correction_quat: np.ndarray, | |
| has_twist: bool, | |
| xfer_axis: str, | |
| transpose: str, | |
| mode: str, | |
| ) -> None: | |
| """ | |
| Port of Blender's SetBoneRotation(). | |
| Drives dst bone rotation to match src bone world-space rotation. | |
| mode: "EULER" | "QUATERNION" | |
| xfer_axis: "X" "Y" "Z" "XY" "XZ" "YZ" "XYZ" | |
| transpose: "NONE" "ZYX" "ZXY" "XZY" "YZX" "YXZ" | |
| """ | |
| src_bone = src_arm.get_bone(src_name) | |
| dst_bone = dst_arm.get_bone(dst_name) | |
| # ------------------------------------------------------------------ | |
| # Get source and destination world-space quaternions (current pose) | |
| # ------------------------------------------------------------------ | |
| src_ws_quat = get_bone_ws_quat(src_bone, src_arm) | |
| dst_ws_quat = get_bone_ws_quat(dst_bone, dst_arm) | |
| # Rotation difference: r such that dst_ws @ r ≈ src_ws | |
| diff = quat_rotation_difference(dst_ws_quat, src_ws_quat) | |
| # FinalQuat = dst_local_pose_delta @ diff @ correction | |
| final_quat = quat_normalize( | |
| quat_mul(quat_mul(dst_bone.pose_rotation_quat, diff), correction_quat) | |
| ) | |
| # ------------------------------------------------------------------ | |
| # Apply axis masking / transpose (EULER mode) | |
| # ------------------------------------------------------------------ | |
| if mode == "EULER": | |
| euler = quat_to_euler(final_quat, order="XYZ") | |
| # Transpose axes | |
| if transpose == "ZYX": | |
| euler = np.array([euler[2], euler[1], euler[0]]) | |
| elif transpose == "ZXY": | |
| euler = np.array([euler[2], euler[0], euler[1]]) | |
| elif transpose == "XZY": | |
| euler = np.array([euler[0], euler[2], euler[1]]) | |
| elif transpose == "YZX": | |
| euler = np.array([euler[1], euler[2], euler[0]]) | |
| elif transpose == "YXZ": | |
| euler = np.array([euler[1], euler[0], euler[2]]) | |
| # else NONE — no change | |
| # Mask axes | |
| if xfer_axis == "X": | |
| euler[1] = 0.0; euler[2] = 0.0 | |
| elif xfer_axis == "Y": | |
| euler[0] = 0.0; euler[2] = 0.0 | |
| elif xfer_axis == "Z": | |
| euler[0] = 0.0; euler[1] = 0.0 | |
| elif xfer_axis == "XY": | |
| euler[2] = 0.0 | |
| elif xfer_axis == "XZ": | |
| euler[1] = 0.0 | |
| elif xfer_axis == "YZ": | |
| euler[0] = 0.0 | |
| # XYZ → no masking | |
| final_quat = euler_to_quat(euler[0], euler[1], euler[2], order="XYZ") | |
| # Twist bone: peel Y rotation off to twist bone | |
| if has_twist and dst_twist_name: | |
| twist_bone = dst_arm.get_bone(dst_twist_name) | |
| y_euler = quat_to_euler(final_quat, order="XYZ")[1] | |
| # Remove Y from main bone | |
| euler_no_y = quat_to_euler(final_quat, order="XYZ") | |
| euler_no_y[1] = 0.0 | |
| final_quat = euler_to_quat(*euler_no_y, order="XYZ") | |
| # Apply Y to twist bone | |
| twist_euler = quat_to_euler(twist_bone.pose_rotation_quat, order="XYZ") | |
| twist_euler[1] = math.degrees(y_euler) | |
| twist_bone.pose_rotation_quat = euler_to_quat(*twist_euler, order="XYZ") | |
| else: # QUATERNION | |
| if final_quat[0] < 0: | |
| final_quat = -final_quat | |
| final_quat = quat_normalize(final_quat) | |
| dst_bone.pose_rotation_quat = final_quat | |
| # Recompute FK | |
| parent = dst_bone.parent | |
| dst_bone._fk(parent.matrix_armature if parent else np.eye(4)) | |
| # --------------------------------------------------------------------------- | |
| # Position setter | |
| # --------------------------------------------------------------------------- | |
| def set_bone_position( | |
| src_arm: Armature, src_name: str, | |
| dst_arm: Armature, dst_name: str, | |
| dst_twist_name: str, | |
| correction: np.ndarray, | |
| gain: float, | |
| ) -> None: | |
| """ | |
| Port of Blender's SetBonePosition(). | |
| Moves dst bone to match src bone world-space position, with offset/gain. | |
| """ | |
| src_bone = src_arm.get_bone(src_name) | |
| dst_bone = dst_arm.get_bone(dst_name) | |
| target_ws = get_bone_position_ws(src_bone, src_arm) | |
| set_bone_position_ws(dst_bone, dst_arm, target_ws) | |
| # Apply correction and gain to pose_location | |
| dst_bone.pose_location[0] = (dst_bone.pose_location[0] + correction[0]) * gain | |
| dst_bone.pose_location[1] = (dst_bone.pose_location[1] + correction[1]) * gain | |
| dst_bone.pose_location[2] = (dst_bone.pose_location[2] + correction[2]) * gain | |
| parent = dst_bone.parent | |
| dst_bone._fk(parent.matrix_armature if parent else np.eye(4)) | |
| # --------------------------------------------------------------------------- | |
| # Pole bone position setter | |
| # --------------------------------------------------------------------------- | |
| def set_bone_position_pole( | |
| src_arm: Armature, src_name: str, | |
| dst_arm: Armature, dst_name: str, | |
| dst_twist_name: str, | |
| pole_distance: float, | |
| ) -> None: | |
| """ | |
| Port of Blender's SetBonePositionPole(). | |
| Positions an IK pole target relative to source limb geometry. | |
| """ | |
| src_bone = src_arm.get_bone(src_name) | |
| dst_bone = dst_arm.get_bone(dst_name) | |
| parent_src = src_bone.parent_recursive[0] if src_bone.parent_recursive else src_bone | |
| base_parent_ws = get_bone_position_ws(parent_src, src_arm) | |
| base_child_ws = get_bone_position_ws(src_bone, src_arm) | |
| # Tail = head + Y-axis direction of bone in world space | |
| src_ws_mat = src_arm.world_matrix @ src_bone.matrix_armature | |
| tail_ws = src_ws_mat[:3, 3] + src_ws_mat[:3, :3] @ np.array([0.0, 1.0, 0.0]) | |
| length_parent = np.linalg.norm(base_child_ws - base_parent_ws) | |
| length_child = np.linalg.norm(tail_ws - base_child_ws) | |
| total = length_parent + length_child | |
| c_p_ratio = length_parent / total if total > 1e-12 else 0.5 | |
| length_pp_to_tail = np.linalg.norm(base_parent_ws - tail_ws) | |
| average_location = get_point_on_vector(base_parent_ws, tail_ws, length_pp_to_tail * c_p_ratio) | |
| distance = np.linalg.norm(base_child_ws - average_location) | |
| if distance > 0.001: | |
| pole_pos = get_point_on_vector(base_child_ws, average_location, pole_distance) | |
| set_bone_position_ws(dst_bone, dst_arm, pole_pos) | |
| parent = dst_bone.parent | |
| dst_bone._fk(parent.matrix_armature if parent else np.eye(4)) | |
| # --------------------------------------------------------------------------- | |
| # Scale setter | |
| # --------------------------------------------------------------------------- | |
| def set_bone_scale( | |
| src_arm: Armature, src_name: str, | |
| dst_arm: Armature, dst_name: str, | |
| src_scale_bone_name: str, | |
| gain: float, | |
| axis: str, | |
| max_scale: float, | |
| min_scale: float, | |
| ) -> None: | |
| """ | |
| Port of Blender's SetBoneScale(). | |
| Scales dst bone based on dot product between two source bone quaternions. | |
| """ | |
| src_bone = src_arm.get_bone(src_name) | |
| dst_bone = dst_arm.get_bone(dst_name) | |
| secondary = src_arm.get_bone(src_scale_bone_name) | |
| q1 = get_bone_ws_quat(src_bone, src_arm) | |
| q2 = get_bone_ws_quat(secondary, src_arm) | |
| amount = quat_dot(q1, q2) * gain | |
| if amount < 0: | |
| amount = -amount | |
| amount = max(min_scale, min(max_scale, amount)) | |
| s = dst_bone.pose_scale | |
| if axis == "X": | |
| s[0] = amount | |
| elif axis == "Y": | |
| s[1] = amount | |
| elif axis == "Z": | |
| s[2] = amount | |
| elif axis == "XY": | |
| s[0] = s[1] = amount | |
| elif axis == "XZ": | |
| s[0] = s[2] = amount | |
| elif axis == "YZ": | |
| s[1] = s[2] = amount | |
| else: # XYZ | |
| s[:] = amount | |
| parent = dst_bone.parent | |
| dst_bone._fk(parent.matrix_armature if parent else np.eye(4)) | |
| # --------------------------------------------------------------------------- | |
| # Correction calculators | |
| # --------------------------------------------------------------------------- | |
| def calc_rotation_offset( | |
| item: BoneMappingItem, | |
| src_arm: Armature, | |
| dst_arm: Armature, | |
| settings: KeeMapSettings, | |
| ) -> None: | |
| """ | |
| Auto-compute the rotation correction factor for one bone mapping. | |
| Port of Blender's CalcRotationOffset(). | |
| Modifies item.correction_factor and item.quat_correction_factor in-place. | |
| """ | |
| if not item.source_bone_name or not item.destination_bone_name: | |
| return | |
| if not src_arm.has_bone(item.source_bone_name): | |
| return | |
| if not dst_arm.has_bone(item.destination_bone_name): | |
| return | |
| dst_bone = dst_arm.get_bone(item.destination_bone_name) | |
| # Snapshot destination bone state | |
| snap_r = dst_bone.pose_rotation_quat.copy() | |
| snap_t = dst_bone.pose_location.copy() | |
| starting_ws_quat = get_bone_ws_quat(dst_bone, dst_arm) | |
| # Apply with identity correction | |
| set_bone_rotation( | |
| src_arm, item.source_bone_name, | |
| dst_arm, item.destination_bone_name, | |
| item.twist_bone_name, | |
| quat_identity(), | |
| False, | |
| item.bone_rotation_application_axis, | |
| item.bone_transpose_axis, | |
| settings.bone_rotation_mode, | |
| ) | |
| dst_arm.update_fk() | |
| modified_ws_quat = get_bone_ws_quat(dst_bone, dst_arm) | |
| # Correction = rotation that takes modified_ws back to starting_ws | |
| q_diff = quat_rotation_difference(modified_ws_quat, starting_ws_quat) | |
| euler = quat_to_euler(q_diff, order="XYZ") | |
| item.correction_factor = euler.copy() | |
| item.quat_correction_factor = q_diff.copy() | |
| # Restore | |
| dst_bone.pose_rotation_quat = snap_r | |
| dst_bone.pose_location = snap_t | |
| parent = dst_bone.parent | |
| dst_bone._fk(parent.matrix_armature if parent else np.eye(4)) | |
| def calc_location_offset( | |
| item: BoneMappingItem, | |
| src_arm: Armature, | |
| dst_arm: Armature, | |
| ) -> None: | |
| """ | |
| Auto-compute position correction for one bone mapping. | |
| Port of Blender's CalcLocationOffset(). | |
| """ | |
| if not item.source_bone_name or not item.destination_bone_name: | |
| return | |
| if not src_arm.has_bone(item.source_bone_name): | |
| return | |
| if not dst_arm.has_bone(item.destination_bone_name): | |
| return | |
| src_bone = src_arm.get_bone(item.source_bone_name) | |
| dst_bone = dst_arm.get_bone(item.destination_bone_name) | |
| source_ws_pos = get_bone_position_ws(src_bone, src_arm) | |
| dest_ws_pos = get_bone_position_ws(dst_bone, dst_arm) | |
| # Snapshot | |
| snap_loc = dst_bone.pose_location.copy() | |
| # Move dest to source position | |
| set_bone_position_ws(dst_bone, dst_arm, source_ws_pos) | |
| dst_arm.update_fk() | |
| moved_pose_loc = dst_bone.pose_location.copy() | |
| # Restore | |
| set_bone_position_ws(dst_bone, dst_arm, dest_ws_pos) | |
| dst_arm.update_fk() | |
| delta = snap_loc - moved_pose_loc | |
| item.position_correction_factor = delta.copy() | |
| def calc_all_corrections( | |
| bone_items: List[BoneMappingItem], | |
| src_arm: Armature, | |
| dst_arm: Armature, | |
| settings: KeeMapSettings, | |
| ) -> None: | |
| """Auto-calculate rotation and position corrections for all mapped bones.""" | |
| for item in bone_items: | |
| calc_rotation_offset(item, src_arm, dst_arm, settings) | |
| if "pole" not in item.name.lower(): | |
| calc_location_offset(item, src_arm, dst_arm) | |
| # --------------------------------------------------------------------------- | |
| # Single-frame transfer | |
| # --------------------------------------------------------------------------- | |
| def transfer_frame( | |
| src_arm: Armature, | |
| dst_arm: Armature, | |
| bone_items: List[BoneMappingItem], | |
| settings: KeeMapSettings, | |
| ) -> Dict[str, Tuple[np.ndarray, np.ndarray, np.ndarray]]: | |
| """ | |
| Apply retargeting for all bone mappings at the current source frame. | |
| src_arm must already have FK updated for the current frame. | |
| Returns a dict of bone_name → (pose_location, pose_rotation_quat, pose_scale) | |
| suitable for writing into a keyframe list. | |
| """ | |
| for item in bone_items: | |
| if not item.source_bone_name or not item.destination_bone_name: | |
| continue | |
| if not src_arm.has_bone(item.source_bone_name): | |
| continue | |
| if not dst_arm.has_bone(item.destination_bone_name): | |
| continue | |
| # Build correction quaternion | |
| if settings.bone_rotation_mode == "EULER": | |
| cf = item.correction_factor | |
| correction_quat = euler_to_quat(cf[0], cf[1], cf[2], order="XYZ") | |
| else: | |
| correction_quat = quat_normalize(item.quat_correction_factor) | |
| # Rotation | |
| if item.set_bone_rotation: | |
| set_bone_rotation( | |
| src_arm, item.source_bone_name, | |
| dst_arm, item.destination_bone_name, | |
| item.twist_bone_name, | |
| correction_quat, | |
| item.has_twist_bone, | |
| item.bone_rotation_application_axis, | |
| item.bone_transpose_axis, | |
| settings.bone_rotation_mode, | |
| ) | |
| dst_arm.update_fk() | |
| # Position | |
| if item.set_bone_position: | |
| if item.postion_type == "SINGLE_BONE_OFFSET": | |
| set_bone_position( | |
| src_arm, item.source_bone_name, | |
| dst_arm, item.destination_bone_name, | |
| item.twist_bone_name, | |
| item.position_correction_factor, | |
| item.position_gain, | |
| ) | |
| else: | |
| set_bone_position_pole( | |
| src_arm, item.source_bone_name, | |
| dst_arm, item.destination_bone_name, | |
| item.twist_bone_name, | |
| -item.position_pole_distance, | |
| ) | |
| dst_arm.update_fk() | |
| # Scale | |
| if item.set_bone_scale and item.scale_secondary_bone_name: | |
| if src_arm.has_bone(item.scale_secondary_bone_name): | |
| set_bone_scale( | |
| src_arm, item.source_bone_name, | |
| dst_arm, item.destination_bone_name, | |
| item.scale_secondary_bone_name, | |
| item.scale_gain, | |
| item.bone_scale_application_axis, | |
| item.scale_max, | |
| item.scale_min, | |
| ) | |
| dst_arm.update_fk() | |
| # Snapshot destination bone state for this frame | |
| result: Dict[str, Tuple[np.ndarray, np.ndarray, np.ndarray]] = {} | |
| for item in bone_items: | |
| if not item.destination_bone_name: | |
| continue | |
| if not dst_arm.has_bone(item.destination_bone_name): | |
| continue | |
| dst_bone = dst_arm.get_bone(item.destination_bone_name) | |
| result[item.destination_bone_name] = ( | |
| dst_bone.pose_location.copy(), | |
| dst_bone.pose_rotation_quat.copy(), | |
| dst_bone.pose_scale.copy(), | |
| ) | |
| return result | |
| # --------------------------------------------------------------------------- | |
| # Full animation transfer | |
| # --------------------------------------------------------------------------- | |
| def transfer_animation( | |
| src_anim, # BVHAnimation or any object with .armature + .apply_frame(i) + .num_frames | |
| dst_arm: Armature, | |
| bone_items: List[BoneMappingItem], | |
| settings: KeeMapSettings, | |
| ) -> List[Dict[str, Tuple[np.ndarray, np.ndarray, np.ndarray]]]: | |
| """ | |
| Transfer all frames from src_anim to dst_arm. | |
| Returns list of keyframe dicts (one per frame sampled). | |
| Equivalent to Blender's PerformAnimationTransfer operator. | |
| """ | |
| keyframes: List[Dict] = [] | |
| step = max(1, settings.keyframe_every_n_frames) | |
| start = settings.start_frame_to_apply | |
| total = settings.number_of_frames_to_apply | |
| end = start + total | |
| src_arm = src_anim.armature | |
| i = start | |
| n_steps = len(range(start, end, step)) | |
| step_i = 0 | |
| while i < end and i < src_anim.num_frames: | |
| src_anim.apply_frame(i) # updates src_arm FK | |
| dst_arm.update_fk() | |
| frame_data = transfer_frame(src_arm, dst_arm, bone_items, settings) | |
| keyframes.append(frame_data) | |
| step_i += 1 | |
| _update_progress("Retargeting", step_i / n_steps) | |
| i += step | |
| _update_progress("Retargeting", 1.0) | |
| return keyframes | |