glTF fixups, parse skeleton definition from avatar_skeleton.xml

This commit is contained in:
Salad Dais
2022-09-01 16:57:36 +00:00
parent 570dbce181
commit 25f533a31b
3 changed files with 139 additions and 231 deletions

View File

@@ -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"
]

View File

@@ -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():

View File

@@ -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()