3
$\begingroup$

I know that shapekeys can be transferred between meshes of similar topology with ease.

My case involves transferring shape keys from ManuelBastioni Lab Character Face to the moustache.

I have written a small script which helped me get this done, however it takes really long to execute. I'm a novice to python and blender scripting and hope you can advise me about improving this code or provide an alternative solution.

In this solution the time can be improved by deleting parts of the source mesh which you think should not affect the destination mesh.

#sorry if this script is poorly written. 
#Originally created to transfer face shapekeys from Manuel Bastioni character face to a moustache made with mesh cards
#Please ensure that the destination and source meshes have the same names and number of shape keys
#place both the meshes at origin and overlap them with similar scale
#To quickly copy shape key names join both meshes and then separate them. Now destination mesh has the same names as source mesh
#import required modules
import bpy
import bmesh
from mathutils import Vector
from time import sleep
import datetime

# commonly used references
scene = bpy.context.scene
context = bpy.context

# step size to increase the sphere selection
increment_radius = .5

#set total vertices incase you want to run for less number of vertices also set specify_end_vertex to True
#set current_vertex index incase you want to continue from another index
#use_one_vertex will transfer the weight of the first vertex with the selection sphere
current_vertex_index = 0
total_vertices = 0
specify_end_vertex = False
use_one_vertex = False

#name of source mesh
src = "man"
#name of destination mesh
dest= "moustache"

#global variables which should not be set
key_name = ""
dest_mesh = None
src_mesh = None
dest_shape_key = None
dest_shape_key_index = 0
src_shape_key = None
src_shape_key_index = 0

print("*************************************************")
def deselect_all():
    for ob in bpy.context.selected_objects:
        ob.select = False

deselect_all()

bpy.ops.object.select_pattern(pattern=dest)
dest_mesh = bpy.context.selected_objects[0]
print(dest_mesh.name)

deselect_all()
bpy.ops.object.select_pattern(pattern=src)
src_mesh = bpy.context.selected_objects[0]
print(src_mesh.name)

if(specify_end_vertex == False):
    total_vertices = len(dest_mesh.data.vertices)
    print("Total vertices: ",total_vertices )


#select required vertices within a radius
def select_vertices(center, radius):    
    global src_mesh
    n = 0
    src_current_vertices = []
    one_vertex = []

    bms = bmesh.new()
    radius_vec = center + Vector((0, 0, radius))
    me = src_mesh.data
    mwi = src_mesh.matrix_world.inverted()
    # put sphere in local coords.
    lco = mwi * center
    r = mwi * (radius_vec) - lco
    closest_length = r.length

    # load mesh
    bms.from_mesh(me)
    # select verts within radius
    for index, v in enumerate(bms.verts):
        v.select = (v.co - lco).length <= r.length 
        n += v.select
        if(v.select):
            src_current_vertices.append(index)
            if(use_one_vertex):
                if((v.co - lco).length <= closest_length):
                    closest_length = (v.co - lco).length
                    one_vertex = []
                    one_vertex.append(index)

    # flush selection, update mesh
    bms.select_flush(False) 
    bms.to_mesh(me)
    bms.clear()
    bms.free()
    #selects only index 0 of close vertices as reference 
    if(use_one_vertex):        
    if(n != 0 ):
        src_current_vertices = []
        src_current_vertices.append(one_vertex[0])
        n = 1
    # appending the length of the array to the end... I know i could use len() but want to be able to return a -1 incase no vertices are selected
    src_current_vertices.append(n)
    return src_current_vertices

#this select function initially starts (if level=0) by matching a point in same space as the source mesh and if it cant find similar positioned point we increment search radius   
def select_required_verts(vert,rad,level=0):    
    if(level > 20):
        return -1
    verts = []
    verts = select_vertices(vert, rad)    
    n = verts.pop()
    if n == 0:
        return select_required_verts(vert,rad + increment_radius, level + 1)
    else:
        verts.append(n)
        return verts

