Freestyle integration into Blender

August 4, 2009

Recent updates of the Freestyle Python API

Filed under: Update — The dev team @ 4:39 AM

This blog entry summarizes the recent updates of the Freestyle Python API.  My commits in the past were mainly concerned with the so-called SWIG/directors dependency removal, aiming at an improved Python API.

1. Background

In Freestyle, the Python programming language is used to write a style that specifies how feature edges, extracted from a 3D scene, are transformed into strokes and drawn onto the canvas.  A style makes use of 5 operators: select (to choose edges of interest), chain (to transform edges into strokes), split (to separate strokes into smaller pieces), sort (to change the drawing order of strokes), and create (to draw strokes).  These 5 operators take as arguments a variety of rules for controlling the operations.  There are 4 types of rules: predicates (to test if a condition is satisfied), functions (to do some computations and return the results), chaining iterators (to specify how to concatenate edges to generate strokes), and stroke shaders (to modify strokes and their attributes such as colors and thickness).  These rules are defined by inheriting the following 8 base classes.

  • UnaryPredicate0D
  • UnaryPredicate1D
  • BinaryPredicate0D
  • BinaryPredicate1D
  • UnaryFunction0D
  • UnaryFunction1D
  • ChainingIterator
  • StrokeShader

Freestyle has many predefined predicates, functions, chaining iterators, and stroke shaders.  In addition, Freestyle allows users to define new rules by subclassing a base class and overriding a certain method.

Definition of a new rule usually requires access to Freestyle’s internal data structures such as feature edges (comprising a view map) and strokes.  Classes for the internal data structures, as well as the aforementioned classes for defining rules, are written in C++.  Moreover, basically every C++ class has the corresponding Python wrapper class that gives access to the C++ class.  There are roughly 150 Python wrappers for the same number of underlying C++ classes, and these Python wrappers as a whole constitute the Freestyle Python API.

Thanks to this multi-language API architecture, defining new rules by inheritance can be done not only in C++ but also in Python.  For example, UnaryPredicate0D is a base class in C++ for defining a predicate, and there is a corresponding Python class (or, strictly speaking, a C extension type) having the same name.  By subclassing the Python class and overriding a certain method (the __call__ method in this case), a new predicate can be easily defined in a style written in Python.

2. Issues that need addressing

Before being integrated into Blender, the original Freestyle had a Python API implemented using another program called SWIG.  This program automatically generates Python wrappers for a given set of C++ classes, reducing the cost of providing and maintaining Python wrappers.  On one hand, SWIG was a very useful tool for rapid development of Python wrappers.  On the other hand, however, there were some drawbacks in the auto-generated Python API from the viewpoint of its users (i.e., Python programmers), as well as from the viewpoint of developers, as detailed below.

2.1. User-visible issues

2.1.1. Iterators

The C++ classes in Freestyle heavily relied on iterators, and SWIG translated the iterators into Python while keeping the C++-like syntax.  For example, changing line thickness of a stroke can be written in Python in the SWIG-based iterator syntax as follows:

    it = stroke.StrokeVerticesBegin()
    while it.isEnd() == 0:
        v = it.getObject()
        v.attribute().setThickness(…)
        it.increment()

Python has its own language features for iteration, so appropriate support for more Python-like iterator syntax was desired.

2.1.2. Cast operators

Freestyle’s C++ classes also relied on cast operators to convert a class instance into a subclass and vice versa.  For example, ViewVertex (representing a vertex in a view map) has two subclasses TVertex (a vertex at an intersection of two edges) and NonTVertex.  A feature edge is represented by a series ViewVertex instances, each of which is actually either a TVertex or NonTVertex instance.  If a subclass-specific method is needed to do some useful thing on the edge, then a cast operator is used to convert a ViewVertex object into TVertex/NonTVertex.  Python does not have the notion of cast operations, so that the ViewVertex wrapper in the SWIG-based Python API had two auxiliary methods castToTVertex() and castToNonTVertex().  There was a number of such castToSomething methods in the SWIG-based Python API, and they looked too awkward.  Since Python is a dynamically typed programming language, a better approach would be possible to make the cast operators unnecessary.

2.1.3. Better support for Python data types

Besides the cast operators, better support for Python’s built-in data types was preferable.  For example, being able to pass lists and tuples would be useful when vectors are required as method arguments.  In addition, Python has a variety of boolean expressions that are equivalent to True and False.  Taking these Python-specific language features into account would contribute to the flexibility of the Python API.

2.2. Implementation issues

2.2.1. The “directors”

The SWIG-based implementation of the Python API relied heavily on a set of functions called directors.  A director is a piece of code for getting a Python class method called from within a C++ class method.  Directors were necessary to enable the cross-language inheritance to define new rules in Python, and they made the API implementation quite difficult to understand and maintain.

2.2.2. Borrowed references

