# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import functools import math import sys from collections.abc import Mapping, Sequence from dataclasses import dataclass from sys import float_info from typing import TYPE_CHECKING, ClassVar, Optional import bpy from bpy.app.translations import pgettext from bpy.props import ( BoolProperty, CollectionProperty, EnumProperty, FloatProperty, FloatVectorProperty, IntProperty, PointerProperty, StringProperty, ) from bpy.types import ( Action, Armature, Context, Image, Material, Mesh, Object, PropertyGroup, ) from mathutils import Matrix, Quaternion, Vector from ...common import convert from ...common.char import DISABLE_TRANSLATION from ...common.logger import get_logger from ...common.rotation import set_rotation_without_mode_change from ...common.vrm1.human_bone import ( HumanBoneName, HumanBoneSpecification, HumanBoneSpecifications, ) from ..property_group import ( BonePropertyGroup, MaterialPropertyGroup, MeshObjectPropertyGroup, StringPropertyGroup, property_group_enum, ) if TYPE_CHECKING: from ..property_group import CollectionPropertyProtocol logger = get_logger(__name__) # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.humanoid.humanBones.humanBone.schema.json class Vrm1HumanBonePropertyGroup(PropertyGroup): node: PointerProperty( # type: ignore[valid-type] type=BonePropertyGroup ) # for UI node_candidates: CollectionProperty( # type: ignore[valid-type] type=StringPropertyGroup ) def update_node_candidates( self, armature_data: Armature, target: HumanBoneSpecification, bpy_bone_name_to_human_bone_specification: dict[str, HumanBoneSpecification], error_bpy_bone_names: Sequence[str], ) -> bool: new_candidates = BonePropertyGroup.find_bone_candidates( armature_data, target, bpy_bone_name_to_human_bone_specification, error_bpy_bone_names, ) if {n.value for n in self.node_candidates} == new_candidates: return False self.node_candidates.clear() # Preserving list order for bone_name in armature_data.bones.keys(): if bone_name not in new_candidates: continue candidate = self.node_candidates.add() candidate.value = bone_name return True if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. node: BonePropertyGroup # type: ignore[no-redef] node_candidates: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.humanoid.humanBones.schema.json class Vrm1HumanBonesPropertyGroup(PropertyGroup): hips: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) spine: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) chest: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) upper_chest: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) neck: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) head: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_eye: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_eye: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) jaw: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_upper_leg: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_lower_leg: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_foot: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_toes: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_upper_leg: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_lower_leg: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_foot: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_toes: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_shoulder: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_upper_arm: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_lower_arm: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_hand: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_shoulder: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_upper_arm: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_lower_arm: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_hand: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_thumb_metacarpal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_thumb_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_thumb_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_index_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_index_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_index_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_middle_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_middle_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_middle_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_ring_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_ring_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_ring_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_little_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_little_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) left_little_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_thumb_metacarpal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_thumb_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_thumb_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_index_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_index_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_index_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_middle_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_middle_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_middle_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_ring_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_ring_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_ring_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_little_proximal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_little_intermediate: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) right_little_distal: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanBonePropertyGroup ) # for UI last_bone_names_str: StringProperty() # type: ignore[valid-type] initial_automatic_bone_assignment: BoolProperty( # type: ignore[valid-type] default=True ) allow_non_humanoid_rig: BoolProperty( # type: ignore[valid-type] name="Allow Non-Humanoid Rig", ) def human_bone_name_to_human_bone( self, ) -> dict[HumanBoneName, Vrm1HumanBonePropertyGroup]: return { HumanBoneName.HIPS: self.hips, HumanBoneName.SPINE: self.spine, HumanBoneName.CHEST: self.chest, HumanBoneName.UPPER_CHEST: self.upper_chest, HumanBoneName.NECK: self.neck, HumanBoneName.HEAD: self.head, HumanBoneName.LEFT_EYE: self.left_eye, HumanBoneName.RIGHT_EYE: self.right_eye, HumanBoneName.JAW: self.jaw, HumanBoneName.LEFT_UPPER_LEG: self.left_upper_leg, HumanBoneName.LEFT_LOWER_LEG: self.left_lower_leg, HumanBoneName.LEFT_FOOT: self.left_foot, HumanBoneName.LEFT_TOES: self.left_toes, HumanBoneName.RIGHT_UPPER_LEG: self.right_upper_leg, HumanBoneName.RIGHT_LOWER_LEG: self.right_lower_leg, HumanBoneName.RIGHT_FOOT: self.right_foot, HumanBoneName.RIGHT_TOES: self.right_toes, HumanBoneName.LEFT_SHOULDER: self.left_shoulder, HumanBoneName.LEFT_UPPER_ARM: self.left_upper_arm, HumanBoneName.LEFT_LOWER_ARM: self.left_lower_arm, HumanBoneName.LEFT_HAND: self.left_hand, HumanBoneName.RIGHT_SHOULDER: self.right_shoulder, HumanBoneName.RIGHT_UPPER_ARM: self.right_upper_arm, HumanBoneName.RIGHT_LOWER_ARM: self.right_lower_arm, HumanBoneName.RIGHT_HAND: self.right_hand, HumanBoneName.LEFT_THUMB_METACARPAL: self.left_thumb_metacarpal, HumanBoneName.LEFT_THUMB_PROXIMAL: self.left_thumb_proximal, HumanBoneName.LEFT_THUMB_DISTAL: self.left_thumb_distal, HumanBoneName.LEFT_INDEX_PROXIMAL: self.left_index_proximal, HumanBoneName.LEFT_INDEX_INTERMEDIATE: self.left_index_intermediate, HumanBoneName.LEFT_INDEX_DISTAL: self.left_index_distal, HumanBoneName.LEFT_MIDDLE_PROXIMAL: self.left_middle_proximal, HumanBoneName.LEFT_MIDDLE_INTERMEDIATE: self.left_middle_intermediate, HumanBoneName.LEFT_MIDDLE_DISTAL: self.left_middle_distal, HumanBoneName.LEFT_RING_PROXIMAL: self.left_ring_proximal, HumanBoneName.LEFT_RING_INTERMEDIATE: self.left_ring_intermediate, HumanBoneName.LEFT_RING_DISTAL: self.left_ring_distal, HumanBoneName.LEFT_LITTLE_PROXIMAL: self.left_little_proximal, HumanBoneName.LEFT_LITTLE_INTERMEDIATE: self.left_little_intermediate, HumanBoneName.LEFT_LITTLE_DISTAL: self.left_little_distal, HumanBoneName.RIGHT_THUMB_METACARPAL: self.right_thumb_metacarpal, HumanBoneName.RIGHT_THUMB_PROXIMAL: self.right_thumb_proximal, HumanBoneName.RIGHT_THUMB_DISTAL: self.right_thumb_distal, HumanBoneName.RIGHT_INDEX_PROXIMAL: self.right_index_proximal, HumanBoneName.RIGHT_INDEX_INTERMEDIATE: self.right_index_intermediate, HumanBoneName.RIGHT_INDEX_DISTAL: self.right_index_distal, HumanBoneName.RIGHT_MIDDLE_PROXIMAL: self.right_middle_proximal, HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE: self.right_middle_intermediate, HumanBoneName.RIGHT_MIDDLE_DISTAL: self.right_middle_distal, HumanBoneName.RIGHT_RING_PROXIMAL: self.right_ring_proximal, HumanBoneName.RIGHT_RING_INTERMEDIATE: self.right_ring_intermediate, HumanBoneName.RIGHT_RING_DISTAL: self.right_ring_distal, HumanBoneName.RIGHT_LITTLE_PROXIMAL: self.right_little_proximal, HumanBoneName.RIGHT_LITTLE_INTERMEDIATE: self.right_little_intermediate, HumanBoneName.RIGHT_LITTLE_DISTAL: self.right_little_distal, } def error_messages(self) -> list[str]: messages: list[str] = [] human_bone_name_to_human_bone = self.human_bone_name_to_human_bone() for name, human_bone in human_bone_name_to_human_bone.items(): specification = HumanBoneSpecifications.get(name) if not human_bone.node.bone_name: if specification.requirement: messages.append( pgettext( 'Please assign Required VRM Human Bone "{name}".' ).format(name=specification.title) ) continue if not specification.parent_requirement: continue if not specification.parent_name: logger.error("No parent for '%s' in spec", name) continue parent = human_bone_name_to_human_bone.get(specification.parent_name) if not parent: logger.error("No parent for '%s' in dict", name) continue parent_specification = specification.parent() if not parent_specification: logger.error("No parent specification for '%s'", name) continue if not parent.node.bone_name: messages.append( pgettext( 'Please assign "{parent_name}"' + ' because "{name}" requires it as its child bone.' ).format( name=specification.title, parent_name=parent_specification.title, ) ) return messages def all_required_bones_are_assigned(self) -> bool: return len(self.error_messages()) == 0 @staticmethod def fixup_human_bones(obj: Object) -> None: armature_data = obj.data if obj.type != "ARMATURE" or not isinstance(armature_data, Armature): return human_bones = get_armature_vrm1_extension(armature_data).humanoid.human_bones # If the same Blender bone is set in multiple bone maps, delete one of them fixup = True while fixup: fixup = False found_node_bone_names: list[str] = [] for human_bone in human_bones.human_bone_name_to_human_bone().values(): if not human_bone.node.bone_name: continue if human_bone.node.bone_name not in found_node_bone_names: found_node_bone_names.append(human_bone.node.bone_name) continue human_bone.node.bone_name = "" fixup = True break @staticmethod def defer_update_all_node_candidates( armature_data_name: str, *, force: bool = False, ) -> None: bpy.app.timers.register( functools.partial( Vrm1HumanBonesPropertyGroup.update_all_node_candidates_timer_callback, armature_data_name, force=force, ) ) @staticmethod def update_all_node_candidates_timer_callback( armature_object_name: str, *, force: bool = False ) -> None: """Wrap to match the type of update_all_node_candidates() to timers.register.""" # Context cannot be passed across frames, so get a new one context = bpy.context Vrm1HumanBonesPropertyGroup.update_all_node_candidates( context, armature_object_name, force=force ) @staticmethod def update_all_node_candidates( context: Optional[Context], armature_data_name: str, *, force: bool = False, ) -> None: if context is None: context = bpy.context armature_data = context.blend_data.armatures.get(armature_data_name) if not isinstance(armature_data, Armature): return human_bones = get_armature_vrm1_extension(armature_data).humanoid.human_bones bone_names_str = "\n".join( bone.name + "\n" + (parent.name if (parent := bone.parent) else "") for bone in sorted(armature_data.bones.values(), key=lambda b: b.name) ) if not force and human_bones.last_bone_names_str == bone_names_str: return human_bones.last_bone_names_str = bone_names_str BonePropertyGroup.update_all_vrm1_node_candidates(armature_data) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. hips: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] spine: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] chest: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] upper_chest: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] neck: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] head: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_eye: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_eye: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] jaw: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_upper_leg: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_lower_leg: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_foot: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_toes: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_upper_leg: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_lower_leg: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_foot: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_toes: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_shoulder: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_upper_arm: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_lower_arm: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_hand: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_shoulder: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_upper_arm: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_lower_arm: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_hand: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_thumb_metacarpal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_thumb_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_thumb_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_index_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_index_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_index_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_middle_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_middle_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_middle_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_ring_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_ring_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_ring_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_little_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_little_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] left_little_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_thumb_metacarpal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_thumb_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_thumb_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_index_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_index_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_index_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_middle_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_middle_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_middle_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_ring_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_ring_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_ring_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_little_proximal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_little_intermediate: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] right_little_distal: Vrm1HumanBonePropertyGroup # type: ignore[no-redef] last_bone_names_str: str # type: ignore[no-redef] initial_automatic_bone_assignment: bool # type: ignore[no-redef] allow_non_humanoid_rig: bool # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.humanoid.schema.json class Vrm1HumanoidPropertyGroup(PropertyGroup): human_bones: PointerProperty(type=Vrm1HumanBonesPropertyGroup) # type: ignore[valid-type] # for T-Pose def update_pose_library(self, _context: Context) -> None: self.pose_marker_name = "" ( pose_enum, ( POSE_AUTO_POSE, POSE_REST_POSITION_POSE, POSE_CURRENT_POSE, POSE_CUSTOM_POSE, ), ) = property_group_enum( ( "autoPose", "Auto", "Auto Pose", "ARMATURE_DATA", 3, ), ( "restPositionPose", "Rest Position", "Rest Position Pose", "ARMATURE_DATA", 0, ), ( "currentPose", "Current Pose", "Current Pose", "ARMATURE_DATA", 1, ), ( "customPose", "Custom Pose", "Custom Pose", "ARMATURE_DATA", 2, ), ) pose: EnumProperty( # type: ignore[valid-type] items=pose_enum.items(), name="T-Pose", description="T-Pose", default=POSE_AUTO_POSE.identifier, ) pose_library: PointerProperty( # type: ignore[valid-type] type=Action, update=update_pose_library, ) pose_marker_name: StringProperty() # type: ignore[valid-type] if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. human_bones: Vrm1HumanBonesPropertyGroup # type: ignore[no-redef] pose: str # type: ignore[no-redef] pose_library: Optional[Action] # type: ignore[no-redef] pose_marker_name: str # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.lookAt.rangeMap.schema.json class Vrm1LookAtRangeMapPropertyGroup(PropertyGroup): input_max_value: FloatProperty( # type: ignore[valid-type] name="Input Max Value", min=0.0001, # https://github.com/pixiv/three-vrm/issues/1197#issuecomment-1498492002 default=90.0, max=180.0, ) output_scale: FloatProperty( # type: ignore[valid-type] name="Output Scale", default=10.0, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. input_max_value: float # type: ignore[no-redef] output_scale: float # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.lookAt.schema.json class Vrm1LookAtPropertyGroup(PropertyGroup): offset_from_head_bone: FloatVectorProperty( # type: ignore[valid-type] name="Offset From Head Bone", size=3, subtype="TRANSLATION", unit="LENGTH", default=(0, 0, 0), ) ( type_enum, ( TYPE_BONE, TYPE_EXPRESSION, ), ) = property_group_enum( ("bone", "Bone", "Bone", "BONE_DATA", 0), ( "expression", "Expression" + DISABLE_TRANSLATION, "Expression", "SHAPEKEY_DATA", 1, ), ) type: EnumProperty( # type: ignore[valid-type] name="Type", items=type_enum.items(), ) range_map_horizontal_inner: PointerProperty( # type: ignore[valid-type] type=Vrm1LookAtRangeMapPropertyGroup, ) range_map_horizontal_outer: PointerProperty( # type: ignore[valid-type] type=Vrm1LookAtRangeMapPropertyGroup, ) range_map_vertical_down: PointerProperty( # type: ignore[valid-type] type=Vrm1LookAtRangeMapPropertyGroup, ) range_map_vertical_up: PointerProperty( # type: ignore[valid-type] type=Vrm1LookAtRangeMapPropertyGroup, ) enable_preview: BoolProperty( # type: ignore[valid-type] name="Enable Preview", ) preview_target_bpy_object: PointerProperty( # type: ignore[valid-type] type=Object, name="Preview Target", ) previous_preview_target_bpy_object_location: FloatVectorProperty( # type: ignore[valid-type] size=3, ) @staticmethod def update_all_previews(context: Context) -> None: for armature_object in context.blend_data.objects: if armature_object.type != "ARMATURE": continue armature_data = armature_object.data if not isinstance(armature_data, Armature): continue look_at = get_armature_vrm1_extension(armature_data).look_at look_at.update_preview(context, armature_object, armature_data) def update_preview( self, _context: Context, armature_object: Object, armature_data: Armature, ) -> None: vrm1 = get_armature_vrm1_extension(armature_data) preview_target_bpy_object = self.preview_target_bpy_object if not preview_target_bpy_object: return to_translation = preview_target_bpy_object.matrix_world.to_translation() if ( Vector(self.previous_preview_target_bpy_object_location) - to_translation ).length_squared < float_info.epsilon: return self.previous_preview_target_bpy_object_location = to_translation head_bone_name = vrm1.humanoid.human_bones.head.node.bone_name head_pose_bone = armature_object.pose.bones.get(head_bone_name) if not head_pose_bone: return # TODO: Honor t-pose action rest_head_bone_matrix = ( armature_object.matrix_world @ head_pose_bone.bone.convert_local_to_pose( Matrix(), head_pose_bone.bone.matrix_local, ) ) rest_head_bone_inverted_rotation = ( rest_head_bone_matrix.to_quaternion().inverted() ) head_parent_pose_bone = head_pose_bone.parent if not head_parent_pose_bone: return head_bone_without_rotation_matrix = ( armature_object.matrix_world @ head_pose_bone.bone.convert_local_to_pose( Matrix(), head_pose_bone.bone.matrix_local, parent_matrix=head_parent_pose_bone.matrix, parent_matrix_local=head_parent_pose_bone.bone.matrix_local, ) ) local_target_translation = ( head_bone_without_rotation_matrix ).inverted_safe() @ to_translation - Vector(self.offset_from_head_bone) forward_vector = rest_head_bone_inverted_rotation @ Vector((0, -1, 0)) right_vector = rest_head_bone_inverted_rotation @ Vector((-1, 0, 0)) up_vector = rest_head_bone_inverted_rotation @ Vector((0, 0, 1)) # logger.warning(f"local_target_translation={dump(local_target_translation)}") # logger.warning(f"forward={dump(forward_vector)}") # logger.warning(f"right={dump(right_vector)}") # logger.warning(f"up={dump(up_vector)}") forward_length = local_target_translation.dot(forward_vector) right_length = local_target_translation.dot(right_vector) up_length = local_target_translation.dot(up_vector) # https://github.com/vrm-c/vrm-specification/blob/0861a66eb2f2b76835322d775678047d616536b3/specification/VRMC_vrm-1.0/lookAt.md?plain=1#L180 if abs(forward_length) < float_info.epsilon: return yaw_degrees = math.degrees(math.atan2(right_length, forward_length)) pitch_degrees = math.degrees( math.atan2( up_length, math.sqrt(math.pow(right_length, 2) + math.pow(forward_length, 2)), ) ) if vrm1.look_at.type == vrm1.look_at.TYPE_BONE.identifier: self.apply_eye_bone_preview( vrm1, yaw_degrees, pitch_degrees, armature_object, HumanBoneName.RIGHT_EYE, ) self.apply_eye_bone_preview( vrm1, yaw_degrees, pitch_degrees, armature_object, HumanBoneName.LEFT_EYE, ) elif vrm1.look_at.type == vrm1.look_at.TYPE_EXPRESSION.identifier: self.apply_expression_preview(vrm1, yaw_degrees, pitch_degrees) # https://github.com/vrm-c/vrm-specification/blob/0861a66eb2f2b76835322d775678047d616536b3/specification/VRMC_vrm-1.0/lookAt.md?plain=1#L230 def apply_eye_bone_preview( self, vrm1: "Vrm1PropertyGroup", yaw_degrees: float, pitch_degrees: float, armature_object: Object, human_bone_name: HumanBoneName, ) -> None: if human_bone_name == HumanBoneName.RIGHT_EYE: range_map_horizontal_right = vrm1.look_at.range_map_horizontal_outer range_map_horizontal_left = vrm1.look_at.range_map_horizontal_inner pose_bone = armature_object.pose.bones.get( vrm1.humanoid.human_bones.right_eye.node.bone_name ) elif human_bone_name == HumanBoneName.LEFT_EYE: range_map_horizontal_right = vrm1.look_at.range_map_horizontal_inner range_map_horizontal_left = vrm1.look_at.range_map_horizontal_outer pose_bone = armature_object.pose.bones.get( vrm1.humanoid.human_bones.left_eye.node.bone_name ) else: return range_map_vertical_up = vrm1.look_at.range_map_vertical_up range_map_vertical_down = vrm1.look_at.range_map_vertical_down if not pose_bone: return parent_pose_bone = pose_bone.parent if not parent_pose_bone: return if yaw_degrees < 0: yaw_degrees = ( -min(abs(yaw_degrees), range_map_horizontal_left.input_max_value) / range_map_horizontal_left.input_max_value * range_map_horizontal_left.output_scale ) else: yaw_degrees = ( min(abs(yaw_degrees), range_map_horizontal_right.input_max_value) / range_map_horizontal_right.input_max_value * range_map_horizontal_right.output_scale ) if pitch_degrees < 0: # down pitch_degrees = ( -min(abs(pitch_degrees), range_map_vertical_down.input_max_value) / range_map_vertical_down.input_max_value * range_map_vertical_down.output_scale ) else: # up pitch_degrees = ( min(abs(pitch_degrees), range_map_vertical_up.input_max_value) / range_map_vertical_up.input_max_value * range_map_vertical_up.output_scale ) # TODO: Honor t-pose action rest_bone_matrix = ( armature_object.matrix_world @ pose_bone.bone.convert_local_to_pose( Matrix(), pose_bone.bone.matrix_local, ) ) reset_bone_inverted_rotation = rest_bone_matrix.to_quaternion().inverted() forward_vector = reset_bone_inverted_rotation @ Vector((0, -1, 0)) right_vector = reset_bone_inverted_rotation @ Vector((-1, 0, 0)) up_vector = reset_bone_inverted_rotation @ Vector((0, 0, 1)) # logger.warning(f"{yaw_degrees=} {pitch_degrees=}") # logger.warning(f"forward={dump(forward_vector)}") # logger.warning(f"right={dump(right_vector)}") # logger.warning(f"up={dump(up_vector)}") rotation = Quaternion( forward_vector.cross(right_vector), math.radians(yaw_degrees) ) @ Quaternion(forward_vector.cross(up_vector), math.radians(pitch_degrees)) # logger.warning(f"rotation={dump(rotation)}") set_rotation_without_mode_change(pose_bone, rotation) # https://github.com/vrm-c/vrm-specification/blob/0861a66eb2f2b76835322d775678047d616536b3/specification/VRMC_vrm-1.0/lookAt.md?plain=1#L258 def apply_expression_preview( self, vrm1: "Vrm1PropertyGroup", yaw_degrees: float, pitch_degrees: float, ) -> None: range_map_horizontal_outer = vrm1.look_at.range_map_horizontal_outer range_map_vertical_up = vrm1.look_at.range_map_vertical_up range_map_vertical_down = vrm1.look_at.range_map_vertical_down look_left = vrm1.expressions.preset.look_left look_right = vrm1.expressions.preset.look_right look_up = vrm1.expressions.preset.look_up look_down = vrm1.expressions.preset.look_down # horizontal if yaw_degrees < 0: # left yaw_weight = ( min(abs(yaw_degrees), range_map_horizontal_outer.input_max_value) / range_map_horizontal_outer.input_max_value * range_map_horizontal_outer.output_scale ) look_left.preview = yaw_weight look_right.preview = 0 else: # right yaw_weight = ( min(yaw_degrees, range_map_horizontal_outer.input_max_value) / range_map_horizontal_outer.input_max_value * range_map_horizontal_outer.output_scale ) look_left.preview = 0 look_right.preview = yaw_weight # vertical if pitch_degrees < 0: # down pitch_weight = ( min(abs(pitch_degrees), range_map_vertical_down.input_max_value) / range_map_vertical_down.input_max_value * range_map_vertical_down.output_scale ) look_down.preview = pitch_weight look_up.preview = 0 else: # up pitch_weight = ( min(pitch_degrees, range_map_vertical_up.input_max_value) / range_map_vertical_up.input_max_value * range_map_vertical_up.output_scale ) look_down.preview = 0 look_up.preview = pitch_weight if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. offset_from_head_bone: Sequence[float] # type: ignore[no-redef] type: str # type: ignore[no-redef] range_map_horizontal_inner: ( # type: ignore[no-redef] Vrm1LookAtRangeMapPropertyGroup ) range_map_horizontal_outer: ( # type: ignore[no-redef] Vrm1LookAtRangeMapPropertyGroup ) range_map_vertical_down: ( # type: ignore[no-redef] Vrm1LookAtRangeMapPropertyGroup ) range_map_vertical_up: Vrm1LookAtRangeMapPropertyGroup # type: ignore[no-redef] enable_preview: bool # type: ignore[no-redef] preview_target_bpy_object: Optional[Object] # type: ignore[no-redef] previous_preview_target_bpy_object_location: ( # type: ignore[no-redef] Sequence[float] ) # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.firstPerson.meshAnnotation.schema.json class Vrm1MeshAnnotationPropertyGroup(PropertyGroup): node: PointerProperty( # type: ignore[valid-type] type=MeshObjectPropertyGroup ) type_enum, __types = property_group_enum( ("auto", "Auto", "", "NONE", 0), ("both", "Both", "", "NONE", 1), ("thirdPersonOnly", "Third-Person Only", "", "NONE", 2), ("firstPersonOnly", "First-Person Only", "", "NONE", 3), ) type: EnumProperty( # type: ignore[valid-type] items=type_enum.items(), name="First Person Type", ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. node: MeshObjectPropertyGroup # type: ignore[no-redef] type: str # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.firstPerson.schema.json class Vrm1FirstPersonPropertyGroup(PropertyGroup): mesh_annotations: CollectionProperty( # type: ignore[valid-type] name="Mesh Annotations", type=Vrm1MeshAnnotationPropertyGroup, ) # for UI active_mesh_annotation_index: IntProperty(min=0) # type: ignore[valid-type] if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. mesh_annotations: CollectionPropertyProtocol[ # type: ignore[no-redef] Vrm1MeshAnnotationPropertyGroup ] active_mesh_annotation_index: int # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.morphTargetBind.schema.json class Vrm1MorphTargetBindPropertyGroup(PropertyGroup): node: PointerProperty( # type: ignore[valid-type] type=MeshObjectPropertyGroup ) index: StringProperty( # type: ignore[valid-type] ) weight: FloatProperty( # type: ignore[valid-type] min=0, default=1, max=1, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. node: MeshObjectPropertyGroup # type: ignore[no-redef] index: str # type: ignore[no-redef] weight: float # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.materialColorBind.schema.json class Vrm1MaterialColorBindPropertyGroup(PropertyGroup): material: PointerProperty( # type: ignore[valid-type] name="Material", type=Material, ) type_enum, __types = property_group_enum( ("color", "Color", "", "NONE", 0), ("emissionColor", "Emission Color", "", "NONE", 1), ("shadeColor", "Shade Color", "", "NONE", 2), ("matcapColor", "Matcap Color", "", "NONE", 5), ("rimColor", "Rim Color", "", "NONE", 3), ("outlineColor", "Outline Color", "", "NONE", 4), ) type: EnumProperty( # type: ignore[valid-type] name="Type", items=type_enum.items(), ) target_value: FloatVectorProperty( # type: ignore[valid-type] name="Target Value", size=4, subtype="COLOR", min=0, max=1, # TODO: hdr emission color? ) def get_target_value_as_rgb(self) -> tuple[float, float, float]: return ( self.target_value[0], self.target_value[1], self.target_value[2], ) def set_target_value_as_rgb(self, value: Sequence[float]) -> None: if len(value) < 3: return self.target_value = ( value[0], value[1], value[2], self.target_value[3], ) target_value_as_rgb: FloatVectorProperty( # type: ignore[valid-type] name="Target Value", size=3, subtype="COLOR", get=get_target_value_as_rgb, set=set_target_value_as_rgb, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. material: Optional[Material] # type: ignore[no-redef] type: str # type: ignore[no-redef] target_value: Sequence[float] # type: ignore[no-redef] target_value_as_rgb: Sequence[float] # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.textureTransformBind.schema.json class Vrm1TextureTransformBindPropertyGroup(PropertyGroup): material: PointerProperty( # type: ignore[valid-type] name="Material", type=Material, ) scale: FloatVectorProperty( # type: ignore[valid-type] size=2, default=(1, 1), ) offset: FloatVectorProperty( # type: ignore[valid-type] size=2, default=(0, 0), ) show_experimental_preview_feature: BoolProperty( # type: ignore[valid-type] name="[Experimental] Show Preview Feature" ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. material: Optional[Material] # type: ignore[no-redef] scale: Sequence[float] # type: ignore[no-redef] offset: Sequence[float] # type: ignore[no-redef] show_experimental_preview_feature: bool # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.expression.schema.json class Vrm1ExpressionPropertyGroup(PropertyGroup): morph_target_binds: CollectionProperty( # type: ignore[valid-type] type=Vrm1MorphTargetBindPropertyGroup ) material_color_binds: CollectionProperty( # type: ignore[valid-type] type=Vrm1MaterialColorBindPropertyGroup ) texture_transform_binds: CollectionProperty( # type: ignore[valid-type] type=Vrm1TextureTransformBindPropertyGroup ) is_binary: BoolProperty( # type: ignore[valid-type] name="Is Binary" ) materials_to_update: CollectionProperty( # type: ignore[valid-type] type=MaterialPropertyGroup, options={"HIDDEN"} ) expression_override_type_enum, __expression_override_types = property_group_enum( ("none", "None", "", "NONE", 0), ("block", "Block", "", "NONE", 1), ("blend", "Blend", "", "NONE", 2), ) def update_preview(self, context: Context) -> None: self.update_previews(self, context) @staticmethod def update_previews( triggered_expression: "Vrm1ExpressionPropertyGroup", context: Context, ) -> None: if not triggered_expression.name: logger.debug("Unnamed expression: %s", type(triggered_expression)) return armature = triggered_expression.id_data if not isinstance(armature, Armature): # This is getting triggered after importing VRMA files logger.error("No armature for %s", triggered_expression.name) return expressions = get_armature_vrm1_extension(armature).expressions name_to_expression_dict = expressions.all_name_to_expression_dict() mouth_block_rate = 0.0 blink_block_rate = 0.0 look_at_block_rate = 0.0 for expression in name_to_expression_dict.values(): if expression.preview < float_info.epsilon: continue if expression.override_mouth == "blend": mouth_block_rate += expression.preview if expression.override_blink == "blend": blink_block_rate += expression.preview if expression.override_look_at == "blend": look_at_block_rate += expression.preview if expression.override_mouth == "block": mouth_block_rate = 1.0 if expression.override_blink == "block": blink_block_rate = 1.0 if expression.override_look_at == "block": look_at_block_rate = 1.0 mouth_blend_factor = max(0, min(1 - mouth_block_rate, 1)) blink_blend_factor = max(0, min(1 - blink_block_rate, 1)) look_at_blend_factor = max(0, min(1 - look_at_block_rate, 1)) shape_key_name_and_key_block_name_to_value: dict[tuple[str, str], float] = {} for name, expression in name_to_expression_dict.items(): if expression.is_binary: # https://github.com/vrm-c/vrm-specification/blob/5b1071b9da1d60bb1bab0b70754615127fc43e9e/specification/VRMC_vrm-1.0/expressions.md?plain=1#L46 preview = 1.0 if expression.preview > 0.5 else 0.0 else: preview = expression.preview if expressions.preset.is_mouth_expression(name): preview *= mouth_blend_factor elif expressions.preset.is_blink_expression(name): preview *= blink_blend_factor elif expressions.preset.is_look_at_expression(name): preview *= look_at_blend_factor for texture_transform_bind in expression.texture_transform_binds: material = texture_transform_bind.material if not material: continue for material_color_bind in expression.material_color_binds: material = material_color_bind.material if not material: continue for morph_target_bind in expression.morph_target_binds: mesh_object = context.blend_data.objects.get( morph_target_bind.node.mesh_object_name ) if not mesh_object or mesh_object.type != "MESH": continue mesh = mesh_object.data if not isinstance(mesh, Mesh): continue mesh_shape_keys = mesh.shape_keys if not mesh_shape_keys: continue shape_key = context.blend_data.shape_keys.get(mesh_shape_keys.name) if not shape_key: continue key_blocks = shape_key.key_blocks if not key_blocks: continue key_block = key_blocks.get(morph_target_bind.index) if not key_block: continue value = morph_target_bind.weight * preview shape_key_name_and_key_block_name = (shape_key.name, key_block.name) shape_key_name_and_key_block_name_to_value[ shape_key_name_and_key_block_name ] = ( shape_key_name_and_key_block_name_to_value.get( shape_key_name_and_key_block_name, 0.0 ) + value ) for ( shape_key_name, key_block_name, ), value in shape_key_name_and_key_block_name_to_value.items(): shape_key = context.blend_data.shape_keys.get(shape_key_name) if not shape_key: continue key_blocks = shape_key.key_blocks if not key_blocks: continue key_block = key_blocks.get(key_block_name) if not key_block: continue key_block.value = value Vrm1ExpressionPropertyGroup.frame_change_post_shape_key_updates.update( shape_key_name_and_key_block_name_to_value ) override_blink: EnumProperty( # type: ignore[valid-type] name="Override Blink", items=expression_override_type_enum.items(), update=update_preview, ) override_look_at: EnumProperty( # type: ignore[valid-type] name="Override Look At", items=expression_override_type_enum.items(), update=update_preview, ) override_mouth: EnumProperty( # type: ignore[valid-type] name="Override Mouth", items=expression_override_type_enum.items(), update=update_preview, ) # for UI show_expanded: BoolProperty() # type: ignore[valid-type] show_expanded_morph_target_binds: BoolProperty( # type: ignore[valid-type] name="Morph Target Binds" ) show_expanded_material_color_binds: BoolProperty( # type: ignore[valid-type] name="Material Color Binds" ) show_expanded_texture_transform_binds: BoolProperty( # type: ignore[valid-type] name="Texture Transform Binds" ) # Since the value of the shape key can only be changed in # frame_change_pre/frame_change_post during animation playback, the # changed value is saved here. frame_change_post_shape_key_updates: ClassVar[dict[tuple[str, str], float]] = {} def get_preview(self) -> float: value = self.get("preview") if isinstance(value, (float, int)): return float(value) return 0.0 def set_preview(self, value_obj: object) -> None: context = bpy.context value = convert.float_or_none(value_obj) if value is None: return current_value = convert.float_or_none(self.get("preview")) if ( current_value is not None and abs(current_value - value) < float_info.epsilon ): return self["preview"] = value Vrm1ExpressionPropertyGroup.update_preview(self, context) preview: FloatProperty( # type: ignore[valid-type] name="Expression", min=0, max=1, subtype="FACTOR", get=get_preview, set=set_preview, ) @classmethod def update_materials(cls, context: Context) -> None: for armature in context.blend_data.armatures: ext = get_armature_vrm1_extension(armature) expressions = ext.expressions for expression in expressions.all_name_to_expression_dict().values(): materials_to_update = expression.materials_to_update if not materials_to_update: continue for material_property_group in materials_to_update: material = material_property_group.material if not material: continue node_tree = material.node_tree if not node_tree: continue # The update() method exists in the documentation, but # the call fails # https://docs.blender.org/api/4.2/bpy.types.NodeTree.html#bpy.types.NodeTree.update # node_tree.update() materials_to_update.clear() active_morph_target_bind_index: IntProperty(min=0) # type: ignore[valid-type] active_material_color_bind_index: IntProperty(min=0) # type: ignore[valid-type] active_texture_transform_bind_index: IntProperty(min=0) # type: ignore[valid-type] if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. morph_target_binds: CollectionPropertyProtocol[ # type: ignore[no-redef] Vrm1MorphTargetBindPropertyGroup ] material_color_binds: CollectionPropertyProtocol[ # type: ignore[no-redef] Vrm1MaterialColorBindPropertyGroup ] texture_transform_binds: CollectionPropertyProtocol[ # type: ignore[no-redef] Vrm1TextureTransformBindPropertyGroup ] is_binary: bool # type: ignore[no-redef] materials_to_update: CollectionPropertyProtocol[ # type: ignore[no-redef] MaterialPropertyGroup ] override_blink: str # type: ignore[no-redef] override_look_at: str # type: ignore[no-redef] override_mouth: str # type: ignore[no-redef] show_expanded: bool # type: ignore[no-redef] show_expanded_morph_target_binds: bool # type: ignore[no-redef] show_expanded_material_color_binds: bool # type: ignore[no-redef] show_expanded_texture_transform_binds: bool # type: ignore[no-redef] preview: float # type: ignore[no-redef] active_morph_target_bind_index: int # type: ignore[no-redef] active_material_color_bind_index: int # type: ignore[no-redef] active_texture_transform_bind_index: int # type: ignore[no-redef] class Vrm1CustomExpressionPropertyGroup(Vrm1ExpressionPropertyGroup): def get_custom_name(self) -> str: return str(self.get("custom_name", "")) def set_custom_name(self, value: str) -> None: if not value or self.get("custom_name") == value: return armature_data = self.id_data if not isinstance(armature_data, Armature): logger.error("No armature for %s", self) return expressions = get_armature_vrm1_extension(armature_data).expressions all_expression_names = expressions.all_name_to_expression_dict().keys() custom_name = value for index in range(sys.maxsize): if index > 0: custom_name = f"{value}.{index:03}" if custom_name not in all_expression_names: break self["custom_name"] = custom_name self.name = custom_name custom_name: StringProperty( # type: ignore[valid-type] name="Name", get=get_custom_name, set=set_custom_name, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. custom_name: str # type: ignore[no-redef] @dataclass(frozen=True) class ExpressionPreset: name: str icon: str mouth: bool blink: bool look_at: bool # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.schema.json class Vrm1ExpressionsPresetPropertyGroup(PropertyGroup): happy: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] angry: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] sad: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] relaxed: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] surprised: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] neutral: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] aa: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] ih: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] ou: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] ee: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] oh: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] blink: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] blink_left: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] blink_right: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] look_up: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] look_down: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] look_left: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] look_right: PointerProperty(type=Vrm1ExpressionPropertyGroup) # type: ignore[valid-type] def expression_preset_and_expressions( self, ) -> tuple[tuple[ExpressionPreset, Vrm1ExpressionPropertyGroup], ...]: return ( ( ExpressionPreset( "happy", "HEART", mouth=False, blink=False, look_at=False ), self.happy, ), ( ExpressionPreset( "angry", "ORPHAN_DATA", mouth=False, blink=False, look_at=False ), self.angry, ), ( ExpressionPreset( "sad", "MOD_FLUIDSIM", mouth=False, blink=False, look_at=False ), self.sad, ), ( ExpressionPreset( "relaxed", "LIGHT_SUN", mouth=False, blink=False, look_at=False ), self.relaxed, ), ( ExpressionPreset( "surprised", "LIGHT_SUN", mouth=False, blink=False, look_at=False ), self.surprised, ), ( ExpressionPreset( "neutral", "VIEW_ORTHO", mouth=False, blink=False, look_at=False ), self.neutral, ), ( ExpressionPreset( "aa", "EVENT_A", mouth=True, blink=False, look_at=False ), self.aa, ), ( ExpressionPreset( "ih", "EVENT_I", mouth=True, blink=False, look_at=False ), self.ih, ), ( ExpressionPreset( "ou", "EVENT_U", mouth=True, blink=False, look_at=False ), self.ou, ), ( ExpressionPreset( "ee", "EVENT_E", mouth=True, blink=False, look_at=False ), self.ee, ), ( ExpressionPreset( "oh", "EVENT_O", mouth=True, blink=False, look_at=False ), self.oh, ), ( ExpressionPreset( "blink", "HIDE_ON", mouth=False, blink=True, look_at=False ), self.blink, ), ( ExpressionPreset( "blinkLeft", "HIDE_ON", mouth=False, blink=True, look_at=False ), self.blink_left, ), ( ExpressionPreset( "blinkRight", "HIDE_ON", mouth=False, blink=True, look_at=False ), self.blink_right, ), ( ExpressionPreset( "lookUp", "ANCHOR_TOP", mouth=False, blink=False, look_at=True ), self.look_up, ), ( ExpressionPreset( "lookDown", "ANCHOR_BOTTOM", mouth=False, blink=False, look_at=True ), self.look_down, ), ( ExpressionPreset( "lookLeft", "ANCHOR_RIGHT", mouth=False, blink=False, look_at=True ), self.look_left, ), ( ExpressionPreset( "lookRight", "ANCHOR_LEFT", mouth=False, blink=False, look_at=True ), self.look_right, ), ) def name_to_expression_dict(self) -> Mapping[str, Vrm1ExpressionPropertyGroup]: return { preset.name: property_group for preset, property_group in self.expression_preset_and_expressions() } def is_mouth_expression(self, expression_name: str) -> bool: return next( ( preset.mouth for preset, _ in self.expression_preset_and_expressions() if preset.name == expression_name ), False, ) def is_blink_expression(self, expression_name: str) -> bool: return next( ( preset.blink for preset, _ in self.expression_preset_and_expressions() if preset.name == expression_name ), False, ) def get_icon(self, expression_name: str) -> Optional[str]: return next( ( preset.icon for preset, _ in self.expression_preset_and_expressions() if preset.name == expression_name ), None, ) def is_look_at_expression(self, expression_name: str) -> bool: return next( ( preset.look_at for preset, _ in self.expression_preset_and_expressions() if preset.name == expression_name ), False, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. happy: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] angry: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] sad: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] relaxed: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] surprised: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] neutral: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] aa: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] ih: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] ou: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] ee: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] oh: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] blink: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] blink_left: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] blink_right: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] look_up: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] look_down: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] look_left: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] look_right: Vrm1ExpressionPropertyGroup # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.expressions.schema.json class Vrm1ExpressionsPropertyGroup(PropertyGroup): preset: PointerProperty( # type: ignore[valid-type] type=Vrm1ExpressionsPresetPropertyGroup, ) custom: CollectionProperty( # type: ignore[valid-type] type=Vrm1CustomExpressionPropertyGroup, ) def all_name_to_expression_dict(self) -> Mapping[str, Vrm1ExpressionPropertyGroup]: result: dict[str, Vrm1ExpressionPropertyGroup] = dict( self.preset.name_to_expression_dict() ) for custom_expression in self.custom: result[custom_expression.custom_name] = custom_expression return result # An empty element is held for the number of expressions for UIList # display of expressions expression_ui_list_elements: CollectionProperty( # type: ignore[valid-type] type=StringPropertyGroup ) active_expression_ui_list_element_index: IntProperty(min=0) # type: ignore[valid-type] def restore_expression_morph_target_bind_object_assignments( self, context: Context ) -> None: for expression in self.all_name_to_expression_dict().values(): for morph_target_bind in expression.morph_target_binds: morph_target_bind.node.restore_object_assignment(context) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. preset: Vrm1ExpressionsPresetPropertyGroup # type: ignore[no-redef] custom: CollectionPropertyProtocol[ # type: ignore[no-redef] Vrm1CustomExpressionPropertyGroup ] expression_ui_list_elements: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] active_expression_ui_list_element_index: int # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.meta.schema.json class Vrm1MetaPropertyGroup(PropertyGroup): avatar_permission_enum, __avatar_permissions = property_group_enum( ("onlyAuthor", "Only Author", "", "NONE", 0), ( "onlySeparatelyLicensedPerson", "Only Separately Licensed Person", "", "NONE", 1, ), ("everyone", "Everyone", "", "NONE", 2), ) commercial_usage_enum, __commercial_usage = property_group_enum( ("personalNonProfit", "Personal Non-Profit", "", "NONE", 0), ("personalProfit", "Personal Profit", "", "NONE", 1), ("corporation", "Corporation", "", "NONE", 2), ) credit_notation_enum, __credit_notation = property_group_enum( ("required", "Required", "", "NONE", 0), ("unnecessary", "Unnecessary", "", "NONE", 1), ) modification_enum, __modification = property_group_enum( ("prohibited", "Prohibited", "", "NONE", 0), ("allowModification", "Allow Modification", "", "NONE", 1), ( "allowModificationRedistribution", "Allow Modification Redistribution", "", "NONE", 2, ), ) vrm_name: StringProperty( # type: ignore[valid-type] name="Name" ) version: StringProperty( # type: ignore[valid-type] name="Version" ) authors: CollectionProperty( # type: ignore[valid-type] type=StringPropertyGroup ) copyright_information: StringProperty( # type: ignore[valid-type] name="Copyright Information" ) contact_information: StringProperty( # type: ignore[valid-type] name="Contact Information" ) references: CollectionProperty( # type: ignore[valid-type] type=StringPropertyGroup ) third_party_licenses: StringProperty( # type: ignore[valid-type] name="Third Party Licenses" ) thumbnail_image: PointerProperty( # type: ignore[valid-type] name="Thumbnail Image", type=Image, ) # license_url: StringProperty( # type: ignore[valid-type] # name="License URL" # ) avatar_permission: EnumProperty( # type: ignore[valid-type] name="Avatar Permission", items=avatar_permission_enum.items(), ) allow_excessively_violent_usage: BoolProperty( # type: ignore[valid-type] name="Allow Excessively Violent Usage" ) allow_excessively_sexual_usage: BoolProperty( # type: ignore[valid-type] name="Allow Excessively Sexual Usage" ) commercial_usage: EnumProperty( # type: ignore[valid-type] name="Commercial Usage", items=commercial_usage_enum.items(), ) allow_political_or_religious_usage: BoolProperty( # type: ignore[valid-type] name="Allow Political or Religious Usage" ) allow_antisocial_or_hate_usage: BoolProperty( # type: ignore[valid-type] name="Allow Antisocial or Hate Usage" ) credit_notation: EnumProperty( # type: ignore[valid-type] name="Credit Notation", items=credit_notation_enum.items(), ) allow_redistribution: BoolProperty( # type: ignore[valid-type] name="Allow Redistribution" ) modification: EnumProperty( # type: ignore[valid-type] name="Modification", items=modification_enum.items(), ) other_license_url: StringProperty( # type: ignore[valid-type] name="Other License URL" ) # for UI active_author_index: IntProperty(min=0) # type: ignore[valid-type] active_reference_index: IntProperty(min=0) # type: ignore[valid-type] if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. vrm_name: str # type: ignore[no-redef] version: str # type: ignore[no-redef] authors: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] copyright_information: str # type: ignore[no-redef] contact_information: str # type: ignore[no-redef] references: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] third_party_licenses: str # type: ignore[no-redef] thumbnail_image: Optional[Image] # type: ignore[no-redef] avatar_permission: str # type: ignore[no-redef] allow_excessively_violent_usage: bool # type: ignore[no-redef] allow_excessively_sexual_usage: bool # type: ignore[no-redef] commercial_usage: str # type: ignore[no-redef] allow_political_or_religious_usage: bool # type: ignore[no-redef] allow_antisocial_or_hate_usage: bool # type: ignore[no-redef] credit_notation: str # type: ignore[no-redef] allow_redistribution: bool # type: ignore[no-redef] modification: str # type: ignore[no-redef] other_license_url: str # type: ignore[no-redef] active_author_index: int # type: ignore[no-redef] active_reference_index: int # type: ignore[no-redef] # https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_vrm-1.0-beta/schema/VRMC_vrm.schema.json class Vrm1PropertyGroup(PropertyGroup): meta: PointerProperty( # type: ignore[valid-type] type=Vrm1MetaPropertyGroup ) humanoid: PointerProperty( # type: ignore[valid-type] type=Vrm1HumanoidPropertyGroup ) first_person: PointerProperty( # type: ignore[valid-type] type=Vrm1FirstPersonPropertyGroup ) look_at: PointerProperty( # type: ignore[valid-type] type=Vrm1LookAtPropertyGroup ) expressions: PointerProperty( # type: ignore[valid-type] type=Vrm1ExpressionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. meta: Vrm1MetaPropertyGroup # type: ignore[no-redef] humanoid: Vrm1HumanoidPropertyGroup # type: ignore[no-redef] first_person: Vrm1FirstPersonPropertyGroup # type: ignore[no-redef] look_at: Vrm1LookAtPropertyGroup # type: ignore[no-redef] expressions: Vrm1ExpressionsPropertyGroup # type: ignore[no-redef] def get_armature_vrm1_extension(armature: Armature) -> Vrm1PropertyGroup: from ..extension import get_armature_extension vrm1: Vrm1PropertyGroup = get_armature_extension(armature).vrm1 return vrm1