Hi, I wrote a script that allows you to morph between Grease Pencil frames. The following animation was created by drawing only 7 frames (for the 7 letters); all other frames were calculated automatically:
How to use (quick guide):
- draw a grease pencil stroke; of course it will be drawn at the current frame (e.g. at frame 1)
- duplicate the stroke (either using copy/paste or the Dope Sheet) and place it at a second frame (e.g. at frame 10); using the sculpt tools, modify the stroke. Be sure not to erase or add points since morphing will only work if the number of points of the strokes is identical
- the script works on all strokes that have at least one point selected. So select at least on point of the stroke
- open a Dope Sheet with a Grease Pencil context
- set the current frame to be something in between your first frame and the last frame (e.g. frame 5)
- execute the script
Notes:
- the script respects locked layers and will not work on those
- in the console there is some output, mostly for debugging purposes
# usage: place current frame somewhere in between keyframes, select at least
# one point of the stroke you want to be morphed and execute the script
# blackno666, March 2016
import bpy
import os
import pdb
S = bpy.context.scene.grease_pencil
def get_key_frames(gplayer_name: str) -> tuple:
"""Get the keyframes left and right of the current frame for a specific
grease pencil layer.
If no such keyframe exists the current frame is returned instead.
"""
start_frame = bpy.context.scene.frame_current
end_frame = start_frame
layer = S.layers[gplayer_name]
for f in layer.frames:
if f.frame_number <= bpy.context.scene.frame_current:
start_frame = f.frame_number
if f.frame_number > end_frame:
end_frame = f.frame_number
break
return (start_frame, end_frame)
def get_frame_index(gplayer_name: str, frame_number: int) -> int:
layer = S.layers[gplayer_name]
index = [f.frame_number for f in layer.frames].index(frame_number)
return index
def get_selected_strokes(gplayer_name: str, frame_number: int) -> list:
"""Get the grease pencil strokes of a specific grease pencil layer and a
specific frame that have at least one point selected.
"""
layer = S.layers[gplayer_name]
frame_index = get_frame_index(gplayer_name, frame_number)
strokes = layer.frames[frame_index].strokes
result = []
for (i, stroke) in enumerate(strokes):
for p in stroke.points:
if p.select:
result.append((i, stroke))
break
return result
def deselect_all_frames():
"""Deselect all frames for all grease pencil layers in the Dope Sheet."""
for layer in S.layers:
for frame in layer.frames:
frame.select = False
def select_frame_left_of_current_frame(gplayer_name: str):
"""Select the frame of a specific grease pencil layer left of the current
frame in the Dope Sheet."""
for frame in S.layers[gplayer_name].frames:
frame.select = False
start_frame = get_key_frames(gplayer_name)[0]
S.layers[gplayer_name].frames[get_frame_index(gplayer_name, start_frame)].select = True
def duplicate_selected_frame(count: int = 1):
"""Duplicate the currently selected frames in the Dope Sheet count times."""
for i in range(0, count):
for area in bpy.context.screen.areas:
if area.type == 'DOPESHEET_EDITOR':
override = bpy.context.copy()
override['area'] = area
bpy.ops.action.duplicate_move(override, ACTION_OT_duplicate={}, TRANSFORM_OT_transform={"mode":'TRANSLATION', "value":(1, 0, 0, 0)})
break
def get_number_of_points(gplayer_name: str, frame_index: int, stroke_index: int) -> int:
"""Get the number of points of a specific grease pencil stroke of a
specific frame of a specific grease pencil layer.
"""
if frame_index >= len(S.layers[gplayer_name].frames):
return -1
if stroke_index >= len(S.layers[gplayer_name].frames[frame_index].strokes):
return -1
return len(S.layers[gplayer_name].frames[frame_index].strokes[stroke_index].points)
def morph():
for layer in S.layers:
layer_name = layer.info
if layer.lock:
print("skipping layer {0} because it is locked
".format(layer_name))
continue
keyframes = get_key_frames(layer_name)
print("processing layer {0} from frame {1} to {2}".format(layer_name, keyframes[0], keyframes[1]))
if keyframes[1] - keyframes[0] < 2:
print(" there are no frames in between keyframes... skipping morphing
")
continue
selected_strokes = get_selected_strokes(layer_name, keyframes[0])
if len(selected_strokes) == 0:
print(" there are no strokes containing a selection... skipping morphing
")
continue
print(" found {0} strokes which have a selection".format(len(selected_strokes)))
duplicated = False
for selected_stroke in selected_strokes:
stroke_index = selected_stroke[0]
print(" checking stroke {0}".format(stroke_index))
num_points1 = get_number_of_points(layer_name, get_frame_index(layer_name, keyframes[0]), stroke_index)
num_points2 = get_number_of_points(layer_name, get_frame_index(layer_name, keyframes[1]), stroke_index)
print(" found {0} points at frame {1} and {2} points at frame {3}".format(num_points1, keyframes[0], num_points2, keyframes[1]))
if num_points1 == num_points2 and keyframes[1]-keyframes[0] > 1:
if not duplicated:
deselect_all_frames()
select_frame_left_of_current_frame(layer_name)
duplicate_selected_frame(keyframes[1]-keyframes[0]-1)
duplicated = True
source_points = selected_stroke[1].points
dest_points = S.layers[layer_name].frames[get_frame_index(layer_name, keyframes[1])].strokes[selected_stroke[0]].points
deltas = []
for i in range(0, len(source_points)):
deltas.append((dest_points[i].co-source_points[i].co)/(keyframes[1]-keyframes[0]))
print(" creating frames {0} to {1}
".format(keyframes[0]+1, keyframes[1]-1))
for i in range(keyframes[0]+1, keyframes[1]):
frame_index = get_frame_index(layer_name, i)
for j in range(len(deltas)):
S.layers[layer_name].frames[frame_index].strokes[stroke_index].points[j].co += (i-keyframes[0])*deltas[j]
os.system('cls')
morph()