Add Easy_Weight
to Addons
#47
@ -34,6 +34,7 @@ from . import change_brush
|
|||||||
from . import weight_paint_context_menu
|
from . import weight_paint_context_menu
|
||||||
from . import vertex_group_operators
|
from . import vertex_group_operators
|
||||||
from . import vertex_group_menu
|
from . import vertex_group_menu
|
||||||
|
from . import rogue_weights
|
||||||
|
|
||||||
# Each module is expected to have a register() and unregister() function.
|
# Each module is expected to have a register() and unregister() function.
|
||||||
modules = [
|
modules = [
|
||||||
@ -43,7 +44,8 @@ modules = [
|
|||||||
change_brush,
|
change_brush,
|
||||||
weight_paint_context_menu,
|
weight_paint_context_menu,
|
||||||
vertex_group_operators,
|
vertex_group_operators,
|
||||||
vertex_group_menu
|
vertex_group_menu,
|
||||||
|
rogue_weights
|
||||||
]
|
]
|
||||||
|
|
||||||
class EasyWeightPreferences(AddonPreferences):
|
class EasyWeightPreferences(AddonPreferences):
|
||||||
|
164
rogue_weights.py
Normal file
164
rogue_weights.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import bpy, sys
|
||||||
|
from bpy.types import Operator, Mesh, VertexGroup, MeshVertex, Object
|
||||||
|
import itertools
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
from .vertex_group_operators import WeightPaintOperator
|
||||||
|
|
||||||
|
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."""
|
||||||
|
|
||||||
|
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, excluded_indicies=[]) -> 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 v.index in excluded_indicies:
|
||||||
|
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:
|
||||||
|
flat_islands = set(itertools.chain.from_iterable(islands))
|
||||||
|
any_unselected_vertex_in_group = find_any_vertex_in_group(mesh, vgroup, excluded_indicies=flat_islands)
|
||||||
|
if not any_unselected_vertex_in_group:
|
||||||
|
break
|
||||||
|
sys.setrecursionlimit(len(mesh.vertices)) # TODO: I guess recursion is bad and we should avoid it here?
|
||||||
|
island = find_weight_island_vertices(mesh, any_unselected_vertex_in_group.index, vgroup.index, vert_index_map, island=[])
|
||||||
|
sys.setrecursionlimit(990)
|
||||||
|
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
|
||||||
|
|
||||||
|
# TODO: This needs to be split off into a separate, even smarter system with more UI:
|
||||||
|
# It should be like the error checking buttons in the 3D Print addon by Campbell:
|
||||||
|
# You press the operator, and it detects and SAVES all the issues that were found:
|
||||||
|
# There is a sidebar panel for this (probably along with other EasyWeight UI)
|
||||||
|
# Pressing the button populates a UIList with vertex groups that were found with multiple islands.
|
||||||
|
# Selecting an entry in the UIList brings up ANOTHER UIList with each island and the number of vertices in them.
|
||||||
|
# A vertex group can be marked as correct, which will save the number of islands it had in the moment when it was considered correct, so on subsequent runs of the operator, it will be ignored as long as it has that number of islands still.
|
||||||
|
# Each island can be selected with a button (also selects the corresponding pose bone)
|
||||||
|
class FocusRogueDeformingWeights(WeightPaintOperator):
|
||||||
|
"""While in weight paint mode, find and focus a deforming vertex group which consists of several islands. The smallest weight island is selected, but no weights are removed. Keep running 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_rogue_deform_weights(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 get_deforming_vgroups(obj):
|
||||||
|
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
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
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.reveal()
|
||||||
|
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_rogue_deform_weights(obj, vert_index_map)
|
||||||
|
|
||||||
|
if vgroup:
|
||||||
|
# Select the smallest island.
|
||||||
|
select_vertices(mesh, min(islands, key=len))
|
||||||
|
|
||||||
|
# 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.')
|
||||||
|
|
||||||
|
if rig:
|
||||||
|
rig.select_set(True)
|
||||||
|
mesh.use_paint_mask_vertex = True
|
||||||
|
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
|
||||||
|
else:
|
||||||
|
if rig:
|
||||||
|
rig.select_set(True)
|
||||||
|
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. Keep running the operator to cycle through groups with multiple weight islands to make sure they are desired.")
|
||||||
|
del obj['skip_groups']
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, "No rogue weights found!")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
classes = [
|
||||||
|
FocusRogueDeformingWeights
|
||||||
|
]
|
||||||
|
|
||||||
|
def register():
|
||||||
|
from bpy.utils import register_class
|
||||||
|
for c in classes:
|
||||||
|
register_class(c)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
from bpy.utils import unregister_class
|
||||||
|
for c in classes:
|
||||||
|
try:
|
||||||
|
unregister_class(c)
|
||||||
|
except RuntimeError:
|
||||||
|
pass # TODO: Sometimes fails to unregister for literally no reason.
|
@ -5,9 +5,9 @@ from .vertex_group_operators import (
|
|||||||
FocusDeformBones,
|
FocusDeformBones,
|
||||||
DeleteUnselectedDeformGroups,
|
DeleteUnselectedDeformGroups,
|
||||||
DeleteUnusedVertexGroups,
|
DeleteUnusedVertexGroups,
|
||||||
CreateMirrorGroups,
|
CreateMirrorGroups
|
||||||
FocusRogueDeformingWeights
|
|
||||||
)
|
)
|
||||||
|
from .rogue_weights import FocusRogueDeformingWeights
|
||||||
|
|
||||||
class MESH_MT_vertex_group_batch_delete(bpy.types.Menu):
|
class MESH_MT_vertex_group_batch_delete(bpy.types.Menu):
|
||||||
bl_label = "Batch Delete"
|
bl_label = "Batch Delete"
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from typing import List, Tuple
|
from typing import List
|
||||||
|
|
||||||
import bpy, sys
|
import bpy
|
||||||
from bpy.types import Operator, Mesh, VertexGroup, MeshVertex, Object
|
from bpy.types import Operator, VertexGroup, Object
|
||||||
import itertools
|
|
||||||
import bmesh
|
|
||||||
from .utils.naming import flip_name
|
from .utils.naming import flip_name
|
||||||
|
|
||||||
|
|
||||||
@ -253,154 +251,19 @@ class CreateMirrorGroups(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."""
|
|
||||||
|
|
||||||
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, excluded_indicies=[]) -> 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 v.index in excluded_indicies:
|
|
||||||
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:
|
|
||||||
flat_islands = set(itertools.chain.from_iterable(islands))
|
|
||||||
any_unselected_vertex_in_group = find_any_vertex_in_group(mesh, vgroup, excluded_indicies=flat_islands)
|
|
||||||
if not any_unselected_vertex_in_group:
|
|
||||||
break
|
|
||||||
sys.setrecursionlimit(len(mesh.vertices)) # TODO: I guess recursion is bad and we should avoid it here?
|
|
||||||
island = find_weight_island_vertices(mesh, any_unselected_vertex_in_group.index, vgroup.index, vert_index_map, island=[])
|
|
||||||
sys.setrecursionlimit(990)
|
|
||||||
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. The smallest weight island is selected, but no weights are removed. Keep running 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_rogue_deform_weights(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 get_deforming_vgroups(obj):
|
|
||||||
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
|
|
||||||
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
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.reveal()
|
|
||||||
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_rogue_deform_weights(obj, vert_index_map)
|
|
||||||
|
|
||||||
if vgroup:
|
|
||||||
# Select the smallest island.
|
|
||||||
select_vertices(mesh, min(islands, key=len))
|
|
||||||
|
|
||||||
# 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.')
|
|
||||||
|
|
||||||
if rig:
|
|
||||||
rig.select_set(True)
|
|
||||||
mesh.use_paint_mask_vertex = True
|
|
||||||
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
|
|
||||||
else:
|
|
||||||
if rig:
|
|
||||||
rig.select_set(True)
|
|
||||||
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. Keep running the operator to cycle through groups with multiple weight islands to make sure they are desired.")
|
|
||||||
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
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
from bpy.utils import register_class
|
from bpy.utils import register_class
|
||||||
for c in classes:
|
for c in classes:
|
||||||
register_class(c)
|
register_class(c)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
for c in classes:
|
for c in classes:
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.props import BoolProperty, EnumProperty
|
from bpy.props import BoolProperty, EnumProperty
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
from .vertex_group_operators import FocusRogueDeformingWeights, DeleteEmptyDeformGroups, DeleteUnusedVertexGroups
|
from .vertex_group_operators import DeleteEmptyDeformGroups, DeleteUnusedVertexGroups
|
||||||
|
from .rogue_weights import FocusRogueDeformingWeights
|
||||||
|
|
||||||
class EASYWEIGHT_OT_wp_context_menu(bpy.types.Operator):
|
class EASYWEIGHT_OT_wp_context_menu(bpy.types.Operator):
|
||||||
""" Custom Weight Paint context menu """
|
""" Custom Weight Paint context menu """
|
||||||
|
Loading…
Reference in New Issue
Block a user