Some classes in the Python API are mainly used to wrap underlying C++ objects that comprise Freestyle’s internal data structures.  If that is the case, the wrapping Python classes  instantiated by Freestyle, and they retain a “borrowed” reference to a wrapped C++ object that is part of the internal data structures.  In addition, these Python classes can also be instantiated by a user.  In the latter case, wrapped C++ objects are typically just a copy of an internal object.  Strictly managing borrowed references to wrapped internal objects was an important issue, since releasing internal objects by mistake easily leads to a crash of Blender, while not releasing copied objects results in memory leaks.

3. Solutions implemented

We took the present opportunity of the Freestyle integrate into Blender to address the above-mentioned issues and make the Freestyle Python API more user-friendly.  The recent commits on the Python API were all intended to address these SWIG-related issues.  Following sections summarize the implemented solutions for each issue.

3.1. Iterators

In addition to the SWIG-based iterator syntax, Python’s native iterator protocol has been implemented.  The Python wrapper classes that support the iterator protocol are: AdjacencyIterator, Interface0DIterator, orientedViewEdgeIterator, Stroke and StrokeVertexIterator.  For example, the previous SWIG-based iterator syntax example can be rewritten as follows:

    for v in stroke:
        v.attribute().setThickness(…)

Related commits:
17661 (Stroke, StrokeIterator)
21889, 22003 (AdjacencyIterator)
22096 (Interface0DIterator, orientedViewEdgeIterator)
22099 (increment/decrement boundary checking)

3.2. Cast operators

Instead of relying on explicit cast operators through the castToSomething methods, introspection-based automatic type conversion has been implemented.  When a C++ class method returns an instance of a generic class (e.g., ViewVertex), the corresponding Python class method returns an instance of a specific class (e.g., TVertex or NonTVertex).  This made all castToSomething methods no longer necessary, so that they were removed from the Python API.  If you need to rewrite old style modules that rely on the cast methods, apply the following rewriting rules:

a) The following 5 methods return an object itself, so just removing a method call will suffice.

  • SVertex.castToSVertex()
  • TVertex.castToViewVertex()
  • TVertex.castToTVertex()
  • NonTVertex.castToViewVertex()
  • NonTVertex.castToNonTVertex()

If you need to handle objects in a different way depending on their types, then you can use Python’s type checking idioms such as “type(obj) is T” and “isinstance(obj, T)”.  Example:

    [Original]
    v = it.getObject()
    # try to convert v into a TVertex object
    vertex = v.castToTVertex()
    if vertex != None:
        … # do something on the TVertex object
    # try to convert v into a NonTVertex object
    vertex = v.castToNonTVertex()
    if vertex != None:
        … # do something on the NonTVertex object

    [Rewritten]
    vertex = it.getObject()
    if type(vertex) is TVertex:
        … # do something on the TVertex object
    elif type(vertex) is NonTVertex:
        … # do something on the NonTVertex object

b) Use SVertex.viewvertex() instead of the following 3 methods.  You don’t need to care about which cast method is appropriate.  SVertex.viewvertex() does, if necessary, introspection-based automatic type conversion for you.

  • SVertex.castToViewVertex()
  • SVertex.castToTVertex()
  • SVertex.castToNonTVertex()

c) Use NonTVertex.svertex() instead of the following method.

  • NonTVertex.castToSVertex()

d) Replace the following method with the Python equivalent shown below.

  • CurvePoint.castToSVertex()

Let cp be a CurvePoint object, then this method can be expressed as follows:

    if cp.t2d() == 0.0:
        return cp.A() # returns an SVertex
    elif cp.t2d() == 1.0:
        return cp.B() # returns an SVertex
    return None

e) Similarly, the following 3 methods

  • CurvePoint.castToViewVertex()
  • CurvePoint.castToTVertex()
  • CurvePoint.castToNonVertex()

can be expressed as follows:

    if cp.t2d() == 0.0:
        return cp.A().viewvertex()
    elif cp.t2d() == 1.0:
        return cp.B().viewvertex()
    return None

Related commits:
21710, 22148 (introspection-based automatic type conversion)
21947 (updates in freestyle_init.py, removal of castToTVertex method calls)
22153 (removal of all castToSomething methods from Interface0D subclasses)

3.3. Better support for Python data types

The Freestyle Python API now employs the Vector class in the Blender.Mathutils module to represent 2D and 3D vectors.  Python class methods accepting vectors also accept lists and tuples if they have the same number of elements.  Boolean expressions in Python are also better supported.

Related commits:
21877, 21947 (vectors, lists and tuples)
19526 (boolean)

3.4. The “directors”

