Blenderyard

October 9, 2014

Under the hood: Stroke animation with Freestyle for Blender

Filed under: Blender, Freestyle — blenderyard @ 3:40 PM

During the Christmas and new year holidays at the end of 2010, I was experimenting an idea to realize animated strokes using Freestyle for Blender.  The idea was to create an animation movie clip in which a set of stylized lines is gradually drawn as time goes.  After a few attempts of coding in Python using the Freestyle Python API, I got a style module that achieved the desired visual effects.  I made several video clips to demonstrate the new stroke drawing module by the end of new year holidays.  I was quite satisfied with the results.

After a while, I came to know that I had deleted the stroke drawing module file by accident.  Relevant .blend files and output movie clips were salvaged, but the core of the coding work was permanently lost.  Since then it was a long-standing to-do item for me to re-implement the stroke drawing module from scratch.

Recently I got a bit of spare time (for the first time since a long time, after the completion of my intensive freelance work on Freestyle and Blender NPR development supported by the Blender Development Fund).  So I gave another try to stroke animation in Freestyle.  Here is the result (click the image below to jump to the finished product posted on Vimeo):

vimeo107822555

Technically speaking, the stroke animation is implemented as follows.  First a set of stylized strokes is created as usual using Freestyle, namely either through the interactive Parameter Editor mode or in the coder-oriented Python Scripting mode.  The basic strategy for animated strokes then is to make only a fraction of the entire stylized strokes visible, where the fraction is set to the ratio of the number of rendered frames (i.e., the current frame number) to the total number of frames.  To make the visual effects as general as possible, I have implemented the stroke drawing effect as a post-processing procedure as follows:

# stroke_anim.py

from freestyle.types import Operators
from freestyle.utils import getCurrentScene

def postprocess(frame_start=None, frame_end=None, interval=0):
    """
    frame_start: the start frame (default Scene.frame_start)
    frame_end: the end frame (default: Scene.frame_end)
    interval: the number of frames inserted as interval between two strokes
    """

    totlen = 0.0
    nstrokes = Operators.get_strokes_size()
    #print('#strokes', nstrokes)
    for i in range(nstrokes):
        stroke = Operators.get_stroke_from_index(i)
        totlen += stroke.length_2d
    #print('totlen', totlen)

    scene = getCurrentScene()
    sta = scene.frame_start if frame_start is None else frame_start
    end = scene.frame_end if frame_end is None else frame_end
    cur = scene.frame_current
    fac = (cur - sta) / (end - sta)
    #print('fac', fac)

    totDrawingFrames = (end - sta + 1) - interval * (nstrokes - 1)
    if totDrawingFrames < 0:
        raise RuntimeError('The number of frames is too small')

    lengthPerFrame = totlen / totDrawingFrames
    #print('lengthPerFrame', lengthPerFrame)

    thresh = (cur - sta + 1) * lengthPerFrame + 1e-6
    #print('thresh', thresh)

    curlen = 0.0
    for i in range(nstrokes):
        stroke = Operators.get_stroke_from_index(i)
        for svert in stroke:
            svert.attribute.visible = curlen + svert.curvilinear_abscissa < thresh
        curlen += stroke.length_2d + lengthPerFrame * interval
    #print('done')

In the case of Suzanne the monkey drawn by thick brush strokes, the following style module was used (that is, a slightly modified version of japanese_bigbrush.py).

# brush_anim.py

from freestyle.types import *
from freestyle.predicates import *
from freestyle.functions import *
from freestyle.chainingiterators import *
from freestyle.shaders import *

class pyProjectedYBP1D(BinaryPredicate1D):
    def __init__(self, iType=IntegrationType.MEAN):
        BinaryPredicate1D.__init__(self)
        self.func = GetProjectedYF1D(iType)
    def __call__(self, i1, i2):
        return (self.func(i1) > self.func(i2))

