Merge pull request #51 from 989onan/MergeArmatures

Merge armatures button
This commit is contained in:
Yusarina
2024-09-18 11:25:57 +01:00
committed by GitHub
8 changed files with 291 additions and 100 deletions
+103
View File
@@ -115,6 +115,9 @@ def get_armature(context: Context, armature_name: Optional[str] = None) -> Optio
def get_armatures(self, context: Context) -> List[Tuple[str, str, str]]: def get_armatures(self, context: Context) -> List[Tuple[str, str, str]]:
return [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'ARMATURE'] return [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'ARMATURE']
def get_armatures_that_are_not_selected(self, context: Context) -> List[Tuple[str, str, str]]:
return [(obj.name, obj.name, "") for obj in bpy.data.objects if ((obj.type == 'ARMATURE') and (obj.name != context.scene.selected_armature))]
def get_selected_armature(context: Context) -> Optional[Object]: def get_selected_armature(context: Context) -> Optional[Object]:
if context.scene.selected_armature: if context.scene.selected_armature:
armature = bpy.data.objects.get(context.scene.selected_armature) armature = bpy.data.objects.get(context.scene.selected_armature)
@@ -141,6 +144,106 @@ def select_current_armature(context: Context) -> bool:
return True return True
return False return False
def apply_pose_as_rest(context: Context, armature_obj: bpy.types.Object, meshes: list[bpy.types.Object]) -> bool:
for obj in meshes:
mesh_data: Mesh = obj.data
if mesh_data.shape_keys:
shape_key_obj_list: list[bpy.types.Object] = []
modifier_armature_name: str = ""
for modifier in obj.modifiers:
if modifier.type == "ARMATURE":
arm_modifier: bpy.types.ArmatureModifier = modifier
if not (arm_modifier.object == armature_obj):
continue
modifier_armature_name = arm_modifier.object.name
if modifier_armature_name == "":
continue
for idx,shape in enumerate(mesh_data.shape_keys.key_blocks):
if idx == 0:
continue
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = obj
obj.select_set(True)
#create duplicate of object
bpy.ops.object.duplicate()
shape_obj = context.view_layer.objects.active
#make current shapekey a separate object
shape_obj.active_shape_key_index = idx
shape_obj.name = shape.name
bpy.ops.object.shape_key_move(type="TOP")
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.shape_key_remove(all=True)
bpy.ops.object.modifier_apply(modifier=modifier_armature_name)
#for modifier_name in [i.name for i in shape_obj.modifiers]:
# bpy.ops.object.modifier_remove(modifier=modifier_name)
shape_key_obj_list.append(shape_obj) #add to a list of shape key objects
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="OBJECT")
context.view_layer.objects.active.select_set(True)
bpy.ops.object.shape_key_remove(all=True)
bpy.ops.object.modifier_apply(modifier=modifier_armature_name)
bpy.ops.object.select_all(action="DESELECT")
for shapekey_obj in shape_key_obj_list:
shapekey_obj.select_set(True)
context.view_layer.objects.active = obj
context.view_layer.objects.active.select_set(True)
try:
bpy.ops.object.join_shapes()
except:
#delete shapekey objects to not leave ourselves in a bad exit state - @989onan
context.view_layer.objects.active = shape_key_obj_list[0]
obj.select_set(False)
bpy.ops.object.delete(confirm=False)
return False
context.view_layer.objects.active = shape_key_obj_list[0]
obj.select_set(False)
bpy.ops.object.delete(confirm=False)
else:
modifier_armature_name: str = ""
for modifier in obj.modifiers:
if modifier.type == "ARMATURE":
arm_modifier: bpy.types.ArmatureModifier = modifier
if not (arm_modifier.object == armature_obj):
continue
modifier_armature_name = arm_modifier.object.name
if modifier_armature_name == "":
continue
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active.select_set(True)
bpy.ops.object.modifier_apply(modifier=modifier_armature_name)
context.view_layer.objects.active = armature_obj
armature_obj.select_set(True)
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="POSE")
bpy.ops.pose.armature_apply(selected=False)
return True
def get_all_meshes(context: Context) -> List[Object]: def get_all_meshes(context: Context) -> List[Object]:
armature = get_selected_armature(context) armature = get_selected_armature(context)
if armature and is_valid_armature(armature): if armature and is_valid_armature(armature):
+21 -4
View File
@@ -4,7 +4,7 @@ from ..core.register import register_property
from bpy.types import Scene, Object, Material, Context from bpy.types import Scene, Object, Material, Context
from bpy.props import BoolProperty, EnumProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty from bpy.props import BoolProperty, EnumProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty
from ..core.addon_preferences import get_preference from ..core.addon_preferences import get_preference
from ..core.common import SceneMatClass, MaterialListBool, get_armatures, get_mesh_items from ..core.common import SceneMatClass, MaterialListBool, get_armatures, get_mesh_items, get_armatures_that_are_not_selected
def register() -> None: def register() -> None:
default_language = get_preference("language", 0) default_language = get_preference("language", 0)
@@ -22,6 +22,23 @@ def register() -> None:
description=t("VisemePanel.selected_mesh.desc") description=t("VisemePanel.selected_mesh.desc")
))) )))
register_property((bpy.types.Scene, "merge_armature_source", bpy.props.EnumProperty(
items=get_armatures_that_are_not_selected,
name=t("MergeArmatures.selected_armature.label"),
description=t("MergeArmatures.selected_armature.label")
)))
register_property((bpy.types.Scene, "merge_armature_apply_transforms", bpy.props.BoolProperty(
default=False,
name=t("MergeArmature.merge_armatures.apply_transforms.label"),
description=t("MergeArmature.merge_armatures.apply_transforms.desc")
)))
register_property((bpy.types.Scene, "merge_armature_align_bones", bpy.props.BoolProperty(
default=False,
name=t("MergeArmature.merge_armatures.align_bones.label"),
description=t("MergeArmature.merge_armatures.align_bones.desc")
)))
register_property((bpy.types.Scene, "avatar_toolkit_language_changed", bpy.props.BoolProperty(default=False))) register_property((bpy.types.Scene, "avatar_toolkit_language_changed", bpy.props.BoolProperty(default=False)))
register_property((bpy.types.Scene, "avatar_toolkit_progress_steps", bpy.props.IntProperty(default=0))) register_property((bpy.types.Scene, "avatar_toolkit_progress_steps", bpy.props.IntProperty(default=0)))
@@ -49,8 +66,8 @@ def register() -> None:
register_property((bpy.types.Scene, "selected_armature", bpy.props.EnumProperty( register_property((bpy.types.Scene, "selected_armature", bpy.props.EnumProperty(
items=get_armatures, items=get_armatures,
name="Selected Armature", name=t("Quick_Access.selected_armature.label"),
description="The currently selected armature for Avatar Toolkit operations" description=t("Quick_Access.selected_armature.desc")
))) )))
#happy with how compressed this get_texture_node_list method is - @989onan #happy with how compressed this get_texture_node_list method is - @989onan
@@ -88,7 +105,7 @@ def register() -> None:
items=get_texture_node_list))) items=get_texture_node_list)))
register_property((Material, "texture_atlas_height", EnumProperty( register_property((Material, "texture_atlas_height", EnumProperty(
name=t("TextureAtlas.height"), name=t("TextureAtlas.height"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height_map").lower()), description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
default=0, default=0,
items=get_texture_node_list))) items=get_texture_node_list)))
register_property((Material, "texture_atlas_roughness", EnumProperty( register_property((Material, "texture_atlas_roughness", EnumProperty(
+108 -92
View File
@@ -4,6 +4,8 @@ from bpy.types import Context, Mesh, Panel, Operator, Armature, EditBone
from ..functions.translations import t from ..functions.translations import t
from ..core.common import get_selected_armature, get_all_meshes from ..core.common import get_selected_armature, get_all_meshes
from ..core import common from ..core import common
from ..core.dictionaries import bone_names
from mathutils import Matrix
@register_wrap @register_wrap
class AvatarToolkit_OT_StartPoseMode(Operator): class AvatarToolkit_OT_StartPoseMode(Operator):
@@ -95,98 +97,10 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
return get_selected_armature(context) != None and context.mode == "POSE" return get_selected_armature(context) != None and context.mode == "POSE"
def execute(self, context: Context): def execute(self, context: Context):
for obj in get_all_meshes(context):
mesh_data: Mesh = obj.data
if mesh_data.shape_keys:
shape_key_obj_list: list[bpy.types.Object] = []
modifier_armature_name: str = ""
for modifier in obj.modifiers:
if modifier.type == "ARMATURE":
arm_modifier: bpy.types.ArmatureModifier = modifier
modifier_armature_name = arm_modifier.object.name
for idx,shape in enumerate(mesh_data.shape_keys.key_blocks):
if idx == 0:
continue
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = obj
obj.select_set(True)
#create duplicate of object
bpy.ops.object.duplicate()
shape_obj = context.view_layer.objects.active
#make current shapekey a separate object
shape_obj.active_shape_key_index = idx
shape_obj.name = shape.name
bpy.ops.object.shape_key_move(type="TOP")
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.shape_key_remove(all=True)
bpy.ops.object.modifier_apply(modifier=modifier_armature_name)
#for modifier_name in [i.name for i in shape_obj.modifiers]:
# bpy.ops.object.modifier_remove(modifier=modifier_name)
shape_key_obj_list.append(shape_obj) #add to a list of shape key objects
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="OBJECT")
context.view_layer.objects.active.select_set(True)
bpy.ops.object.shape_key_remove(all=True)
bpy.ops.object.modifier_apply(modifier=modifier_armature_name)
bpy.ops.object.select_all(action="DESELECT")
for shapekey_obj in shape_key_obj_list:
shapekey_obj.select_set(True)
context.view_layer.objects.active = obj
context.view_layer.objects.active.select_set(True)
try:
bpy.ops.object.join_shapes()
except:
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
#delete shapekey objects to not leave ourselves in a bad exit state - @989onan
context.view_layer.objects.active = shape_key_obj_list[0]
obj.select_set(False)
bpy.ops.object.delete(confirm=False)
return {'CANCELLED'}
context.view_layer.objects.active = shape_key_obj_list[0]
obj.select_set(False)
bpy.ops.object.delete(confirm=False)
else:
modifier_armature_name: str = ""
for modifier in obj.modifiers:
if modifier.type == "ARMATURE":
arm_modifier: bpy.types.ArmatureModifier = modifier
modifier_armature_name = arm_modifier.object.name
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active.select_set(True)
bpy.ops.object.modifier_apply(modifier=modifier_armature_name)
armature_obj: bpy.types.Object = get_selected_armature(context)
context.view_layer.objects.active = armature_obj
armature_obj.select_set(True)
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.mode_set(mode="POSE")
bpy.ops.pose.armature_apply(selected=False)
if common.apply_pose_as_rest(armature_obj=get_selected_armature(context),meshes=get_all_meshes(context), context=context):
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'FINISHED'}
return {'FINISHED'} return {'FINISHED'}
@register_wrap @register_wrap
@@ -338,6 +252,108 @@ class AvatarToolkit_OT_MergeBonesToParents(Operator):
bone_child.parent = armature_data.edit_bones[bone].parent bone_child.parent = armature_data.edit_bones[bone].parent
armature_data.edit_bones.remove(armature_data.edit_bones[bone]) armature_data.edit_bones.remove(armature_data.edit_bones[bone])
bpy.ops.object.mode_set(mode=prev_mode) bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'} return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_MergeArmatures(Operator):
bl_idname = "avatar_toolkit.merge_armatures"
bl_label = t("MergeArmature.merge_armatures.label")
bl_description = t("MergeArmature.merge_armatures.desc").format(selected_armature_label=t("MergeArmatures.selected_armature.label"))
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return (common.get_selected_armature(context) is not None) and (context.scene.merge_armature_source is not None)
def make_active(self, obj: bpy.types.Object, context: Context):
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = obj
obj.select_set(True)
def execute(cls, context: Context) -> set[str]:
source_armature: bpy.types.Object = bpy.data.objects[context.scene.merge_armature_source]
source_armature_data: Armature = source_armature.data
target_armature: bpy.types.Object = common.get_selected_armature(context)
target_armature_data: Armature = target_armature.data
parent_dictionary: dict[str, list[str]] = {}
cls.make_active(obj=source_armature, context=context)
if context.scene.merge_armature_apply_transforms:
target_armature.select_set(True)
for obj in target_armature.children:
obj.select_set(True)
for obj in source_armature.children:
obj.select_set(True)
bpy.ops.object.transform_apply()
if context.scene.merge_armature_align_bones:
if not context.scene.merge_armature_apply_transforms:
source_armature.matrix_world = target_armature.matrix_world
def children_bone_recursive(parent_bone) -> list[bpy.types.PoseBone]:
child_bones = []
child_bones.append(parent_bone)
for child in parent_bone.children:
child_bones.extend(children_bone_recursive(child))
return child_bones
bpy.ops.object.mode_set(mode='POSE')
source_armature_bone_names = [j.name for j in children_bone_recursive(
source_armature.pose.bones[
next(bone.name for bone in source_armature.pose.bones if common.simplify_bonename(bone.name) in bone_names['hips']) #Find bone that matches dictionary for hips before continuing.
]
)] #bones are default in order of parent child.
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = target_armature
bpy.ops.object.mode_set(mode='EDIT')
for source_bone_name in source_armature_bone_names:
if source_bone_name in target_armature_data.edit_bones:
obj = source_armature
editbone = target_armature_data.edit_bones[source_bone_name]
bone = obj.pose.bones[source_bone_name]
bone.matrix = editbone.matrix
else:
continue
if not common.apply_pose_as_rest(armature_obj=source_armature,meshes=[i for i in source_armature.children if i.type == 'MESH'], context=context):
cls.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'FINISHED'}
cls.make_active(obj=source_armature, context=context)
bpy.ops.object.mode_set(mode='EDIT')
source_armature_data: Armature = source_armature.data
for bone_name in [i.name for i in source_armature_data.edit_bones]:
if bone_name in target_armature_data.bones:
parent_dictionary[bone_name] = [i.name for i in source_armature_data.edit_bones[bone_name].children]
source_armature_data.edit_bones.remove(source_armature_data.edit_bones[bone_name])
bpy.ops.object.mode_set(mode='OBJECT')
cls.make_active(obj=target_armature, context=context)
source_armature.select_set(True)
bpy.ops.object.join()
target_armature: bpy.types.Object = common.get_selected_armature(context)
cls.make_active(obj=target_armature, context=context)
bpy.ops.object.mode_set(mode='EDIT')
for bone_name, bone_name_list in parent_dictionary.items():
if bone_name in target_armature_data.edit_bones:
for bone_child in bone_name_list:
target_armature_data.edit_bones[bone_child].parent = target_armature_data.edit_bones[bone_name]
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
+15
View File
@@ -62,6 +62,8 @@
"Optimization.selecting_meshes": "Selecting meshes...", "Optimization.selecting_meshes": "Selecting meshes...",
"Optimization.transform_apply_failed": "Transform apply failed", "Optimization.transform_apply_failed": "Transform apply failed",
"Optimization.vertex_excluded": "Shapekey has a moved vertex at index \"{index}\", excluding from double merging!", "Optimization.vertex_excluded": "Shapekey has a moved vertex at index \"{index}\", excluding from double merging!",
"Quick_Access.selected_armature.label": "Selected Armature",
"Quick_Access.selected_armature.desc": "The currently \"targeted\" armature for Avatar Toolkit operations",
"Quick_Access.export": "Export", "Quick_Access.export": "Export",
"Quick_Access.export_fbx.desc": "Export the model as FBX", "Quick_Access.export_fbx.desc": "Export the model as FBX",
"Quick_Access.export_fbx.label": "Export FBX", "Quick_Access.export_fbx.label": "Export FBX",
@@ -162,6 +164,19 @@
"Tools.merge_bones_to_parents.label": "Merge Bones to Individual Parents", "Tools.merge_bones_to_parents.label": "Merge Bones to Individual Parents",
"Tools.remove_zero_weight_bones.threshold.label": "Weight Threshold", "Tools.remove_zero_weight_bones.threshold.label": "Weight Threshold",
"Tools.remove_zero_weight_bones.threshold.desc": "If a bone is not weighted to any part of any mesh under the armature with a threshold greater than this, it is removed", "Tools.remove_zero_weight_bones.threshold.desc": "If a bone is not weighted to any part of any mesh under the armature with a threshold greater than this, it is removed",
"MergeArmatures.select_armature": "Please select an armature",
"MergeArmatures.title.label": "Merge Armatures:",
"MergeArmatures.label": "Merge Armatures",
"MergeArmatures.selected_armature.label": "Armature to Merge From",
"MergeArmatures.selected_armature.desc": "The armature that should be merged into the targeted armature for Avatar Toolkit.",
"MergeArmatures.target_armature.label": "Armature to Merge To",
"MergeArmatures.target_armature.desc": "The armature that should be the target for merging armatures.",
"MergeArmature.merge_armatures.label": "Merge Armatures Together",
"MergeArmature.merge_armatures.desc": "Merge {selected_armature_label} to the targeted armature for Avatar Toolkit.",
"MergeArmature.merge_armatures.align_bones.label": "Align Bones",
"MergeArmature.merge_armatures.align_bones.desc": "Align bones from source armature to target armature,\nstretching bones to match before merging.",
"MergeArmature.merge_armatures.apply_transforms.label": "Apply Transforms",
"MergeArmature.merge_armatures.apply_transforms.desc": "Apply transforms on armature and it's meshes before merging.",
"VisemePanel.create_visemes": "Create Visemes", "VisemePanel.create_visemes": "Create Visemes",
"VisemePanel.creating_viseme": "Creating viseme: {viseme_name}", "VisemePanel.creating_viseme": "Creating viseme: {viseme_name}",
"VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}", "VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}",
+1 -1
View File
@@ -66,7 +66,7 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel):
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 4 bl_order = 5
def draw(self, context: Context): def draw(self, context: Context):
layout = self.layout layout = self.layout
+40
View File
@@ -0,0 +1,40 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from bpy.types import Panel, Context
from ..core.common import get_selected_armature
from ..functions.translations import t
from ..functions.armature_modifying import AvatarToolkit_OT_MergeArmatures
@register_wrap
class AvatarToolkit_PT_MergeArmaturesPanel(Panel):
bl_label = t("MergeArmatures.label")
bl_idname = "OBJECT_PT_avatar_toolkit_merge_armatures"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 4
def draw(self, context: Context):
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.label(text=t("MergeArmatures.title.label"), icon='ARMATURE_DATA')
layout.separator(factor=0.5)
row = layout.row(align=True)
row.prop(context.scene, property="selected_armature",text=t("MergeArmatures.target_armature.label"),icon="STYLUS_PRESSURE")
row = layout.row(align=True)
row.prop(context.scene, property="merge_armature_source",icon="SORT_DESC")
row = layout.row(align=True)
row.prop(context.scene, property="merge_armature_align_bones")
row = layout.row(align=True)
row.prop(context.scene, property="merge_armature_apply_transforms")
row = layout.row(align=True)
row.operator(operator=AvatarToolkit_OT_MergeArmatures.bl_idname,icon="ARMATURE_DATA")
else:
layout.label(text=t("MergeArmatures.select_armature"), icon='ERROR')
+1 -1
View File
@@ -11,7 +11,7 @@ class AvatarToolkitSettingsPanel(bpy.types.Panel):
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 6 bl_order = 7
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
+1 -1
View File
@@ -13,7 +13,7 @@ class AvatarToolkitVisemePanel(bpy.types.Panel):
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 5 bl_order = 6
def draw(self, context: bpy.types.Context) -> None: def draw(self, context: bpy.types.Context) -> None:
layout = self.layout layout = self.layout