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.
|
- **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.
|
||||||
|
@ -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"
|
||||||
|
@ -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():
|
||||||
|
Loading…
Reference in New Issue
Block a user