Spaces:
Running on Zero
Running on Zero
| """ | |
| skeleton.py | |
| Pure-Python armature / pose-bone system. | |
| Design matches Blender's pose-mode semantics: | |
| - bone.rest_matrix_local = 4×4 rest pose in parent space (edit-mode) | |
| - bone.pose_rotation_quat = local rotation DELTA from rest (≡ bone.rotation_quaternion) | |
| - bone.pose_location = local translation DELTA from rest (≡ bone.location) | |
| - bone.pose_scale = local scale (≡ bone.scale) | |
| - bone.matrix_armature = FK-computed 4×4 in armature space (≡ bone.matrix in pose mode) | |
| Armature.world_matrix corresponds to arm.matrix_world. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from typing import Dict, List, Optional, Tuple | |
| from .math3d import ( | |
| quat_identity, quat_normalize, quat_mul, | |
| quat_to_matrix4, matrix4_to_quat, | |
| translation_matrix, scale_matrix, trs_to_matrix4, matrix4_to_trs, | |
| vec3, | |
| ) | |
| class PoseBone: | |
| def __init__( | |
| self, | |
| name: str, | |
| rest_matrix_local: np.ndarray, # 4×4, in parent local space | |
| parent: Optional["PoseBone"] = None, | |
| ): | |
| self.name = name | |
| self.parent: Optional[PoseBone] = parent | |
| self.children: List[PoseBone] = [] | |
| self.rest_matrix_local: np.ndarray = rest_matrix_local.copy() | |
| # Pose state — start at rest (delta = identity) | |
| self.pose_rotation_quat: np.ndarray = quat_identity() | |
| self.pose_location: np.ndarray = vec3() | |
| self.pose_scale: np.ndarray = np.ones(3) | |
| # Cached FK result — call armature.update_fk() to refresh | |
| self._matrix_armature: np.ndarray = np.eye(4) | |
| # ----------------------------------------------------------------------- | |
| # Properties | |
| # ----------------------------------------------------------------------- | |
| def matrix_armature(self) -> np.ndarray: | |
| """4×4 FK result in armature space. Refresh with armature.update_fk().""" | |
| return self._matrix_armature | |
| def head(self) -> np.ndarray: | |
| """Bone head position in armature space.""" | |
| return self._matrix_armature[:3, 3].copy() | |
| def tail(self) -> np.ndarray: | |
| """ | |
| Approximate tail position (Y-axis in bone space, length 1). | |
| Works for Y-along-bone convention (Blender / BVH default). | |
| """ | |
| y_axis = self._matrix_armature[:3, :3] @ np.array([0.0, 1.0, 0.0]) | |
| return self._matrix_armature[:3, 3] + y_axis | |
| # ----------------------------------------------------------------------- | |
| # FK | |
| # ----------------------------------------------------------------------- | |
| def _compute_local_matrix(self) -> np.ndarray: | |
| """rest_local @ T(pose_loc) @ R(pose_rot) @ S(pose_scale).""" | |
| T = translation_matrix(self.pose_location) | |
| R = quat_to_matrix4(self.pose_rotation_quat) | |
| S = scale_matrix(self.pose_scale) | |
| return self.rest_matrix_local @ T @ R @ S | |
| def _fk(self, parent_matrix: np.ndarray) -> None: | |
| self._matrix_armature = parent_matrix @ self._compute_local_matrix() | |
| for child in self.children: | |
| child._fk(self._matrix_armature) | |
| # ----------------------------------------------------------------------- | |
| # Parent-chain helpers (Blender: bone.parent_recursive) | |
| # ----------------------------------------------------------------------- | |
| def parent_recursive(self) -> List["PoseBone"]: | |
| chain: List[PoseBone] = [] | |
| cur = self.parent | |
| while cur is not None: | |
| chain.append(cur) | |
| cur = cur.parent | |
| return chain | |
| class Armature: | |
| """ | |
| Collection of PoseBones with a world transform. | |
| Corresponds to a Blender armature object. | |
| """ | |
| def __init__(self, name: str = "Armature"): | |
| self.name = name | |
| self.world_matrix: np.ndarray = np.eye(4) # arm.matrix_world | |
| self._bones: Dict[str, PoseBone] = {} | |
| self._roots: List[PoseBone] = [] | |
| # ----------------------------------------------------------------------- | |
| # Construction helpers | |
| # ----------------------------------------------------------------------- | |
| def add_bone(self, bone: PoseBone, parent_name: Optional[str] = None) -> PoseBone: | |
| self._bones[bone.name] = bone | |
| if parent_name and parent_name in self._bones: | |
| parent = self._bones[parent_name] | |
| bone.parent = parent | |
| parent.children.append(bone) | |
| elif bone.parent is None: | |
| self._roots.append(bone) | |
| return bone | |
| def pose_bones(self) -> Dict[str, PoseBone]: | |
| return self._bones | |
| def get_bone(self, name: str) -> PoseBone: | |
| if name not in self._bones: | |
| raise KeyError(f"Bone '{name}' not found in armature '{self.name}'") | |
| return self._bones[name] | |
| def has_bone(self, name: str) -> bool: | |
| return name in self._bones | |
| # ----------------------------------------------------------------------- | |
| # FK update | |
| # ----------------------------------------------------------------------- | |
| def update_fk(self) -> None: | |
| """Recompute all bone armature-space matrices via FK.""" | |
| for root in self._roots: | |
| root._fk(np.eye(4)) | |
| # ----------------------------------------------------------------------- | |
| # Snapshot / restore (for calc-correction passes) | |
| # ----------------------------------------------------------------------- | |
| def snapshot(self) -> Dict[str, Tuple[np.ndarray, np.ndarray, np.ndarray]]: | |
| return { | |
| name: ( | |
| bone.pose_rotation_quat.copy(), | |
| bone.pose_location.copy(), | |
| bone.pose_scale.copy(), | |
| ) | |
| for name, bone in self._bones.items() | |
| } | |
| def restore(self, snap: Dict[str, Tuple[np.ndarray, np.ndarray, np.ndarray]]) -> None: | |
| for name, (r, t, s) in snap.items(): | |
| if name in self._bones: | |
| self._bones[name].pose_rotation_quat = r.copy() | |
| self._bones[name].pose_location = t.copy() | |
| self._bones[name].pose_scale = s.copy() | |
| self.update_fk() | |