diff --git a/addon_examples/mesh_mangler.py b/addon_examples/mesh_mangler.py index 259b54c..46b12fc 100644 --- a/addon_examples/mesh_mangler.py +++ b/addon_examples/mesh_mangler.py @@ -8,7 +8,7 @@ applied to the mesh before upload. I personally use manglers to strip bounding box materials you need to add to give a mesh an arbitrary center of rotation / scaling. """ - +from hippolyzer.lib.base.helpers import reorient_coord from hippolyzer.lib.base.mesh import MeshAsset from hippolyzer.lib.proxy.addons import AddonManager @@ -16,23 +16,8 @@ import local_mesh AddonManager.hot_reload(local_mesh, require_addons_loaded=True) -def _reorient_coord(coord, orientation, normals=False): - coords = [] - for axis in orientation: - axis_idx = abs(axis) - 1 - if normals: - # Normals have a static domain from -1.0 to 1.0, just negate. - new_coord = coord[axis_idx] if axis >= 0 else -coord[axis_idx] - else: - new_coord = coord[axis_idx] if axis >= 0 else 1.0 - coord[axis_idx] - coords.append(new_coord) - if coord.__class__ in (list, tuple): - return coord.__class__(coords) - return coord.__class__(*coords) - - -def _reorient_coord_list(coord_list, orientation, normals=False): - return [_reorient_coord(x, orientation, normals) for x in coord_list] +def _reorient_coord_list(coord_list, orientation, min_val: int | float = 0): + return [reorient_coord(x, orientation, min_val) for x in coord_list] def reorient_mesh(orientation): @@ -47,7 +32,7 @@ def reorient_mesh(orientation): # flipping the axes around. material["Position"] = _reorient_coord_list(material["Position"], orientation) # Are you even supposed to do this to the normals? - material["Normal"] = _reorient_coord_list(material["Normal"], orientation, normals=True) + material["Normal"] = _reorient_coord_list(material["Normal"], orientation, min_val=-1) return mesh return _reorienter diff --git a/hippolyzer/lib/base/anim_utils.py b/hippolyzer/lib/base/anim_utils.py index 42855ac..174f3fe 100644 --- a/hippolyzer/lib/base/anim_utils.py +++ b/hippolyzer/lib/base/anim_utils.py @@ -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 diff --git a/hippolyzer/lib/base/helpers.py b/hippolyzer/lib/base/helpers.py index 77c8444..8c2cc85 100644 --- a/hippolyzer/lib/base/helpers.py +++ b/hippolyzer/lib/base/helpers.py @@ -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) diff --git a/hippolyzer/lib/base/llanim.py b/hippolyzer/lib/base/llanim.py index 1d4941e..55c4075 100644 --- a/hippolyzer/lib/base/llanim.py +++ b/hippolyzer/lib/base/llanim.py @@ -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( diff --git a/hippolyzer/lib/base/mesh_skeleton.py b/hippolyzer/lib/base/mesh_skeleton.py index 68bdf87..68b7ddd 100644 --- a/hippolyzer/lib/base/mesh_skeleton.py +++ b/hippolyzer/lib/base/mesh_skeleton.py @@ -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() diff --git a/tests/base/test_skeleton.py b/tests/base/test_skeleton.py index e7b8f4c..538ab48 100644 --- a/tests/base/test_skeleton.py +++ b/tests/base/test_skeleton.py @@ -30,3 +30,9 @@ class TestSkeleton(unittest.TestCase): [0., 0., 0., 1.] ]) np.testing.assert_equal(expected_mat, self.skeleton["mNeck"].matrix) + + def test_get_inverse_joint(self): + self.assertEqual("R_CLAVICLE", self.skeleton["L_CLAVICLE"].inverse.name) + self.assertEqual(None, self.skeleton["mChest"].inverse) + self.assertEqual("mHandMiddle1Right", self.skeleton["mHandMiddle1Left"].inverse.name) + self.assertEqual("RIGHT_HANDLE", self.skeleton["LEFT_HANDLE"].inverse.name)