diff --git a/hippolyzer/lib/base/datatypes.py b/hippolyzer/lib/base/datatypes.py index d49ed4f..54135b0 100644 --- a/hippolyzer/lib/base/datatypes.py +++ b/hippolyzer/lib/base/datatypes.py @@ -39,6 +39,9 @@ class _IterableStub: __iter__: Callable +RAD_TO_DEG = 180 / math.pi + + class TupleCoord(recordclass.datatuple, _IterableStub): # type: ignore __options__ = { "fast_new": False, @@ -372,5 +375,5 @@ class TaggedUnion(recordclass.datatuple): # type: ignore __all__ = [ "Vector3", "Vector4", "Vector2", "Quaternion", "TupleCoord", "UUID", "RawBytes", "StringEnum", "JankStringyBytes", "TaggedUnion", - "IntEnum", "IntFlag", "flags_to_pod", "Pretty" + "IntEnum", "IntFlag", "flags_to_pod", "Pretty", "RAD_TO_DEG" ] diff --git a/hippolyzer/lib/base/gltftools.py b/hippolyzer/lib/base/gltftools.py index 3ccafa1..8508efc 100644 --- a/hippolyzer/lib/base/gltftools.py +++ b/hippolyzer/lib/base/gltftools.py @@ -7,10 +7,6 @@ WIP LLMesh -> glTF converter, for testing eventual glTF -> LLMesh conversion log # * * The weird scaling on the collision volumes / fitted mesh bones will clearly be a problem. # Blender's Collada importer / export stuff appears to sidestep these problems (by only applying # the bind shape matrix to the model in the viewport?) -# * Do we actually need to convert to GLTF coordinates space, or is it enough to parent everything -# to a correctly rotated scene node? That would definitely be less nasty. I wasn't totally sure -# how that would affect coordinates on re-exports if you did export selected without selecting the -# scene root, though. This seems to be what assimp does so maybe it's fine? import math import pprint @@ -25,7 +21,7 @@ import transformations from hippolyzer.lib.base.colladatools import llsd_to_mat4 from hippolyzer.lib.base.mesh import LLMeshSerializer, MeshAsset, positions_from_domain, SkinSegmentDict, VertexWeight -from hippolyzer.lib.base.mesh_skeleton import required_joint_hierarchy, SKELETON_JOINTS +from hippolyzer.lib.base.mesh_skeleton import AVATAR_SKELETON from hippolyzer.lib.base.serialization import BufferReader @@ -70,7 +66,11 @@ def sl_mat4_to_gltf(mat: np.ndarray) -> List[float]: This should only be done immediately before storing the matrix in a glTF structure! """ + # TODO: This is probably not correct. We definitely need to flip Z but there's + # probably a better way to do it. decomp = [sl_to_gltf_coords(x) for x in transformations.decompose_matrix(mat)] + trans = decomp[3] + decomp[3] = (trans[0], trans[1], -trans[2]) return list(transformations.compose_matrix(*decomp).flatten(order='F')) @@ -91,9 +91,15 @@ def sl_weights_to_gltf(sl_weights: List[List[VertexWeight]]) -> Tuple[np.ndarray weights = np.zeros((len(sl_weights), 4), dtype=np.float32) for i, vert_weights in enumerate(sl_weights): + # We need to re-normalize these since the quantization can mess them up + collected_weights = [] for j, vert_weight in enumerate(vert_weights): joints[i, j] = vert_weight.joint_idx - weights[i, j] = vert_weight.weight + collected_weights.append(vert_weight.weight) + weight_sum = sum(collected_weights) + if weight_sum: + for j, weight in enumerate(collected_weights): + weights[i, j] = weight / weight_sum return joints, weights @@ -124,12 +130,10 @@ class GLTFBuilder: mesh: Optional[gltflib.Mesh] = None, transform: Optional[np.ndarray] = None, ) -> gltflib.Node: - if transform is None: - transform = np.identity(4) node = gltflib.Node( name=name, mesh=self.model.meshes.index(mesh) if mesh else None, - matrix=sl_mat4_to_gltf(transform), + matrix=sl_mat4_to_gltf(transform) if transform is not None else None, children=[], ) self.model.nodes.append(node) @@ -264,7 +268,7 @@ class GLTFBuilder: # that expect to be able to reorient the entire mesh through the # inverse bind matrices. joints: Dict[str, gltflib.Node] = {} - required_joints = required_joint_hierarchy(skin['joint_names']) + required_joints = AVATAR_SKELETON.get_required_joints(skin['joint_names']) # If this is present, it overrides the joint position from the skeleton definition if 'alt_inverse_bind_matrix' in skin: joint_overrides = dict(zip(skin['joint_names'], skin['alt_inverse_bind_matrix'])) @@ -272,19 +276,22 @@ class GLTFBuilder: joint_overrides = {} for joint_name in required_joints: - joint_pos = SKELETON_JOINTS[joint_name].translation + joint = AVATAR_SKELETON[joint_name] + joint_matrix = joint.matrix override = joint_overrides.get(joint_name) + decomp = list(transformations.decompose_matrix(joint_matrix)) if override: # We specifically only want the translation from the override! - joint_pos = transformations.translation_from_matrix(llsd_to_mat4(override)) - node = self.add_node(joint_name, transform=transformations.compose_matrix(translate=tuple(joint_pos))) + decomp_override = transformations.decompose_matrix(llsd_to_mat4(override)) + decomp[3] = decomp_override[3] + joint_matrix = transformations.compose_matrix(*decomp) + node = self.add_node(joint_name, transform=joint_matrix) joints[joint_name] = node # Add each joint to the child list of their respective parents for joint_name, joint_node in joints.items(): - parent_name = SKELETON_JOINTS[joint_name].parent - if parent_name: - joints[parent_name].children.append(self.model.nodes.index(joint_node)) + if parent := AVATAR_SKELETON[joint_name].parent: + joints[parent().name].children.append(self.model.nodes.index(joint_node)) return joints def add_skin(self, name: str, joint_nodes: Dict[str, gltflib.Node], skin_seg: SkinSegmentDict) -> gltflib.Skin: @@ -292,11 +299,14 @@ class GLTFBuilder: for joint_name in skin_seg['joint_names']: joints_arr.append(self.model.nodes.index(joint_nodes[joint_name])) - # TODO: glTF also doesn't have a concept of a "bind shape matrix" per its skinning docs, - # so we may have to mix that into the mesh data or inverse bind matrices somehow. + # glTF also doesn't have a concept of a "bind shape matrix" like Collada does + # per its skinning docs, so we have to mix it into the inverse bind matrices. + # See https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_020_Skins.md + # TODO: apply the bind shape matrix to the mesh data instead? inv_binds = [] - for inv_bind in skin_seg['inverse_bind_matrix']: - inv_bind = llsd_to_mat4(inv_bind) + bind_shape_matrix = llsd_to_mat4(skin_seg['bind_shape_matrix']) + for joint_name, inv_bind in zip(skin_seg['joint_names'], skin_seg['inverse_bind_matrix']): + inv_bind = np.matmul(llsd_to_mat4(inv_bind), bind_shape_matrix) inv_binds.append(sl_mat4_to_gltf(inv_bind)) inv_binds_data = np.array(inv_binds, dtype=np.float32).tobytes() buffer_view = self.add_buffer_view(inv_binds_data, target=None) @@ -340,7 +350,6 @@ def build_gltf_mesh(builder: GLTFBuilder, mesh: MeshAsset, name: str): skin_seg: Optional[SkinSegmentDict] = mesh.segments.get('skin') skin = None - armature_node = None mesh_transform = np.identity(4) if skin_seg: mesh_transform = llsd_to_mat4(skin_seg['bind_shape_matrix']) @@ -351,6 +360,7 @@ def build_gltf_mesh(builder: GLTFBuilder, mesh: MeshAsset, name: str): builder.scene.nodes.append(gltf_model.nodes.index(armature_node)) armature_node.children.append(gltf_model.nodes.index(joint_nodes['mPelvis'])) skin = builder.add_skin("Armature", joint_nodes, skin_seg) + # skin = None mesh_node = builder.add_node( name, @@ -359,15 +369,32 @@ def build_gltf_mesh(builder: GLTFBuilder, mesh: MeshAsset, name: str): ) if skin and False: # TODO: This badly mangles up the mesh right now, especially given the - # collision volume scales. Investigate using a more accurate skeleton def. + # collision volume scales. This is an issue in blender where it doesn't + # apply the inverse bind matrices relative to the scale and rotation of + # the bones themselves, as it should. Blender's glTF loader tries to recover + # from this by applying certain transforms as a pose, but the damage has + # been done by that point. Nobody else runs really runs into this because + # they have the good sense to not use some nightmare abomination rig with + # scaling and rotation on the skeleton like SL does. + # + # I can't see any way to properly support this without changing how Blender + # handles armatures to make inverse bind matrices relative to bone scale and rot + # (which they probably won't and shouldn't do, since there's no internal concept + # of bone scale or rot in Blender right now.) + # + # Should investigate an Avastar-style approach of optionally retargeting + # to a Blender-compatible rig with translation-only bones, and modify + # the bind matrices to accommodate. The glTF importer supports metadata through + # the "extras" fields, so we can potentially abuse the "bind_mat" metadata field + # that Blender already uses for the "Keep Bind Info" Collada import / export hack. + mesh_node.matrix = None mesh_node.skin = gltf_model.skins.index(skin) - armature_node.children.append(gltf_model.nodes.index(mesh_node)) - builder.scene.nodes.append(gltf_model.nodes.index(armature_node)) - else: - # Add a root node that will re-orient our scene into GLTF coords. - # scene_node = builder.add_node("scene") - # scene_node.children.append(gltf_model.nodes.index(mesh_node)) - builder.scene.nodes.append(gltf_model.nodes.index(mesh_node)) + + builder.scene.nodes.append(gltf_model.nodes.index(mesh_node)) + + for node in builder.model.nodes: + if not node.children: + node.children = None def main(): diff --git a/hippolyzer/lib/base/mesh_skeleton.py b/hippolyzer/lib/base/mesh_skeleton.py index b5c668b..e2d8e57 100644 --- a/hippolyzer/lib/base/mesh_skeleton.py +++ b/hippolyzer/lib/base/mesh_skeleton.py @@ -1,212 +1,90 @@ +import dataclasses +import weakref from typing import * -from hippolyzer.lib.base.datatypes import Vector3 +import transformations +from lxml import etree + +from hippolyzer.lib.base.datatypes import Vector3, RAD_TO_DEG +from hippolyzer.lib.base.helpers import get_resource_filename -class JointNode(NamedTuple): - parent: Optional[str] +MAYBE_JOINT_REF = Optional[Callable[[], "JointNode"]] + + +@dataclasses.dataclass(unsafe_hash=True) +class JointNode: + name: str + parent: MAYBE_JOINT_REF translation: Vector3 + pivot: Vector3 # pivot point for the joint, generally the same as translation + rotation: Vector3 # Euler rotation in degrees + scale: Vector3 + type: str # bone or collision_volume + + @property + def matrix(self): + return transformations.compose_matrix( + scale=tuple(self.scale), + angles=tuple(self.rotation / RAD_TO_DEG), + translate=tuple(self.translation), + ) -# Joint translation data resolved from the male collada file -# TODO: Get this from avatar_skeleton.xml instead? We may need other transforms. -SKELETON_JOINTS = { - 'mPelvis': JointNode(parent=None, translation=Vector3(0.0, 0.0, 1.067)), - 'PELVIS': JointNode(parent='mPelvis', translation=Vector3(-0.01, 0.0, -0.02)), - 'BUTT': JointNode(parent='mPelvis', translation=Vector3(-0.06, 0.0, -0.1)), - 'mSpine1': JointNode(parent='mPelvis', translation=Vector3(0.0, 0.0, 0.084)), - 'mSpine2': JointNode(parent='mSpine1', translation=Vector3(0.0, 0.0, -0.084)), - 'mTorso': JointNode(parent='mSpine2', translation=Vector3(0.0, 0.0, 0.084)), - 'BELLY': JointNode(parent='mTorso', translation=Vector3(0.028, 0.0, 0.04)), - 'LEFT_HANDLE': JointNode(parent='mTorso', translation=Vector3(0.0, 0.1, 0.058)), - 'RIGHT_HANDLE': JointNode(parent='mTorso', translation=Vector3(0.0, -0.1, 0.058)), - 'LOWER_BACK': JointNode(parent='mTorso', translation=Vector3(0.0, 0.0, 0.023)), - 'mSpine3': JointNode(parent='mTorso', translation=Vector3(-0.015, 0.0, 0.205)), - 'mSpine4': JointNode(parent='mSpine3', translation=Vector3(0.015, 0.0, -0.205)), - 'mChest': JointNode(parent='mSpine4', translation=Vector3(-0.015, 0.0, 0.205)), - 'CHEST': JointNode(parent='mChest', translation=Vector3(0.028, 0.0, 0.07)), - 'LEFT_PEC': JointNode(parent='mChest', translation=Vector3(0.119, 0.082, 0.042)), - 'RIGHT_PEC': JointNode(parent='mChest', translation=Vector3(0.119, -0.082, 0.042)), - 'UPPER_BACK': JointNode(parent='mChest', translation=Vector3(0.0, 0.0, 0.017)), - 'mNeck': JointNode(parent='mChest', translation=Vector3(-0.01, 0.0, 0.251)), - 'NECK': JointNode(parent='mNeck', translation=Vector3(0.0, 0.0, 0.02)), - 'mHead': JointNode(parent='mNeck', translation=Vector3(0.0, 0.0, 0.076)), - 'HEAD': JointNode(parent='mHead', translation=Vector3(0.02, 0.0, 0.07)), - 'mSkull': JointNode(parent='mHead', translation=Vector3(0.0, 0.0, 0.079)), - 'mEyeRight': JointNode(parent='mHead', translation=Vector3(0.098, -0.036, 0.079)), - 'mEyeLeft': JointNode(parent='mHead', translation=Vector3(0.098, 0.036, 0.079)), - 'mFaceRoot': JointNode(parent='mHead', translation=Vector3(0.025, 0.0, 0.045)), - 'mFaceEyeAltRight': JointNode(parent='mFaceRoot', translation=Vector3(0.073, -0.036, 0.034)), - 'mFaceEyeAltLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.073, 0.036, 0.034)), - 'mFaceForeheadLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.061, 0.035, 0.083)), - 'mFaceForeheadRight': JointNode(parent='mFaceRoot', translation=Vector3(0.061, -0.035, 0.083)), - 'mFaceEyebrowOuterLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.064, 0.051, 0.048)), - 'mFaceEyebrowCenterLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.07, 0.043, 0.056)), - 'mFaceEyebrowInnerLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.075, 0.022, 0.051)), - 'mFaceEyebrowOuterRight': JointNode(parent='mFaceRoot', translation=Vector3(0.064, -0.051, 0.048)), - 'mFaceEyebrowCenterRight': JointNode(parent='mFaceRoot', translation=Vector3(0.07, -0.043, 0.056)), - 'mFaceEyebrowInnerRight': JointNode(parent='mFaceRoot', translation=Vector3(0.075, -0.022, 0.051)), - 'mFaceEyeLidUpperLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.073, 0.036, 0.034)), - 'mFaceEyeLidLowerLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.073, 0.036, 0.034)), - 'mFaceEyeLidUpperRight': JointNode(parent='mFaceRoot', translation=Vector3(0.073, -0.036, 0.034)), - 'mFaceEyeLidLowerRight': JointNode(parent='mFaceRoot', translation=Vector3(0.073, -0.036, 0.034)), - 'mFaceEar1Left': JointNode(parent='mFaceRoot', translation=Vector3(0.0, 0.08, 0.002)), - 'mFaceEar2Left': JointNode(parent='mFaceEar1Left', translation=Vector3(-0.019, 0.018, 0.025)), - 'mFaceEar1Right': JointNode(parent='mFaceRoot', translation=Vector3(0.0, -0.08, 0.002)), - 'mFaceEar2Right': JointNode(parent='mFaceEar1Right', translation=Vector3(-0.019, -0.018, 0.025)), - 'mFaceNoseLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.086, 0.015, -0.004)), - 'mFaceNoseCenter': JointNode(parent='mFaceRoot', translation=Vector3(0.102, 0.0, 0.0)), - 'mFaceNoseRight': JointNode(parent='mFaceRoot', translation=Vector3(0.086, -0.015, -0.004)), - 'mFaceCheekLowerLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.05, 0.034, -0.031)), - 'mFaceCheekUpperLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.07, 0.034, -0.005)), - 'mFaceCheekLowerRight': JointNode(parent='mFaceRoot', translation=Vector3(0.05, -0.034, -0.031)), - 'mFaceCheekUpperRight': JointNode(parent='mFaceRoot', translation=Vector3(0.07, -0.034, -0.005)), - 'mFaceJaw': JointNode(parent='mFaceRoot', translation=Vector3(-0.001, 0.0, -0.015)), - 'mFaceChin': JointNode(parent='mFaceJaw', translation=Vector3(0.074, 0.0, -0.054)), - 'mFaceTeethLower': JointNode(parent='mFaceJaw', translation=Vector3(0.021, 0.0, -0.039)), - 'mFaceLipLowerLeft': JointNode(parent='mFaceTeethLower', translation=Vector3(0.045, 0.0, 0.0)), - 'mFaceLipLowerRight': JointNode(parent='mFaceTeethLower', translation=Vector3(0.045, 0.0, 0.0)), - 'mFaceLipLowerCenter': JointNode(parent='mFaceTeethLower', translation=Vector3(0.045, 0.0, 0.0)), - 'mFaceTongueBase': JointNode(parent='mFaceTeethLower', translation=Vector3(0.039, 0.0, 0.005)), - 'mFaceTongueTip': JointNode(parent='mFaceTongueBase', translation=Vector3(0.022, 0.0, 0.007)), - 'mFaceJawShaper': JointNode(parent='mFaceRoot', translation=Vector3(0.0, 0.0, 0.0)), - 'mFaceForeheadCenter': JointNode(parent='mFaceRoot', translation=Vector3(0.069, 0.0, 0.065)), - 'mFaceNoseBase': JointNode(parent='mFaceRoot', translation=Vector3(0.094, 0.0, -0.016)), - 'mFaceTeethUpper': JointNode(parent='mFaceRoot', translation=Vector3(0.02, 0.0, -0.03)), - 'mFaceLipUpperLeft': JointNode(parent='mFaceTeethUpper', translation=Vector3(0.045, 0.0, -0.003)), - 'mFaceLipUpperRight': JointNode(parent='mFaceTeethUpper', translation=Vector3(0.045, 0.0, -0.003)), - 'mFaceLipCornerLeft': JointNode(parent='mFaceTeethUpper', translation=Vector3(0.028, -0.019, -0.01)), - 'mFaceLipCornerRight': JointNode(parent='mFaceTeethUpper', translation=Vector3(0.028, 0.019, -0.01)), - 'mFaceLipUpperCenter': JointNode(parent='mFaceTeethUpper', translation=Vector3(0.045, 0.0, -0.003)), - 'mFaceEyecornerInnerLeft': JointNode(parent='mFaceRoot', translation=Vector3(0.075, 0.017, 0.032)), - 'mFaceEyecornerInnerRight': JointNode(parent='mFaceRoot', translation=Vector3(0.075, -0.017, 0.032)), - 'mFaceNoseBridge': JointNode(parent='mFaceRoot', translation=Vector3(0.091, 0.0, 0.02)), - 'mCollarLeft': JointNode(parent='mChest', translation=Vector3(-0.021, 0.085, 0.165)), - 'L_CLAVICLE': JointNode(parent='mCollarLeft', translation=Vector3(0.02, 0.0, 0.02)), - 'mShoulderLeft': JointNode(parent='mCollarLeft', translation=Vector3(0.0, 0.079, 0.0)), - 'L_UPPER_ARM': JointNode(parent='mShoulderLeft', translation=Vector3(0.0, 0.12, 0.01)), - 'mElbowLeft': JointNode(parent='mShoulderLeft', translation=Vector3(0.0, 0.248, 0.0)), - 'L_LOWER_ARM': JointNode(parent='mElbowLeft', translation=Vector3(0.0, 0.1, 0.0)), - 'mWristLeft': JointNode(parent='mElbowLeft', translation=Vector3(0.0, 0.205, 0.0)), - 'L_HAND': JointNode(parent='mWristLeft', translation=Vector3(0.01, 0.05, 0.0)), - 'mHandMiddle1Left': JointNode(parent='mWristLeft', translation=Vector3(0.013, 0.101, 0.015)), - 'mHandMiddle2Left': JointNode(parent='mHandMiddle1Left', translation=Vector3(-0.001, 0.04, -0.006)), - 'mHandMiddle3Left': JointNode(parent='mHandMiddle2Left', translation=Vector3(-0.001, 0.049, -0.008)), - 'mHandIndex1Left': JointNode(parent='mWristLeft', translation=Vector3(0.038, 0.097, 0.015)), - 'mHandIndex2Left': JointNode(parent='mHandIndex1Left', translation=Vector3(0.017, 0.036, -0.006)), - 'mHandIndex3Left': JointNode(parent='mHandIndex2Left', translation=Vector3(0.014, 0.032, -0.006)), - 'mHandRing1Left': JointNode(parent='mWristLeft', translation=Vector3(-0.01, 0.099, 0.009)), - 'mHandRing2Left': JointNode(parent='mHandRing1Left', translation=Vector3(-0.013, 0.038, -0.008)), - 'mHandRing3Left': JointNode(parent='mHandRing2Left', translation=Vector3(-0.013, 0.04, -0.009)), - 'mHandPinky1Left': JointNode(parent='mWristLeft', translation=Vector3(-0.031, 0.095, 0.003)), - 'mHandPinky2Left': JointNode(parent='mHandPinky1Left', translation=Vector3(-0.024, 0.025, -0.006)), - 'mHandPinky3Left': JointNode(parent='mHandPinky2Left', translation=Vector3(-0.015, 0.018, -0.004)), - 'mHandThumb1Left': JointNode(parent='mWristLeft', translation=Vector3(0.031, 0.026, 0.004)), - 'mHandThumb2Left': JointNode(parent='mHandThumb1Left', translation=Vector3(0.028, 0.032, -0.001)), - 'mHandThumb3Left': JointNode(parent='mHandThumb2Left', translation=Vector3(0.023, 0.031, -0.001)), - 'mCollarRight': JointNode(parent='mChest', translation=Vector3(-0.021, -0.085, 0.165)), - 'R_CLAVICLE': JointNode(parent='mCollarRight', translation=Vector3(0.02, 0.0, 0.02)), - 'mShoulderRight': JointNode(parent='mCollarRight', translation=Vector3(0.0, -0.079, 0.0)), - 'R_UPPER_ARM': JointNode(parent='mShoulderRight', translation=Vector3(0.0, -0.12, 0.01)), - 'mElbowRight': JointNode(parent='mShoulderRight', translation=Vector3(0.0, -0.248, 0.0)), - 'R_LOWER_ARM': JointNode(parent='mElbowRight', translation=Vector3(0.0, -0.1, 0.0)), - 'mWristRight': JointNode(parent='mElbowRight', translation=Vector3(0.0, -0.205, 0.0)), - 'R_HAND': JointNode(parent='mWristRight', translation=Vector3(0.01, -0.05, 0.0)), - 'mHandMiddle1Right': JointNode(parent='mWristRight', translation=Vector3(0.013, -0.101, 0.015)), - 'mHandMiddle2Right': JointNode(parent='mHandMiddle1Right', translation=Vector3(-0.001, -0.04, -0.006)), - 'mHandMiddle3Right': JointNode(parent='mHandMiddle2Right', translation=Vector3(-0.001, -0.049, -0.008)), - 'mHandIndex1Right': JointNode(parent='mWristRight', translation=Vector3(0.038, -0.097, 0.015)), - 'mHandIndex2Right': JointNode(parent='mHandIndex1Right', translation=Vector3(0.017, -0.036, -0.006)), - 'mHandIndex3Right': JointNode(parent='mHandIndex2Right', translation=Vector3(0.014, -0.032, -0.006)), - 'mHandRing1Right': JointNode(parent='mWristRight', translation=Vector3(-0.01, -0.099, 0.009)), - 'mHandRing2Right': JointNode(parent='mHandRing1Right', translation=Vector3(-0.013, -0.038, -0.008)), - 'mHandRing3Right': JointNode(parent='mHandRing2Right', translation=Vector3(-0.013, -0.04, -0.009)), - 'mHandPinky1Right': JointNode(parent='mWristRight', translation=Vector3(-0.031, -0.095, 0.003)), - 'mHandPinky2Right': JointNode(parent='mHandPinky1Right', translation=Vector3(-0.024, -0.025, -0.006)), - 'mHandPinky3Right': JointNode(parent='mHandPinky2Right', translation=Vector3(-0.015, -0.018, -0.004)), - 'mHandThumb1Right': JointNode(parent='mWristRight', translation=Vector3(0.031, -0.026, 0.004)), - 'mHandThumb2Right': JointNode(parent='mHandThumb1Right', translation=Vector3(0.028, -0.032, -0.001)), - 'mHandThumb3Right': JointNode(parent='mHandThumb2Right', translation=Vector3(0.023, -0.031, -0.001)), - 'mWingsRoot': JointNode(parent='mChest', translation=Vector3(-0.014, 0.0, 0.0)), - 'mWing1Left': JointNode(parent='mWingsRoot', translation=Vector3(-0.099, 0.105, 0.181)), - 'mWing2Left': JointNode(parent='mWing1Left', translation=Vector3(-0.168, 0.169, 0.067)), - 'mWing3Left': JointNode(parent='mWing2Left', translation=Vector3(-0.181, 0.183, 0.0)), - 'mWing4Left': JointNode(parent='mWing3Left', translation=Vector3(-0.171, 0.173, 0.0)), - 'mWing4FanLeft': JointNode(parent='mWing3Left', translation=Vector3(-0.171, 0.173, 0.0)), - 'mWing1Right': JointNode(parent='mWingsRoot', translation=Vector3(-0.099, -0.105, 0.181)), - 'mWing2Right': JointNode(parent='mWing1Right', translation=Vector3(-0.168, -0.169, 0.067)), - 'mWing3Right': JointNode(parent='mWing2Right', translation=Vector3(-0.181, -0.183, 0.0)), - 'mWing4Right': JointNode(parent='mWing3Right', translation=Vector3(-0.171, -0.173, 0.0)), - 'mWing4FanRight': JointNode(parent='mWing3Right', translation=Vector3(-0.171, -0.173, 0.0)), - 'mHipRight': JointNode(parent='mPelvis', translation=Vector3(0.034, -0.129, -0.041)), - 'R_UPPER_LEG': JointNode(parent='mHipRight', translation=Vector3(-0.02, 0.05, -0.22)), - 'mKneeRight': JointNode(parent='mHipRight', translation=Vector3(-0.001, 0.049, -0.491)), - 'R_LOWER_LEG': JointNode(parent='mKneeRight', translation=Vector3(-0.02, 0.0, -0.2)), - 'mAnkleRight': JointNode(parent='mKneeRight', translation=Vector3(-0.029, 0.0, -0.468)), - 'R_FOOT': JointNode(parent='mAnkleRight', translation=Vector3(0.077, 0.0, -0.041)), - 'mFootRight': JointNode(parent='mAnkleRight', translation=Vector3(0.112, 0.0, -0.061)), - 'mToeRight': JointNode(parent='mFootRight', translation=Vector3(0.109, 0.0, 0.0)), - 'mHipLeft': JointNode(parent='mPelvis', translation=Vector3(0.034, 0.127, -0.041)), - 'L_UPPER_LEG': JointNode(parent='mHipLeft', translation=Vector3(-0.02, -0.05, -0.22)), - 'mKneeLeft': JointNode(parent='mHipLeft', translation=Vector3(-0.001, -0.046, -0.491)), - 'L_LOWER_LEG': JointNode(parent='mKneeLeft', translation=Vector3(-0.02, 0.0, -0.2)), - 'mAnkleLeft': JointNode(parent='mKneeLeft', translation=Vector3(-0.029, 0.001, -0.468)), - 'L_FOOT': JointNode(parent='mAnkleLeft', translation=Vector3(0.077, 0.0, -0.041)), - 'mFootLeft': JointNode(parent='mAnkleLeft', translation=Vector3(0.112, 0.0, -0.061)), - 'mToeLeft': JointNode(parent='mFootLeft', translation=Vector3(0.109, 0.0, 0.0)), - 'mTail1': JointNode(parent='mPelvis', translation=Vector3(-0.116, 0.0, 0.047)), - 'mTail2': JointNode(parent='mTail1', translation=Vector3(-0.197, 0.0, 0.0)), - 'mTail3': JointNode(parent='mTail2', translation=Vector3(-0.168, 0.0, 0.0)), - 'mTail4': JointNode(parent='mTail3', translation=Vector3(-0.142, 0.0, 0.0)), - 'mTail5': JointNode(parent='mTail4', translation=Vector3(-0.112, 0.0, 0.0)), - 'mTail6': JointNode(parent='mTail5', translation=Vector3(-0.094, 0.0, 0.0)), - 'mGroin': JointNode(parent='mPelvis', translation=Vector3(0.064, 0.0, -0.097)), - 'mHindLimbsRoot': JointNode(parent='mPelvis', translation=Vector3(-0.2, 0.0, 0.084)), - 'mHindLimb1Left': JointNode(parent='mHindLimbsRoot', translation=Vector3(-0.204, 0.129, -0.125)), - 'mHindLimb2Left': JointNode(parent='mHindLimb1Left', translation=Vector3(0.002, -0.046, -0.491)), - 'mHindLimb3Left': JointNode(parent='mHindLimb2Left', translation=Vector3(-0.03, -0.003, -0.468)), - 'mHindLimb4Left': JointNode(parent='mHindLimb3Left', translation=Vector3(0.112, 0.0, -0.061)), - 'mHindLimb1Right': JointNode(parent='mHindLimbsRoot', translation=Vector3(-0.204, -0.129, -0.125)), - 'mHindLimb2Right': JointNode(parent='mHindLimb1Right', translation=Vector3(0.002, 0.046, -0.491)), - 'mHindLimb3Right': JointNode(parent='mHindLimb2Right', translation=Vector3(-0.03, 0.003, -0.468)), - 'mHindLimb4Right': JointNode(parent='mHindLimb3Right', translation=Vector3(0.112, 0.0, -0.061)), -} +@dataclasses.dataclass +class Skeleton: + joint_dict: Dict[str, JointNode] + + def __getitem__(self, item: str) -> JointNode: + return self.joint_dict[item] + + @classmethod + def _parse_node_children(cls, joint_dict: Dict[str, JointNode], node: etree.ElementBase, parent: MAYBE_JOINT_REF): + name = node.get('name') + joint = JointNode( + name=name, + parent=parent, + translation=_get_vec_attr(node, "pos", Vector3()), + pivot=_get_vec_attr(node, "pivot", Vector3()), + rotation=_get_vec_attr(node, "rot", Vector3()), + scale=_get_vec_attr(node, "scale", Vector3(1, 1, 1)), + type=node.tag, + ) + joint_dict[name] = joint + for child in node.iterchildren(): + cls._parse_node_children(joint_dict, child, weakref.ref(joint)) + + @classmethod + def from_xml(cls, node: etree.ElementBase): + joint_dict = {} + cls._parse_node_children(joint_dict, node, None) + return cls(joint_dict) + + def get_required_joints(self, joint_names: Collection[str]) -> Set[str]: + """Get all joints required to have a chain from all joints up to the root joint""" + required = set(joint_names) + for joint_name in joint_names: + joint_node = self.joint_dict.get(joint_name) + while joint_node: + required.add(joint_node.name) + if not joint_node.parent: + break + joint_node = joint_node.parent() + return required -def required_joint_hierarchy(joint_names: Collection[str]) -> Set[str]: - """Get all joints required to have a chain from all joints up to the root joint""" - required = set(joint_names) - for joint_name in joint_names: - while parent_node := SKELETON_JOINTS.get(joint_name): - required.add(joint_name) - joint_name = parent_node.parent - return required +def load_avatar_skeleton() -> Skeleton: + skel_path = get_resource_filename("lib/base/data/avatar_skeleton.xml") + with open(skel_path, 'r') as f: + skel_root = etree.fromstring(f.read()) + return Skeleton.from_xml(skel_root.getchildren()[0]) -# def load_skeleton_nodes() -> etree.ElementBase: -# # TODO: this sucks. Can't we construct nodes with the appropriate transformation -# # matrices from the data in `avatar_skeleton.xml`? -# skel_path = get_resource_filename("lib/base/data/male_collada_joints.xml") -# with open(skel_path, 'r') as f: -# return etree.fromstring(f.read()) -# -# -# def print_skeleton(): -# import pprint -# skel_root = load_skeleton_nodes() -# -# dae = collada.Collada() -# joints = {} -# for skel_node in skel_root.iter(): -# # xpath is loathsome so this is easier. -# if "node" not in skel_node.tag or skel_node.get('type') != 'JOINT': -# continue -# col_node = collada.scene.Node.load(dae, skel_node, {}) -# -# par_node = skel_node.getparent() -# parent_name = None -# if par_node and par_node.get('name'): -# parent_name = par_node.get('name') -# translation = Vector3(*transformations.translation_from_matrix(col_node.matrix)) -# joints[col_node.id] = JointNode(parent=parent_name, translation=translation) -# pprint.pprint(joints, sort_dicts=False) +def _get_vec_attr(node, attr_name, default) -> Vector3: + attr_val = node.get(attr_name, None) + if not attr_val: + return default + return Vector3(*(float(x) for x in attr_val.split(" ") if x)) + + +AVATAR_SKELETON = load_avatar_skeleton()