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
5 changed files with 175 additions and 145 deletions
Showing only changes of commit d7dd9f2559 - Show all commits

View File

@ -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
View 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.

View File

@ -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"

View File

@ -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:

View File

@ -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 """