Add Easy_Weight
to Addons
#47
@ -1,10 +1,12 @@
|
|||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from typing import List
|
from bpy.types import Operator, Mesh, VertexGroup, MeshVertex, Object
|
||||||
|
import bmesh
|
||||||
from .utils.naming import flip_name
|
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:
|
for m in mesh_ob.modifiers:
|
||||||
if m.type=='ARMATURE':
|
if m.type=='ARMATURE':
|
||||||
return m.object
|
return m.object
|
||||||
@ -14,7 +16,7 @@ def delete_vgroups(mesh_ob, vgroups):
|
|||||||
mesh_ob.vertex_groups.remove(vg)
|
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)
|
arm_ob = get_deforming_armature(mesh_ob)
|
||||||
all_vgroups = mesh_ob.vertex_groups
|
all_vgroups = mesh_ob.vertex_groups
|
||||||
deforming_vgroups = []
|
deforming_vgroups = []
|
||||||
@ -23,7 +25,7 @@ def get_deforming_vgroups(mesh_ob) -> List[bpy.types.VertexGroup]:
|
|||||||
deforming_vgroups.append(all_vgroups[b.name])
|
deforming_vgroups.append(all_vgroups[b.name])
|
||||||
return deforming_vgroups
|
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)
|
deforming_vgroups = get_deforming_vgroups(mesh_ob)
|
||||||
empty_deforming_groups = [vg for vg in deforming_vgroups if not vgroup_has_weight(mesh_ob, vg)]
|
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
|
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"""
|
"""Delete vertex groups which are associated to deforming bones but don't have any weights"""
|
||||||
bl_idname = "object.delete_empty_deform_vgroups"
|
bl_idname = "object.delete_empty_deform_vgroups"
|
||||||
bl_label = "Delete Empty Deform Groups"
|
bl_label = "Delete Empty Deform Groups"
|
||||||
@ -85,12 +87,12 @@ class DeleteEmptyDeformGroups(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class WeightPaintOperator(bpy.types.Operator):
|
class WeightPaintOperator(Operator):
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
obj = context.object
|
obj = context.object
|
||||||
rig = context.pose_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):
|
class DeleteUnselectedDeformGroups(WeightPaintOperator):
|
||||||
"""Delete deforming vertex groups that do not correspond to any selected pose bone"""
|
"""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
|
bone.select = True
|
||||||
|
|
||||||
class FocusDeformBones(WeightPaintOperator):
|
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_idname = "object.focus_deform_vgroups"
|
||||||
bl_label = "Focus Deforming Bones"
|
bl_label = "Focus Deforming Bones"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
@ -152,7 +154,7 @@ class FocusDeformBones(WeightPaintOperator):
|
|||||||
return {'FINISHED'}
|
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."""
|
"""Return a list of vertex groups directly referenced by the object's attributes."""
|
||||||
referenced_vgroups = []
|
referenced_vgroups = []
|
||||||
for member in dir(py_ob):
|
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)
|
referenced_vgroups.append(vg)
|
||||||
return referenced_vgroups
|
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 = []
|
mask_vgroups = []
|
||||||
if not mesh_ob.data.shape_keys:
|
if not mesh_ob.data.shape_keys:
|
||||||
return mask_vgroups
|
return mask_vgroups
|
||||||
@ -197,7 +199,7 @@ def delete_unused_vgroups(mesh_ob) -> List[str]:
|
|||||||
delete_vgroups(mesh_ob, groups_to_delete)
|
delete_vgroups(mesh_ob, groups_to_delete)
|
||||||
return names
|
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"""
|
"""Delete non-deforming vertex groups which are not used by any modifiers, shape keys or constraints"""
|
||||||
bl_idname = "object.delete_unused_vgroups"
|
bl_idname = "object.delete_unused_vgroups"
|
||||||
bl_label = "Delete Unused Non-Deform Groups"
|
bl_label = "Delete Unused Non-Deform Groups"
|
||||||
@ -217,7 +219,7 @@ class DeleteUnusedVertexGroups(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class CreateMirrorGroups(bpy.types.Operator):
|
class CreateMirrorGroups(Operator):
|
||||||
"""Create missing Left/Right vertex groups to ensure correct behaviour of Mirror modifier"""
|
"""Create missing Left/Right vertex groups to ensure correct behaviour of Mirror modifier"""
|
||||||
bl_idname = "object.ensure_mirror_vgroups"
|
bl_idname = "object.ensure_mirror_vgroups"
|
||||||
bl_label = "Ensure Mirror Groups"
|
bl_label = "Ensure Mirror Groups"
|
||||||
@ -251,12 +253,142 @@ class CreateMirrorGroups(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
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 = [
|
classes = [
|
||||||
DeleteEmptyDeformGroups,
|
DeleteEmptyDeformGroups,
|
||||||
FocusDeformBones,
|
FocusDeformBones,
|
||||||
DeleteUnselectedDeformGroups,
|
DeleteUnselectedDeformGroups,
|
||||||
DeleteUnusedVertexGroups,
|
DeleteUnusedVertexGroups,
|
||||||
CreateMirrorGroups,
|
CreateMirrorGroups,
|
||||||
|
FocusRogueDeformingWeights
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -272,4 +404,4 @@ def unregister():
|
|||||||
try:
|
try:
|
||||||
unregister_class(c)
|
unregister_class(c)
|
||||||
except RuntimeError:
|
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