fix bad armature merging issues

also merge all bones isn't needed. we should do that by default

This also now uses dictionary matching to find bone types like hips, spine, and chest that should be merged.

Deletes bone shared and merges armatures, and parents bones back, causing a seamless merge.
This commit is contained in:
989onan
2025-02-18 19:30:56 -05:00
parent 855bb84e76
commit 07adaa590b
4 changed files with 57 additions and 44 deletions
+1
View File
@@ -1,3 +1,4 @@
*.pyc *.pyc
.vscode/settings.json .vscode/settings.json
core/preferences.json
+5 -6
View File
@@ -455,12 +455,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default="" default=""
) )
merge_all_bones: BoolProperty(
name=t('MergeArmature.merge_all'),
description=t('MergeArmature.merge_all_desc'),
default=True
)
apply_transforms: BoolProperty( apply_transforms: BoolProperty(
name=t('MergeArmature.apply_transforms'), name=t('MergeArmature.apply_transforms'),
description=t('MergeArmature.apply_transforms_desc'), description=t('MergeArmature.apply_transforms_desc'),
@@ -529,7 +523,10 @@ def register() -> None:
"""Register the Avatar Toolkit property group""" """Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties") logger.info("Registering Avatar Toolkit properties")
try: try:
bpy.utils.register_class(ZeroWeightBoneItem)
bpy.utils.register_class(AvatarToolkitSceneProperties) bpy.utils.register_class(AvatarToolkitSceneProperties)
except ValueError: except ValueError:
# Class already registered, we can continue # Class already registered, we can continue
pass pass
@@ -544,7 +541,9 @@ def unregister() -> None:
except: except:
pass pass
try: try:
bpy.utils.unregister_class(ZeroWeightBoneItem)
bpy.utils.unregister_class(AvatarToolkitSceneProperties) bpy.utils.unregister_class(AvatarToolkitSceneProperties)
except RuntimeError: except RuntimeError:
pass pass
logger.debug("Properties unregistered successfully") logger.debug("Properties unregistered successfully")
+51 -37
View File
@@ -2,7 +2,7 @@ import bpy
import numpy as np import numpy as np
from typing import List, Optional, Dict, Set, Tuple, Any from typing import List, Optional, Dict, Set, Tuple, Any
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
from ...core.dictionaries import bone_names
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.translations import t from ...core.translations import t
from ...core.common import ( from ...core.common import (
@@ -10,7 +10,8 @@ from ...core.common import (
fix_zero_length_bones, fix_zero_length_bones,
clear_unused_data_blocks, clear_unused_data_blocks,
join_mesh_objects, join_mesh_objects,
remove_unused_shapekeys remove_unused_shapekeys,
simplify_bonename
) )
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
@@ -52,7 +53,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
wm.progress_update(80) wm.progress_update(80)
# Get settings from scene properties # Get settings from scene properties
merge_all_bones: bool = context.scene.avatar_toolkit.merge_all_bones
join_meshes: bool = context.scene.avatar_toolkit.join_meshes join_meshes: bool = context.scene.avatar_toolkit.join_meshes
# Merge armatures # Merge armatures
@@ -60,7 +60,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
base_armature_name, base_armature_name,
merge_armature_name, merge_armature_name,
mesh_only=False, mesh_only=False,
merge_all_bones=merge_all_bones,
join_meshes=join_meshes, join_meshes=join_meshes,
operator=self operator=self
) )
@@ -100,16 +99,12 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec
base_parent: Optional[Object] = base_armature.parent base_parent: Optional[Object] = base_armature.parent
if merge_parent or base_parent: if merge_parent or base_parent:
if context.scene.merge_all_bones: for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]: if parent:
if parent: if not is_transform_clean(parent):
if not is_transform_clean(parent): logger.error("Parent transforms are not clean")
logger.error("Parent transforms are not clean") return False
return False bpy.data.objects.remove(parent, do_unlink=True)
bpy.data.objects.remove(parent, do_unlink=True)
else:
logger.error("Parent relationships need fixing")
return False
return True return True
def is_transform_clean(obj: Object) -> bool: def is_transform_clean(obj: Object) -> bool:
@@ -135,7 +130,6 @@ def merge_armatures(
base_armature_name: str, base_armature_name: str,
merge_armature_name: str, merge_armature_name: str,
mesh_only: bool, mesh_only: bool,
merge_all_bones: bool = False,
join_meshes: bool = False, join_meshes: bool = False,
operator: Optional[Operator] = None operator: Optional[Operator] = None
) -> None: ) -> None:
@@ -174,25 +168,50 @@ def merge_armatures(
# Store original parent relationships # Store original parent relationships
original_parents: Dict[str, Optional[str]] = {} original_parents: Dict[str, Optional[str]] = {}
for bone in merge_armature.data.bones: merge_armature_data: bpy.types.Armature = merge_armature.data
for bone in merge_armature_data.bones:
original_parents[bone.name] = bone.parent.name if bone.parent else None original_parents[bone.name] = bone.parent.name if bone.parent else None
#create reverse lookup
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
# Get base bone names # Get base bone names
base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones} base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones}
base_armature_standards: Dict[str,Optional[str]] = {}
for bone in base_bone_names:
if simplify_bonename(bone) in reverse_bone_lookup:
base_armature_standards[reverse_bone_lookup[simplify_bonename(bone)]] = bone
# Switch to edit mode on merge armature and rename bones # Switch to edit mode on merge armature and rename bones
bpy.context.view_layer.objects.active = merge_armature bpy.context.view_layer.objects.active = merge_armature
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
# Handle bone renaming based on merge_all_bones setting # Handle bone renaming/removing to target armature.
for bone in merge_armature.data.edit_bones: bone_names_source: list[str] = [bone.name for bone in merge_armature_data.edit_bones]
if not merge_all_bones: for bone in bone_names_source:
# Only rename bones that don't exist in base armature bone_name = bone
if bone.name not in base_bone_names: if bone_name not in base_bone_names: #not auto mergable to original
bone.name += '.merge'
if simplify_bonename(bone_name) in reverse_bone_lookup: #if is a standard bone through standard translation.
if reverse_bone_lookup[simplify_bonename(bone_name)] in base_armature_standards: #if this bone equals for example, "hips", does a bone that should be "hips" exist on our target armature?
#if so, rename this bone to that one
merge_armature_data.edit_bones[bone_name].name = base_armature_standards[reverse_bone_lookup[simplify_bonename(bone_name)]]
bone_name = merge_armature_data.edit_bones[bone_name].name
#adjust original parents list to point to the new name.
for child_bone in merge_armature_data.edit_bones[bone_name]:
original_parents[child_bone.name] = bone_name
#then remove so it doesn't clash when merged.
merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
continue
#if it really doesn't have a counter part, just don't bother.
else: else:
# Rename all bones from merge armature merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
bone.name += '.merge'
# Return to object mode # Return to object mode
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
@@ -204,14 +223,15 @@ def merge_armatures(
bpy.context.view_layer.objects.active = base_armature bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.join() bpy.ops.object.join()
base_armature_data: bpy.types.Armature = base_armature.data
# Restore parent relationships # Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones: for bone in base_armature_data.edit_bones:
base_name: str = bone.name.replace('.merge', '') if bone.name in original_parents:
if base_name in original_parents: parent_name: Optional[str] = original_parents[bone.name]
parent_name: Optional[str] = original_parents[base_name]
if parent_name: if parent_name:
parent_bone: Optional[EditBone] = base_armature.data.edit_bones.get(parent_name) parent_bone: Optional[EditBone] = base_armature_data.edit_bones.get(parent_name)
if parent_bone: if parent_bone:
bone.parent = parent_bone bone.parent = parent_bone
@@ -250,11 +270,6 @@ def merge_armatures(
# Remove any remaining .merge bones # Remove any remaining .merge bones
bpy.context.view_layer.objects.active = base_armature bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.mode_set(mode='EDIT')
edit_bones: List[EditBone] = base_armature.data.edit_bones
bones_to_remove: List[EditBone] = [bone for bone in edit_bones if bone.name.endswith('.merge')]
for bone in bones_to_remove:
edit_bones.remove(bone)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
# Final cleanup # Final cleanup
@@ -298,8 +313,7 @@ def adjust_merge_armature_transforms(
def detect_bones_to_merge( def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones, base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones, merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float, tolerance: float
merge_all_bones: bool
) -> List[str]: ) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance""" """Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
bones_to_merge: List[str] = [] bones_to_merge: List[str] = []
@@ -314,7 +328,7 @@ def detect_bones_to_merge(
merge_bone_position: np.ndarray = np.array(merge_bone.head) merge_bone_position: np.ndarray = np.array(merge_bone.head)
found_match: bool = False found_match: bool = False
if merge_all_bones and merge_bone.name in base_bones_positions: if merge_bone.name in base_bones_positions:
# If merging same bones by name # If merging same bones by name
bones_to_merge.append(merge_bone.name) bones_to_merge.append(merge_bone.name)
found_match = True found_match = True
-1
View File
@@ -155,7 +155,6 @@ class AvatarToolKit_PT_CustomPanel(Panel):
# Group related options together # Group related options together
transform_col: UILayout = col.column(align=True) transform_col: UILayout = col.column(align=True)
transform_col.prop(toolkit, "merge_all_bones")
transform_col.prop(toolkit, "apply_transforms") transform_col.prop(toolkit, "apply_transforms")
col.separator(factor=0.5) col.separator(factor=0.5)