#set the new vertex position on the shape key
def set_vertex_position(v_pos):
    global current_vertex_index
    bmg = bmesh.new()
    bmg.from_mesh(dest_mesh.data)
    bmg.verts.ensure_lookup_table()
    layer_name = bmg.verts.layers.shape.get(key_name)
    bmg.verts[current_vertex_index][layer_name] = v_pos
    bmg.to_mesh(dest_mesh.data)
    bmg.free()


def update_vertex():
    global current_vertex_index
    global total_vertices    
    global dest_mesh    
    global src_mesh

    if(current_vertex_index >= total_vertices ):
        return

    bpy.ops.object.select_pattern(pattern=dest)
    obj = context.active_object
    current_vertex = obj.matrix_world * dest_mesh.data.vertices[current_vertex_index].co
    print("Current vertex", current_vertex_index, " ", current_vertex)
    deselect_all()
    bpy.ops.object.select_pattern(pattern=src)
    src_current_vertices = select_required_verts(current_vertex,0)

    n = src_current_vertices.pop()
    if(n == -1):
        print("FAIL")
        return
    deselect_all()

    src_total_distance = 0
    src_directions = []
    src_distances  = []

    bpy.context.object.active_shape_key_index = src_shape_key_index
    src_shape_key.value = 0
    bpy.ops.object.select_pattern(pattern=src)

    me = src_mesh.to_mesh(scene, True, 'PREVIEW')
    result_position = Vector()

    for v in src_current_vertices:
        result_position += me.vertices[v].co

    bpy.data.meshes.remove(me)

    result_position /= len(src_current_vertices)

    #Calculate new positions
    src_shape_key.value = 1    
    me = src_mesh.to_mesh(scene, True, 'PREVIEW')
    result_position2 = Vector()
    for v in src_current_vertices:
        result_position2 += me.vertices[v].co
    bpy.data.meshes.remove(me)    
    result_position2 /= len(src_current_vertices)    

    result = result_position2 - result_position + current_vertex

    bpy.ops.object.select_pattern(pattern=dest)
    bpy.context.scene.objects.active = dest_mesh
    bpy.context.object.active_shape_key_index = dest_shape_key_index    
    src_shape_key.value = 0
    set_vertex_position(result)    
    bpy.context.object.active_shape_key_index = 0
    current_vertex_index += 1

#store shapekey index and reference in global variables
def get_shapekey(p_key_name):
    global dest_shape_key
    global dest_shape_key_index 
    global src_shape_key
    global src_shape_key_index
    deselect_all()
    for index, sk in enumerate(dest_mesh.data.shape_keys.key_blocks):
        if sk.name == p_key_name:
            dest_shape_key = sk    
            dest_shape_key_index = index

    bpy.context.object.active_shape_key_index = dest_shape_key_index
    dest_shape_key.value = 0
    deselect_all()

    for index, sk in enumerate(src_mesh.data.shape_keys.key_blocks):
        if sk.name == p_key_name:
            src_shape_key = sk    
            src_shape_key_index = index
    bpy.context.object.active_shape_key_index = src_shape_key_index
    src_shape_key.value = 0        
    deselect_all()

#Iterate all shape keys in src mesh and get its reference and index and then update vertex positions
for shape_key_iter in src_mesh.data.shape_keys.key_blocks:
    global key_name
    key_name = shape_key_iter.name
    #exclude basis shapekey and Expressions_IDHumans_max (used in Manuel Bastioni Lab
    if(key_name!='basis' and key_name!='Expressions_IDHumans_max'):
        get_shapekey(shape_key_iter.name)
        while(current_vertex_index < total_vertices):  
            update_vertex()
        current_vertex_index = 0
        dest_shape_key.value = 0

Link to blend file download

