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
.vscode/settings.json
core/preferences.json
+5 -6
View File
@@ -455,12 +455,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=""
)
merge_all_bones: BoolProperty(
name=t('MergeArmature.merge_all'),
description=t('MergeArmature.merge_all_desc'),
default=True
)
apply_transforms: BoolProperty(
name=t('MergeArmature.apply_transforms'),
description=t('MergeArmature.apply_transforms_desc'),
@@ -529,7 +523,10 @@ def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
try:
bpy.utils.register_class(ZeroWeightBoneItem)
bpy.utils.register_class(AvatarToolkitSceneProperties)
except ValueError:
# Class already registered, we can continue
pass
@@ -544,7 +541,9 @@ def unregister() -> None:
except:
pass
try:
bpy.utils.unregister_class(ZeroWeightBoneItem)
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
except RuntimeError:
pass
logger.debug("Properties unregistered successfully")
+51 -37
View File
@@ -2,7 +2,7 @@ import bpy
import numpy as np
from typing import List, Optional, Dict, Set, Tuple, Any
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.translations import t
from ...core.common import (
@@ -10,7 +10,8 @@ from ...core.common import (
fix_zero_length_bones,
clear_unused_data_blocks,
join_mesh_objects,
remove_unused_shapekeys
remove_unused_shapekeys,
simplify_bonename
)
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
@@ -52,7 +53,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
wm.progress_update(80)
# 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
# Merge armatures
@@ -60,7 +60,6 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
base_armature_name,
merge_armature_name,
mesh_only=False,
merge_all_bones=merge_all_bones,
join_meshes=join_meshes,
operator=self
)
@@ -100,16 +99,12 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec
base_parent: Optional[Object] = base_armature.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)]:
if parent:
if not is_transform_clean(parent):
logger.error("Parent transforms are not clean")
return False
bpy.data.objects.remove(parent, do_unlink=True)
else:
logger.error("Parent relationships need fixing")
return False
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
if parent:
if not is_transform_clean(parent):
logger.error("Parent transforms are not clean")
return False
bpy.data.objects.remove(parent, do_unlink=True)
return True
def is_transform_clean(obj: Object) -> bool:
@@ -135,7 +130,6 @@ def merge_armatures(
base_armature_name: str,
merge_armature_name: str,
mesh_only: bool,
merge_all_bones: bool = False,
join_meshes: bool = False,
operator: Optional[Operator] = None
) -> None:
@@ -174,25 +168,50 @@ def merge_armatures(
# Store original parent relationships
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
#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
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
bpy.context.view_layer.objects.active = merge_armature
bpy.ops.object.mode_set(mode='EDIT')
# Handle bone renaming based on merge_all_bones setting
for bone in merge_armature.data.edit_bones:
if not merge_all_bones:
# Only rename bones that don't exist in base armature
if bone.name not in base_bone_names:
bone.name += '.merge'
# Handle bone renaming/removing to target armature.
bone_names_source: list[str] = [bone.name for bone in merge_armature_data.edit_bones]
for bone in bone_names_source:
bone_name = bone
if bone_name not in base_bone_names: #not auto mergable to original
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:
# Rename all bones from merge armature
bone.name += '.merge'
merge_armature_data.edit_bones.remove(merge_armature_data.edit_bones[bone_name])
# Return to object mode
bpy.ops.object.mode_set(mode='OBJECT')
@@ -204,14 +223,15 @@ def merge_armatures(
bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.join()
base_armature_data: bpy.types.Armature = base_armature.data
# Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones:
base_name: str = bone.name.replace('.merge', '')
if base_name in original_parents:
parent_name: Optional[str] = original_parents[base_name]
for bone in base_armature_data.edit_bones:
if bone.name in original_parents:
parent_name: Optional[str] = original_parents[bone.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:
bone.parent = parent_bone
@@ -250,11 +270,6 @@ def merge_armatures(
# Remove any remaining .merge bones
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')
# Final cleanup
@@ -298,8 +313,7 @@ def adjust_merge_armature_transforms(
def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float,
merge_all_bones: bool
tolerance: float
) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
bones_to_merge: List[str] = []
@@ -314,7 +328,7 @@ def detect_bones_to_merge(
merge_bone_position: np.ndarray = np.array(merge_bone.head)
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
bones_to_merge.append(merge_bone.name)
found_match = True
-1
View File
@@ -155,7 +155,6 @@ class AvatarToolKit_PT_CustomPanel(Panel):
# Group related options together
transform_col: UILayout = col.column(align=True)
transform_col.prop(toolkit, "merge_all_bones")
transform_col.prop(toolkit, "apply_transforms")
col.separator(factor=0.5)