Add Easy_Weight to Addons #47

Merged
Nick Alberelli merged 48 commits from feature/easy_weights into main 2023-05-17 22:13:57 +02:00
Showing only changes of commit 27c7bd747d - Show all commits

View File

@ -1,10 +1,12 @@
from typing import List, Tuple
import bpy
from typing import List
from bpy.types import Operator, Mesh, VertexGroup, MeshVertex, Object
import bmesh
from .utils.naming import flip_name
def get_deforming_armature(mesh_ob) -> bpy.types.Object:
def get_deforming_armature(mesh_ob) -> Object:
for m in mesh_ob.modifiers:
if m.type=='ARMATURE':
return m.object
@ -14,7 +16,7 @@ def delete_vgroups(mesh_ob, vgroups):
mesh_ob.vertex_groups.remove(vg)
def get_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
def get_deforming_vgroups(mesh_ob) -> List[VertexGroup]:
arm_ob = get_deforming_armature(mesh_ob)
all_vgroups = mesh_ob.vertex_groups
deforming_vgroups = []
@ -23,7 +25,7 @@ def get_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
deforming_vgroups.append(all_vgroups[b.name])
return deforming_vgroups
def get_empty_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
def get_empty_deforming_vgroups(mesh_ob) -> List[VertexGroup]:
deforming_vgroups = get_deforming_vgroups(mesh_ob)
empty_deforming_groups = [vg for vg in deforming_vgroups if not vgroup_has_weight(mesh_ob, vg)]
@ -62,7 +64,7 @@ def vgroup_has_weight(mesh_ob, vgroup) -> bool:
return False
class DeleteEmptyDeformGroups(bpy.types.Operator):
class DeleteEmptyDeformGroups(Operator):
"""Delete vertex groups which are associated to deforming bones but don't have any weights"""
bl_idname = "object.delete_empty_deform_vgroups"
bl_label = "Delete Empty Deform Groups"
@ -85,12 +87,12 @@ class DeleteEmptyDeformGroups(bpy.types.Operator):
return {'FINISHED'}
class WeightPaintOperator(bpy.types.Operator):
class WeightPaintOperator(Operator):
@classmethod
def poll(cls, context):
obj = context.object
rig = context.pose_object
return all((context.mode=='PAINT_WEIGHT', obj, rig, obj.vertex_groups))
return context.mode == 'PAINT_WEIGHT' and obj and rig and obj.vertex_groups
class DeleteUnselectedDeformGroups(WeightPaintOperator):
"""Delete deforming vertex groups that do not correspond to any selected pose bone"""
@ -130,7 +132,7 @@ def reveal_bone(bone, select=True):
bone.select = True
class FocusDeformBones(WeightPaintOperator):
"""Reveal the layers of, unhide, and select the bones of all deforming vertex groups"""
"""While in Weight Paint Mode, reveal the layers of, unhide, and select the bones of all deforming vertex groups"""
bl_idname = "object.focus_deform_vgroups"
bl_label = "Focus Deforming Bones"
bl_options = {'REGISTER', 'UNDO'}
@ -152,7 +154,7 @@ class FocusDeformBones(WeightPaintOperator):
return {'FINISHED'}
def get_referenced_vgroups(mesh_ob: bpy.types.Object, py_ob: object) -> List[bpy.types.VertexGroup]:
def get_referenced_vgroups(mesh_ob: Object, py_ob: object) -> List[VertexGroup]:
"""Return a list of vertex groups directly referenced by the object's attributes."""
referenced_vgroups = []
for member in dir(py_ob):
@ -164,7 +166,7 @@ def get_referenced_vgroups(mesh_ob: bpy.types.Object, py_ob: object) -> List[bpy
referenced_vgroups.append(vg)
return referenced_vgroups
def get_shape_key_mask_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
def get_shape_key_mask_vgroups(mesh_ob) -> List[VertexGroup]:
mask_vgroups = []
if not mesh_ob.data.shape_keys:
return mask_vgroups
@ -197,7 +199,7 @@ def delete_unused_vgroups(mesh_ob) -> List[str]:
delete_vgroups(mesh_ob, groups_to_delete)
return names
class DeleteUnusedVertexGroups(bpy.types.Operator):
class DeleteUnusedVertexGroups(Operator):
"""Delete non-deforming vertex groups which are not used by any modifiers, shape keys or constraints"""
bl_idname = "object.delete_unused_vgroups"
bl_label = "Delete Unused Non-Deform Groups"
@ -217,7 +219,7 @@ class DeleteUnusedVertexGroups(bpy.types.Operator):
return {'FINISHED'}
class CreateMirrorGroups(bpy.types.Operator):
class CreateMirrorGroups(Operator):
"""Create missing Left/Right vertex groups to ensure correct behaviour of Mirror modifier"""
bl_idname = "object.ensure_mirror_vgroups"
bl_label = "Ensure Mirror Groups"
@ -251,12 +253,142 @@ class CreateMirrorGroups(bpy.types.Operator):
return {'FINISHED'}
def build_vert_index_map(mesh) -> dict:
"""Build a dictionary of vertex indicies pointing to a list of other vertex indicies that the vertex is connected to by an edge."""
bm = bmesh.from_edit_mesh(mesh)
v_dict = {}
for vert in bm.verts:
connected_verts = []
for be in vert.link_edges:
for connected_vert in be.verts:
if connected_vert.index == vert.index: continue
connected_verts.append(connected_vert.index)
v_dict[vert.index] = connected_verts
return v_dict
def find_weight_island_vertices(mesh: Mesh, vert_idx: int, group_index: int, vert_idx_map: dict, island=[]) -> List[int]:
"""Recursively find all vertices that are connected to a vertex by edges, and are also in the same vertex group."""
# I am sick of bmesh, so you must build a vertex connection map with build_vert_index_map and pass it in here.
mesh.vertices[vert_idx].select = True
island.append(vert_idx)
for connected_vert_idx in vert_idx_map[vert_idx]: # For each edge connected to the vert
if connected_vert_idx in island: # Avoid infinite recursion!
continue
for g in mesh.vertices[connected_vert_idx].groups: # For each group this other vertex belongs to
if g.group == group_index and g.weight > 0: # If this vert is in the group
find_weight_island_vertices(mesh, connected_vert_idx, group_index, vert_idx_map, island) # Continue recursion
return island
def find_any_vertex_in_group(mesh: Mesh, vgroup: VertexGroup, is_selected=None) -> MeshVertex:
"""Return the index of the first vertex we find which is part of the
vertex group and optinally, has a specified selection state."""
for v in mesh.vertices:
if is_selected != None and v.select != is_selected:
continue
for g in v.groups:
if vgroup.index == g.group:
return v
return None
def build_weight_islands_in_group(mesh: Mesh, vgroup: VertexGroup, vert_index_map: dict) -> List[List[int]]:
"""Return a list of lists of vertex indicies: Weight islands within this vertex group."""
islands = []
while True:
any_unselected_vertex_in_group = find_any_vertex_in_group(mesh, vgroup, is_selected=False)
if not any_unselected_vertex_in_group:
break
island = find_weight_island_vertices(mesh, any_unselected_vertex_in_group.index, vgroup.index, vert_index_map, [])
islands.append(island)
return islands
def select_vertices(mesh: Mesh, vert_indicies: List[int]):
assert bpy.context.mode != 'EDIT_MESH', "Object must not be in edit mode, otherwise vertex selection doesn't work!"
for vi in vert_indicies:
mesh.vertices[vi].select = True
class FocusRogueDeformingWeights(WeightPaintOperator):
"""While in weight paint mode, find and focus a deforming vertex group which consists of several islands. Automatic fixing is dangerous, so this operator does not remove weights automatically. Keep using this until no more rogue weights are found"""
bl_idname = "object.focus_rogue_weights"
bl_label = "Focus Rogue Weights"
bl_options = {'REGISTER', 'UNDO'}
@staticmethod
def save_skip_group(obj, vgroup):
"""Store this group's name in a custom property so it can be skipped on subsequent runs."""
if 'skip_groups' not in obj:
obj['skip_groups'] = []
l = obj['skip_groups']
if type(l) != list:
l = l.to_list()
l.append(vgroup.name)
obj['skip_groups'] = l
@staticmethod
def find_vgroup_with_multiple_islands(obj: Object, vert_index_map: dict) -> Tuple[VertexGroup, List[List[int]]]:
"""Return the first vertex group we find that has multiple islands, as well as the islands."""
mesh = obj.data
for vgroup in obj.vertex_groups:
if 'skip_groups' in obj and vgroup.name in obj['skip_groups']:
continue
obj.vertex_groups.active_index = vgroup.index
islands = build_weight_islands_in_group(mesh, vgroup, vert_index_map)
if len(islands) > 1:
return vgroup, islands
def execute(self, context):
rig = context.pose_object
obj = context.object
org_vg_idx = obj.vertex_groups.active_index
org_mode = obj.mode
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_mode(type='VERT')
bpy.ops.mesh.select_all(action='DESELECT')
mesh = obj.data
vert_index_map = build_vert_index_map(mesh)
bpy.ops.object.mode_set(mode='OBJECT')
vgroup, islands = self.find_vgroup_with_multiple_islands(obj, vert_index_map)
if vgroup:
# Select islands except the one with the most verts.
largest_island = max(islands, key=len)
for island in islands:
if island == largest_island: continue
select_vertices(mesh, island)
# Support the case where the user chooses not to fix the rogue weights: Perhaps they are intentional
self.save_skip_group(obj, vgroup)
self.report({'INFO'}, f'Found Vertex Group "{vgroup.name}" with {len(islands)} islands. Fix or ignore it, then keep running this operator to find the next group with rogue weights.')
mesh.use_paint_mask_vertex = True
if rig:
rig.select_set(True)
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
else:
obj.vertex_groups.active_index = org_vg_idx
bpy.ops.object.mode_set(mode=org_mode)
if 'skip_groups' in obj and len(obj['skip_groups']) > 0:
self.report({'INFO'}, f"No rogue weights found, but {len(obj['skip_groups'])} were skipped. Run the operator again to cycle through groups with rogue weights.")
del obj['skip_groups']
else:
self.report({'INFO'}, "No rogue weights found!")
return {'FINISHED'}
classes = [
DeleteEmptyDeformGroups,
FocusDeformBones,
DeleteUnselectedDeformGroups,
DeleteUnusedVertexGroups,
CreateMirrorGroups,
FocusRogueDeformingWeights
]
@ -272,4 +404,4 @@ def unregister():
try:
unregister_class(c)
except RuntimeError:
pass # sometimes fails to unregister for literally no reason.
pass # TODO: Sometimes fails to unregister for literally no reason.