9
$\begingroup$

I'm updating all the vertex coordinates of a mesh frame-by-frame in python and need to animate but can not figure out how. Here is a small abstracted script - the python isn't pretty, but it makes an icosphere gaussian hiccup (a little like this coarse GIF):

ico hiccup

import bpy
import bmesh
import numpy as np

pi = np.pi
make_ico = bpy.ops.mesh.primitive_ico_sphere_add
ok = make_ico(subdivisions=3, size = 2.0, location = (0,0,3))

ao = bpy.context.active_object
oo = bpy.ops.object
ok = oo.mode_set(mode='EDIT')
me = bmesh.from_edit_mesh(ao.data)
ico0 = np.array([vert.co for vert in me.verts]) # get ico vertices
ok = oo.mode_set(mode='OBJECT')

n_frames = 101
bpy.context.scene.frame_end = n_frames

zmin, zmax = ico0[:,2].min(), ico0[:,2].max()
zc = np.linspace(zmin, zmaz, n_frames)

sig = 0.3
ico = np.zeros_like(ico0)
data = []
for i in range(n_frames):
    ico[:,:2] = (1.0 + np.exp(-(ico0[:,2] - zc[i])**2/(2.*sig**2)))[:,None] * ico0[:,:2]
    ico[:,2] = ico0[:,2]
    data.append(ico.copy())

for i_frame in range(n_frames):
    ok = oo.mode_set(mode='EDIT')
    me = bmesh.from_edit_mesh(ao.data)  #DO THIS EVERY TIME?
    for (vert, co) in zip(me.verts, data[i_frame]):
        vert.co = co
    bmesh.update_edit_mesh(ao.data)  # update
    ok = oo.mode_set(mode='OBJECT')
    bpy.context.scene.frame_current = i_frame + 1
    # something about keyframes HERE?
$\endgroup$
  • 1
    $\begingroup$ do you want to keyframe this, or do you want to attach a frame change event handler (to update the object as a function of the frame number) ? $\endgroup$ – zeffii Sep 2 '15 at 13:37
  • $\begingroup$ I think in both examples I didn't keyframe the pristine icosphere state, maybe you can figure it out -- else I'll return to this later $\endgroup$ – zeffii Sep 3 '15 at 6:12
  • 1
    $\begingroup$ I prefer @pinkvertex 's answer, consider accepting that instead -- had I realized it was possible to keyframe coordinates directly I wouldn't have suggested shapekeys $\endgroup$ – zeffii Sep 3 '15 at 14:20
8
$\begingroup$

It is also possible to directly keyframe the positions of the mesh vertices

import bpy
from mathutils import Vector

def insert_keyframe(fcurves, frame, values):
    for fcu, val in zip(fcurves, values):
        fcu.keyframe_points.insert(frame, val, {'FAST'})

obj = bpy.context.active_object
mesh = obj.data
action = bpy.data.actions.new("MeshAnimation")

mesh.animation_data_create()
mesh.animation_data.action = action

data_path = "vertices[%d].co"
vec_z = Vector((0.0, 0.0, 1.0))

frames = 0, 25, 50, 75, 100
values = 0.0, 3.0, 5.0, 3.0, 0.0

for v in mesh.vertices:
    fcurves = [action.fcurves.new(data_path % v.index, i) for i in range(3)]
    co_rest = v.co

    for t, value in zip(frames, values):
        co_kf = co_rest + value * vec_z
        insert_keyframe(fcurves, t, co_kf)     
$\endgroup$
  • 1
    $\begingroup$ nice, so that looks like this then: gist.github.com/zeffii/5593f5aab165de8d2043 $\endgroup$ – zeffii Sep 3 '15 at 14:08
  • $\begingroup$ This is great @pinkvertex - a third unique answer! I hadn't known about animation_data and actions before. Opening yet another new window into Blender for me. Thank you! (now the graph editor looks quite interesting) $\endgroup$ – uhoh Sep 4 '15 at 2:56
  • $\begingroup$ @zeffii thanks for plugging it back in to the original code - this helps a lot. $\endgroup$ – uhoh Sep 4 '15 at 3:01
  • $\begingroup$ Which is more efficient, keyframing vertices or using shapekeys? $\endgroup$ – Geremia Jan 7 '17 at 15:16
