I’m writing a script to mirror the skin weight painting from one side to the other…
Maybe my first question should be, is there already a build in function to do that? What I find is that blender does a mirror of the all weight painting but I didn’t find how to overwrite one side using the other and mirroring also the names of the groups.
So, if that option doesn’t exist, I’m creating a script to do that!
for now I’m almost able to do it, but the way I do it is really complicated and the script takes a long time to run because for each vertex on the good side, it needs to select vertex of the mesh and compare the coordinates to see if it’s the mirror one.
So an easier solution would be to be able to select a vertex by giving the supposed location with some tolerance. is it possible?
here is the full script… but it takes a lot of time, so if you can check and see where I can make it better so that it could go faster!
thank you.
Mathias.
bl_info = { "name": "Copy Mirror Vertex Weight",
"author": "Mathias Aubry",
"version": (1, 0),
"blender": (2, 6, 4),
"location": "View3D > Tool Shelf > Copy Mirror Weight",
"description": "Copy the weight of each vertex on one side and paste it on the other side",
"category": "Rigging"}
import bpy, bmesh
from bpy.props import *
from bpy.types import Operator, Panel
def copy_mirror_weight(Axis,Way,Pattern,special_pattern,left_side,right_side,tolerance):
print('start')
# 1- the origine should be in the middle of the object!
# entry by the user
# 3- + to - or the other way around
# side normal for pos to neg and inverse for the other way around
# how to mirror the name of the groups?
#items = [('4', '_l and _r', '4'),('3', '_L and _R', '3'),('2', '.l and .r', '2'),('1', '.L and .R', '1')])
ori_side='.L'
dest_side='.R'
if special_pattern==False:
if Pattern==1 and Way=="reverse":
ori_side='.L'
dest_side='.R'
if Pattern==1 and Way=="normal":
ori_side='.R'
dest_side='.L'
if Pattern==2 and Way=="reverse":
ori_side='.l'
dest_side='.r'
if Pattern==2 and Way=="normal":
ori_side='.r'
dest_side='.l'
if Pattern==3 and Way=="reverse":
ori_side='_L'
dest_side='_R'
if Pattern==3 and Way=="normal":
ori_side='_R'
dest_side='_L'
if Pattern==4 and Way=="reverse":
ori_side='_l'
dest_side='_r'
if Pattern==4 and Way=="normal":
ori_side='_r'
dest_side='_l'
if special_pattern==True:
if Way=="normal":
ori_side=left_side
dest_side=right_side
if Way=="reverse":
ori_side=right_side
dest_side=left_side
# end of entry!!!
obj = bpy.context.active_object
mesh = bpy.data.objects[obj.name].data.name
x_multiply = 1
y_multiply = 1
z_multiply = 1
if Axis == 'X':
axis_num = 0
x_multiply = -1
if Axis == 'Y':
axis_num = 1
y_multiply = -1
if Axis == 'Z':
axis_num = 2
z_multiply = -1
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
## create the dict for the groups attache the index to the name
DictGroup = {} ##ask the index it will give you the name
DictGroupINV = {} ## ask the name, it will give you the index
i=0
for group in bpy.context.active_object.vertex_groups:
DictGroup[i]=group.name
i+=1
for item in DictGroup:
DictGroupINV[DictGroup[item]]=item
#for each vertices of the mesh
for vertex in bpy.data.meshes[mesh].vertices:
# is it on the right side?
if (vertex.co[axis_num]>2*tolerance and Way=="normal") or (vertex.co[axis_num]<-2*tolerance and Way=="reverse"): ##select the side
print(vertex.index)
vertex_x = vertex.co[0]*x_multiply
vertex_y = vertex.co[1]*y_multiply
vertex_z = vertex.co[2]*z_multiply
myGroupVertex = {}
myNewGroupVertex = {}
for groups in bpy.data.meshes[mesh].vertices[vertex.index].groups:
myGroupVertex[groups.group] = groups.weight
for OneGroup in myGroupVertex:
if obj.vertex_groups[OneGroup].name.find(ori_side)>-1:
mirror_normal_group_name = obj.vertex_groups[OneGroup].name.replace(ori_side,dest_side)
myNewGroupVertex[DictGroupINV[mirror_normal_group_name]] = myGroupVertex[OneGroup]
if obj.vertex_groups[OneGroup].name.find(dest_side)>-1:
mirror_normal_group_name = obj.vertex_groups[OneGroup].name.replace(dest_side,ori_side)
myNewGroupVertex[DictGroupINV[mirror_normal_group_name]] = myGroupVertex[OneGroup]
if obj.vertex_groups[OneGroup].name.find(ori_side)==-1 and obj.vertex_groups[OneGroup].name.find(ori_side)==-1:
myNewGroupVertex[OneGroup] = myGroupVertex[OneGroup]
for vertexMI in bpy.data.meshes[mesh].vertices:
if vertex_x-tolerance<vertexMI.co[0]<vertex_x+tolerance:
if vertex_y-tolerance<vertexMI.co[1]<vertex_y+tolerance:
if vertex_z-tolerance<vertexMI.co[2]<vertex_z+tolerance:
bmesh.from_edit_mesh(bpy.data.meshes[mesh]).verts[vertexMI.index].select=True
##remove the older group
bpy.context.tool_settings.vertex_group_weight = 0
for group in bpy.data.meshes[mesh].vertices[vertexMI.index].groups:
bpy.ops.object.vertex_group_set_active(group = DictGroup[group.group])
bpy.ops.object.vertex_group_assign()
bpy.ops.object.vertex_group_clean(limit=0, all_groups=True, keep_single=False)
##add the new ones
for OneGroupVertex in myNewGroupVertex.keys():
## select the group
#print(DictGroup[OneGroupVertex])
bpy.ops.object.vertex_group_set_active(group = DictGroup[OneGroupVertex])
## set the weight
bpy.context.tool_settings.vertex_group_weight = myNewGroupVertex[OneGroupVertex]
## assign
bpy.ops.object.vertex_group_assign(new=False)
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
print('end')
def copy_mirror_weight_start():
obj=bpy.context.selected_objects[0].name
if len(bpy.context.selected_objects)!=1 or bpy.context.active_object.type!='MESH' or len(bpy.data.objects[obj].vertex_groups)==0:
window_error()
else:
window_mirror()
def window_error():
class DialogOperator(bpy.types.Operator):
bl_idname = "object.dialog_operator"
bl_label = "Error - Select only one object (mesh) with vertex groups"
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
bpy.utils.register_class(DialogOperator)
# Invoke the dialog when loading
bpy.ops.object.dialog_operator('INVOKE_DEFAULT')
def window_mirror():
class DialogOperator(bpy.types.Operator):
bl_idname = "object.dialog_operator"
bl_label = "Copy Mirror Weight"
enum_Axis = EnumProperty(name="On Which Axis?", default='X',
items = [('Z', 'Z axis', 'Z'),('Y', 'Y axis', 'Y'),('X', 'X axis', 'X')])
enum_Way = EnumProperty(name="Which Way?", default='normal',
items = [('reverse', 'right(-) to left(+)', 'reverse'),('normal', 'left(+) to right(-)', 'normal')])
enum_Pattern = EnumProperty(name="Which Pattern?", default='1',
items = [('4', '_l and _r', '4'),('3', '_L and _R', '3'),('2', '.l and .r', '2'),('1', '.L and .R', '1')])
special_pattern = BoolProperty(name="or... Use my own patter")
left_side = StringProperty(name="My own Patter, Left Side",default=".Left")
right_side = StringProperty(name="My own Patter, Right Side",default=".Right")
tolerance = FloatProperty(name="Tolerance", min=0, max=100, precision=3, default=0.001)
def execute(self, context):
copy_mirror_weight(self.enum_Axis,self.enum_Way,self.enum_Pattern,self.special_pattern,self.left_side,self.right_side,self.tolerance)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
bpy.utils.register_class(DialogOperator)
# Invoke the dialog when loading
bpy.ops.object.dialog_operator('INVOKE_DEFAULT')
#
# Menu in tools region
#
class VIEW3D_PT_tools_cloud(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_label = "Copy Mirror Weight"
bl_context = "objectmode"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
active_obj = context.active_object
layout = self.layout
col = layout.column(align=True)
col.label(text="Select one object")
col.label(text="With some group vertex Group")
col.operator("my.button", text="Copy Mirror Weight").number=1
# Button
class OBJECT_OT_Button(bpy.types.Operator):
bl_idname = "my.button"
bl_label = "Button"
number = bpy.props.IntProperty()
row = bpy.props.IntProperty()
loc = bpy.props.StringProperty()
def execute(self, context):
copy_mirror_weight_start()
return{'FINISHED'}
def register():
bpy.utils.register_module(__name__)
if __name__ == "__main__":
register()
I have a couple suggestions for ways that you can speed your system up. You basically have an O(N^2) algorithm here and typical ways to speed algorithms like this up are to trade memory for speed or to try to split the problem up and then combine the sub-problems.
I would try splitting the model and build some index lists into the vertices.
Run though the model once and assign each vertex into a “source” and “target” list. Now you just have to iterate the “source” list and just check for items in the “target” field.
Now don’t have 1 ‘target’ list. Have 3 target lists. One sorted by X, one by Y, and one by Z
Now you can use the ‘bisect’ module find target vertices much more quickly by:
bisect_left and bisect_right to find all the vertices that have an X value in your target range
bisect_left and bisect_right to find all the vertices that have a Y value in your target range
bisect_left and bisect_right to find all the vertices that have a Z value in your target range.
Create a Set() for each selection and Set.intersect() them to find your target value.
I think that would take your algorithm from O(n^2) to O(n log(n)) which would be a big improvement on large data sets. It all comes down to what the runtime performance of the Set() construction and intersection cost is.
Another strategy would be to break the model into pieces then solve the individual pieces. Since there is no “work” to combine the sub-problems I suspect that this would also be an O(n log(n)) algorithm. This is not so different from doing a mergesort or quicksort type of “divide and conquer” algorithm. The problem here would be dividing the model and solving the sub-problems in a memory efficient way. I think the straight forward, recursive implementation would chew up a lot of memory.
so with your advice kastoria, I check and change a few things but I notice something. Finding the mirror vertex is not what takes time, but it’s to assign the groups… so there isn’t much I can do about that.
For Safetyman, when you have a group named for example hip01.L but there isn’t a group hip01.R yet, the script is still going to create one.
I changed a few things and rearrange it… I’m still in the testing part. but with my old laptop, with a model of 4600 vertices, it takes 4.5 min to run!!! lol
I also added another button to clean the vertex, ie it removes groups assign to a vertex that are at 0.000
That’s interesting. I had another thought about how you could approach this that might be interesting. You can say that your problem is, “I want to mirror the weights from one side of the mesh to the other.”
I’m curious if this script runs faster than your current script and if it works correctly. It will only work correctly if your mesh has a “center line” across the mirror axis and if the weights between the two sides are evenly painted on the center line. As far as I know, most models created with x-axis symmetry or the mirror modifier will have a center line. Also this version will only work correctly if there is just 1 Mesh Object in the scene (but this can be fixed, I’m just being lazy.)
This tries to speed things up by taking advantage of the group structure that Blender has already built. At a high level, it takes the current model, deletes the “right side”, duplicates the “left side”, mirrors them back to the “right side”, then renames the groups in-place.
To use it, you select your target mesh in the 3D view and run the script. If this works then there are some improvements that would have to happen for general usage. The main improvement would be a better method of getting the ‘new’ mesh after it has been separated from the source mesh.
import bpy
def mirrorWeights(obj):
"""Mirrors the weights from L to R across the x-axis."""
# Ensure Object mode
bpy.ops.object.mode_set(mode="OBJECT")
# Select the right-side vertices
for v in obj.data.vertices:
if v.co.x < 0:
v.select = True
else:
v.select = False
# Delete them
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.delete()
# Select the left-side vertices
bpy.ops.object.mode_set(mode="OBJECT")
for v in obj.data.vertices:
if v.co.x >= 0:
v.select = True
# Duplicate and separate them
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.duplicate()
bpy.ops.mesh.separate(type="SELECTED")
bpy.ops.object.mode_set(mode="OBJECT")
# Find the new Object
newObj = None
for searchObj in bpy.data.objects:
if searchObj.name != obj.name and type(searchObj.data) == bpy.types.Mesh:
newObj = searchObj
break
# Mirror the vertices in the new object
for v in newObj.data.vertices:
v.co.x = v.co.x * -1.0
# Assign all "right" groups to a temp name
for group in newObj.vertex_groups:
if group.name.endswith("_R"):
group.name = group.name + "_tmp"
# Rename all the "left" groups to be "right"
for group in newObj.vertex_groups:
if group.name.endswith("_L"):
group.name = group.name[0:-2] + "_R"
# Rename all the "tmp" groups to be the "left" groups
for group in newObj.vertex_groups:
if group.name.endswith("_tmp"):
group.name = group.name[0:-6] + "_L"
# Join the two objects
obj.select = True
newObj.select = True
bpy.context.scene.objects.active = obj
bpy.ops.object.join()
# Remove doubles
for v in obj.data.vertices:
v.select = True
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.remove_doubles()
bpy.ops.object.mode_set(mode="OBJECT")
if __name__ == "__main__":
print("--- RUNNING ---")
mirrorWeights(bpy.context.active_object)
I tried it and it was really quick… to delete one half of my character!!! lol
I see what you want to accomplish and it would be definitely faster, the problem with that method (and I’m not sure yet if my method will solve the problem) is that if you have other info on your mesh like shape key, it might mess these other things.
but if you have any other ideas… I would take them!!!
And for Safetyman, in reality for now, my script doesn’t add the group automatically, I’m working on it right now
in that version, as Safetyman mentioned (and in fact was necessary) the script now creates mirrored groups if they were not present, but only if they are used on the original side.
For example let say that you have Group.L but no Group.R and that vertices on side L use Group.L when you mirror the weight, the script will create Group.R. But if no vertices is associating with Group.L it won’t create it.
Also to be able to save time, I added a feature, there is now a function in the Edit mode tool panel. In Edit Mode, you can select the vertices that you want to mirror. You need to select the vertices on the good side, the original side, and they will overwrite their mirrored vertices, even if these one are not selected. (in your selection it’s not a problem if you select some on the center line or the mirrored side, it won’t take them into account).
Hope it make thanks and I hope you find it useful.
Mathias.
there is in total 4 functions in this script,2 in object mode
2 in edit mode mesh
the 2 in object mode,copy mirror vertex
[INDENT=2]copy all the vertex weight of one side from the object, and past it on the other side, mirroring the name when possible
it is important to have the center geometry on EXACTLY on the center, on the axes on which you are going to mirror!
you also need to check yourlocal axes, since they are the one that matter for this script.
it could take some time to run, even minutes[/INDENT] clean vertex
[INDENT=2]remove every group that are at 0 for each vertex (not in the vertexGroup for the object)
it could take some time to run, even minutes[/INDENT]
the 2 in edit mode,copy mirror vertex
[INDENT=2]copy SELECTED vertex weight of one side from the object, and past it on the other side, mirroring the name when possible
it is important to have the center geometry on EXACTLY on the center, on the axes on which you are going to mirror!
you also need to check your local axes, since they are the one that matter for this script.
it could take some time to run, even minutes but less than the all object[/INDENT] copy vertex weight
[INDENT=2]copy the weight of the first vertex selected and past the exact same weight to the other (no mirror on this one)
make sure that your first selection is only ONE vertex, and then you can select others even using a cloud selection.[/INDENT]
Ok, so I updated the code below. It’s for the whole mesh but by changing the way of assigning the weight, I was able to go from 5min to 15sec. I’m pretty happy about that! I will update it so that it can apply to selected vertices in Edit mode.
here is my magic command:
bpy.data.objects[‘obj’].vertex_groups[‘groupName’].add([index_vertex],weight,‘REPLACE’)
example:
bpy.data.objects[‘Plane’].vertex_groups[‘root01.C’].add([100],1,‘REPLACE’)
Which is what you would do to assign the weight manually and it’s fine for one vertex but really long for thousands of them. So here is the code for the new version.
import bpy, bmesh
import datetime
from bpy.props import *
from bpy.types import Operator, Panel
def copy_mirror_weight(Axis,Way,Pattern,special_pattern,left_side,right_side,tolerance):
start = (datetime.datetime.now())
print('start')
#items = [('4', '_l and _r', '4'),('3', '_L and _R', '3'),('2', '.l and .r', '2'),('1', '.L and .R', '1')])
L_side=''
R_side=''
ori_side=''
dest_side=''
if Pattern=='1':
L_side='.L'
R_side='.R'
if Pattern=='2':
L_side='.l'
R_side='.r'
if Pattern=='3':
L_side='_L'
R_side='_R'
if Pattern=='4':
L_side='_l'
R_side='_r'
if special_pattern==True:
ori_side=left_side
dest_side=right_side
obj = bpy.context.active_object
mesh = bpy.data.objects[obj.name].data.name
x_multiply = 1
y_multiply = 1
z_multiply = 1
if Axis == 'X':
axis_num = 0
x_multiply = -1
if Axis == 'Y':
axis_num = 1
y_multiply = -1
if Axis == 'Z':
axis_num = 2
z_multiply = -1
## create the dict for the groups attache the index to the name
DictGroup = {} ##ask the index it will give you the name
DictGroupINV = {} ## ask the name, it will give you the index
i=0
for group in bpy.context.active_object.vertex_groups:
DictGroup[i]=group.name
i+=1
for item in DictGroup:
DictGroupINV[DictGroup[item]]=item
#for each vertices of the mesh
DictVertexOri = {}
DictVertexDest = {}
DictMirror = {}
L_side_count = 0
R_side_count = 0
for vertex in bpy.data.meshes[mesh].vertices:
#create the dicts for the good side
#print (vertex.co[axis_num])
if (vertex.co[axis_num]>tolerance and Way=='normal') or (vertex.co[axis_num]<-tolerance and Way=='reverse'):#compare with the tolerance and position of the origin
DictVertexOri[vertex.index]=vertex.co
#create the dicts for the mirror side
if (vertex.co[axis_num]<tolerance and Way=='normal') or (vertex.co[axis_num]>-tolerance and Way=='reverse'):#compare with the tolerance and position of the origin
#print("MIRROR")
DictVertexDest[vertex.index]=vertex.co
for vertex in DictVertexOri:
for groups in bpy.data.meshes[mesh].vertices[vertex].groups: #when it's the automatic pattern, it checks the most used on the original side .L or .R and assign them after
if obj.vertex_groups[groups.group].name.find(L_side)>-1:
L_side_count+=1
if obj.vertex_groups[groups.group].name.find(R_side)>-1:
R_side_count+=1
if L_side_count>R_side_count and special_pattern==False:
ori_side=L_side
dest_side=R_side
if R_side_count>L_side_count and special_pattern==False:
ori_side=R_side
dest_side=L_side
vertexCoXOri = DictVertexOri[vertex][0]
vertexCoYOri = DictVertexOri[vertex][1]
vertexCoZOri = DictVertexOri[vertex][2]
for vertexMirror in DictVertexDest: #check the coordinates of each axis, the multiply is inversing the mirror axes from -x.xxxx to x.xxxx so that the side doesn't matter
if vertexCoXOri-tolerance<DictVertexDest[vertexMirror][0]*x_multiply<vertexCoXOri+tolerance:
if vertexCoYOri-tolerance<DictVertexDest[vertexMirror][1]*y_multiply<vertexCoYOri+tolerance:
if vertexCoZOri-tolerance<DictVertexDest[vertexMirror][2]*z_multiply<vertexCoZOri+tolerance:
DictMirror[vertex]=vertexMirror
for vertex in DictMirror:
myGroupVertex = {}
myMirrorGroupVertex = {}
for groups in bpy.data.meshes[mesh].vertices[vertex].groups:
myGroupVertex[groups.group] = groups.weight #create a dictionary nameGroup = weight
for OneGroup in myGroupVertex: #create a new dictionnary with the name mirror
if obj.vertex_groups[OneGroup].name.find(ori_side)>-1:
mirror_normal_group_name = obj.vertex_groups[OneGroup].name.replace(ori_side,dest_side)
if mirror_normal_group_name in DictGroupINV:
myMirrorGroupVertex[DictGroupINV[mirror_normal_group_name]] = myGroupVertex[OneGroup]
else:
#if this group doesn't exist yet add it to the group and the dictionnary
length=len(DictGroup)
bpy.context.active_object.vertex_groups.new(name=mirror_normal_group_name)
DictGroup[length] = mirror_normal_group_name
DictGroupINV[mirror_normal_group_name]=length
if obj.vertex_groups[OneGroup].name.find(dest_side)>-1:
mirror_normal_group_name = obj.vertex_groups[OneGroup].name.replace(dest_side,ori_side)
if mirror_normal_group_name in DictGroupINV:
myMirrorGroupVertex[DictGroupINV[mirror_normal_group_name]] = myGroupVertex[OneGroup]
else:
#if this group doesn't exist yet add it to the group and the dictionnary
length=len(DictGroup)
bpy.context.active_object.vertex_groups.new(name=mirror_normal_group_name)
DictGroup[length] = mirror_normal_group_name
DictGroupINV[mirror_normal_group_name]=length
if obj.vertex_groups[OneGroup].name.find(ori_side)==-1 and obj.vertex_groups[OneGroup].name.find(dest_side)==-1:
myMirrorGroupVertex[OneGroup] = myGroupVertex[OneGroup]
##remove the older group
for group in bpy.data.meshes[mesh].vertices[DictMirror[vertex]].groups:
print(DictGroup[group.group])
print(DictMirror[vertex])
obj.vertex_groups[DictGroup[group.group]].remove([DictMirror[vertex]])
#add the new ones
for OneGroupVertex in myMirrorGroupVertex.keys():
obj.vertex_groups[DictGroup[OneGroupVertex]].add([DictMirror[vertex]],myMirrorGroupVertex[OneGroupVertex],'REPLACE')
print('end')
end = (datetime.datetime.now())
timing = end-start
print(timing)
def window_mirror():
class DialogOperator(bpy.types.Operator):
bl_idname = "object.dialog_operator"
bl_label = "Copy Mirror Weight - WARNING USE LOCAL AXES"
enum_Axis = EnumProperty(name="On Which Axis?", default='X',
items = [('Z', 'Z axis', 'Z'),('Y', 'Y axis', 'Y'),('X', 'X axis', 'X')])
enum_Way = EnumProperty(name="Which Way?", default='normal',
items = [('reverse', '(-) to (+)', 'reverse'),('normal', '(+) to (-)', 'normal')])
enum_Pattern = EnumProperty(name="Which Pattern?", default='1',
items = [('4', '_l and _r', '4'),('3', '_L and _R', '3'),('2', '.l and .r', '2'),('1', '.L and .R', '1')])
special_pattern = BoolProperty(name="or... Use my own patter")
left_side = StringProperty(name="My own Patter, Left Side",default=".Left")
right_side = StringProperty(name="My own Patter, Right Side",default=".Right")
tolerance = FloatProperty(name="Tolerance", min=0, max=100, precision=3, default=0.001)
def execute(self, context):
copy_mirror_weight(self.enum_Axis,self.enum_Way,self.enum_Pattern,self.special_pattern,self.left_side,self.right_side,self.tolerance)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
bpy.utils.register_class(DialogOperator)
# Invoke the dialog when loading
bpy.ops.object.dialog_operator('INVOKE_DEFAULT')
window_mirror()
So ideally, I could weight paint one side of a character (with proper naming), run your script and it will mirror all the weight painting to the other side?
Hey guys, don’t know if that’s what you are looking for, but I’ve noticed that you can already mirror a vertex group with the builtin tools.
1: Go to Object Data -> Vertex Groups
2: Select the group you want to mirror to the other side.
3: Press “copy vertex group”.
4: Select the copy and press “Mirror vertex group”.
5: Change the name of the mirrored copy accordingly.
thanks Robo, it’s very nice. I don’t know how to send that to the dev and I’m not sure if they would be interested but it’s very nice that you think so!
Hey Robo, I didn’t see your last post.
It’s true, you can do it this way, and there is actually a lot of tools that help to mirror the weight painting. I find Blender is really advance for that.
My script do all of the mirroring and the “renaming” at the same time… or I hope it does! lol. I did it because often when I’m rigging and testing new way to rig, I create one side of the rig, test it, and then mirror it if it works well. When I do mirror it, usually I’m well advance in the weight painting and mirroring all the groups one by one is a bit time consuming.
But it’s because I’m a bit messy when I’m rigging! lol
Super appreciative for this. it’s my first time posting to this forum. I wanted to give back to the community so I updated this script to work in blender 2.79 and made it installable as a normal addon.
save the python below to MirrorAllVertexGroups.py and install it. it will add a menu to the normal vertex group menu called “Mirror All Vertex Groups”.
Blender and it’s community is awesome.
cheers
-bay
ps: I added it to a github here just so it didn’t get lost or if I ended up changing it more. MathiasA, should I take it down, link, adjust credit etc in some way?
bl_info = {
'name': 'Mirror All Vertex Groups',
'author': 'mathiasA',
'version': (0, 1),
'blender': (2, 6, 7),
'category': 'Animation',
'location': 'Mesh > Vertex Group Menu',
'wiki_url': ''}
import bpy, bmesh
import datetime
from bpy.props import *
from bpy.types import Operator, Panel
def copy_mirror_weight(Axis,Way,Pattern,special_pattern,left_side,right_side,tolerance):
start = (datetime.datetime.now())
print('start')
bpy.ops.object.mode_set(mode='OBJECT')
#items = [('4', '_l and _r', '4'),('3', '_L and _R', '3'),('2', '.l and .r', '2'),('1', '.L and .R', '1')])
L_side=''
R_side=''
ori_side=''
dest_side=''
if Pattern=='1':
L_side='.L'
R_side='.R'
if Pattern=='2':
L_side='.l'
R_side='.r'
if Pattern=='3':
L_side='_L'
R_side='_R'
if Pattern=='4':
L_side='_l'
R_side='_r'
if special_pattern==True:
ori_side=left_side
dest_side=right_side
obj = bpy.context.active_object
mesh = bpy.data.objects[obj.name].data.name
x_multiply = 1
y_multiply = 1
z_multiply = 1
if Axis == 'X':
axis_num = 0
x_multiply = -1
if Axis == 'Y':
axis_num = 1
y_multiply = -1
if Axis == 'Z':
axis_num = 2
z_multiply = -1
## create the dict for the groups attache the index to the name
DictGroup = {} ##ask the index it will give you the name
DictGroupINV = {} ## ask the name, it will give you the index
i=0
for group in bpy.context.active_object.vertex_groups:
DictGroup[i]=group.name
i+=1
for item in DictGroup:
DictGroupINV[DictGroup[item]]=item
#for each vertices of the mesh
DictVertexOri = {}
DictVertexDest = {}
DictMirror = {}
L_side_count = 0
R_side_count = 0
for vertex in bpy.data.meshes[mesh].vertices:
#create the dicts for the good side
#print (vertex.co[axis_num])
if (vertex.co[axis_num]>tolerance and Way=='normal') or (vertex.co[axis_num]<-tolerance and Way=='reverse'): #compare with the tolerance and position of the origin
DictVertexOri[vertex.index]=vertex.co
#create the dicts for the mirror side
if (vertex.co[axis_num]<tolerance and Way=='normal') or (vertex.co[axis_num]>-tolerance and Way=='reverse'):#compare with the tolerance and position of the origin
#print("MIRROR")
DictVertexDest[vertex.index]=vertex.co
for vertex in DictVertexOri:
for groups in bpy.data.meshes[mesh].vertices[vertex].groups: #when it's the automatic pattern, it checks the most used on the original side .L or .R and assign them after
if obj.vertex_groups[groups.group].name.find(L_side)>-1:
L_side_count+=1
if obj.vertex_groups[groups.group].name.find(R_side)>-1:
R_side_count+=1
if L_side_count>R_side_count and special_pattern==False:
ori_side=L_side
dest_side=R_side
if R_side_count>L_side_count and special_pattern==False:
ori_side=R_side
dest_side=L_side
vertexCoXOri = DictVertexOri[vertex][0]
vertexCoYOri = DictVertexOri[vertex][1]
vertexCoZOri = DictVertexOri[vertex][2]
for vertexMirror in DictVertexDest: #check the coordinates of each axis, the multiply is inversing the mirror axes from -x.xxxx to x.xxxx so that the side doesn't matter
if vertexCoXOri-tolerance<DictVertexDest[vertexMirror][0]*x_multiply<vertexCoXOri+tolerance:
if vertexCoYOri-tolerance<DictVertexDest[vertexMirror][1]*y_multiply<vertexCoYOri+tolerance:
if vertexCoZOri-tolerance<DictVertexDest[vertexMirror][2]*z_multiply<vertexCoZOri+tolerance:
DictMirror[vertex]=vertexMirror
for vertex in DictMirror:
myGroupVertex = {}
myMirrorGroupVertex = {}
for groups in bpy.data.meshes[mesh].vertices[vertex].groups:
myGroupVertex[groups.group] = groups.weight #create a dictionary nameGroup = weight
for OneGroup in myGroupVertex: #create a new dictionnary with the name mirror
if obj.vertex_groups[OneGroup].name.find(ori_side)>-1:
mirror_normal_group_name = obj.vertex_groups[OneGroup].name.replace(ori_side,dest_side)
if mirror_normal_group_name in DictGroupINV:
myMirrorGroupVertex[DictGroupINV[mirror_normal_group_name]] = myGroupVertex[OneGroup]
else:
#if this group doesn't exist yet add it to the group and the dictionnary
length=len(DictGroup)
bpy.context.active_object.vertex_groups.new(name=mirror_normal_group_name)
DictGroup[length] = mirror_normal_group_name
DictGroupINV[mirror_normal_group_name]=length
if obj.vertex_groups[OneGroup].name.find(dest_side)>-1:
mirror_normal_group_name = obj.vertex_groups[OneGroup].name.replace(dest_side,ori_side)
if mirror_normal_group_name in DictGroupINV:
myMirrorGroupVertex[DictGroupINV[mirror_normal_group_name]] = myGroupVertex[OneGroup]
else:
#if this group doesn't exist yet add it to the group and the dictionnary
length=len(DictGroup)
bpy.context.active_object.vertex_groups.new(name=mirror_normal_group_name)
DictGroup[length] = mirror_normal_group_name
DictGroupINV[mirror_normal_group_name]=length
if obj.vertex_groups[OneGroup].name.find(ori_side)==-1 and obj.vertex_groups[OneGroup].name.find(dest_side)==-1:
myMirrorGroupVertex[OneGroup] = myGroupVertex[OneGroup]
##remove the older group
for group in bpy.data.meshes[mesh].vertices[DictMirror[vertex]].groups:
print(DictGroup[group.group])
print(DictMirror[vertex])
obj.vertex_groups[DictGroup[group.group]].remove([DictMirror[vertex]])
#add the new ones
for OneGroupVertex in myMirrorGroupVertex.keys():
obj.vertex_groups[DictGroup[OneGroupVertex]].add([DictMirror[vertex]],myMirrorGroupVertex[OneGroupVertex],'REPLACE')
print('end')
end = (datetime.datetime.now())
timing = end-start
print(timing)
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
class MirrorAllVertexGroups(bpy.types.Operator):
"""Mirror All Vertex groups"""
bl_idname = "object.mirror_all_vertexgroups"
bl_label = "Mirror All Vertex Groups"
bl_options = {'REGISTER', 'UNDO'}
action = bpy.props.StringProperty()
enum_Axis = EnumProperty(name="On Which Axis?", default='X',
items = [('Z', 'Z axis', 'Z'),('Y', 'Y axis', 'Y'),('X', 'X axis', 'X')])
enum_Way = EnumProperty(name="Which Way?", default='normal',
items = [('reverse', '(-) to (+)', 'reverse'),('normal', '(+) to (-)', 'normal')])
enum_Pattern = EnumProperty(name="Which Pattern?", default='1',
items = [('4', '_l and _r', '4'),('3', '_L and _R', '3'),('2', '.l and .r', '2'),('1', '.L and .R', '1')])
special_pattern = BoolProperty(name="or... Use my own patter")
left_side = StringProperty(name="My own Patter, Left Side",default=".Left")
right_side = StringProperty(name="My own Patter, Right Side",default=".Right")
tolerance = FloatProperty(name="Tolerance", min=0, max=100, precision=3, default=0.001)
def execute(self, context):
copy_mirror_weight(self.enum_Axis,self.enum_Way,self.enum_Pattern,self.special_pattern,self.left_side,self.right_side,self.tolerance)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def menu_func_mirror(self, context):
self.layout.separator()
self.layout.operator(MirrorAllVertexGroups.bl_idname, text="Mirror all vertex groups", icon='ARROW_LEFTRIGHT')
bpy.ops.object.dialog_operator('INVOKE_DEFAULT')
def register():
bpy.utils.register_class(MirrorAllVertexGroups)
bpy.types.MESH_MT_vertex_group_specials.append(menu_func_mirror)
def unregister():
bpy.types.MESH_MT_vertex_group_specials.remove(menu_func_mirror)
bpy.utils.unregister_class(MirrorAllVertexGroups)
if __name__ == "__main__":
register()
Thank you for posting. That’s quite an old post you revived there!
Completely forgot about it!
That’s really nice of you to have updated this script and post it on github! Don’t take it down!!! But feel free to adjust the credit to add your name like : ‘author’ : ‘mathiasA updated by Spiraloid’,
I wonder if it’s going to work with 2.8! this new version is awsome!