Monday, November 1, 2021

weights transfer partial mesh to whole in blender

Hi,

This is a tool i wrote for blender 2.79 to transfer weights from a partial mesh to a whole mesh. the partial mesh needs to be exactly same as whole mesh with some faces removed.  it also requires the source bones to be named the same as the destination bones.  The tool is still a work in progress and hasn’t been tested on full faces yet. (modify use at your own risk)


 

Happy Scripting! and Happy Sketching!
Nate

 

#simple tool to help transfer weights from a partial mesh to a whole mesh.
#it assumes partial mesh is whole mesh with some faces deleted.
#
#modify use at your own risk

#last modified
#102621 -- working on initial release
#101521,1016 -- wrote main class - working on blocking

bl_info = {
    "name":"weights copy tool",
    "description":"copy weights from a partial mesh to whole mesh",
    "category": "Object",
    "author":"Nathaniel Anozie",
    "blender":(2,79,0)
}


import bpy

from bpy.props import(
    StringProperty,
    PointerProperty
    )

from bpy.types import(
    Operator,
    Panel,
    PropertyGroup
    )


class WeightsTransferOperator(Operator):
    """transfer weights from a partial mesh to a whole mesh
    """
    bl_idname = "obj.do_weightstransfer"
    bl_label = "Weights Transfer"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
    
        self.report({'INFO'}, "Starting Weights Transfer ...")
        wtc_obj = WeightsTransferCreator(source_mesh = context.scene.weightstransfer_prop.source_mesh, 
                                destination_mesh= context.scene.weightstransfer_prop.destination_mesh, 
                                remainder_bone_source = context.scene.weightstransfer_prop.remainder_bone_source,
                                remainder_bone_destination = context.scene.weightstransfer_prop.remainder_bone_destination
                                )
        wtc_obj.run()

        self.report({'INFO'}, "Completed Weights Transfer")
        return {'FINISHED'}
        
class WeightsTransferPanel(Panel):
    bl_label = "Weights Transfer Panel"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    
    def draw(self, context):
        
        #here we add textfields and button to ui
        #
        layout = self.layout
        layout.label(text = "Weights Transfer tool")
        
        #4 text fields in a row
        #text fields
        layout.prop( context.scene.weightstransfer_prop, "source_mesh", text = "source mesh" )
        layout.prop( context.scene.weightstransfer_prop, "destination_mesh", text = "destination mesh" )
        layout.prop( context.scene.weightstransfer_prop, "remainder_bone_source", text = "source remainder bone" )
        layout.prop( context.scene.weightstransfer_prop, "remainder_bone_destination", text = "destination remainder bone" )
        
        #button
        layout.operator( "obj.do_weightstransfer")
 
 
class WeightsTransferProperties(PropertyGroup):
    #here we make each textfield
    source_mesh = StringProperty(
        name = "source_mesh",
        description = "this is the source partial mesh object name. it should have the same faces as destination mesh with some deleted"
        )

    destination_mesh = StringProperty(
        name = "destination_mesh",
        description = "this is the destination whole mesh object name"
        )

    remainder_bone_source = StringProperty(
        name = "remainder_bone_source",
        description = "bone to hold remainder weights of source mesh"
        )

    remainder_bone_destination = StringProperty(
        name = "remainder_bone_destination",
        description = "bone to hold remainder weights of destination mesh"
        )

def register():
    bpy.utils.register_class(WeightsTransferOperator)
    bpy.utils.register_class(WeightsTransferPanel)
    bpy.utils.register_class(WeightsTransferProperties)
    #here we name the property that holds all our textfields
    bpy.types.Scene.weightstransfer_prop = PointerProperty(
        type = WeightsTransferProperties
        )

def unregister():
    bpy.utils.unregister_class(WeightsTransferOperator)
    bpy.utils.unregister_class(WeightsTransferPanel)
    bpy.utils.unregister_class(WeightsTransferProperties)
    del bpy.types.Scene.weightstransfer_prop


