From f16105517e4d28ccbd3bba5bddb4f85c5963b395 Mon Sep 17 00:00:00 2001 From: 989onan Date: Wed, 2 Apr 2025 22:09:27 -0400 Subject: [PATCH] fix flip animation add to menu fix resonite animx importer bug add flip animations add flip animation keyframes to help users rekey and remake animations as if they were mirrored. --- core/common.py | 9 +- core/resonite_loader/resonite_animx.py | 1 - core/resonite_utils.py | 2 +- functions/tools/bone_tools.py | 110 ++++++++++++++++++++++++- resources/translations/en_US.json | 2 + ui/tools_panel.py | 37 +++++---- ui/uv_tools.py | 2 +- 7 files changed, 140 insertions(+), 23 deletions(-) diff --git a/core/common.py b/core/common.py index de56360..4e563d5 100644 --- a/core/common.py +++ b/core/common.py @@ -15,10 +15,9 @@ from bpy.types import (Context, Object, Modifier, EditBone, Operator, Material, from functools import lru_cache from bpy.props import PointerProperty, IntProperty, StringProperty from bpy.utils import register_class -from ..core.logging_setup import logger -from ..core.translations import t -from ..core.dictionaries import bone_names -from .dictionaries import reverse_bone_lookup, bone_names +from .logging_setup import logger +from .translations import t +from .dictionaries import reverse_bone_lookup, simplify_bonename class SceneMatClass(PropertyGroup): mat: PointerProperty(type=Material) @@ -383,7 +382,7 @@ def clear_unused_data_blocks() -> int: if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection)) return initial_count - final_count -def identify_bones(arm_data: bpy.types.Armature, context: bpy.types.Context) -> Dict[str,str]: +def identify_bones(arm_data: bpy.types.Armature) -> Dict[str,str]: """Identify bone names in an armature based on our reverse dictionary, so there is no confusion to what a bone is. Essentially makes a dictionary of keys from dictionaries.bone_names like "hips", and the corosponding value is the bone that can be mapped to that key.""" returned: Dict[str,str] = {} diff --git a/core/resonite_loader/resonite_animx.py b/core/resonite_loader/resonite_animx.py index ad6c4cf..4d15155 100644 --- a/core/resonite_loader/resonite_animx.py +++ b/core/resonite_loader/resonite_animx.py @@ -3,7 +3,6 @@ from os import replace from re import S from types import FrameType -import lz4.block from . import resonite_types from . import common diff --git a/core/resonite_utils.py b/core/resonite_utils.py index 5312061..02d56ed 100644 --- a/core/resonite_utils.py +++ b/core/resonite_utils.py @@ -77,7 +77,7 @@ class AvatarToolkit_OT_ConvertResonite(Operator): total_bones = len(arm_data.bones) with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress: - for key_simple,bone_name in identify_bones(arm_data,context).items(): + for key_simple,bone_name in identify_bones(arm_data).items(): if key_simple in resonite_translations: new_name = resonite_translations[key_simple] diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index ff420e9..9df48ce 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -9,6 +9,7 @@ from ...core.common import ( ProgressTracker, restore_bone_transforms, remove_unused_vertex_groups, + identify_bones, ) from ...core.armature_validation import validate_armature, validate_bone_hierarchy @@ -303,4 +304,111 @@ class AvatarToolKit_OT_RemoveSelectedBones(Operator): toolkit.zero_weight_bones.clear() self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones))) - return {'FINISHED'} \ No newline at end of file + return {'FINISHED'} + + +class AvatarToolKit_OT_FlipCurrentKeyFrames(Operator): + """Operator to flip the selected bone keyframes using blender's flip pose.""" + bl_idname = "avatar_toolkit.flip_pose_frames" + bl_label = t("Tools.flip_pose_frames") + bl_description = t("Tools.flip_pose_frames_desc") + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + """Check if operator can be executed""" + armature = get_active_armature(context) + if not armature: + return False + if context.mode != 'POSE': + return False + if not armature.animation_data: + return False + valid, _, _ = validate_armature(armature) + return valid + + def execute(self, context: Context) -> set[str]: + armature = get_active_armature(context) + + + + armature_data: bpy.types.Armature = armature.data + + standard_mappings: Dict[str,str] = identify_bones(armature_data) + + + + + # Do we need this? If flipping in the future has issues, then uncommenting this may help - @989onan + #To make sure our flip pose is extremely reliable, we're gonna temp rename all bones to standard names to make the posing work. + #for standard,bone_name in standard_mappings.items(): + # armature_data.bones[bone_name].name = standard + + #save our selection + selected: list[bool] = [False] * len(armature_data.bones) + armature_data.bones.foreach_get("select", selected) + #select everything + armature_data.bones.foreach_set("select", [False] * len(armature_data.bones)) + + + + #create a set for every frame time where we need to key a keyframe for the flipped pose + times: Dict[float,list[bpy.types.FCurve]] = {} + for curve in armature.animation_data.action.fcurves: + if not curve.data_path.startswith("pose"): + continue + for point in curve.keyframe_points: + if point.select_control_point: + if point.co.x not in times: + times[point.co.x] = [] + + times[point.co.x].append(curve) + + for time,curves in times.items(): + context.scene.frame_set(frame=int(time), subframe=float(time-float(int(time)))) + armature_data.bones.foreach_set("select", [True] * len(armature_data.bones)) + bpy.ops.pose.copy() + armature_data.bones.foreach_set("select", [False] * len(armature_data.bones)) + bpy.ops.pose.paste(flipped=True,selected_mask=False) + + + + + for curve in curves: + + bone_name: str = curve.data_path.replace("pose.bones[\"","") + bone_name = bone_name[:bone_name.index("\"")] + + armature_data.bones[bone_name].select = True + + bpy.ops.pose.select_mirror(extend=False) + + #this can get the opposite side bone's data path and key it, if it is ever needed - @989onan + #for bone in armature_data.bones: + # if bone.select == True: + # bone_name = bone.name + # break + #new_path = curve.data_path[:curve.data_path.index("[")+1]+"\""+bone_name+"\""+curve.data_path[curve.data_path.index("]"):] + + if armature.keyframe_insert(data_path=curve.data_path, index=curve.array_index, frame=time): + #if armature.keyframe_insert(data_path=new_path, index=curve.array_index, frame=time): + continue + self.report({'ERROR'}, f"Keyframe insertion for key with data path \"{curve.data_path}\" and frame {time} failed!") + return {'FINISHED'} + + + + + + + + + + # Do we need this? If flipping in the future has issues, then uncommenting this may help - @989onan + #bring our names back as to not break their model. + #for standard,bone_name in standard_mappings.items(): + # armature_data.bones[standard].name = bone_name + + # restore selection + armature_data.bones.foreach_set("select", selected) + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 80a6e93..fb0cc33 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -205,6 +205,8 @@ "Tools.zero_weight_bones_found": "Zero weight bones found: {bones}", "Tools.remove_selected_bones": "Remove Selected Bones", "Tools.remove_selected_bones_desc": "Remove selected zero weight bones from armature", + "Tools.flip_pose_frames": "Flip Selected Armature Key Frames", + "Tools.flip_pose_frames_desc": "Takes the selected keyframes and sets them to a mirrored pose, gotten from the opposite side of the armature on that frame.\nSelecting the entire animation's keyframes will flip the entire animation.", "Tools.bones_removed": "Removed {count} bones", "Tools.vertex_groups_removed": "Removed {count} vertex groups.", "Tools.clean_constraints": "Delete Bone Constraints", diff --git a/ui/tools_panel.py b/ui/tools_panel.py index a819ed3..bc9f106 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -7,24 +7,18 @@ from ..core.translations import t from ..core.resonite_utils import AvatarToolkit_OT_ConvertResonite from ..functions.tools.mesh_separation import AvatarToolKit_OT_SeparateByLooseParts, AvatarToolKit_OT_SeparateByMaterials from ..functions.tools.additional_tools import AvatarToolkit_OT_ApplyTransforms, AvatarToolkit_OT_CleanShapekeys -from ..functions.tools.bone_tools import AvatarToolKit_OT_CreateDigitigradeLegs, AvatarToolKit_OT_DeleteBoneConstraints, AvatarToolKit_OT_RemoveSelectedBones, AvatarToolKit_OT_RemoveZeroWeightBones, AvatarToolKit_OT_RemoveZeroWeightVertexGroups +from ..functions.tools.bone_tools import ( + AvatarToolKit_OT_CreateDigitigradeLegs, + AvatarToolKit_OT_DeleteBoneConstraints, + AvatarToolKit_OT_RemoveSelectedBones, + AvatarToolKit_OT_RemoveZeroWeightBones, + AvatarToolKit_OT_RemoveZeroWeightVertexGroups, + AvatarToolKit_OT_FlipCurrentKeyFrames +) from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature from ..functions.tools.merge_tools import AvatarToolkit_OT_MergeToActive, AvatarToolkit_OT_MergeToParent, AvatarToolkit_OT_ConnectBones from ..functions.tools.rigify_converter import AvatarToolkit_OT_ConvertRigifyToUnity - -class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList): - """UI List for displaying zero weight bones with selection options""" - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if self.layout_type in {'DEFAULT', 'COMPACT'}: - row = layout.row(align=True) - row.prop(item, "selected", text="") - row.label(text=item.name) - if item.has_children: - row.label(text="", icon='OUTLINER_OB_ARMATURE') - if item.is_deform: - row.label(text="", icon='MOD_ARMATURE') - class AvatarToolKit_PT_ToolsPanel(Panel): """Panel containing various tools for avatar customization and optimization""" bl_label: str = t("Tools.label") @@ -63,6 +57,8 @@ class AvatarToolKit_PT_ToolsPanel(Panel): col.label(text=t("Tools.bone_title"), icon='BONE_DATA') col.separator(factor=0.5) col.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade"), icon='BONE_DATA') + col.operator(AvatarToolKit_OT_FlipCurrentKeyFrames.bl_idname,text=t("Tools.flip_pose_frames"),icon="ACTION") + # Standardization Tools standardize_box: UILayout = bone_box.box() @@ -121,3 +117,16 @@ class AvatarToolKit_PT_ToolsPanel(Panel): col.separator(factor=0.5) col.operator(AvatarToolkit_OT_ConvertRigifyToUnity.bl_idname, icon='ARMATURE_DATA') col.prop(context.scene.avatar_toolkit, "merge_twist_bones") + + +class AVATAR_TOOLKIT_UL_ZeroWeightBones(UIList): + """UI List for displaying zero weight bones with selection options""" + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row(align=True) + row.prop(item, "selected", text="") + row.label(text=item.name) + if item.has_children: + row.label(text="", icon='OUTLINER_OB_ARMATURE') + if item.is_deform: + row.label(text="", icon='MOD_ARMATURE') \ No newline at end of file diff --git a/ui/uv_tools.py b/ui/uv_tools.py index 4bb60eb..d72e303 100644 --- a/ui/uv_tools.py +++ b/ui/uv_tools.py @@ -12,7 +12,7 @@ class AvatarToolKit_PT_UVTools(Panel): bl_region_type = 'UI' bl_category = "UV Tools" bl_parent_id = AvatarToolKit_PT_UVPanel.bl_idname - bl_order = 3 + bl_order = 0 bl_options = {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: