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:
@@ -1,3 +1,4 @@
|
||||
|
||||
*.pyc
|
||||
.vscode/settings.json
|
||||
core/preferences.json
|
||||
|
||||
+5
-6
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user