7
$\begingroup$

Yes, this is a separate answer because the technique is completely different.

Step 1 -- make shapekeys via a script.

import bpy
import bmesh
import numpy as np

## ------- part 1 --- do this once.
sig = 0.3
n_frames = 101
bpy.context.scene.frame_end = n_frames

pi = np.pi
make_ico = bpy.ops.mesh.primitive_ico_sphere_add
make_ico(subdivisions=3, size = 2.0, location = (0,0,3))
obj = bpy.context.active_object
me = obj.data

ico0 = np.array([v.co for v in me.vertices]) # get ico vertices
zmin, zmax = ico0[:,2].min(), ico0[:,2].max()
zc = np.linspace(zmin, zmax, n_frames)
ico = np.zeros_like(ico0)
data = []

for i in range(n_frames):
    ico[:,:2] = (1.0 + np.exp(-(ico0[:,2] - zc[i])**2/(2.*sig**2)))[:,None] * ico0[:,:2]
    ico[:,2] = ico0[:,2]
    data.append(ico.copy())

Step 2 -- keyframe the mute state

The simplest way that requires the least amount tears might be keyframing the .mute state, (on for the frame wanted, off for the frame after that..) have all .value set to 1.

## ------- part 2  ---set up keyblocks / shape_keys

for i_frame in range(n_frames):
    block = obj.shape_key_add(name=str(i_frame), from_mix=False)  # returns a key_blocks member
    block.value = 1.0
    block.mute = True
    for (vert, co) in zip(block.data, data[i_frame]):
        vert.co = co

    # keyframe off on frame zero
    block.mute = True
    block.keyframe_insert(data_path='mute', frame=0, index=-1)

    block.mute = False
    block.keyframe_insert(data_path='mute', frame=i_frame + 1, index=-1)

    block.mute = True
    block.keyframe_insert(data_path='mute', frame=i_frame + 2, index=-1)    

A brief explanation of the keyframing. Essentially it modulates the influence of the shapekey. No shapekey should have influence on any frame other than the desired one. In practice this means:

  • putting a keyframe ahead of all keyframes (frame 0 for instance) to set the influence of all shape_keys to the mute=on position.
  • Then on the frame you want the effect to be full you set mute=off (full effect), and the next frame you mute=on.

enter image description here

$\endgroup$
  • $\begingroup$ These are both fascinating! I'm torn which one to accept - I'll check back in a bit after I try them. This is great! $\endgroup$ – uhoh Sep 2 '15 at 17:07
  • $\begingroup$ i'm not sure this is a great way to do this.. but it'll give you some idea $\endgroup$ – zeffii Sep 2 '15 at 20:25
  • $\begingroup$ Where's step 2? A pointcache would probably be better, but I believe we need Alembic support in Blender first. $\endgroup$ – CoDEmanX Sep 2 '15 at 21:19
  • $\begingroup$ @CoDEmanX i look forward to Alembic support! $\endgroup$ – zeffii Sep 3 '15 at 6:16
  • $\begingroup$ So @zeffii has given two unique answers. Since I have to choose one, I've accepting this answer simply because it uses keyframes, and keyframes were mentioned in the question. Both work! Thank you as always! $\endgroup$ – uhoh Sep 3 '15 at 7:58
6
$\begingroup$

I'd consider getting away from bmesh_from_edit_mesh, and re-evaluating why you might need bmesh at all. This can be done with the basic Mesh structure.

Here as a function of the event handler 'pre frame change'