The dependency on directors has been largely simplified.  Now the Freestyle Python API clearly defines that only the following class methods can be overridden by Python subclasses to write user-defined rules:

  • UnaryPredicate0D.__call__()
  • UnaryPredicate1D.__call__()
  • BinaryPredicate0D.__call__()
  • BinaryPredicate1D.__call__()
  • UnaryFunction0DDouble.__call__()
  • UnaryFunction0DEdgeNature.__call__()
  • UnaryFunction0DFloat.__call__()
  • UnaryFunction0DId.__call__()
  • UnaryFunction0DMaterial.__call__()
  • UnaryFunction0DUnsigned.__call__()
  • UnaryFunction0DVec2f.__call__()
  • UnaryFunction0DVec3f.__call__()
  • UnaryFunction0DVectorViewShape.__call__()
  • UnaryFunction0DViewShape.__call__()
  • UnaryFunction1DDouble.__call__()
  • UnaryFunction1DEdgeNature.__call__()
  • UnaryFunction1DFloat.__call__()
  • UnaryFunction1DUnsigned.__call__()
  • UnaryFunction1DVec2f.__call__()
  • UnaryFunction1DVec3f.__call__()
  • UnaryFunction1DVectorViewShape.__call__()
  • UnaryFunction1DVoid.__call__()
  • ChainingIterator.init()
  • ChainingIterator.traverse()
  • StrokeShader.shade()

For the purpose of reducing various complications due to the directors, it appeared vital to determine which classes (and more specifically which of their methods) can be inherited (overridden) by user-defined Python subclasses.  The above list of overridable Python class methods was determined by the following reasons.

  1. Classes in Freestyle are roughly classified into three categories: 1) those classes for building Freestyle’s internal data structures; 2) classes for predefined predicates, functions, chaining iterators, and stroke shaders; and 3) base classes for defining new rules.  Inheriting Python classes in the first category does not make sense, since their main purpose is simply to give access to the internal data structures.  Derived classes for predefined rules are also not intended to be further subclassed, since their __call__ methods are rather monolithic and not extensible.  Consequently, only the classes in the third category are considered suitable as base classes for user-defined subclasses.
  2. Since the Freestyle core is written in C++, a director is necessary whenever a user-defined rule in Python has to be called within from the C++ side.  In fact, the C++ class methods being wrapped by the Python class methods listed above employ a director to make this happen.  On the other hand, Python class methods may be invoked within from the Python side (for example, AndUP1D and OrUP1D, written in Python, invoke the __call__ methods of given predicate classes).  Therefore, an appropriate measure is necessary to avoid infinite recursion between the C++ and Python sides.  Having a director call in every C++ class method together with a measure for infinite loop prevention is costly in terms of code maintenance and very likely to be useless.  Therefore, keeping the list of director-dependent class methods as small as possible is desired.

Related commits:
22096, 22148 (simplification of directors)
21930, 22002, 22093 (infinite loop prevention)

3.5. Borrowed references

A “borrowed” flag was added to the definitions of relevant Python classes in order to indicate whether or not a Python wrapper object has a reference to a C++ object that comprises the internal data structures.  The deconstructors of the Python classes check this flag and release a wrapped C++ object only when it is not part of the internal data structures.  The following Python classes were modified: Interface1D and its subclasses (Chain, FEdge, FEdgeSharp, FEdgeSmooth, FrsCurve, Stroke, ViewEdge), Interface0D and its subclasses (CurvePoint, NonTVertex, SVertex, StrokeVertex, TVertex, ViewVertex), FrsMaterial, SShape, StrokeAttribute, and ViewShape.

Related commits:
22148 (borrowed references)

3.6. Additional improvements

The Freesytle Python API is fairly large and its implementation is now done manually, so that complete functionality coverage of all C++ classes is an important goal.  Some of the recent commits are related to this issue, and those functionalities in C++ that are required by standard styles have been already available.

Another issue is stability, since the process of writing a style in Python is inevitably error-prone.  Runtime errors should be properly trapped by the API implementation and reported to users in the form of Python exceptions.  To improve the stability of application programs (i.e., styles), error handling in both C++ and Python layers has been largely enhanced.  Now most trivial programming mistakes just result in a Python traceback and do not break Blender.

Related commits:
19342 (better error handling on the C++ side)
21921 (context functions)
21946, 21947 (undocumented utility functions)

4. Summary

The most significant update for general Freestyle users is that now all standard styles that come with Freestyle for Blender should run without any Python traceback, thanks to the above-mentioned Python API improvements and consolidation.  There are still missing features such as textured strokes and steerable view maps, and some styles may not yield any strokes.  The rest of the standard styles, however, are expected to work fine.

Our future work on the Python API includes more improvements for stability, better integration of Blender-specific data types, and API documentation updates.

1 Comment »

  1. […] remove the old SWIG/C++-based syntax from the API (see the blog entries on December 1, 2008 and on August 4, 2009 for more information on the syntactic improvements).  This means that existing style modules […]

    Pingback by Weekly update March 29-April 4 « Freestyle integration into Blender — April 7, 2010 @ 12:40 AM


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: