Attach Meshes
This commit is contained in:
@@ -1,19 +1,15 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
from typing import List, Optional, Dict, Set
|
||||
from mathutils import Vector
|
||||
from bpy.types import Context, Object, Operator
|
||||
|
||||
from ...core.logging_setup import logger
|
||||
from ...core.translations import t
|
||||
from ...core.common import (
|
||||
get_active_armature,
|
||||
get_all_meshes,
|
||||
fix_zero_length_bones,
|
||||
clear_unused_data_blocks,
|
||||
validate_armature,
|
||||
join_mesh_objects,
|
||||
fix_uv_coordinates,
|
||||
remove_unused_shapekeys
|
||||
)
|
||||
|
||||
@@ -40,7 +36,7 @@ class AvatarToolkit_OT_MergeArmature(Operator):
|
||||
|
||||
if not base_armature or not merge_armature:
|
||||
logger.error(f"Armature not found: {merge_armature_name}")
|
||||
self.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
|
||||
self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Remove Rigid Bodies and Joints
|
||||
@@ -80,21 +76,6 @@ class AvatarToolkit_OT_MergeArmature(Operator):
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
def calculate_bone_orientation(mesh, vertices):
|
||||
"""Calculate optimal bone orientation based on mesh geometry."""
|
||||
|
||||
if not vertices:
|
||||
return Vector((0, 0, 0.1)), 0.0
|
||||
|
||||
coords = [mesh.data.vertices[v.index].co for v in vertices]
|
||||
min_co = Vector(map(min, zip(*coords)))
|
||||
max_co = Vector(map(max, zip(*coords)))
|
||||
dimensions = max_co - min_co
|
||||
|
||||
roll_angle = 0.0
|
||||
|
||||
return dimensions, roll_angle
|
||||
|
||||
def delete_rigidbodies_and_joints(armature: Object):
|
||||
"""Delete rigid bodies and joints associated with the armature."""
|
||||
to_delete = []
|
||||
@@ -398,15 +379,6 @@ def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
|
||||
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
|
||||
mesh.vertex_groups.remove(vg_from)
|
||||
|
||||
def add_armature_modifier(mesh: Object, armature: Object):
|
||||
"""Add armature modifier to mesh."""
|
||||
for mod in mesh.modifiers:
|
||||
if mod.type == 'ARMATURE':
|
||||
mesh.modifiers.remove(mod)
|
||||
|
||||
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
|
||||
modifier.object = armature
|
||||
|
||||
def remove_unused_vertex_groups(mesh: Object):
|
||||
"""Remove vertex groups with no weights."""
|
||||
for vg in mesh.vertex_groups:
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import bpy
|
||||
from bpy.types import Operator, Context, Object
|
||||
from mathutils import Vector
|
||||
from typing import Set, Optional
|
||||
|
||||
from ...core.logging_setup import logger
|
||||
from ...core.translations import t
|
||||
from ...core.common import (
|
||||
get_active_armature,
|
||||
validate_armature,
|
||||
get_all_meshes,
|
||||
ProgressTracker,
|
||||
calculate_bone_orientation,
|
||||
add_armature_modifier
|
||||
)
|
||||
|
||||
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'}
|
||||
|
||||
@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
|
||||
|
||||
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)
|
||||
|
||||
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
|
||||
# Validation steps
|
||||
is_valid, error_msg = validate_mesh_transforms(mesh)
|
||||
if not is_valid:
|
||||
raise ValueError(error_msg)
|
||||
progress.step(t("AttachMesh.validate_transforms"))
|
||||
|
||||
is_valid, error_msg = validate_mesh_name(armature, mesh_name)
|
||||
if not is_valid:
|
||||
raise ValueError(error_msg)
|
||||
progress.step(t("AttachMesh.validate_name"))
|
||||
|
||||
# Parent mesh to armature
|
||||
mesh.parent = armature
|
||||
mesh.parent_type = 'OBJECT'
|
||||
progress.step(t("AttachMesh.parent_mesh"))
|
||||
|
||||
# Setup vertex groups
|
||||
if mesh.vertex_groups:
|
||||
for vg in mesh.vertex_groups:
|
||||
mesh.vertex_groups.remove(vg)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
mesh.select_set(True)
|
||||
context.view_layer.objects.active = mesh
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
vg = 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"))
|
||||
|
||||
# Create and setup bone
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
armature.select_set(True)
|
||||
context.view_layer.objects.active = armature
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
attach_to_bone = armature.data.edit_bones.get(attach_bone_name)
|
||||
if not attach_to_bone:
|
||||
raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name))
|
||||
|
||||
mesh_bone = armature.data.edit_bones.new(mesh_name)
|
||||
mesh_bone.parent = attach_to_bone
|
||||
progress.step(t("AttachMesh.create_bone"))
|
||||
|
||||
# Calculate bone placement
|
||||
verts_in_group = [v for v in mesh.data.vertices
|
||||
for g in v.groups if g.group == vg.index]
|
||||
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
|
||||
|
||||
# Set bone position and orientation
|
||||
center = Vector((0, 0, 0))
|
||||
for v in verts_in_group:
|
||||
center += mesh.data.vertices[v.index].co
|
||||
center /= len(verts_in_group)
|
||||
|
||||
mesh_bone.head = center
|
||||
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
|
||||
mesh_bone.roll = roll_angle
|
||||
progress.step(t("AttachMesh.position_bone"))
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
add_armature_modifier(mesh, armature)
|
||||
progress.step(t("AttachMesh.add_modifier"))
|
||||
|
||||
logger.info(f"Successfully attached mesh {mesh_name} to bone {attach_bone_name}")
|
||||
self.report({'INFO'}, t("AttachMesh.success"))
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to attach mesh: {str(e)}")
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
def validate_mesh_transforms(mesh):
|
||||
"""Validate mesh transforms are suitable for attaching."""
|
||||
if not mesh:
|
||||
return False, "Mesh not found"
|
||||
|
||||
# Check for non-uniform scale
|
||||
scale = 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."""
|
||||
if mesh_name in armature.data.bones:
|
||||
return False, f"Bone named '{mesh_name}' already exists in armature"
|
||||
return True, ""
|
||||
Reference in New Issue
Block a user