class WeightsTransferCreator(object):
    """this object helps with transfering weights from a partial source mesh to a whole destination mesh
    """
    def __init__(self, source_mesh = None, destination_mesh = None, bones_source_to_destination = None, remainder_bone_source = None, remainder_bone_destination = None ):
        """
        source_mesh - this is the source partial mesh object str name
        destination_mesh - this is the destination whole mesh object str name
        bones_source_to_destination - this is a mapping from the source bones to destination bones as dictionary ex: {'bone_a':'bone_a','bone_b':'bone_b'}
         if nothing provided it assumes same bone names in destination as there are in source
        remainder_bone_source - bone to hold remainder weights of source mesh
        remainder_bone_destination - bone to hold remainder weights of destination mesh
        """
        #verify inputs exist (meshes exist, bones exist etc)
        self.source_mesh = source_mesh
        self.destination_mesh = destination_mesh
        self.remainder_bone_source = remainder_bone_source
        self.remainder_bone_destination = remainder_bone_destination
        self.bones_source_to_destination = {}
        if bones_source_to_destination:
            self.bones_source_to_destination = bones_source_to_destination
        else:
            #assume same bone names in destination as there are in source
            self.bones_source_to_destination = self._getDefaultSourceToDestinationMapping(mesh_name = source_mesh, excludes = [remainder_bone_source])
        
    def run(self):
        """to be called by user to do the entire weight transfer process
        """
        vertex_source_to_destination = self._getVertexIDSourceToDestination()
        if not vertex_source_to_destination:
            print("could not do the weight transfer cannot find source to destination vertex mapping")
            return
        self._transferWeights(vertex_source_to_destination)
    
    def _transferWeights(self, vertices_source_to_destination = None ):
        """this changes weights on destination mesh. it uses source meshes weights
        vertices_source_to_destination - mapping of source vertex id to its closes destination vertex ex: {0:3,1:2}
        """
        #print("vertices_source_to_destination >>",vertices_source_to_destination)
        
        #loop vertices of source mesh
        for src_vert_id, dest_vert_id in vertices_source_to_destination.items():
            bones_affecting_vertex = self._getBoneNamesAffectingVertex( vertex_id = src_vert_id, mesh_name = self.source_mesh )
            
            #get weights from source mesh
            for src_bone, dest_bone in self.bones_source_to_destination.items():
                #skip if src_bone has no weights on source vertex
                if src_bone not in bones_affecting_vertex:
                    continue
                src_weight = bpy.data.objects[self.source_mesh].vertex_groups[src_bone].weight(src_vert_id)
                #print("for destination vert %s and dest bone %s source weight is %s" %(dest_vert_id, dest_bone, src_weight) )
                ##do the actual setting of individual bone weight on destination mesh
                bpy.data.objects[self.destination_mesh].vertex_groups[dest_bone].add([dest_vert_id], src_weight, 'REPLACE' )
                
            #handle the remainder bone weight to be set once per vertex
            src_remainder_weight = bpy.data.objects[self.source_mesh].vertex_groups[self.remainder_bone_source].weight(src_vert_id)
            #print("for destination vert %s and dest bone %s source remainder weight is %s" %(dest_vert_id, dest_bone, src_remainder_weight) )              
            ##set the remainder weight on destination mesh
            bpy.data.objects[self.destination_mesh].vertex_groups[self.remainder_bone_destination].add([dest_vert_id], src_remainder_weight, 'REPLACE' )

    def _getBoneNamesAffectingVertex( self, vertex_id = None, mesh_name = None ):
        """get all bone names that have a weight on vertex id
        mesh_name - mesh object name str that has given vertex_id
        """
        result = []
        vgListGroups = bpy.data.objects[mesh_name].vertex_groups
        for grp in bpy.data.objects[mesh_name].data.vertices[vertex_id].groups:
            if grp.weight > 0.0:
                vgListIndex = grp.group
                #print(vgListGroups[vgListIndex].name)
                result.append(vgListGroups[vgListIndex].name)
        return result

    def _getVertexIDSourceToDestination(self):
        """uses nearest position of vertices to find closes destination mesh vertex to source mesh vertex
        """
        result = {}
        close_enough_dist = 0.00001 #closer than this counts as a match
        
        #loop MeshVertex objects of source finding closest in destination
        for src_vert in bpy.data.objects[self.source_mesh].data.vertices:
            src_pos = src_vert.co
            for dest_vert in bpy.data.objects[self.destination_mesh].data.vertices:
                dest_pos = dest_vert.co
                if (src_pos - dest_pos).magnitude < close_enough_dist:
                    #found closest vertex
                    result[src_vert.index] = dest_vert.index
                    break
        #might be nice to check if number keys match number verts of source mesh to indicate we found a match for all verts
        if len(bpy.data.objects[self.source_mesh].data.vertices) != len(result.keys()):
            print("check that source partial mesh is the whole mesh with some faces deleted. need them to overlap exactly")
            return False
        return result

    def _getDefaultSourceToDestinationMapping(self, mesh_name = None, excludes = []):
        """returns a dictionary of form {"Bone.001":"Bone.001","Bone.002":"Bone.002"} 
        they are all the bone names bound to mesh excluding the bone names in excludes list.
        it assumes mesh_name is already bound to some bones
        """
        result = {}
        bones = [vg.name for vg in bpy.data.objects[mesh_name].vertex_groups if vg.name not in excludes]
        for bone in bones:
            result[bone] = bone
        return result

