2
$\begingroup$

Geometry-holding objects have handy methods to get their data as an evaluated mesh :

However these can't be used on Grease Pencil objects and throw an exception :

RuntimeError: Error: Object does not have geometry data

There is an operator : bpy.ops.gpencil.convert which can be used to convert a grease pencil object to a curve object, but it can only convert one layer at a time and it requires holding a reference to a 3D view area, which might not always be available.

How can I convert a Grease Pencil object to a curve object or a mesh object using lower-level API instead of an operator ?

$\endgroup$

1 Answer 1

3
$\begingroup$

The limitation is easily explained by the complexity of a Grease Pencil object and its data structure, best explained on this other question.

A GP object can hold several layers, which can hold information for each frame of the animation, and each frame can hold an indefinite number of strokes, which can hold an indefinite number of points.

For the sake of ease of use the script will be considering the grease pencil object is not animated and holds only one frame worth of data, but it could be added as a feature afterwards.

It will however be able to export different objects for each layer in the GP object.

The final script :

import bpy


def main():
    gpencil_object = bpy.data.objects["Suzanne"]
    convert_gpencil_to_curve(gpencil_object, flatten_layers=True)
    convert_gpencil_to_curve(gpencil_object, flatten_layers=False)
    convert_gpencil_to_mesh(gpencil_object, flatten_layers=True)
    convert_gpencil_to_mesh(gpencil_object, flatten_layers=False)


def new_convert_mesh_object(collection, name):    
    mesh = bpy.data.meshes.new(name=name)
    convert_object = bpy.data.objects.new(name=name, object_data=mesh)
    collection.objects.link(convert_object)
    return convert_object


def new_convert_curve_object(collection, name):    
    curve = bpy.data.curves.new(name=name, type="CURVE")
    curve.dimensions = "3D"
    convert_object = bpy.data.objects.new(name=name, object_data=curve)
    collection.objects.link(convert_object)
    return convert_object


def convert_gpencil_to_mesh(gpencil_object, flatten_layers=True):
    gp_col = gpencil_object.users_collection[0]
    gp = gpencil_object.data
    if flatten_layers:
        obj = new_convert_mesh_object(gp_col, gpencil_object.name + "_mesh")
        vertices = []
        edges = []
    for layer in gp.layers:
        if not flatten_layers:                    
            obj = new_convert_mesh_object(gp_col, f"{gpencil_object.name}_mesh_layer_{layer.info}")
            vertices = []
            edges = []
        for frame in layer.frames:
            for stroke in frame.strokes:
                for i, point in enumerate(stroke.points):
                    vertices.append(point.co)
                    if i > 0:
                        edges.append((len(vertices) - 1,len(vertices) - 2))
        if not flatten_layers:
            obj.data.from_pydata(vertices, edges, ())
    if flatten_layers:
            obj.data.from_pydata(vertices, edges, ())


def convert_gpencil_to_curve(gpencil_object, flatten_layers=True):
    gp_col = gpencil_object.users_collection[0]
    gp = gpencil_object.data
    if flatten_layers:
        obj = new_convert_curve_object(gp_col, gpencil_object.name + "_curve")
    for layer in gp.layers:
        if not flatten_layers:                    
            obj = new_convert_curve_object(gp_col, f"{gpencil_object.name}_curve_layer_{layer.info}")
        for frame in layer.frames:
            for stroke in frame.strokes:
                spline = obj.data.splines.new(type="POLY")                   
                spline.points.add(len(stroke.points) - 1) # Spline starts with 1 point
                for i, point in enumerate(stroke.points):
                    spline.points[i].co = [v for v in point.co] + [1]


if __name__ == "__main__":
    main()

The explanations :

  1. Convert to curve

First we define a handy function to create a new curve object and link it to the GP object collection, then return it.

def new_convert_curve_object(collection, name):    
    curve = bpy.data.curves.new(name=name, type="CURVE")
    curve.dimensions = "3D"
    convert_object = bpy.data.objects.new(name=name, object_data=curve)
    collection.objects.link(convert_object)
    return convert_object

Then we define the behaviour of the curve converter :

def convert_gpencil_to_curve(gpencil_object, flatten_layers=True):
    gp_col = gpencil_object.users_collection[0]
    gp = gpencil_object.data
    if flatten_layers:
        obj = new_convert_curve_object(gp_col, gpencil_object.name + "_curve")
    for layer in gp.layers:
        if not flatten_layers:                    
            obj = new_convert_curve_object(gp_col, f"{gpencil_object.name}_curve_layer_{layer.info}")
        for frame in layer.frames:
            for stroke in frame.strokes:
                spline = obj.data.splines.new(type="POLY")                   
                spline.points.add(len(stroke.points) - 1) # Spline starts with 1 point
                for i, point in enumerate(stroke.points):
                    spline.points[i].co = [v for v in point.co] + [1]

Here we want either to create only one object and write splines to it if we flatten the GP object's layers, or create one curve object per layer and accordingly populate the splines.

We reference the gpencil object (Suzanne for testing purposes):

gpencil_object = bpy.data.objects["Suzanne"]

Then we call it with layer flattening :

convert_gpencil_to_curve(gpencil_object, flatten_layers=True)

A new curve is created :

enter image description here

And all the layers have been flattened :

enter image description here

Calling the method with flatten_layers=False yields :

convert_gpencil_to_curve(gpencil_object, flatten_layers=False)

Two curves have been added (one for each layer) :

enter image description here

And the objects are separated :

enter image description here

  1. Convert to mesh

It works pretty much the same way. Define our handy object creator :

def new_convert_mesh_object(collection, name):    
    mesh = bpy.data.meshes.new(name=name)
    convert_object = bpy.data.objects.new(name=name, object_data=mesh)
    collection.objects.link(convert_object)
    return convert_object

Define the converting process :

def convert_gpencil_to_mesh(gpencil_object, flatten_layers=True):
    gp_col = gpencil_object.users_collection[0]
    gp = gpencil_object.data
    if flatten_layers:
        obj = new_convert_mesh_object(gp_col, gpencil_object.name + "_mesh")
        vertices = []
        edges = []
    for layer in gp.layers:
        if not flatten_layers:                    
            obj = new_convert_mesh_object(gp_col, f"{gpencil_object.name}_mesh_layer_{layer.info}")
            vertices = []
            edges = []
        for frame in layer.frames:
            for stroke in frame.strokes:
                for i, point in enumerate(stroke.points):
                    vertices.append(point.co)
                    if i > 0:
                        edges.append((len(vertices) - 1,len(vertices) - 2))
        if not flatten_layers:
            obj.data.from_pydata(vertices, edges, ())
    if flatten_layers:
            obj.data.from_pydata(vertices, edges, ())


convert_gpencil_to_mesh(gpencil_object, flatten_layers=True)
convert_gpencil_to_mesh(gpencil_object, flatten_layers=False)

yields pretty much the same result as for the curve.

enter image description here

enter image description here

$\endgroup$

You must log in to answer this question.

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