1
$\begingroup$

While changing Grease Pencil frames 'frame_number' attribute, the viewport doesn't update well, when switching their order (needs manual action to complete that refresh/sort).

To reproduce the error, you can simply add a Grease Pencil monkey to an empty scene, and quickly sculpt his face a bit at frame 10 (or get the blendfile here : https://developer.blender.org/T83590 )

Select the monkey and run the script :

import bpy

ob = bpy.context.object
gp_ob = ob.data


#it gets the frames at time 1 and 10
for layer in gp_ob.layers :
    for frame in layer.frames :
        if frame.frame_number == 1 :
            frame_1 = frame #should be layer.frames[0]
        if frame.frame_number == 10 :
            frame_10 = frame #should be layer.frames[1]
        
    #and switch the frames order
    frame_1.frame_number = 10
    frame_10.frame_number = 1

at first, it seems like nothing changed, (frame_1 displayed from time 1 to 9, frame_10 from 10)

manually click on a frame symbol in the grease pencil dopesheet, now it looks like frame_10 is displayed all along the timeline

manually move a frame symbol along the timeline (you can even replace it at time 1 ) now everything is as expected, (frame_10 displayed from time 1 to 9, frame_1 from 10)

My guess is that the frames indices have to be recalculated because displaying frames[1] before frames[0] is confusing Blender. So i was looking for a kind of frames.update() or layer.sort_frames() method which doesn't seem to exist.

Am i missing something ?

$\endgroup$

2 Answers 2

2
$\begingroup$

As Antonio Vasquez suggested me : "The frame number is not designed to be used in this way. The correct way to do this is to replace the strokes or replace the entire frame, but not just change the frame number. There are many things that happen under the hood and they cannot be changed as easily as it seems."

So here i am, trying to create a function moving frames by copying his strokes and points. As there are no stroke.copy() neither points.copy() methods, the function is longer than expected.

import bpy

def move_GPframe(layer, frame, frame_number):
    ### move Gpencil frames in a proper way
    #First check if there's already a frame at frame_number (and remove it)
    for _frame in layer.frames :
        if _frame.frame_number == frame_number :
            layer.frames.remove(_frame)
    
    #create destination frame
    new_frame = layer.frames.new(frame_number)
    for stroke in frame.strokes :
        #create strokes
        new_stroke = new_frame.strokes.new()
        for attr in dir(stroke):
            try :
                #copy attributes
                setattr(new_stroke, attr, getattr(stroke, attr))
            except AttributeError: 
                pass
        
        #create points
        new_stroke.points.add(count = len(stroke.points)) 
        for i, point in enumerate(stroke.points) :
            new_point = new_stroke.points[i]
            for attr in dir(stroke):
                try :
                    #copy attributes
                    setattr(new_point, attr, getattr(point, attr))
                except AttributeError: 
                    pass
    #remove source frame
    layer.frames.remove(frame)
    return new_frame


######

ob = bpy.context.object
gp_ob = ob.data.id_data


#it gets the frames at time 1 and 10
for layer in gp_ob.layers :
    for frame in layer.frames :
        if frame.frame_number == 1 :
            frame_1 = frame #should be layer.frames[0]
        if frame.frame_number == 10 :
            frame_10 = frame #should be layer.frames[1]
            
    ## old way to do :
    #frame_1.frame_number = 10
    #frame_10.frame_number = 1
    
    ## proper way :
    frame_1 = move_GPframe(layer, frame_1, 0)#need to move the frame_1 at time 0, or it will overwrite frame_10
    frame_10 = move_GPframe(layer, frame_1, 1)
    frame_1 = move_GPframe(layer, frame_1, 10)

I feel that it can be really simplified, but at least it works. Any suggestions ?

$\endgroup$
1
  • $\begingroup$ Interesting stuff, I was working on a "set-exposure-length" addon but doesn't look doable. Good insight from Antonio, even tho I'm not sure I really understand. When selecting a keyframe and moving it with G aren't we just changing the frame_number? $\endgroup$
    – pevinkinel
    Jan 5, 2021 at 13:35
1
$\begingroup$

Well, i keep answering my own question, in case someone finds this helpful someday. The previous solution was not enough to me because it creates some 'Fills' viewport artefacts to the drawings (which needs again a manual action to set up right), and it kills the drawing at the destination frame which can be inconvenient in some cases.

So, i found a dirty but efficient way to force blender to sort the frames indices at the end of the script, allowing me to keep working with frame_number attributes :

def refresh_dopesheets():
    refresh_GP_dopesheet()
    refresh_DS_dopesheet()

def refresh_GP_dopesheet() :  
    #dirty way to force blender to refresh frames indices in grease pencil dopesheet
    if bpy.context.object.type == 'GPENCIL' :
        cur_areatype = str(bpy.context.area.type)
        bpy.context.area.type = 'DOPESHEET_EDITOR'
        cur_space_mode = str(bpy.context.area.spaces[0].mode)
        bpy.context.area.spaces[0].mode = 'GPENCIL'
        bpy.ops.action.mirror(type = 'XAXIS')
        bpy.ops.action.mirror(type = 'XAXIS')
        bpy.context.area.spaces[0].mode = cur_space_mode
        bpy.context.area.type = cur_areatype

def refresh_DS_dopesheet() :  
    #dirty way to force blender to refresh frames indices in regular dopesheet
    if bpy.context.object.type == 'GPENCIL' :
        cur_areatype = str(bpy.context.area.type)
        bpy.context.area.type = 'DOPESHEET_EDITOR'
        cur_space_mode = str(bpy.context.area.spaces[0].mode)
        bpy.context.area.spaces[0].mode = 'DOPESHEET'
        bpy.ops.action.mirror(type = 'XAXIS')
        bpy.ops.action.mirror(type = 'XAXIS')
        bpy.context.area.spaces[0].mode = cur_space_mode
        bpy.context.area.type = cur_areatype

I did it for regular dopesheet too, cause the same issue happens with keyframe_points[i].co.x way to move keyframes in time. Using the initial example, simply add a line :

import bpy

ob = bpy.context.object
gp_ob = ob.data.id_data


#it gets the frames at time 1 and 10
for layer in gp_ob.layers :
    for frame in layer.frames :
        if frame.frame_number == 1 :
            frame_1 = frame #should be layer.frames[0]
        if frame.frame_number == 10 :
            frame_10 = frame #should be layer.frames[1]
        
    #and switch the frames order
    frame_1.frame_number = 10
    frame_10.frame_number = 1

#refresh things :
refresh_GP_dopesheet()

Hope it can help someone someday.

$\endgroup$

You must log in to answer this question.

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