"""
import bpy
import sys
sys.path.append("/users/Nathaniel/Documents/src_blender/python/weightTools")
import naPartPaintToWholeAddon as mod
import imp
imp.reload(mod)

obj = mod.WeightsTransferCreator(source_mesh = "partial", 
                                destination_mesh="whole", 
                                remainder_bone_source = "Bone",
                                remainder_bone_destination = "Bone"
                                )
obj.run()

"""

"""
obj = mod.WeightsTransferCreator(source_mesh = "partial", 
                                destination_mesh="whole", 
                                remainder_bone_source = "Bone",
                                remainder_bone_destination = "Bone",
                                bones_source_to_destination = {"Bone.001":"Bone.001","Bone.002":"Bone.002"}
                                )
obj.run()
"""


"""getting world position of a vertex or face
this gets local position - it doesnt change when mesh is moved in object space
#uses vertex id
>>> bpy.data.objects['Cube'].data.vertices[4].co
Vector((1.0, -1.0, -1.0))

#uses face id
>>> bpy.data.objects['Cube'].data.polygons[2].center
Vector((1.0, 0.0, 0.0))

"""

"""getting all the bones with some weight on a vertex

#getting a bone weighted to a vertex id (vertex id is 1, assumes groups has a zero index
>>> bpy.data.objects['Cube'].data.vertices[1].groups[0].group
1
#the number returned above is the index in panel of vertex groups
>>> bpy.data.objects['Cube'].vertex_groups[1].name
'Bone.001'

#two ways to find weight of a vertex id. (vertex id is 1)
>>> bpy.data.objects['Cube'].vertex_groups[1].weight(1)
1.0
#or
>>> bpy.data.objects['Cube'].data.vertices[1].groups[0].weight
1.0

####to get all bone names with some weight for vertex id (vertex id is 5)
import bpy
vgListGroups = bpy.data.objects['Cube'].vertex_groups
for grp in bpy.data.objects['Cube'].data.vertices[5].groups:
    if grp.weight > 0.0:
        vgListIndex = grp.group
        print(vgListGroups[vgListIndex].name)
"""


"""setting weight on a vertex id and bone name (vertex id is 1)
#need to be in object mode
>>> bpy.data.objects['Cube'].vertex_groups['Bone'].add([1], 0.5, 'REPLACE' )
"""