Operators.select(QuantitativeInvisibilityUP1D(0))
Operators.bidirectional_chain(ChainSilhouetteIterator(), NotUP1D(QuantitativeInvisibilityUP1D(0)))
## Splits strokes at points of highest 2D curavture
## when there are too many abrupt turns in it
func = pyInverseCurvature2DAngleF0D()
Operators.recursive_split(func, pyParameterUP0D(0.2, 0.8), NotUP1D(pyHigherNumberOfTurnsUP1D(3, 0.5)), 2)
## Keeps only long enough strokes
Operators.select(pyHigherLengthUP1D(100))
## Sorts strokes by the maximum projected Y coordinate value
Operators.sort(pyProjectedYBP1D(IntegrationType.MAX))
shaders_list = [
    pySamplingShader(10),
    BezierCurveShader(30),
    pyNonLinearVaryingThicknessShader(2, 16, 0.6),
    ConstantColorShader(0, 0, 0),
    TipRemoverShader(10),
    ]
Operators.create(TrueUP1D(), shaders_list)

import stroke_anim; stroke_anim.postprocess(interval=10)

Notice the last line in the stylization script where the post-processing procedure is invoked.  By animation rendering from 1 to some frame count (400 in the case of brush-stroked Suzanne), a set of stylized lines created by brush_anim.py will be transformed into a sequence of animation frames where the strokes are “drawn” over time.  The interval parameter specifies a pause between two strokes in units of frames.  Stroke animation can also be done in the Parameter Editor mode of Freestyle by manually modifying script/freestyle/module/parameter_editor.py in your Blender installation as indicated above in the code example.

I believe the idea of having post-processing effects after the creation of Freestyle strokes can be generalized in the future in such a way that artists can specify either user-defined Python procedures or predefined Parameter Editor options to further retouch auto-generated stylized lines.  Some coding effort is required for now, however, to reproduce the documented stroke animation effects.

Advertisements

