4
$\begingroup$

I've seen a sweet technique for baking vertex animation to keyframes by means of creating a shape key for every frame and keying the shape key to come on at the right time. An awesome user on this site (shoutout to Jerryno) wrote a script to take care of this process in one shot. I'm going to go ahead and include that script in my question in case that link ever dies; here it is:

import bpy

obj = bpy.context.active_object
start = bpy.context.scene.frame_start
end = bpy.context.scene.frame_end

def insert_keyframe(sk, f):
    sk.keyframe_insert("value", frame=f-1)
    sk.keyframe_insert("value", frame=f+1)
    sk.value = 1.0
    sk.keyframe_insert("value", frame=f)

meshes = []
for f in range(start, end+1):
    bpy.context.scene.frame_set(f)
    meshes.append(obj.to_mesh(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW'))

obj.shape_key_add(name="Basis", from_mix=False)
for i in range(0, end-start+1):
    key = obj.shape_key_add(name=str(i+start), from_mix=False)
    insert_keyframe(key, i+start)
    for vert_id in range(len(obj.data.vertices)):
        key.data[vert_id].co = meshes[i].vertices[vert_id].co

for mesh in meshes:
    bpy.data.meshes.remove(mesh)

I've run in to some cases, however, where my vertex animation periodically pauses for a bunch of frames, then resumes, then pauses for a bunch of frames, resumes, etc. It seems needless to have all of those duplicate keyframes/shapes during the time when my animation is paused--and since performance is alright not the best on meshes with tons of shape keys I'd love to optimize the script so that it only creates a shape key/keyframe if the vertex data has changed from the previous frame.

I understand that in the code I'd need to do some kind of comparison between the vertices in each frame, but I don't even begin to have an idea how that would be done. Could someone point me in the right direction?

$\endgroup$

3 Answers 3

2
$\begingroup$

Here is the improved script without duplicate shapekeys:

import bpy
import sys

def update_progress(job_title, progress):
    block = int(round(20*progress))
    msg = "\r{0}: [{1}] {2}%".format(job_title,
                                     "#"*block + "-"*(20-block),
                                     round(progress*100, 2))
    if progress >= 1: msg += " DONE\r\n"
    sys.stdout.write(msg)
    sys.stdout.flush()

obj = bpy.context.active_object
start = bpy.context.scene.frame_start
end = bpy.context.scene.frame_end

# generate unique meshes from animation
meshes_at_frames = []
for f in range(start, end+1):
    update_progress("Generating unique meshes from animation", (f-start)/(end-start+1))
    bpy.context.scene.frame_set(f)
    curr_mesh = obj.to_mesh(scene=bpy.context.scene,
                            apply_modifiers=True,
                            settings='PREVIEW')
    if meshes_at_frames:
        prev_mesh = meshes_at_frames[-1][0]
        new_mesh = False
        for curr_vert, prev_vert in zip(curr_mesh.vertices, prev_mesh.vertices):
            if curr_vert.co != prev_vert.co:
                meshes_at_frames.append((curr_mesh,f))
                new_mesh = True
                break
        # cleanup
        if not new_mesh:
            bpy.data.meshes.remove(curr_mesh)
    else:
        meshes_at_frames.append((curr_mesh,f))
update_progress("Generating unique meshes from animation", 1)

# add unique keyframed shape keys to reconstruct animation
obj.shape_key_add(name="Basis", from_mix=False)
prev_skey = None
for mesh, f in meshes_at_frames:
    update_progress("Adding shapekeys", f/(end+1))
    curr_skey = obj.shape_key_add(name=str(f), from_mix=False)
    for vert_id in range(len(obj.data.vertices)):
        curr_skey.data[vert_id].co = mesh.vertices[vert_id].co

    # cleanup
    bpy.data.meshes.remove(mesh)

    # keyframe start of current
    curr_skey.keyframe_insert("value", frame=f-1)
    curr_skey.value = 1.0
    curr_skey.keyframe_insert("value", frame=f)

    # keyframe end of previous
    if prev_skey:
        prev_skey.keyframe_insert("value", frame=f-1)
        prev_skey.value = 0.0
        prev_skey.keyframe_insert("value", frame=f)
    prev_skey = curr_skey
update_progress("Adding shapekeys", 1)

Same as with previous version: deform modifiers after running script need to be turned off or deleted.

It performs slower than previous one due to comparing mesh vertices if they differ. Worst case is when the mesh is the same across many frames, best case is when mesh changes every frame on first vertex (or every vertex).

For 1M poly meshes and hundreds of frames this comparing can take couple of minutes, so I also added a progress reporting to the system console.

$\endgroup$
3
  • $\begingroup$ Great script Jerryno, thanks for sharing. Isn't it possible to simplify this algorithm by using using Absolute shape keys? Then you just need to keyframe the Evaluation Time of the object, with all keys being one frame time apart. $\endgroup$
    – R. Navega
    Commented Feb 3, 2018 at 5:33
  • 1
    $\begingroup$ @R.Navega Yes, that's how it could also be done. I pulled chunks of code from something I had that had a bit different usage, so that's why it is how it is. Both approaches have weaknesses, for example relative shape keys export better to and from other apps. Absolute shape keys are not really shape keys, some apps might have problems with that. $\endgroup$ Commented Feb 3, 2018 at 9:51
  • $\begingroup$ Hm, I've just seen that the 'frame time' for absolute shape keys is read-only, so doing something like setting them one frame apart isn't possible. You can still use them for like, 'reset the timings' (so all keys are in steps of 10), and then keyframe 'evaluation time' in 10 unit steps each frame. It's less stuff to have to keyframe. I understand your point about exporting, thanks for the warning. $\endgroup$
    – R. Navega
    Commented Feb 3, 2018 at 23:55
5
$\begingroup$

For Blender 2.91:

I rewrote sambler's script to work for 2.91. At some point the API changed, not sure at what version. The to_mesh() function works differently now, and stores its results in a temporary object that gets reused every time to_mesh() is called, so I ditched the mesh datatype and stored vertex information as an array of Vectors. Thanks for Somebody285 for describing the new way to get the modified mesh.

import bpy
import sys

def update_progress(job_title, progress):
    block = int(round(20*progress))
    msg = "\r{0}: [{1}] {2}%".format(job_title,
                                     "#"*block + "-"*(20-block),
                                     round(progress*100, 2))
    if progress >= 1: msg += " DONE\r\n"
    sys.stdout.write(msg)
    sys.stdout.flush()

def convert_mesh_to_vector_array(curr_mesh):
    list = []
    for v in curr_mesh.vertices:
        list.append(v.co.copy())
    return list

start = bpy.context.scene.frame_start
end = bpy.context.scene.frame_end

#We only have to do this once:
ob = bpy.context.active_object #getting the object we want.
dg = bpy.context.evaluated_depsgraph_get() #getting the dependency graph


# generate unique meshes from animation
arrays_at_frames = []
prev_arr = None
for f in range(start, end+1):
    bpy.context.scene.frame_set(f)
    #This has to be done every time the object updates:
    ob = ob.evaluated_get(dg) #this gives us the evaluated version of the object. Aka with all modifiers and deformations applied.
    curr_mesh = ob.to_mesh() #turn it into the mesh data block we want.
    
    curr_arr = convert_mesh_to_vector_array(curr_mesh)

    if arrays_at_frames:
        new_mesh = False
        for curr_vert, prev_vert in zip(curr_arr, prev_arr):
            if curr_vert != prev_vert:
                arrays_at_frames.append((curr_arr,f))
                new_mesh = True
                break
        if new_mesh:
            prev_arr = curr_arr
            
    else:
        arrays_at_frames.append((curr_arr,f))
        prev_arr = curr_arr
        
update_progress("Generating unique meshes from animation", 1)

ob = bpy.context.active_object
# add unique keyframed shape keys to reconstruct animation
ob.shape_key_add(name="Basis", from_mix=False)
prev_skey = None
for arr, f in arrays_at_frames:
    update_progress("Adding shapekeys", f/(end+1))
    curr_skey = ob.shape_key_add(name=str(f), from_mix=False)
    for vert_id in range(len(ob.data.vertices)):
        curr_skey.data[vert_id].co = arr[vert_id]

    # keyframe start of current
    curr_skey.keyframe_insert("value", frame=f-1)
    curr_skey.value = 1.0
    curr_skey.keyframe_insert("value", frame=f)

    # keyframe end of previous
    if prev_skey:
        prev_skey.keyframe_insert("value", frame=f-1)
        prev_skey.value = 0.0
        prev_skey.keyframe_insert("value", frame=f)
    prev_skey = curr_skey
update_progress("Adding shapekeys", 1)
$\endgroup$
2
$\begingroup$

The line key.data[vert_id].co = meshes[i].vertices[vert_id].co assigns the simulated mesh location to the shapekey. You will want to test the locations of meshes[i].vertices with the previous locations to decide if you want to add a shapekey.

My first untested idea to try would be -

import bpy

obj = bpy.context.active_object
start = bpy.context.scene.frame_start
end = bpy.context.scene.frame_end

def insert_keyframe(sk, f):
    sk.keyframe_insert("value", frame=f-1)
    sk.keyframe_insert("value", frame=f+1)
    sk.value = 1.0
    sk.keyframe_insert("value", frame=f)

def verts_moved(mesh1,mesh2):
    changed = False
    for v in range(len(mesh1.vertices)):
        if mesh1.vertices[v].co != mesh2.vertices[v].co:
            changed = True
            break
    return changed

meshes = []
for f in range(start, end+1):
    bpy.context.scene.frame_set(f)
    meshes.append(obj.to_mesh(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW'))

obj.shape_key_add(name="Basis", from_mix=False)
for i in range(1, end-start+1):
    if verts_moved(meshes[i-1],meshes[i]):
        key = obj.shape_key_add(name=str(i+start), from_mix=False)
        insert_keyframe(key, i+start)
        for vert_id in range(len(obj.data.vertices)):
            key.data[vert_id].co = meshes[i].vertices[vert_id].co

for mesh in meshes:
    bpy.data.meshes.remove(mesh)
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .