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. - **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. - **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. 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 ### Force Apply Mirror Modifier
In Blender, you cannot apply a mirror modifier to meshes that have shape keys. 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): def draw(self, context):
layout = self.layout 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( layout.operator(
"object.vertex_group_mirror", "object.vertex_group_mirror",
text="Mirror Active Group (Proximity)", text="Mirror Active Group (Proximity)",
@ -49,7 +46,21 @@ class MESH_MT_vertex_group_symmetry(bpy.types.Menu):
layout.separator() 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): class MESH_MT_vertex_group_sort(bpy.types.Menu):
bl_label = "Sort" bl_label = "Sort"

View File

@ -1,9 +1,11 @@
from typing import List from typing import List, Tuple, Dict
import bpy import bpy
from bpy.types import Operator, VertexGroup, Object from bpy.types import Operator, VertexGroup, Object
from bpy.props import EnumProperty
from .utils.naming import flip_name from .utils.naming import flip_name
from mathutils.kdtree import KDTree
def get_deforming_armature(mesh_ob) -> Object: def get_deforming_armature(mesh_ob) -> Object:
for m in mesh_ob.modifiers: 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.") self.report({'INFO'}, f"Deleted {len(deleted_names)} unused non-deform groups.")
return {'FINISHED'} return {'FINISHED'}
# TODO: This is now unused, remove it.
class CreateMirrorGroups(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"
@ -254,12 +256,154 @@ class CreateMirrorGroups(Operator):
return {'FINISHED'} 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 = [ classes = [
DeleteEmptyDeformGroups, DeleteEmptyDeformGroups,
FocusDeformBones, FocusDeformBones,
DeleteUnselectedDeformGroups, DeleteUnselectedDeformGroups,
DeleteUnusedVertexGroups, DeleteUnusedVertexGroups,
CreateMirrorGroups, CreateMirrorGroups,
SymmetrizeVertexGroups,
] ]
def register(): def register():