How to repeat a cyclic animation of an object/armature from command line with Blender API

I need to render a cyclic animation of an object-armature from the command line. At the moment I am stack on how to repeat the keyframes, ie how to copy the keyframes of the first time points and paste them at the end of the animation the same object. I was able to do that with 2 different functions. One uses ‘NLA_EDITOR’:

def extend_cyclic_animation_nla(obj, n_repeatitions):
    
    for obj_i in bpy.data.objects:
        obj_i.select = False

    bpy.context.area.type = 'NLA_EDITOR'
    bpy.ops.nla.action_pushdown(channel_index=1)
    obj.animation_data.nla_tracks["NlaTrack"].strips["Armature|mixamo.com|Layer0"].repeat = n_repeatitions
    bpy.context.area.type = 'TEXT_EDITOR'

The second function uses ‘DOPESHEET_EDITOR’:

def extend_cyclic_animation_dopesheet(obj, n_repeatitions, frame_end):

    for obj_i in bpy.data.objects:
        obj_i.select = False
    
    bpy.context.area.type = 'DOPESHEET_EDITOR'
    bpy.context.space_data.dopesheet.show_only_selected = True

    obj.select = True
    bpy.ops.action.copy()
    bpy.context.scene.frame_current = frame_end
    for i in range(n_repeatitions):
        
        bpy.ops.action.paste()
        bpy.context.scene.frame_current += frame_end  

    bpy.context.space_data.dopesheet.show_only_selected = False
    bpy.context.scene.frame_current = tmp
    bpy.context.area.type = 'TEXT_EDITOR'

My problem is that they only work when I run them from the blender window. When I run them from the command line I get an error. the reason is that when I run the code from the command line in the background bpy.context.area seems to be set to None and None does not have an attribute ‘type’. The error is:

bpy.context.area.type = 'DOPESHEET_EDITOR'
AttributeError: 'NoneType' object has no attribute 'type'

So, My question is given an object with a cyclic animation data, how can I copy (or repeat) the keyframes of that object and paste them at the end of the same animation from the command line? I am using blender 2.79.

bpy.ops commands support overriding of context: https://docs.blender.org/api/2.79/bpy.ops.html?highlight=override%20context#overriding-context
Maybe you can override the “area” parameter? But this likely won’t work.

The problem with your script is that you are accessing all of the data through context. You could do something like bpy.data.objects.animation_data.action The bpy.data object offers a generic way of accessing all the data in the .blend file. It’s great!

Thanks for your help. Please, can you actually write some code I can try out?

I am not entirely sure from reading your code what you want. At first I thought you wanted to cycle the f-curves within an action, but upon a second look it seems you also want to create a repitition in the NLA editor. It looks like you can access the NLA information from object.animation_data.nla_tracks NlaTracks(bpy_struct) — Blender 2.79.0 855d2955c49 - API documentation, and then there are NLA Strips inside, something like:

tracks = ob.animation_data.nla_tracks
strips = tracks.active.strips
strips = [strip for strip in strips if strip.active == True]
# This is a "list comprehension"
#This is equivalent to:
strips = []
for strip in strips:
    if (strip.active == True):
        strips.append(strip)
# but this takes so many lines of code!! It's probably slower, too.
prev_strip = strips[0]
for i in range(extend):
    length = (prev_strip.frame_end - prev_strip.frame_start)
    frame_start = prev_strip.frame_start + length * i
    frame_end = frame_start + length
    strip = strips.new(prev_strip.name, frame_start, prev_strip.action)
    strip.frame_end = frame_end

Please note that I haven’t tested the above code and it likely will require some changes before it will run. I don’t have any scenes on-hand with NLA tracks.

Anyhow, for keyframes, they are fairly simple to handle. They’re stored in action.fcurves.keyframe_points and they can be added with action.fcurves.keyframe_points.insert()
My advice is, add the keyframes with insert first, and collect them in a list. Add the same number of keyframes as in the original animation (and you may need to add a final keyframe at the end to duplicate the first keyframe) and collect them as you do (insert() returns the keyframe object) and then go into each one and copy the relevant values using something like this method (How to copy all properties of an object to another object, in Python? - Stack Overflow).

Good luck :slight_smile:

1 Like

That you very much for your help. Your code is very helpful and gives me a deeper understanding of how to access the animation data.

Honestly, I was able to sove my specific issue by appling a cyclic modifier to each fcurve of the object. I did that with the following function:

def extend_cyclic_animation_command_line(obj, n_repetitions_after=0, n_repetitions_before=0):
    if obj.animation_data is not None and obj.animation_data.action is not None:
        for fcurves_f in obj.animation_data.action.fcurves:
            new_modifier = fcurves_f.modifiers.new(type='CYCLES')
            # default n_repetitions_after is 0 which means 
            # infinite repetitions after the end of the fcurves_f
            new_modifier.cycles_after = n_repetitions_after
            # default n_repetitions_before is 0 which means 
            # infinite repetitions before the start of the fcurves_f
            new_modifier.cycles_before = n_repetitions_before


extend_cyclic_animation_command_line(bpy.data.objects['Body_a'])

However, I really find your commend very helpful and now I may update my function based on your advice and code. Thanks a lot.

Great! I’m glad it helped. Good luck with your animation :smiley:

yes, thanks. Look I have updated my code.

def transfer_action_to_nla_tracks(obj, strip_name='new_strip', start_frame=1):

    new_track = obj.animation_data.nla_tracks.new()
    new_strip = new_track.strips.new(strip_name, start_frame, obj.animation_data.action.copy)

    bpy.data.actions.remove(obj.animation_data.action, do_unlink=True)

    return new_strip


def repeat_strip_from_command_line(strip, n_repetitions):
    strip.repeat = n_repetitions


new_strip = transfer_action_to_nla_tracks(bpy.data.objects['Body_a'], strip_name='new_strip', start_frame=1)
repeat_strip_from_command_line(new_strip, 5)

I know it’s quite different from what you suggested, but your example made me better understand how to access the nla_tracks and the strips. Thanks again. :+1:

1 Like