8 Comments »

  1. This is great work, and I’m very eager to able to implement this, but I am a complete noob as far as python goes. I made new text data blocks, and named copied the two script samples in, and changed the names to the commented first lines. Then I used Freestyle with Python Scripting Mode, and selected brush_anim.py for the line style. But how would I use other line styles, and how do I control the length of the animation? Is there a way to use this in Parameter Edit Mode?

    Thank you for your great work!

    Comment by John Grigni — October 17, 2014 @ 1:57 AM

    • The third example (lily) was done with a modified version of sketchy_topology_broken.py that comes with Freestyle. Add the last line of brush_anim.py quoted below to the end of the script and you should be able to have the same effect.

      import stroke_anim; stroke_anim.postprocess(interval=10)

      The second and the fourth example were made with a modified version of parameter_editor.py. This script is the implementation of the Parameter Editor mode. Although the file is actually not supposed to be modified by users, I took the easiest approach. So again make the same modification as described above to 2.72/scripts/freestyle/modules/parameter_editor.py. Keep the same indentation level with the previous line, as illustrated below:

      Operators.create(TrueUP1D(), shaders_list)

      import stroke_anim; stroke_anim.postprocess(interval=10) # ADDED

      The fourth example (toy car) is made of three different line styles. I made animation renders of the three separately and combined the renders using the compositor. The stroke_anim module does not take care of multiple line styles, so you need to address by yourself what to render and what not to render.

      Hope this helps,
      T.K.

      Comment by blenderyard — October 17, 2014 @ 4:24 PM

    • Forgot to mention how to control the length of the animation. The length is by default determined by the start and end frame settings. You can also specify the start and end frames as follows:

      import stroke_anim; stroke_anim.postprocess(frame_start=100, frame_end=200, interval=10)

      Comment by blenderyard — October 17, 2014 @ 5:05 PM

  2. This is awesome!

    Two things: I think I’d want to figure out a method of controlling the order of drawing, I can’t tell if that’s possible by simply modifying the current script, could be through a grayscale texture map, could be an object distance thing, or simply setting start and end frames for different object groups. I know some python, I know some blender but I’m not that versed in blender+python and what I’m reading on the blenderNPR blog isn’t making much sense. The second thing: it would sure be nice if you could just save the parametric settings in a python file. That is all.

    Comment by veggiet — May 9, 2015 @ 6:26 AM

    • I’m looking at the code right now to see if maybe there was a way that I could replace the frame calculation with a scene attribute. The scene attribute would theoretically range between 0 not drawn and 1 totally drawn. But I’m thoroughly confused, I thought that the ‘fac’ variable could be replaced, but then I realized that it seems to not be used in the rest of the code. when I printed the outputs of totlen, curlen, thresh, and fac, the numbers seems nonsensical. like so:

      totlen 3257.833183288574
      fac 1.0
      thresh 2150.4908707569894
      curlen 5986.067868801172

      Maybe it’s my naivety, but the code looks like your get value inbetween ‘curlen’ and ‘totlen,’ but as you can see curlen is greater than ‘totlen’ and yet the drawing is only halfway done.

      Comment by veggiet — May 9, 2015 @ 1:58 PM

      • It’s becoming a little clear ‘thresh’ seems like the value to manipulate in relation to curlen, but it’s not a perfect percentage (i.e. at the last frame in the sequence thresh does not equal curlen,) and curlen starts at zero, so I don’t think I could just: thresh = factor * curlen . Could I?

        I tried it (without making a new comment) and it seemed to work. I’m not sure that manipulating drawing speed like this is the effect I want, but I need to actually render the animation to see that:

        # stroke_anim.py

        from freestyle.types import Operators
        from freestyle.utils import getCurrentScene

        def postprocess(frame_start=None, frame_end=None, interval=0):
        “””
        frame_start: the start frame (default Scene.frame_start)
        frame_end: the end frame (default: Scene.frame_end)
        interval: the number of frames inserted as interval between two strokes
        “””

        totlen = 0.0
        nstrokes = Operators.get_strokes_size()
        #print(‘#strokes’, nstrokes)
        for i in range(nstrokes):
        stroke = Operators.get_stroke_from_index(i)
        totlen += stroke.length_2d
        print(‘totlen’, totlen)

        scene = getCurrentScene()
        sta = scene.frame_start if frame_start is None else frame_start
        end = scene.frame_end if frame_end is None else frame_end
        cur = scene.frame_current
        fac = scene.get(‘dfactor’) * (end – sta)#(cur – sta) / (end – sta) #
        interval = scene.get(‘dinterval’)
        print(‘current frame’,cur)
        print(‘fac’, fac)

        totDrawingFrames = (end – sta + 1) – interval * (nstrokes – 1)
        if totDrawingFrames < 0:
        raise RuntimeError('The number of frames is too small')

        lengthPerFrame = totlen / totDrawingFrames
        #print('lengthPerFrame', lengthPerFrame)

        thresh = (fac – sta + 1) * lengthPerFrame + 1e-6
        print('thresh', thresh)

        curlen = 0
        for i in range(nstrokes):
        stroke = Operators.get_stroke_from_index(i)
        for svert in stroke:
        svert.attribute.visible = curlen + svert.curvilinear_abscissa < thresh
        curlen += stroke.length_2d + lengthPerFrame * interval
        #print('done')

        print('curlen',curlen)
        curlen = 0.0

        Comment by veggiet — May 9, 2015 @ 2:41 PM

      • My code does have a problem with changing frame lengths, i.e. My full video will be 363 frames, while I wanted to run a test render of 86 frames, and freestyle drew nothing.

        Comment by veggiet — May 9, 2015 @ 2:57 PM

      • Well I think I’m going to take a break from this project for a little bit. My last issue involved using the scripts in two separate scenes, and the second scene giving me the “‘The number of frames is too small'” error, and I can’t figure out why. I’m not doing anything terribly different in the second scene, it’s a similar text object, the keyframes on my factor value are different

        Comment by veggiet — May 9, 2015 @ 11:42 PM


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: