2
$\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$
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.

| improve this answer | |
$\endgroup$
  • $\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 Feb 3 '18 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$ – Jaroslav Jerryno Novotny Feb 3 '18 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 Feb 3 '18 at 23:55
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)
| improve this answer | |
$\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.