As you can tell, there's no switching in and out of edit mode for the purpose of getting the Bmesh from edit mode. For what you are doing here there's little gain (that I can see..) to using bmesh data. So you can use the standard Mesh data

import bpy
import bmesh
import numpy as np

## ------- part 1 --- do this once.
sig = 0.3
n_frames = 101
bpy.context.scene.frame_end = n_frames

pi = np.pi
make_ico = bpy.ops.mesh.primitive_ico_sphere_add
make_ico(subdivisions=3, size = 2.0, location = (0,0,3))
obj = bpy.context.active_object
me = obj.data

ico0 = np.array([v.co for v in me.vertices]) # get ico vertices
zmin, zmax = ico0[:,2].min(), ico0[:,2].max()
zc = np.linspace(zmin, zmax, n_frames)
ico = np.zeros_like(ico0)
data = []

for i in range(n_frames):
    ico[:,:2] = (1.0 + np.exp(-(ico0[:,2] - zc[i])**2/(2.*sig**2)))[:,None] * ico0[:,:2]
    ico[:,2] = ico0[:,2]
    data.append(ico.copy())

## ------- part 2  ---set up frame change event handler,
def my_handler(scene):
    i_frame = scene.frame_current
    if not (0 <= i_frame < len(data)):
        return

    obj = bpy.data.objects['Icosphere'] # be explicit.
    me = obj.data
    for (vert, co) in zip(me.vertices, data[i_frame]):
        vert.co = co
    me.update()

bpy.app.handlers.frame_change_pre.append(my_handler)    

A few warnings here, useful things to be aware of.

Run this script once, it will add a function to the event handler system. This function (my_handler) will be called on each frame change (either you scrub the timeline, or hit play..to render an animation -- any kind of frame change).

  • If you run the script again, another function (identical content, but different function reference) is added to the event handler, be aware of this

  • If you change the content of the my_handler function and run the script again it will do undesirable things, like run both versions of the my_handler function.

So instead of running the script over and over, and not understanding why it doesn't seem to behave right, you must remove the handler before registering a new one by the same name - code defensively to avoid 'stale' handler functions.

  • If you don't have any additional event handlers set-up it's easy to wipe the list using some variant of

    handlers = bpy.app.handlers
    num_handlers = len(handlers.frame_change_pre)
    del handlers.frame_change_pre[0:num_handlers]
    
    # or simply
    bpy.app.handlers.frame_change_pre.clear()
    
  • Or.. you might want to remove handlers by function name (this is useful if you have a variety of these things going on)

    my_handler_list = bpy.app.handlers.frame_change_pre
    fin = len(my_handler_list)
    for idx, func in enumerate(reversed(my_handler_list)):
        if func.__name__ == 'my_handler':
            my_handler_list.pop(fin-1-idx)
    
    # I suspect there's a neater way to remove handlers by name
    # but I stopped as soon as it worked..
    

Maybe obvious, but worth mentioning -- if you create the icosphere, and rerun the script it will create another. You probably don't want that and need to add a check at the top ' does A exist no? then add A else use existing A '

If you need a bmesh representation you can get it from obj.data using:

bm = bmesh.new()
bm.from_mesh(obj.data)

as shown in TextEditor -> Templates -> Python -> Bmesh Simple


To get a perfect loop

Add the pristine coordinates as a first and last member of the data list. so it looks like this

data = [pristine, modified1, modified2... modified_last, pristine]

see: https://gist.github.com/zeffii/229ac803bdad0fdc7813

$\endgroup$
  • $\begingroup$ Wow! Something very new to me. This is a window into more of Blender's workings. Thank you @zeffii! Also thanks for the tutorial on basic care and feeding of event handlers. The use of bpy.ops.mesh.primitive_ico_sphere_add here was just the quickest way to get a bunch of vertices for the question script. $\endgroup$ – uhoh Sep 3 '15 at 8:03

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.