Copy and Mirror Weight vertex

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?

Thank you very much for your help.

Mathias

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()

This would be very useful. Does it also automatically assign and name the various vertex groups so that a rig will recognize them?

not yet, but that would be a good idea… I will check that!

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.

thank you kastoria, and btw thank you Safetyman

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. :slight_smile:

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

here is the code!

looking for some feedback :wink:
Enjoy!

copy_mirror_weight_vertex.zip (3.14 KB)

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)

Hey kastoria,

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

thank you kastoria

Yeah, it would probably not work on shape keys and it would mirror all the UVs as well.

Looks like you are coming along with it. I’ll be keeping an eye on this thread. Thanks for the effort so far.

here is a new version.

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 your local 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]

Attachments

copy_mirror_weight_vertex.zip (4.56 KB)

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’)

this commande replace 3 of my previous one:

set the active group

bpy.ops.object.vertex_group_set_active(group = DictGroup[OneGroupVertex])

set the weight in the vertex group tool

bpy.context.tool_settings.vertex_group_weight = myNewGroupVertex[OneGroupVertex]

assign

bpy.ops.object.vertex_group_assign(new=False)

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?

yes, It should work like that. :slight_smile:

if you test it, I would love to have some feedback!

Mathias.

Please send this script upstream this feature is a must have.

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.

Hope this helps.

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!

:wink:

Mathias.

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

Thanks again.

Mathias.

Hey All,

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()
2 Likes

Hey, Spiraloid!!!

Thank you for posting. That’s quite an old post you revived there! :wink:
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!

Thank you again for your post!
Have a good day

Mathias.

3 Likes