# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import math from collections.abc import Iterator, Mapping from contextlib import contextmanager from dataclasses import dataclass from sys import float_info from typing import Optional, Union import bpy from bpy.types import ( Armature, Context, Object, Pose, PoseBone, ) from mathutils import Euler, Matrix, Quaternion, Vector from ..common import ops from ..common.logger import get_logger from ..common.rotation import ( get_rotation_as_quaternion, set_rotation_without_mode_change, ) from ..common.vrm0.human_bone import HumanBoneName as Vrm0HumanBoneName from ..common.vrm1.human_bone import HumanBoneName as Vrm1HumanBoneName from ..common.workspace import save_workspace from .extension import VrmAddonArmatureExtensionPropertyGroup, get_armature_extension from .vrm0.property_group import Vrm0HumanoidPropertyGroup from .vrm1.property_group import Vrm1HumanoidPropertyGroup logger = get_logger(__name__) def set_bone_direction_to_align_child_bone( context: Context, armature: Object, direction: Vector, bone: PoseBone, child_bone: PoseBone, ) -> None: world_bone_matrix = armature.matrix_world @ bone.matrix world_child_bone_matrix = armature.matrix_world @ child_bone.matrix world_bone_length = ( world_child_bone_matrix.translation - world_bone_matrix.translation ).length world_child_bone_from_translation = world_child_bone_matrix.translation world_child_bone_to_translation = ( world_bone_matrix.translation + direction * world_bone_length ) bone_local_child_bone_from_translation = ( armature.matrix_world @ bone.matrix ).inverted_safe() @ world_child_bone_from_translation bone_local_child_bone_to_translation = ( armature.matrix_world @ bone.matrix ).inverted_safe() @ world_child_bone_to_translation if bone_local_child_bone_from_translation.length_squared < float_info.epsilon: return if bone_local_child_bone_to_translation.length_squared < float_info.epsilon: return rotation = bone_local_child_bone_from_translation.rotation_difference( bone_local_child_bone_to_translation ) set_rotation_without_mode_change(bone, rotation @ get_rotation_as_quaternion(bone)) context.view_layer.update() def set_bone_direction_to_align_z_world_location( context: Context, armature: Object, direction: Vector, bone: PoseBone, from_world_location: Vector, ) -> None: world_bone_translation = (armature.matrix_world @ bone.matrix).to_translation() aligned_from_world_location = Vector( ( from_world_location.x, world_bone_translation.y, from_world_location.z, ) ) aligned_to_world_location = world_bone_translation + direction bone_local_aligned_from_world_location = ( armature.matrix_world @ bone.matrix ).inverted_safe() @ aligned_from_world_location bone_local_aligned_to_world_location = ( armature.matrix_world @ bone.matrix ).inverted_safe() @ aligned_to_world_location if bone_local_aligned_from_world_location.length_squared < float_info.epsilon: return if bone_local_aligned_to_world_location.length_squared < float_info.epsilon: return rotation = bone_local_aligned_from_world_location.rotation_difference( bone_local_aligned_to_world_location ) set_rotation_without_mode_change(bone, rotation @ get_rotation_as_quaternion(bone)) context.view_layer.update() @dataclass(frozen=True) class ChainSingleChild: direction: Vector vrm0_human_bone_names: tuple[Vrm0HumanBoneName, ...] vrm1_human_bone_names: tuple[Vrm1HumanBoneName, ...] def execute(self, context: Context, armature: Object) -> None: armature_data = armature.data if not isinstance(armature_data, Armature): raise TypeError ext = get_armature_extension(armature_data) if ext.is_vrm0(): humanoid = ext.vrm0.humanoid bones: list[PoseBone] = [] for human_bone in humanoid.human_bones: human_bone_name = Vrm0HumanBoneName.from_str(human_bone.bone) if not human_bone_name: continue if human_bone_name not in self.vrm0_human_bone_names: continue bone = armature.pose.bones.get(human_bone.node.bone_name) if not bone: continue bones.append(bone) else: human_bones = ext.vrm1.humanoid.human_bones human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone() bones = [ bone for bone in [ armature.pose.bones.get(human_bone.node.bone_name) for human_bone in [ human_bone_name_to_human_bone.get(human_bone_name) for human_bone_name in self.vrm1_human_bone_names ] if human_bone ] if bone ] if len(bones) < 2: return root_bone = bones[0] tip_bone = bones[-1] chained_bones: list[PoseBone] = [] searching_bone: Optional[PoseBone] = tip_bone while True: if not searching_bone: return chained_bones.insert(0, searching_bone) if searching_bone == root_bone: break searching_bone = searching_bone.parent for bone, child_bone in zip(chained_bones, chained_bones[1:]): set_bone_direction_to_align_child_bone( context, armature, self.direction, bone, child_bone ) @dataclass(frozen=True) class ChainHorizontalMultipleChildren: direction: Vector vrm0_parent_human_bone_name: Vrm0HumanBoneName vrm0_human_bone_names: tuple[Vrm0HumanBoneName, ...] vrm1_parent_human_bone_name: Vrm1HumanBoneName vrm1_human_bone_names: tuple[Vrm1HumanBoneName, ...] def execute(self, context: Context, armature: Object) -> None: armature_data = armature.data if not isinstance(armature_data, Armature): raise TypeError ext = get_armature_extension(armature_data) if ext.is_vrm0(): humanoid = ext.vrm0.humanoid bones: list[PoseBone] = [] parent_bone: Optional[PoseBone] = None for human_bone in humanoid.human_bones: human_bone_name = Vrm0HumanBoneName.from_str(human_bone.bone) if not human_bone_name: continue if human_bone_name == self.vrm0_parent_human_bone_name: parent_bone = armature.pose.bones.get(human_bone.node.bone_name) elif human_bone_name in self.vrm0_human_bone_names: bone = armature.pose.bones.get(human_bone.node.bone_name) if bone: bones.append(bone) else: human_bones = ext.vrm1.humanoid.human_bones human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone() bones = [ bone for bone in [ armature.pose.bones.get(human_bone.node.bone_name) for human_bone in [ human_bone_name_to_human_bone.get(human_bone_name) for human_bone_name in self.vrm1_human_bone_names ] if human_bone ] if bone ] parent_human_bone = human_bone_name_to_human_bone.get( self.vrm1_parent_human_bone_name ) parent_bone = None if parent_human_bone: parent_bone = armature.pose.bones.get(parent_human_bone.node.bone_name) if not bones: return world_location = Vector((0, 0, 0)) for bone in bones: world_location += (armature.matrix_world @ bone.matrix).to_translation() world_location /= len(bones) if not parent_bone: return set_bone_direction_to_align_z_world_location( context, armature, self.direction, parent_bone, world_location ) def reset_root_to_human_bone_translation( pose: Pose, ext: VrmAddonArmatureExtensionPropertyGroup ) -> None: # Reset positions from root bone to any Human bone # No particular reason for not resetting all bones, just intuition bones: list[PoseBone] = [bone for bone in pose.bones if not bone.parent] while bones: bone = bones.pop() bone.location = Vector((0, 0, 0)) if ext.is_vrm1(): human_bone_name_to_human_bones = ( ext.vrm1.humanoid.human_bones.human_bone_name_to_human_bone() ) if any( True for human_bone in human_bone_name_to_human_bones.values() if human_bone.node.bone_name == bone.name ): continue if ext.is_vrm0() and any( True for human_bone in ext.vrm0.humanoid.human_bones if human_bone.node.bone_name == bone.name ): continue bones.extend(bone.children) def set_estimated_humanoid_t_pose(context: Context, armature: Object) -> bool: armature_data = armature.data if not isinstance(armature_data, Armature): return False ext = get_armature_extension(armature_data) if ext.is_vrm0(): if not ext.vrm0.humanoid.all_required_bones_are_assigned(): return False else: human_bones = ext.vrm1.humanoid.human_bones if not human_bones.all_required_bones_are_assigned(): return False for bone in armature.pose.bones: set_rotation_without_mode_change(bone, Quaternion()) reset_root_to_human_bone_translation(armature.pose, ext) context.view_layer.update() # https://github.com/vrm-c/vrm-specification/blob/73855bb77d431a3374212551a4fa48e043be3ced/specification/VRMC_vrm-1.0/tpose.md chains: tuple[Union[ChainSingleChild, ChainHorizontalMultipleChildren], ...] = ( ChainSingleChild( Vector((-1, 0, 0)), ( Vrm0HumanBoneName.RIGHT_UPPER_ARM, Vrm0HumanBoneName.RIGHT_LOWER_ARM, Vrm0HumanBoneName.RIGHT_HAND, ), ( Vrm1HumanBoneName.RIGHT_UPPER_ARM, Vrm1HumanBoneName.RIGHT_LOWER_ARM, Vrm1HumanBoneName.RIGHT_HAND, ), ), ChainHorizontalMultipleChildren( Vector((-1, 0, 0)), Vrm0HumanBoneName.RIGHT_HAND, ( Vrm0HumanBoneName.RIGHT_INDEX_PROXIMAL, Vrm0HumanBoneName.RIGHT_MIDDLE_PROXIMAL, Vrm0HumanBoneName.RIGHT_RING_PROXIMAL, Vrm0HumanBoneName.RIGHT_LITTLE_PROXIMAL, ), Vrm1HumanBoneName.RIGHT_HAND, ( Vrm1HumanBoneName.RIGHT_INDEX_PROXIMAL, Vrm1HumanBoneName.RIGHT_MIDDLE_PROXIMAL, Vrm1HumanBoneName.RIGHT_RING_PROXIMAL, Vrm1HumanBoneName.RIGHT_LITTLE_PROXIMAL, ), ), ChainSingleChild( Vector((-math.sqrt(0.5), -math.sqrt(0.5), 0)), ( Vrm0HumanBoneName.RIGHT_THUMB_PROXIMAL, Vrm0HumanBoneName.RIGHT_THUMB_INTERMEDIATE, Vrm0HumanBoneName.RIGHT_THUMB_DISTAL, ), ( Vrm1HumanBoneName.RIGHT_THUMB_METACARPAL, Vrm1HumanBoneName.RIGHT_THUMB_PROXIMAL, Vrm1HumanBoneName.RIGHT_THUMB_DISTAL, ), ), ChainSingleChild( Vector((-1, 0, 0)), ( Vrm0HumanBoneName.RIGHT_INDEX_PROXIMAL, Vrm0HumanBoneName.RIGHT_INDEX_INTERMEDIATE, Vrm0HumanBoneName.RIGHT_INDEX_DISTAL, ), ( Vrm1HumanBoneName.RIGHT_INDEX_PROXIMAL, Vrm1HumanBoneName.RIGHT_INDEX_INTERMEDIATE, Vrm1HumanBoneName.RIGHT_INDEX_DISTAL, ), ), ChainSingleChild( Vector((-1, 0, 0)), ( Vrm0HumanBoneName.RIGHT_MIDDLE_PROXIMAL, Vrm0HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE, Vrm0HumanBoneName.RIGHT_MIDDLE_DISTAL, ), ( Vrm1HumanBoneName.RIGHT_MIDDLE_PROXIMAL, Vrm1HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE, Vrm1HumanBoneName.RIGHT_MIDDLE_DISTAL, ), ), ChainSingleChild( Vector((-1, 0, 0)), ( Vrm0HumanBoneName.RIGHT_RING_PROXIMAL, Vrm0HumanBoneName.RIGHT_RING_INTERMEDIATE, Vrm0HumanBoneName.RIGHT_RING_DISTAL, ), ( Vrm1HumanBoneName.RIGHT_RING_PROXIMAL, Vrm1HumanBoneName.RIGHT_RING_INTERMEDIATE, Vrm1HumanBoneName.RIGHT_RING_DISTAL, ), ), ChainSingleChild( Vector((-1, 0, 0)), ( Vrm0HumanBoneName.RIGHT_LITTLE_PROXIMAL, Vrm0HumanBoneName.RIGHT_LITTLE_INTERMEDIATE, Vrm0HumanBoneName.RIGHT_LITTLE_DISTAL, ), ( Vrm1HumanBoneName.RIGHT_LITTLE_PROXIMAL, Vrm1HumanBoneName.RIGHT_LITTLE_INTERMEDIATE, Vrm1HumanBoneName.RIGHT_LITTLE_DISTAL, ), ), ChainSingleChild( Vector((0, 0, -1)), ( Vrm0HumanBoneName.RIGHT_UPPER_LEG, Vrm0HumanBoneName.RIGHT_LOWER_LEG, Vrm0HumanBoneName.RIGHT_FOOT, ), ( Vrm1HumanBoneName.RIGHT_UPPER_LEG, Vrm1HumanBoneName.RIGHT_LOWER_LEG, Vrm1HumanBoneName.RIGHT_FOOT, ), ), ChainSingleChild( Vector((1, 0, 0)), ( Vrm0HumanBoneName.LEFT_UPPER_ARM, Vrm0HumanBoneName.LEFT_LOWER_ARM, Vrm0HumanBoneName.LEFT_HAND, ), ( Vrm1HumanBoneName.LEFT_UPPER_ARM, Vrm1HumanBoneName.LEFT_LOWER_ARM, Vrm1HumanBoneName.LEFT_HAND, ), ), ChainHorizontalMultipleChildren( Vector((1, 0, 0)), Vrm0HumanBoneName.LEFT_HAND, ( Vrm0HumanBoneName.LEFT_INDEX_PROXIMAL, Vrm0HumanBoneName.LEFT_MIDDLE_PROXIMAL, Vrm0HumanBoneName.LEFT_RING_PROXIMAL, Vrm0HumanBoneName.LEFT_LITTLE_PROXIMAL, ), Vrm1HumanBoneName.LEFT_HAND, ( Vrm1HumanBoneName.LEFT_INDEX_PROXIMAL, Vrm1HumanBoneName.LEFT_MIDDLE_PROXIMAL, Vrm1HumanBoneName.LEFT_RING_PROXIMAL, Vrm1HumanBoneName.LEFT_LITTLE_PROXIMAL, ), ), ChainSingleChild( Vector((math.sqrt(0.5), -math.sqrt(0.5), 0)), ( Vrm0HumanBoneName.LEFT_THUMB_PROXIMAL, Vrm0HumanBoneName.LEFT_THUMB_INTERMEDIATE, Vrm0HumanBoneName.LEFT_THUMB_DISTAL, ), ( Vrm1HumanBoneName.LEFT_THUMB_METACARPAL, Vrm1HumanBoneName.LEFT_THUMB_PROXIMAL, Vrm1HumanBoneName.LEFT_THUMB_DISTAL, ), ), ChainSingleChild( Vector((1, 0, 0)), ( Vrm0HumanBoneName.LEFT_INDEX_PROXIMAL, Vrm0HumanBoneName.LEFT_INDEX_INTERMEDIATE, Vrm0HumanBoneName.LEFT_INDEX_DISTAL, ), ( Vrm1HumanBoneName.LEFT_INDEX_PROXIMAL, Vrm1HumanBoneName.LEFT_INDEX_INTERMEDIATE, Vrm1HumanBoneName.LEFT_INDEX_DISTAL, ), ), ChainSingleChild( Vector((1, 0, 0)), ( Vrm0HumanBoneName.LEFT_MIDDLE_PROXIMAL, Vrm0HumanBoneName.LEFT_MIDDLE_INTERMEDIATE, Vrm0HumanBoneName.LEFT_MIDDLE_DISTAL, ), ( Vrm1HumanBoneName.LEFT_MIDDLE_PROXIMAL, Vrm1HumanBoneName.LEFT_MIDDLE_INTERMEDIATE, Vrm1HumanBoneName.LEFT_MIDDLE_DISTAL, ), ), ChainSingleChild( Vector((1, 0, 0)), ( Vrm0HumanBoneName.LEFT_RING_PROXIMAL, Vrm0HumanBoneName.LEFT_RING_INTERMEDIATE, Vrm0HumanBoneName.LEFT_RING_DISTAL, ), ( Vrm1HumanBoneName.LEFT_RING_PROXIMAL, Vrm1HumanBoneName.LEFT_RING_INTERMEDIATE, Vrm1HumanBoneName.LEFT_RING_DISTAL, ), ), ChainSingleChild( Vector((1, 0, 0)), ( Vrm0HumanBoneName.LEFT_LITTLE_PROXIMAL, Vrm0HumanBoneName.LEFT_LITTLE_INTERMEDIATE, Vrm0HumanBoneName.LEFT_LITTLE_DISTAL, ), ( Vrm1HumanBoneName.LEFT_LITTLE_PROXIMAL, Vrm1HumanBoneName.LEFT_LITTLE_INTERMEDIATE, Vrm1HumanBoneName.LEFT_LITTLE_DISTAL, ), ), ChainSingleChild( Vector((0, 0, -1)), ( Vrm0HumanBoneName.LEFT_UPPER_LEG, Vrm0HumanBoneName.LEFT_LOWER_LEG, Vrm0HumanBoneName.LEFT_FOOT, ), ( Vrm1HumanBoneName.LEFT_UPPER_LEG, Vrm1HumanBoneName.LEFT_LOWER_LEG, Vrm1HumanBoneName.LEFT_FOOT, ), ), ) for chain in chains: chain.execute(context, armature) return True @dataclass(frozen=True) class PoseBonePose: matrix_basis: Matrix rotation_mode: str rotation_quaternion: Quaternion rotation_axis_angle: tuple[float, float, float, float] rotation_euler: Euler scale: Vector location: Vector bone_select: bool @staticmethod def save(pose: Pose) -> Mapping[str, "PoseBonePose"]: return { bone.name: PoseBonePose( matrix_basis=bone.matrix_basis.copy(), rotation_mode=bone.rotation_mode, rotation_quaternion=bone.rotation_quaternion.copy(), rotation_axis_angle=( bone.rotation_axis_angle[0], bone.rotation_axis_angle[1], bone.rotation_axis_angle[2], bone.rotation_axis_angle[3], ), rotation_euler=bone.rotation_euler.copy(), scale=bone.scale.copy(), location=bone.location.copy(), bone_select=(bone.bone.select if bpy.app.version < (5, 0) else False), ) for bone in pose.bones } @staticmethod def load( context: Context, pose: Pose, bone_name_to_pose_bone_pose: Mapping[str, "PoseBonePose"], ) -> None: context.view_layer.update() bones = [bone for bone in pose.bones if not bone.parent] while bones: bone = bones.pop() pose_bone_pose = bone_name_to_pose_bone_pose.get(bone.name) if pose_bone_pose: bone.matrix_basis = pose_bone_pose.matrix_basis.copy() # Directly restoring bone.matrix seems more efficient, but doing so # can cause problems when constraints are attached # https://github.com/saturday06/VRM-Addon-for-Blender/issues/671 bone.rotation_mode = pose_bone_pose.rotation_mode bone.rotation_axis_angle = list(pose_bone_pose.rotation_axis_angle) bone.rotation_quaternion = pose_bone_pose.rotation_quaternion.copy() bone.rotation_euler = pose_bone_pose.rotation_euler.copy() bone.scale = pose_bone_pose.scale.copy() bone.location = pose_bone_pose.location.copy() if bpy.app.version < (5, 0): bone.bone.select = pose_bone_pose.bone_select bones.extend(bone.children) context.view_layer.update() def leave_setup_humanoid_t_pose( context: Context, armature: Object, saved_pose_bone_pose: Mapping[str, PoseBonePose], saved_pose_position: str, *, saved_vrm1_look_at_preview: bool, ) -> None: with save_workspace(context, armature): bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="POSE") PoseBonePose.load(context, armature.pose, saved_pose_bone_pose) bpy.ops.object.mode_set(mode="OBJECT") armature_data = armature.data if not isinstance(armature_data, Armature): return armature_data.pose_position = saved_pose_position ext = get_armature_extension(armature_data) if ( ext.is_vrm1() and ext.vrm1.look_at.enable_preview != saved_vrm1_look_at_preview ): ext.vrm1.look_at.enable_preview = saved_vrm1_look_at_preview @contextmanager def setup_humanoid_t_pose( context: Context, armature: Object, ) -> Iterator[None]: armature_data = armature.data if not isinstance(armature_data, Armature): raise TypeError ext = get_armature_extension(armature_data) if ext.is_vrm0(): humanoid: Union[Vrm0HumanoidPropertyGroup, Vrm1HumanoidPropertyGroup] = ( ext.vrm0.humanoid ) else: humanoid = ext.vrm1.humanoid pose = humanoid.pose action = humanoid.pose_library pose_marker_name = humanoid.pose_marker_name if pose != humanoid.POSE_CUSTOM_POSE.identifier: action = None pose_marker_name = "" if ( pose == humanoid.POSE_CURRENT_POSE.identifier and armature_data.pose_position == "REST" ): yield return if pose == humanoid.POSE_REST_POSITION_POSE.identifier or ( pose == humanoid.POSE_CUSTOM_POSE.identifier and not (action and action.name in context.blend_data.actions) ): saved_pose_position = armature_data.pose_position armature_data.pose_position = "REST" try: yield finally: armature_data.pose_position = saved_pose_position return with save_workspace(context, armature): bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="POSE") saved_pose_position = armature_data.pose_position if armature_data.pose_position != "POSE": armature_data.pose_position = "POSE" context.view_layer.update() saved_pose_bone_pose = PoseBonePose.save(armature.pose) if bpy.app.version < (5, 0): for bone in armature.pose.bones: bone.bone.select = False ext = get_armature_extension(armature_data) saved_vrm1_look_at_preview = ext.vrm1.look_at.enable_preview if ext.is_vrm1() and ext.vrm1.look_at.enable_preview: # TODO: It would be helpful to warn in advance if this is # reached during export ext.vrm1.look_at.enable_preview = False if ext.vrm1.look_at.type == ext.vrm1.look_at.TYPE_BONE.identifier: human_bones = ext.vrm1.humanoid.human_bones left_eye_bone_name = human_bones.left_eye.node.bone_name left_eye_bone = armature.pose.bones.get(left_eye_bone_name) if left_eye_bone: set_rotation_without_mode_change(left_eye_bone, Quaternion()) right_eye_bone_name = human_bones.right_eye.node.bone_name right_eye_bone = armature.pose.bones.get(right_eye_bone_name) if right_eye_bone: set_rotation_without_mode_change(right_eye_bone, Quaternion()) if pose == humanoid.POSE_AUTO_POSE.identifier: ops.vrm.make_estimated_humanoid_t_pose(armature_object_name=armature.name) elif pose == humanoid.POSE_CUSTOM_POSE.identifier: if action and action.name in context.blend_data.actions: pose_marker_frame = 0 if pose_marker_name: for search_pose_marker in action.pose_markers.values(): if search_pose_marker.name == pose_marker_name: pose_marker_frame = search_pose_marker.frame break armature.pose.apply_pose_from_action( action, evaluation_time=pose_marker_frame ) else: # TODO: It would be helpful to warn in advance if this is # reached during export ops.vrm.make_estimated_humanoid_t_pose( armature_object_name=armature.name ) context.view_layer.update() try: yield # After yield, bpy native objects may be deleted or frames may advance # making them invalid. Accessing them in this state can cause crashes, # so be careful not to access potentially invalid native objects after yield finally: leave_setup_humanoid_t_pose( context, armature, saved_pose_bone_pose, saved_pose_position, saved_vrm1_look_at_preview=saved_vrm1_look_at_preview, )