$\endgroup$
  • $\begingroup$ Have you tried profiling your code? $\endgroup$ – JakeD Oct 5 '18 at 12:16
  • $\begingroup$ Thanks for this suggestion. I haven't tried that yet. I will post back after implementing the updates suggested by Blender Dadaist. $\endgroup$ – fBlah Oct 5 '18 at 13:34
  • $\begingroup$ Always try and avoid calling operators multiple times in a script. (update_vertex(...) method above for instance) Can you provide a run down of the theory behind method you are using in code above to transfer shapekeys? . Have you considered 3 vertex parenting, moustache to man? $\endgroup$ – batFINGER Oct 5 '18 at 14:03
  • $\begingroup$ There is a for loop at the end which runs update_vertex() which updates 1 vertex inside 1 shapekey (indexes for vertex and shapekey are global sry... :/) update vertex first gets surrounding vertices in src mesh around a vertex in dest mesh. After getting a mean of the affecting vertices copies new position to dest mesh vertex. Then increment dest mesh vertex. Repeat for all vertices. increment shape key index. Repeat for all shapekeys. I plan to export the model for use in unreal engine so cant use vertex parenting. $\endgroup$ – fBlah Oct 5 '18 at 14:20
1
$\begingroup$

Thank you everyone for your awesome advice!

The improved code is as follows and it completes in ~ 4 seconds for my test blend provided. (which is a big improvement from ~5 mins)

I hope that someone can make this into a button in blender.

# sorry if this script is poorly written. I am a n00b
# Originally created to transfer face shapekeys from Manuel Bastioni character face to a moustache made with mesh cards
# place both the meshes at origin and overlap them with similar scale
# import required modules
# I hope that the basis key is always located at index 0
import bpy
import bmesh
from mathutils import Vector
from time import time
start_time = time()
# step size to increase the sphere selection
increment_radius = .05

# set total vertices incase you want to run for less number of vertices also set specify_end_vertex to True
# set current_vertex index incase you want to continue from another index
# use_one_vertex will transfer the weight of the closest vertex within the selection sphere
current_vertex_index = 0
total_vertices       = 0
specify_end_vertex   = False
use_one_vertex       = True

# name of source mesh
src_name  = "man"
# name of destination mesh
dest_name = "moustache"
# shape keys to ignore
excluded_shape_keys = ['basis', 'Expressions_IDHumans_max'] 

# global variables which should not be set
dest_mesh            = None
src_mesh             = None
src_mwi              = None
dest_shape_key_index = 0
src_shape_key_index  = 0
do_once_per_vertex   = False
src_chosen_vertices  = []
current_vertex       = None

# select required vertices within a radius and return array of indices
def select_vertices(center, radius):    
    global src_mesh, src_mwi    
    src_chosen_vertices = []
    closest_vertex_index = -1
    radius_vec = center + Vector((0, 0, radius))        
    # put selection sphere in local coords.
    lco = src_mwi * center
    r   = src_mwi * (radius_vec) - lco
    closest_length = r.length        

    # select verts within radius
    for index, v in enumerate(src_mesh.data.shape_keys.key_blocks[0].data):
        is_selected = (v.co - lco).length <= r.length     
        if(is_selected):
            src_chosen_vertices.append(index)
            if(use_one_vertex):
                if((v.co - lco).length <= closest_length):
                    closest_length = (v.co - lco).length
                    closest_vertex_index = index            

    # update closest vertex
    if(use_one_vertex):                
        src_chosen_vertices = []
        if(closest_vertex_index > - 1):
            src_chosen_vertices.append(closest_vertex_index)            

    return src_chosen_vertices

# this select function initially starts (if level=0) by matching a point in same space as the source mesh and if it cant find similar positioned point we increment search radius   
def select_required_verts(vert,rad,level=0):    
    verts = []
    if(level > 20):
        return verts 
    verts = select_vertices(vert, rad)    
    if(len(verts) == 0):
        return select_required_verts(vert,rad + increment_radius, level + 1)
    else:        
        return verts

# set the new vertex position on the shape key
def set_vertex_position(v_pos):
    global current_vertex_index, dest_shape_key_index, dest_mesh
    dest_mesh.data.shape_keys.key_blocks[dest_shape_key_index].data[current_vertex_index].co = v_pos    

