Add tools for mirroring animations

This commit is contained in:
Salad Dais
2025-06-28 04:23:18 +00:00
parent a2b49fdc44
commit e20a4a01ad
6 changed files with 91 additions and 22 deletions

View File

@@ -3,10 +3,12 @@ Assorted utilities to make creating animations from scratch easier
"""
import copy
from typing import List, Union
from typing import List, Union, Mapping
from hippolyzer.lib.base.datatypes import Vector3, Quaternion
from hippolyzer.lib.base.llanim import PosKeyframe, RotKeyframe
from hippolyzer.lib.base.llanim import PosKeyframe, RotKeyframe, JOINTS_DICT, Joint
from hippolyzer.lib.base.mesh_skeleton import AVATAR_SKELETON
from hippolyzer.lib.base.multidict import OrderedMultiDict
def smooth_step(t: float):
@@ -89,3 +91,35 @@ def smooth_rot(start: Quaternion, end: Quaternion, inter_frames: int, time: floa
smooth_t = smooth_step(t)
frames.append(RotKeyframe(time=time + (t * duration), rot=rot_interp(start, end, smooth_t)))
return frames + [RotKeyframe(time=time + duration, rot=end)]
def mirror_joints(joints_dict: Mapping[str, Joint]) -> JOINTS_DICT:
"""Mirror a joints dict so left / right are swapped, including transformations"""
new_joints: JOINTS_DICT = OrderedMultiDict()
for joint_name, joint in joints_dict.items():
inverse_joint_node = AVATAR_SKELETON[joint_name].inverse
if not inverse_joint_node:
new_joints[joint_name] = joint
continue
# Okay, this is one we have to actually mirror
new_joint = Joint(joint.priority, [], [])
for rot_keyframe in joint.rot_keyframes:
new_joint.rot_keyframes.append(RotKeyframe(
time=rot_keyframe.time,
# Just need to mirror on yaw and roll
rot=Quaternion.from_euler(*(rot_keyframe.rot.to_euler() * Vector3(-1, 1, -1)))
))
for pos_keyframe in joint.pos_keyframes:
new_joint.pos_keyframes.append(PosKeyframe(
time=pos_keyframe.time,
# Y is left / right so just negate it.
pos=pos_keyframe.pos * Vector3(1, -1, 1)
))
new_joints[inverse_joint_node.name] = new_joint
return new_joints

View File

@@ -195,3 +195,21 @@ def create_logged_task(
task = asyncio.create_task(coro, name=name)
add_future_logger(task, name, logger)
return task
def reorient_coord(coord, new_orientation, min_val: int | float = 0):
"""
Reorient a coordinate instance such that its components are negated and transposed appropriately.
For ex:
reorient_coord((1,2,3), (3,-2,-1)) == (3,-2,-1)
"""
min_val = abs(min_val)
coords = []
for axis in new_orientation:
axis_idx = abs(axis) - 1
new_coord = coord[axis_idx] if axis >= 0 else min_val - coord[axis_idx]
coords.append(new_coord)
if coord.__class__ in (list, tuple):
return coord.__class__(coords)
return coord.__class__(*coords)

View File

@@ -15,6 +15,8 @@ CONSTRAINT_DATACLASS = se.ForwardSerializable(lambda: se.Dataclass(Constraint))
POSKEYFRAME_DATACLASS = se.ForwardSerializable(lambda: se.Dataclass(PosKeyframe))
ROTKEYFRAME_DATACLASS = se.ForwardSerializable(lambda: se.Dataclass(RotKeyframe))
JOINTS_DICT = OrderedMultiDict[str, "Joint"]
@dataclasses.dataclass
class Animation:
@@ -29,7 +31,7 @@ class Animation:
ease_in_duration: float = se.dataclass_field(se.F32)
ease_out_duration: float = se.dataclass_field(se.F32)
hand_pose: HandPose = se.dataclass_field(lambda: se.IntEnum(HandPose, se.U32), default=0)
joints: OrderedMultiDict[str, Joint] = se.dataclass_field(se.MultiDictAdapter(
joints: JOINTS_DICT = se.dataclass_field(se.MultiDictAdapter(
se.Collection(se.U32, se.Tuple(se.CStr(), JOINT_DATACLASS)),
))
constraints: List[Constraint] = se.dataclass_field(

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import copy
import dataclasses
import re
import weakref
from typing import *
@@ -74,6 +75,29 @@ class JointNode:
children.append(node)
return children
@property
def inverse(self) -> Optional[JointNode]:
l_re = re.compile(r"(.*?(?:_|\b))L((?:_|\b).*)")
r_re = re.compile(r"(.*?(?:_|\b))R((?:_|\b).*)")
inverse_name = None
if "Left" in self.name:
inverse_name = self.name.replace("Left", "Right")
elif "LEFT" in self.name:
inverse_name = self.name.replace("LEFT", "RIGHT")
elif l_re.match(self.name):
inverse_name = re.sub(l_re, r"\1R\2", self.name)
elif "Right" in self.name:
inverse_name = self.name.replace("Right", "Left")
elif "RIGHT" in self.name:
inverse_name = self.name.replace("RIGHT", "LEFT")
elif r_re.match(self.name):
inverse_name = re.sub(r_re, r"\1L\2", self.name)
if inverse_name:
return self.skeleton().joint_dict.get(inverse_name)
return None
@property
def descendents(self) -> Set[JointNode]:
descendents: Set[JointNode] = set()