13
$\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$
3
  • 2
    $\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
    Commented Sep 2, 2015 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
    Commented Sep 3, 2015 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
    Commented Sep 3, 2015 at 14:20

4 Answers 4

12
$\begingroup$

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

Edit updated for 2.8.

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, options={'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, 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$
4
  • 1
    $\begingroup$ nice, so that looks like this then: gist.github.com/zeffii/5593f5aab165de8d2043 $\endgroup$
    – zeffii
    Commented Sep 3, 2015 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
    Commented Sep 4, 2015 at 2:56
  • $\begingroup$ @zeffii thanks for plugging it back in to the original code - this helps a lot. $\endgroup$
    – uhoh
    Commented Sep 4, 2015 at 3:01
  • 2
    $\begingroup$ Which is more efficient, keyframing vertices or using shapekeys? $\endgroup$
    – Geremia
    Commented Jan 7, 2017 at 15:16
9
$\begingroup$

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

Updated for 2.8

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, radius = 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$
7
  • $\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
    Commented Sep 2, 2015 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
    Commented Sep 2, 2015 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
    Commented Sep 2, 2015 at 21:19
  • $\begingroup$ @CoDEmanX i look forward to Alembic support! $\endgroup$
    – zeffii
    Commented Sep 3, 2015 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
    Commented Sep 3, 2015 at 7:58
7
$\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$
1
  • $\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
    Commented Sep 3, 2015 at 8:03
4
$\begingroup$

MeshVertex.keyframe_insert

As with all objects in blender that can be keyframed, a mesh vertex bpy.types.MeshVertex has a keyframe_insert method.

Python console, set a random value to each vertex $X$ coordinate, and keyframe.

>>> from random import random
>>> for v in C.object.data.vertices:
...     v.co.x = random()
...     v.keyframe_insert("co", index=0)
...     
True
True
True
True
True
True
True
True

indicating that keyframes have been inserted succesfully for all 8 verts.

Keyframe all coordinates of vert 0 to 10 at frame 250.

>>> C.object.data.vertices[0].co = (10, 10, 10)
>>> C.object.data.vertices[0].keyframe_insert("co", frame=250)
True

Using keyframe_insert of a vert is creating animation data, an action on the mesh, the ID owner of the vert.

It would be far quicker to create the action and fcurves directly, see [accepted answer]. Notice the correlation, action created on mesh, data path set to each fcurve

data_path = "vertices[%d].co" % vert.index

Suggest check out all methods outlined in answers as looping and setting with keyframe_insert is probably going to be the slowest as vert count increases.

Warning looping over and setting keyframes for high vert count meshes will be slow, as may animation in viewport.

Related Polygon Material_index Animation

$\endgroup$

You must log in to answer this question.

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