# update 1 vertex of destination mesh
def update_vertex():
    global current_vertex_index, total_vertices, dest_mesh, src_mesh, src_chosen_vertices, do_once_per_vertex, current_vertex

    if(current_vertex_index >= total_vertices ):
        return

    if(do_once_per_vertex):
        current_vertex = dest_mesh.matrix_world * dest_mesh.data.shape_keys.key_blocks[0].data[current_vertex_index].co       
        src_chosen_vertices = select_required_verts(current_vertex,0)   
        do_once_per_vertex = False;

    if(len(src_chosen_vertices) == 0):
        print("Failed to find surrounding vertices | Try increasing increment radius | vertex index ", current_vertex_index, " at shape key index ", src_shape_key_index)
        current_vertex_index += 1
        return                 

    result_position = Vector()    
    for v in src_chosen_vertices:
        result_position +=  src_mesh.data.shape_keys.key_blocks[0].data[v].co    
    result_position /= len(src_chosen_vertices)

    result_position2 = Vector()
    for v in src_chosen_vertices:
        result_position2 += src_mesh.data.shape_keys.key_blocks[src_shape_key_index].data[v].co        
    result_position2 /= len(src_chosen_vertices)    
    result = result_position2 - result_position + current_vertex
    set_vertex_position(result)

# store shapekey index and reference in global variables
def update_global_shapekey_indices(p_key_name):
    global dest_shape_key_index, src_shape_key_index        
    for index, sk in enumerate(dest_mesh.data.shape_keys.key_blocks):
        if sk.name == p_key_name:            
            dest_shape_key_index = index
    for index, sk in enumerate(src_mesh.data.shape_keys.key_blocks):
        if sk.name == p_key_name:            
            src_shape_key_index = index

dest_mesh   = bpy.data.objects[dest_name]
src_mesh    = bpy.data.objects[src_name]
src_mwi     = src_mesh.matrix_world.inverted()

if(specify_end_vertex == False):
    total_vertices = len(dest_mesh.data.vertices)
    print("Total vertices: ",total_vertices )

# Check if dest_mesh has any shape key if not create one
if(not hasattr(dest_mesh.data.shape_keys, "key_blocks")):
    dest_mesh.shape_key_add(name="basis")
# add missing shape keys to dest_mesh    
for src_shape_key_iter in src_mesh.data.shape_keys.key_blocks:
    valid_shape_key = False    
    for dest_shape_key_iter in dest_mesh.data.shape_keys.key_blocks:
        if(src_shape_key_iter.name == dest_shape_key_iter.name):
            valid_shape_key = True
    if(not valid_shape_key):
        dest_mesh.shape_key_add(name=src_shape_key_iter.name)

# all vertices in destination mesh
while(current_vertex_index < total_vertices):
    do_once_per_vertex = True
    # Iterate all shape keys 
    for shape_key_iter in src_mesh.data.shape_keys.key_blocks:    
        key_name = shape_key_iter.name
        # exclude shapekeys not needed
        if(not (key_name in excluded_shape_keys)):
            update_global_shapekey_indices(shape_key_iter.name)            
            update_vertex()            
    current_vertex_index += 1

print("Total Time: ", time() - start_time)
$\endgroup$
0
$\begingroup$

In my opinion, to access shape key data you don't need a bmesh. You can save a lot of time by doing away with the conversion between bmesh to mesh and the related bpy.ops calls (done iteratively in the code) .

Maybe here's how the problem could be approached:

  • Get the src and dest objects (bpy.data.objects[name_of_the_obj])
  • loop through the srcObj.data.shape_keys.key_blocks
    • for each key block you will get the coordinate vector, modified for that shape key, in key_blocks[x].data[y].co where x is the shape key index and y is the index of the vertex
    • For the destination vertex with index idx, set the new coordinates by changing the value of destObj.data.shape_keys.key_blocks[a].data[idx].co = newCo, assuming the shape key in src with index x corresponds to that with index a in dest. I believe, you already have the logic for finding the src-dest vertex mapping
    • if there's no corresponding shape key, you can add it by calling obj.shape_key_add, this will return the new key_block object
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.