0
$\begingroup$

So I need to cycle through a selected bone's keyframes to do some action at each keyframe. It must be independent of the scene frame range as keyframes can be outside of it, and I also must not create keyframes at frames that weren't keyed in the first place which jumping to the playback start/end would do in my case).

I guessed the brute solution would be to bpy.ops.screen.keyframe_jump(next=False) till there's nothing to get to the very first keyframe, and then do if forward again to do the actual keyframe actions.
But I suspected a smarter way would be to get a list of the bone's keyed frames and cycles through them according to that list.

So I manage to get the following code to get the armature's keyframe list, and remove all duplicates.

import bpy
from bpy.types import (Context, Operator, )

def get_keyed_frames():
    KEYED_FRAMES_ARRAY = []
    fcurves = bpy.context.active_object.animation_data.action.fcurves

    for curve in fcurves:
        keyframePoints = curve.keyframe_points
        for keyframe in keyframePoints:
#            if dev_mode == True:
#                print('TOTAL FRAMES ARE: {}'.format(keyframe.co[0]))
            KEYED_FRAMES_ARRAY.append(keyframe.co[0])
            KEYED_FRAMES_ARRAY = list(dict.fromkeys(KEYED_FRAMES_ARRAY))
    return KEYED_FRAMES_ARRAY

print(get_keyed_frames())

But I couldn't make this work on a specific bone, so currently it picks up all the armature's keyed frames. Is there an active_bone or even selected bone equivalent to bpy.context.active_object.animation_data.action.fcurves ?

Or maybe a better/smarter way to cycle through each keyed frame of a bone.

$\endgroup$

2 Answers 2

1
$\begingroup$

Get pose bones keyframe points

import bpy

def get_fcs(obj):
    try:    return obj.animation_data.action.fcurves
    except: return None

def get_pose_bones_frames():
    obj = bpy.context.object
    fcs = get_fcs(obj)

    if fcs is None:
        print("No animation_data / invalid object")
        return None

    bones = obj.data.bones
    ARRAY = []
    for fc in fcs:
        path = fc.data_path
        index = fc.array_index

        if path[: 10] != "pose.bones": continue

        bone = bones[path[12 : path.find("]") - 1]]
        if not bone.select: continue

        ARRAY.append({
            "path": path,
            "index": index,
            "points": [kp.co[0] for kp in fc.keyframe_points]
        })

    return ARRAY

ARRAY = get_pose_bones_frames()
if ARRAY is None:
    print("fail")
else:
    for A in ARRAY:
        print(f'path: {A["path"]}, index: {A["index"]}')
        print(A["points"])
        print("")
$\endgroup$
2
  • $\begingroup$ That doesn't seem to solve my issue, I still get every keyframes of every bone in the armature, i need only the selected bones $\endgroup$
    – Lauloque
    Commented Jul 26, 2022 at 15:24
  • $\begingroup$ You can check the bone is selected from all fcurve or check is have fcurve from all selected bone. $\endgroup$
    – X Y
    Commented Jul 26, 2022 at 18:41
0
$\begingroup$

With the help of @X Y's answer, I made something that does exactly what I need.

It's basically the same, but with a part that reduces XY's "path" down to only the bones names, and removing all duplicates so that we have one list of keyed frames per bones.
Then followed by a simple test to show how to make use of it.

import bpy, re

dev_mode = True
C = bpy.context

def get_fcs(obj):
    try:    return obj.animation_data.action.fcurves
    except: return None

def get_keyed_frames():
    fcs = get_fcs(bpy.context.object)

    if fcs is None:
        print("No animation_data / invalid object")
        return None

    ARRAY = []
    d = "\"]"
    
    for fc in fcs:
        bone_name = fc.data_path
        index = fc.array_index

        ### EXTRACT BONES PATHS FROM FCURVES
        if bone_name[: 10] != "pose.bones": continue
        ### EXTRACT BONE NAMES FROM BONES PATHS
        try:
            bone_name = re.search('pose.bones\["(.+?)\"].', bone_name).group(1)
        except AttributeError:
            bone_name = "search failed!"
        ### BUILD 
        ARRAY.append({
            "bone_name": bone_name,
            "keyed_frames": [kp.co[0] for kp in fc.keyframe_points]
        })
        
        #### CLEANUP DUPLICATES
        keyed_frames_list = [] 
        for i in ARRAY: 
            if i not in keyed_frames_list: 
                keyed_frames_list.append(i) 
        
    if ARRAY is None and dev_mode == True:
        print("Failed to get list of Bones")

    return keyed_frames_list

keyed_frames_list = get_keyed_frames()

for A in keyed_frames_list:
    print("A = ", A["bone_name"])
    print("active bone is ", C.active_bone.name, "\n \n")
    if A["bone_name"] == C.active_bone.name:
        print("########  Using keyed_frames_list of",A["bone_name"],"  ########")
        for B in A["keyed_frames"]:
            C.scene.frame_current = int(B)
            print('jumped to frame ', int(B))
            # adding a keyframe to the bone's visibility as a test of it working
            C.active_bone.keyframe_insert(data_path="hide", frame=int(B))
        break

Now in my addon, this is used inside a loop that cycles through a list of selected bones, deselecting everything and selecting/setting them active as the cycle goes to a new bone. And it all works well.

$\endgroup$
1
  • $\begingroup$ hm... Maybe I spoke too soon. It does work, but only on a small scale rig. Try this on a regular character rig that can have hundreds or even thousands of fcurves, and it just instantly freezes Blender. I will try to find a way to force this to only get the selected bones' fcurves. $\endgroup$
    – Lauloque
    Commented Jul 29, 2022 at 16:29

You must log in to answer this question.

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