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
3 changed files with 162 additions and 6 deletions
Showing only changes of commit 52806cfd09 - Show all commits

View File

@ -45,6 +45,7 @@ The Vertex Groups context menu is re-organized with more icons and better labels
- **Ensure Mirror Groups**: If your object has a Mirror modifier, this will create any missing vertex groups.
- **Focus Deforming Bones**: Reveal and select all bones deforming this mesh. Only in Weight Paint mode.
If you have any more suggestions, feel free to open an Issue with a feature request.
- **Symmetrize Vertex Groups**: Symmetrizes vertex groups from left to right side, creating missing groups as needed.
### Force Apply Mirror Modifier
In Blender, you cannot apply a mirror modifier to meshes that have shape keys.

View File

@ -33,9 +33,6 @@ class MESH_MT_vertex_group_symmetry(bpy.types.Menu):
def draw(self, context):
layout = self.layout
# TODO: Add an operator that duplicates and mirrors active group
# TODO: Add an operator that duplicates and mirrors groups of all selected pose bones, or simply all groups.
# All this functionality could be merged into a single operator with a pop-up that asks you if you want proximity or topology based mirroring for the active group, all groups, or selected pose bones.
layout.operator(
"object.vertex_group_mirror",
text="Mirror Active Group (Proximity)",
@ -49,7 +46,21 @@ class MESH_MT_vertex_group_symmetry(bpy.types.Menu):
layout.separator()
layout.operator(CreateMirrorGroups.bl_idname, icon='MOD_MIRROR')
layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize Active Group",
icon='MOD_MIRROR'
).groups = 'ACTIVE'
layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize Selected Bones' Groups",
icon='MOD_MIRROR'
).groups = 'BONES'
layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize ALL Groups",
icon='MOD_MIRROR'
).groups = 'ALL'
class MESH_MT_vertex_group_sort(bpy.types.Menu):
bl_label = "Sort"

View File

@ -1,9 +1,11 @@
from typing import List
from typing import List, Tuple, Dict
import bpy
from bpy.types import Operator, VertexGroup, Object
from bpy.props import EnumProperty
from .utils.naming import flip_name
from mathutils.kdtree import KDTree
def get_deforming_armature(mesh_ob) -> Object:
for m in mesh_ob.modifiers:
@ -219,7 +221,7 @@ class DeleteUnusedVertexGroups(Operator):
self.report({'INFO'}, f"Deleted {len(deleted_names)} unused non-deform groups.")
return {'FINISHED'}
# TODO: This is now unused, remove it.
class CreateMirrorGroups(Operator):
"""Create missing Left/Right vertex groups to ensure correct behaviour of Mirror modifier"""
bl_idname = "object.ensure_mirror_vgroups"
@ -254,12 +256,154 @@ class CreateMirrorGroups(Operator):
return {'FINISHED'}
def get_symmetry_mapping(*
,obj: Object
,axis = 'X' # Only X axis is supported for now, since bpy.utils.flip_name() only supports X symmetry, as well as the "Mirror Vertex Group" checkbox in weight paint modeonly supports X symmetry.
,symmetrize_pos_to_neg = False
) -> Dict[int, int]:
"""
Create a mapping of vertex indicies, such that the index on one side maps
to the index on the opposite side of the mesh on a given axis.
"""
assert axis in 'XYZ', "Axis must be X, Y or Z!"
vertices = obj.data.vertices
size = len(vertices)
kd = KDTree(size)
for i, v in enumerate(vertices):
kd.insert(v.co, i)
kd.balance()
coord_i = 'XYZ'.find(axis)
# Figure out the function that will be used to determine whether a vertex
# should be skipped or not.
zero_or_more = lambda x: x >= 0
zero_or_less = lambda x: x <= 0
skip_func = zero_or_more if symmetrize_pos_to_neg else zero_or_less
# For any vertex with an X coordinate > 0, try to find a vertex at
# the coordinate with X flipped.
vert_map = {}
bad_counter = 0
for vert_idx, vert in enumerate(vertices):
if abs(vert.co[coord_i]) < 0.0001:
vert_map[vert_idx] = vert_idx
continue
# if skip_func(vert.co[coord_i]):
# continue
flipped_co = vert.co.copy()
flipped_co[coord_i] *= -1
_opposite_co, opposite_idx, dist = kd.find(flipped_co)
if dist > 0.1: # pretty big threshold, for testing.
bad_counter += 1
continue
if opposite_idx in vert_map.values():
# This vertex was already mapped, and another vertex just matched with it.
# No way to tell which is correct. Input mesh should just be more symmetrical.
bad_counter += 1
continue
vert_map[vert_idx] = opposite_idx
return vert_map
def symmetrize_vertex_group(*
,obj: Object
,vg_name: str
,symmetry_mapping: Dict[int, int]
,right_to_left = False
):
"""
Symmetrize weights of a single group. The symmetry_mapping should first be
calculated with get_symmetry_mapping().
"""
vg = obj.vertex_groups.get(vg_name)
if not vg:
return
opp_name = flip_name(vg_name)
opp_vg = obj.vertex_groups.get(opp_name)
if not opp_vg:
opp_vg = obj.vertex_groups.new(name=opp_name)
skip_func = None
if vg != opp_vg:
# Clear weights of the opposite group from all vertices.
opp_vg.remove(range(len(obj.data.vertices)))
else:
# If the name isn't flippable, only remove weights of vertices
# whose X coord >= 0.
# Figure out the function that will be used to determine whether a vertex
# should be skipped or not.
zero_or_more = lambda x: x >= 0
zero_or_less = lambda x: x <= 0
skip_func = zero_or_more if right_to_left else zero_or_less
# Write the new, mirrored weights
for src_idx, dst_idx in symmetry_mapping.items():
vert = obj.data.vertices[src_idx]
if skip_func != None and skip_func(vert.co.x):
continue
try:
src_weight = vg.weight(src_idx)
if src_weight == 0:
continue
except RuntimeError:
continue
opp_vg.add([dst_idx], src_weight, 'REPLACE')
class SymmetrizeVertexGroups(Operator):
"""Symmetrize weights of vertex groups"""
bl_idname = "object.symmetrize_vertex_weights"
bl_label = "Symmetrize Vertex Weights"
bl_options = {'REGISTER', 'UNDO'}
groups: EnumProperty(
name = "Subset"
,description = "Subset of vertex groups that should be symmetrized"
,items=[
('ACTIVE', 'Active', 'Active')
,('BONES', 'Selected Bones', 'Selected Bones')
,('ALL', 'All', 'All')
]
)
@classmethod
def poll(cls, context):
obj = context.object
if not (obj and obj.type=='MESH'):
return False
return obj.vertex_groups
def execute(self, context):
obj = context.object
symmetry_mapping = get_symmetry_mapping(obj=obj)
vgs = [obj.vertex_groups.active]
if self.groups == 'SELECTED':
# Get vertex groups of selected bones.
vgs = [obj.vertex_groups.get(pb.name) for pb in context.selected_pose_bones]
elif self.groups == 'ALL':
vgs = obj.vertex_groups
for vg in vgs:
symmetrize_vertex_group(
obj=obj,
vg_name=vg.name,
symmetry_mapping=symmetry_mapping
)
return {'FINISHED'}
classes = [
DeleteEmptyDeformGroups,
FocusDeformBones,
DeleteUnselectedDeformGroups,
DeleteUnusedVertexGroups,
CreateMirrorGroups,
SymmetrizeVertexGroups,
]
def register():