Fixes and Improvements

- Improved typing in some areas.
- Improved code readability in some areas.
- Delete bone constraints would error out if the user is in edit mode, we now start in Object mode first.
- Fixed Eye tracking Ajust string not being in the translation files.
- There is now a selection box to select the mesh in the current active armature for viseme creation instead of the user having to select it in the 3D scene.
- Viseme preview mode won't allow you to start it if your in a other mode, you need to be in Object mode now.
- Combine Materials won't allow you to start it if your in a other mode, you need to be in Object mode now.
- Added Japanese and Korean UI Languages.
This commit is contained in:
Yusarina
2024-12-18 02:44:26 +00:00
parent c5d07892c2
commit 8665292c7b
15 changed files with 1338 additions and 778 deletions
+89 -91
View File
@@ -1,7 +1,7 @@
import bpy
import numpy as np
from typing import List, Optional, Dict, Set
from bpy.types import Context, Object, Operator
from typing import List, Optional, Dict, Set, Tuple, Any
from bpy.types import Context, Object, Operator, ArmatureModifier, EditBone, VertexGroup, Mesh, ShapeKey
from ...core.logging_setup import logger
from ...core.translations import t
@@ -13,26 +13,27 @@ from ...core.common import (
remove_unused_shapekeys
)
class AvatarToolkit_OT_MergeArmature(Operator):
bl_idname = 'avatar_toolkit.merge_armatures'
bl_label = t('MergeArmature.label')
bl_description = t('MergeArmature.desc')
bl_options = {'REGISTER', 'UNDO'}
class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
"""Operator for merging two armatures together with their associated meshes"""
bl_idname: str = 'avatar_toolkit.merge_armatures'
bl_label: str = t('MergeArmature.label')
bl_description: str = t('MergeArmature.desc')
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return len(get_all_meshes(context)) > 1
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
try:
wm = context.window_manager
wm.progress_begin(0, 100)
# Get both armatures
base_armature_name = context.scene.merge_armature_into
merge_armature_name = context.scene.merge_armature
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
base_armature_name: str = context.scene.merge_armature_into
merge_armature_name: str = context.scene.merge_armature
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
@@ -51,15 +52,15 @@ class AvatarToolkit_OT_MergeArmature(Operator):
wm.progress_update(80)
# Get settings from scene properties
merge_all_bones = context.scene.avatar_toolkit.merge_all_bones
join_meshes = context.scene.avatar_toolkit.join_meshes
merge_all_bones: bool = context.scene.avatar_toolkit.merge_all_bones
join_meshes: bool = context.scene.avatar_toolkit.join_meshes
# Merge armatures
merge_armatures(
base_armature_name,
merge_armature_name,
mesh_only=False,
merge_all_bones=context.scene.avatar_toolkit.merge_all_bones,
merge_all_bones=merge_all_bones,
join_meshes=join_meshes,
operator=self
)
@@ -76,10 +77,10 @@ class AvatarToolkit_OT_MergeArmature(Operator):
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def delete_rigidbodies_and_joints(armature: Object):
"""Delete rigid bodies and joints associated with the armature."""
to_delete = []
parent = armature
def delete_rigidbodies_and_joints(armature: Object) -> None:
"""Delete rigid bodies and joints associated with an armature"""
to_delete: List[Object] = []
parent: Object = armature
while parent.parent:
parent = parent.parent
@@ -94,9 +95,9 @@ def delete_rigidbodies_and_joints(armature: Object):
bpy.data.objects.remove(obj, do_unlink=True)
def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool:
"""Validate parents and transformations of armatures before merging."""
merge_parent = merge_armature.parent
base_parent = base_armature.parent
"""Validate parent relationships and transformations of armatures"""
merge_parent: Optional[Object] = merge_armature.parent
base_parent: Optional[Object] = base_armature.parent
if merge_parent or base_parent:
if context.scene.merge_all_bones:
@@ -112,21 +113,21 @@ def validate_parents_and_transforms(merge_armature: Object, base_armature: Objec
return True
def is_transform_clean(obj: Object) -> bool:
"""Check if an object's transforms are at default values."""
"""Check if object transforms are at default values"""
for i in range(3):
if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0:
return False
return True
def prepare_mesh_vertex_groups(mesh: Object):
"""Prepare mesh by assigning all vertices to a new vertex group."""
def prepare_mesh_vertex_groups(mesh: Object) -> None:
"""Initialize mesh vertex groups for merging process"""
if mesh.vertex_groups:
for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh.name)
vg: VertexGroup = mesh.vertex_groups.new(name=mesh.name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
@@ -136,14 +137,14 @@ def merge_armatures(
mesh_only: bool,
merge_all_bones: bool = False,
join_meshes: bool = False,
operator=None
):
"""Main function to merge two armatures."""
operator: Optional[Operator] = None
) -> None:
"""Main function to merge two armatures with their associated meshes and data"""
logger.info(f"Merging armatures: {merge_armature_name} into {base_armature_name}")
tolerance = 0.00008726647 # around 0.005 degrees
tolerance: float = 0.00008726647 # around 0.005 degrees
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
@@ -172,12 +173,12 @@ def merge_armatures(
fix_zero_length_bones(merge_armature)
# Store original parent relationships
original_parents = {}
original_parents: Dict[str, Optional[str]] = {}
for bone in merge_armature.data.bones:
original_parents[bone.name] = bone.parent.name if bone.parent else None
# Get base bone names
base_bone_names = set(bone.name for bone in base_armature.data.bones)
base_bone_names: Set[str] = {bone.name for bone in base_armature.data.bones}
# Switch to edit mode on merge armature and rename bones
bpy.context.view_layer.objects.active = merge_armature
@@ -206,11 +207,11 @@ def merge_armatures(
# Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones:
base_name = bone.name.replace('.merge', '')
base_name: str = bone.name.replace('.merge', '')
if base_name in original_parents:
parent_name = original_parents[base_name]
parent_name: Optional[str] = original_parents[base_name]
if parent_name:
parent_bone = 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
@@ -223,7 +224,7 @@ def merge_armatures(
# Process vertex groups if not mesh_only
if not mesh_only:
meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
meshes: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
process_vertex_groups(meshes)
# Remove zero weight vertex groups if enabled
@@ -235,9 +236,9 @@ def merge_armatures(
# Join meshes if requested
if join_meshes:
meshes_to_join = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
meshes_to_join: List[Object] = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
if meshes_to_join:
joined_mesh = join_mesh_objects(bpy.context, meshes_to_join)
joined_mesh: Optional[Object] = join_mesh_objects(bpy.context, meshes_to_join)
if joined_mesh:
logger.info(f"Joined meshes into {joined_mesh.name}")
@@ -250,8 +251,8 @@ 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 = base_armature.data.edit_bones
bones_to_remove = [bone for bone in edit_bones if bone.name.endswith('.merge')]
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')
@@ -259,14 +260,13 @@ def merge_armatures(
# Final cleanup
clear_unused_data_blocks()
def validate_merge_armature_transforms(
base_armature: Object,
merge_armature: Object,
mesh_merge: Optional[Object],
tolerance: float
) -> bool:
"""Validate transforms of both armatures and mesh."""
"""Validate transforms of both armatures and mesh"""
for i in [0, 1, 2]:
if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance:
return False
@@ -280,10 +280,10 @@ def validate_merge_armature_transforms(
def adjust_merge_armature_transforms(
merge_armature: Object,
mesh_merge: Object
):
"""Adjust transforms of the merge armature."""
old_loc = list(merge_armature.location)
old_scale = list(merge_armature.scale)
) -> None:
"""Adjust transforms of the merge armature"""
old_loc: List[float] = list(merge_armature.location)
old_scale: List[float] = list(merge_armature.scale)
for i in [0, 1, 2]:
merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i]
@@ -295,25 +295,24 @@ def adjust_merge_armature_transforms(
mesh_merge.rotation_euler[i] = 0
mesh_merge.scale[i] = 1
def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float,
merge_all_bones: bool
) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance."""
bones_to_merge = []
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance"""
bones_to_merge: List[str] = []
# Cache base bone positions
base_bones_positions = {
base_bones_positions: Dict[str, np.ndarray] = {
bone.name: np.array(bone.head) for bone in base_edit_bones
}
# Smart bone detection
for merge_bone in merge_edit_bones:
merge_bone_position = np.array(merge_bone.head)
found_match = False
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 merging same bones by name
@@ -333,17 +332,16 @@ def detect_bones_to_merge(
return bones_to_merge
def process_vertex_groups(meshes: List[Object]):
"""Process vertex groups in meshes."""
def process_vertex_groups(meshes: List[Object]) -> None:
"""Process vertex groups in meshes"""
for mesh in meshes:
vg_names = {vg.name for vg in mesh.vertex_groups}
merge_vg_names = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
vg_names: Set[str] = {vg.name for vg in mesh.vertex_groups}
merge_vg_names: List[str] = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
for vg_merge_name in merge_vg_names:
base_name = vg_merge_name[:-6]
vg_merge = mesh.vertex_groups.get(vg_merge_name)
vg_base = mesh.vertex_groups.get(base_name)
base_name: str = vg_merge_name[:-6]
vg_merge: Optional[VertexGroup] = mesh.vertex_groups.get(vg_merge_name)
vg_base: Optional[VertexGroup] = mesh.vertex_groups.get(base_name)
if vg_merge is None:
continue
@@ -353,20 +351,20 @@ def process_vertex_groups(meshes: List[Object]):
else:
vg_merge.name = base_name
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
"""Mix vertex group weights."""
vg_from = mesh.vertex_groups.get(vg_from_name)
vg_to = mesh.vertex_groups.get(vg_to_name)
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str) -> None:
"""Mix vertex group weights"""
vg_from: Optional[VertexGroup] = mesh.vertex_groups.get(vg_from_name)
vg_to: Optional[VertexGroup] = mesh.vertex_groups.get(vg_to_name)
if not vg_from or not vg_to:
return
num_vertices = len(mesh.data.vertices)
weights_from = np.zeros(num_vertices)
weights_to = np.zeros(num_vertices)
num_vertices: int = len(mesh.data.vertices)
weights_from: np.ndarray = np.zeros(num_vertices)
weights_to: np.ndarray = np.zeros(num_vertices)
idx_from = vg_from.index
idx_to = vg_to.index
idx_from: int = vg_from.index
idx_to: int = vg_to.index
for v in mesh.data.vertices:
for g in v.groups:
@@ -375,14 +373,14 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
elif g.group == idx_to:
weights_to[v.index] = g.weight
weights_combined = np.clip(weights_from + weights_to, 0.0, 1.0)
weights_combined: np.ndarray = np.clip(weights_from + weights_to, 0.0, 1.0)
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
mesh.vertex_groups.remove(vg_from)
def remove_unused_vertex_groups(mesh: Object):
"""Remove vertex groups with no weights."""
def remove_unused_vertex_groups(mesh: Object) -> None:
"""Remove vertex groups with no weights"""
for vg in mesh.vertex_groups:
has_weights = False
has_weights: bool = False
for vert in mesh.data.vertices:
for group in vert.groups:
if group.group == vg.index and group.weight > 0.001:
@@ -393,9 +391,9 @@ def remove_unused_vertex_groups(mesh: Object):
if not has_weights:
mesh.vertex_groups.remove(vg)
def apply_armature_to_mesh(armature: Object, mesh: Object):
"""Apply armature deformation to mesh."""
armature_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
def apply_armature_to_mesh(armature: Object, mesh: Object) -> None:
"""Apply armature deformation to mesh"""
armature_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature
if bpy.app.version >= (3, 5):
@@ -407,15 +405,15 @@ def apply_armature_to_mesh(armature: Object, mesh: Object):
with bpy.context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=armature_mod.name)
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context):
"""Apply armature deformation to mesh with shape keys."""
old_active_index = mesh.active_shape_key_index
old_show_only = mesh.show_only_shape_key
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys"""
old_active_index: int = mesh.active_shape_key_index
old_show_only: bool = mesh.show_only_shape_key
mesh.show_only_shape_key = True
shape_keys = mesh.data.shape_keys.key_blocks
vertex_groups = []
mutes = []
shape_keys: List[ShapeKey] = mesh.data.shape_keys.key_blocks
vertex_groups: List[str] = []
mutes: List[bool] = []
for sk in shape_keys:
vertex_groups.append(sk.vertex_group)
@@ -423,23 +421,23 @@ def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, contex
mutes.append(sk.mute)
sk.mute = False
disabled_mods = []
disabled_mods: List[Any] = []
for mod in mesh.modifiers:
if mod.show_viewport:
mod.show_viewport = False
disabled_mods.append(mod)
arm_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod: ArmatureModifier = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature
co_length = len(mesh.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single)
co_length: int = len(mesh.data.vertices) * 3
eval_cos: np.ndarray = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys):
mesh.active_shape_key_index = i
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh.evaluated_get(depsgraph)
eval_mesh: Mesh = mesh.evaluated_get(depsgraph)
eval_mesh.data.vertices.foreach_get('co', eval_cos)
shape_key.data.foreach_set('co', eval_cos)
+30 -22
View File
@@ -1,7 +1,7 @@
import bpy
from bpy.types import Operator, Context, Object
from bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
from mathutils import Vector
from typing import Set, Optional
from typing import Set, Optional, List, Any
from ...core.logging_setup import logger
from ...core.translations import t
@@ -15,28 +15,34 @@ from ...core.common import (
)
class AvatarToolkit_OT_AttachMesh(Operator):
"""Attach a mesh to an armature bone with automatic weight setup"""
bl_idname = "avatar_toolkit.attach_mesh"
bl_label = t("AttachMesh.label")
bl_description = t("AttachMesh.desc")
bl_options = {'REGISTER', 'UNDO'}
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
bl_idname: str = "avatar_toolkit.attach_mesh"
bl_label: str = t("AttachMesh.label")
bl_description: str = t("AttachMesh.desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
"""Check if operator can be executed"""
armature: Optional[Object] = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting mesh attachment process")
mesh_name = context.scene.avatar_toolkit.attach_mesh
armature = get_active_armature(context)
attach_bone_name = context.scene.avatar_toolkit.attach_bone
mesh = bpy.data.objects.get(mesh_name)
mesh_name: str = context.scene.avatar_toolkit.attach_mesh
armature: Object = get_active_armature(context)
attach_bone_name: str = context.scene.avatar_toolkit.attach_bone
mesh: Optional[Object] = bpy.data.objects.get(mesh_name)
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
# Validation steps
is_valid: bool
error_msg: str
is_valid, error_msg = validate_mesh_transforms(mesh)
if not is_valid:
raise ValueError(error_msg)
@@ -63,7 +69,7 @@ class AvatarToolkit_OT_AttachMesh(Operator):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh_name)
vg: VertexGroup = mesh.vertex_groups.new(name=mesh_name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
progress.step(t("AttachMesh.setup_weights"))
@@ -83,12 +89,14 @@ class AvatarToolkit_OT_AttachMesh(Operator):
progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement
verts_in_group = [v for v in mesh.data.vertices
verts_in_group: List[Any] = [v for v in mesh.data.vertices
for g in v.groups if g.group == vg.index]
dimensions: Vector
roll_angle: float
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
# Set bone position and orientation
center = Vector((0, 0, 0))
center: Vector = Vector((0, 0, 0))
for v in verts_in_group:
center += mesh.data.vertices[v.index].co
center /= len(verts_in_group)
@@ -111,20 +119,20 @@ class AvatarToolkit_OT_AttachMesh(Operator):
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def validate_mesh_transforms(mesh):
"""Validate mesh transforms are suitable for attaching."""
def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
"""Validate mesh transforms are suitable for attaching"""
if not mesh:
return False, "Mesh not found"
# Check for non-uniform scale
scale = mesh.scale
scale: Vector = mesh.scale
if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001:
return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
return True, ""
def validate_mesh_name(armature, mesh_name):
"""Validate mesh name doesn't conflict with existing bones."""
def validate_mesh_name(armature: Object, mesh_name: str) -> tuple[bool, str]:
"""Validate mesh name doesn't conflict with existing bones"""
if mesh_name in armature.data.bones:
return False, f"Bone named '{mesh_name}' already exists in armature"
return True, ""
return True, ""