Add Easy_Weight
to Addons
#47
@ -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.
|
Loading…
Reference in New Issue
Block a user