[Script]Grease Pencil morphing

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:

http://imageshack.com/a/img924/7448/i1znu8.gif

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()

as for some text morphing case just like that in your gif, is there any way to convert charactor into GP?

Hi, not that I know of. And even if such a functionality existed, it probably would not be of too much help. If you take the font I used, the “l” for example would consist of four corner points. Any you just cannot morph those 4 points to form an “e”.
So I used a text as a template and basically carbon copied it with the grease pencil.

here is some codesnippet i found on blender.stackexchange that converts objects to greasepencil, but it just works on a single frame and creates something like a wireframe effect on Charakters, because it draws all edges as greasepencil

import bpy
from mathutils import Vector

def convert_curve_to_gp(obj):

#---- CHANGED ----
scene = bpy.context.scene
mesh = obj.data

layer = scene.grease_pencil.layers.new("layer", set_active=True)
frame = layer.frames.new(scene.frame_current)
#---- /CHANGED ----

mesh = obj.to_mesh(scene, apply_modifiers=False, settings='PREVIEW')
for edge in mesh.edges:
    stroke = frame.strokes.new()
    stroke.draw_mode = '3DSPACE'
    stroke.points.add(2)
    stroke.points[0].co = obj.matrix_world * Vector(mesh.vertices[edge.vertices[0]].co)
    stroke.points[1].co = obj.matrix_world * Vector(mesh.vertices[edge.vertices[1]].co)

bpy.data.meshes.remove(mesh)

obj = bpy.context.object
convert_curve_to_gp(obj)

Even though this is the Coding forum, I decided to reformat the donnydarko’s code so inexperienced users can use it as well - especially since I consider the code quite useful.


import bpy
from mathutils import Vector

def convert_curve_to_gp(obj):

#---- CHANGED ----
    scene = bpy.context.scene
    mesh = obj.data

    layer = scene.grease_pencil.layers.new("layer", set_active=True)
    frame = layer.frames.new(scene.frame_current)
    #---- /CHANGED ----

    mesh = obj.to_mesh(scene, apply_modifiers=False, settings='PREVIEW')
    for edge in mesh.edges:
        stroke = frame.strokes.new()
        stroke.draw_mode = '3DSPACE'
        stroke.points.add(2)
        stroke.points[0].co = obj.matrix_world * Vector(mesh.vertices[edge.vertices[0]].co)
        stroke.points[1].co = obj.matrix_world * Vector(mesh.vertices[edge.vertices[1]].co)

    bpy.data.meshes.remove(mesh)

obj = bpy.context.object
convert_curve_to_gp(obj)