Add Easy_Weight
to Addons
#47
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user