Curve scripting

Curve Scripting #

In this tutorial, we will create a family of fibers of the Hopf fibration, lying over a great circle in $S^2$:

Some fibers of the Hopf fibration

We will use Blender Python, and assume some familiarity with (ordinary) Python. Working through this tutorial takes about 20 minutes, though understanding it might take longer. There is explanation of how the code works and some other sample code at the end.

In brief #

Create a new scene, delete the default cube, and add three new collections, called something like “Black curves”, “Red curves”, and “Blue curves”.

A blank scene with some collections added

Then, go to the Scripting tab, on the far right. In the Text menu, select New. Then paste the following code in the new text:

from bpy import context, ops
from math import cos, sin
from mathutils import Vector
from random import TWOPI
import cmath

def stereo(z,w):
	"Return the stereographic projection of a point (z,w) in C^2"
	return Vector((z.real / (1-w.imag), z.imag / (1-w.imag), w.real / (1-w.imag)))

def add_curve(startpt):
	ops.curve.primitive_bezier_circle_add(enter_editmode=True)
	curve = context.active_object
	ops.curve.subdivide(number_cuts=36)
	bez_points = curve.data.splines[0].bezier_points
	sz = len(bez_points)
	i_to_theta = TWOPI / sz
	j = complex(0,1)
	for i in range(0, sz, 1):
		newz = startpt[0]*cmath.exp(i * i_to_theta*j)
		neww = startpt[1]*cmath.exp(i * i_to_theta*j)
		bez_points[i].co = stereo(newz,neww)
	ops.object.mode_set(mode='OBJECT')
	return curve

phi = TWOPI/4+.01
N=33
curves = list()
for k in range(N-3):
	theta = TWOPI*k/N
	for i in range(k,k+1):
		startpt = (complex(sin(theta)*sin(phi)*cos(TWOPI*i/(4*N)),cos(theta)*sin(phi)*cos(TWOPI*i/(4*N))),complex(sin(phi)*sin(TWOPI*i/(4*N)),cos(phi)))
		(x,y,z) = stereo(startpt[0],startpt[1])        
		curve = add_curve(startpt)
		curves.append(curve)

Pasting a script

Select the “Black curves” collection in the layers panel, and then click the “play” icon above the text you created.

Running a script

The result should be thirty new circles. If you don’t see a bunch of new circles in the viewport, look for errors in the console or the status bar:

Running a script

(Unfortunately, some of these messages disappear as soon as you click the mouse or type.)

Errors, if they happened, are likely because

  • Something went wrong with the copy and paste (e.g., indentation got lost), or
  • The Blender Python API has changed between the version I tested this tutorial on and the version you’re using, in a way that breaks the behavior of curves. (Blender is changing its curve system, so this is quite possible, unfortunately.)

You can download a copy of the Blender Python file I’m using here; if the problem might be a pasting one, download it, save it, and open it under Text:Open.

Assuming things went as expected, go back to the Layout tab. The picture at the top is drawn looking down the z-axis, so click on the z in the rotation widget to get that view, pan the view so the Hopf curves are centered, and select View:Align View:Align Active Camera to View.

Aligning the View

Select the camera and, in the camera tab, change the focal length so that all the curves are in the frame. (You could switch to an orthographic camera here if you think it would look better, but it usually doesn’t.)

Aligning the View

Select the curve you want to be red and the curve you want to be blue and move them to the corresponding collections. Then give the curves some thickness. Select the first one and in the curve tab, give it a bevel depth of 0.025, say.

Aligning the View

The select all the curves, starting from the first one (by shift-clicking on the last curve), right-click (or equivalent) on the bevel depth, and select Copy to Selected.

Copying a Property

Select the Black Curves collection and add Grease Pencil:Collection Line Art. Do the same for the Blue Curves and Red Curves. Maybe also move the resulting LineArt objects to the main collection or their own collection and give them descriptive names.

Adding Line Art

Renaming Line Art

Change the materials for the blue curves and red curves line art to blue and red colors, export, and clean up in a vector graphics program if desired.

Coloring the Line Art

Decoding the Python #

Most of the code is, I think, self-explanatory. The part that is new to Blender is mostly at the beginning and end of the add_curve function:

def add_curve(startpt):
	ops.curve.primitive_bezier_circle_add(enter_editmode=True)
	curve = context.active_object
	ops.curve.subdivide(number_cuts=36)
	bez_points = curve.data.splines[0].bezier_points
	...
	for i in range(0, sz, 1):
		...
		bez_points[i].co = stereo(newz,neww)
	ops.object.mode_set(mode='OBJECT')
	return curve

The first line in the function adds a new default Bézier circle, made of 4 control points. One can also add a non-closed curve, with primitive_bezier_curve_add. An easy way to find what commands of this type are available is by typing

bpy.ops.curves. 

in the console (not the text editor) and hitting the tab key. (The analogue for meshes is bpy.ops.mesh.)

The curve is added to the active collection. The argument enter_editmode=True means that we are switched to the curve’s Edit Mode when it is added, in anticipation of subdividing it. The second line assigns the curve just added to a variable. The third subdivides it, adding 36 points to each existing segment (so the result has 4x36+4 = 148 vertices).

The fourth line gets a list of the control points for the curve. A curve can have multiple splines (connected components). This curve has one, so all its data is in data.splines[0]. It’s probably worth looking at the list of attributes of a single control point. First, assign a curve to a variable in the console, by selecting a curve in the viewport and then typing

curve = bpy.context.active_object

Then type:

curve.data.splines[0].bezier_points[0]. 

and hitting tab. Some useful properties are co which has the coordinates, a Vector instance, and handle_left (another Vector) and handle_left_type, which is currently set to AUTO. (You can see what options are available by setting the handle type the usual way and then going back to inspect it in the console. For example, one could set the handle to ALIGNED or VECTOR.)

Tab completion

Vectors behave how you would expect; you create them with mathutils.Vector((x,y,z)) where x, y, and z are floats.

The last line before the return statement returns us to Object Mode instead of Edit Mode, ready to add the next curve (or whatever).

The code also keeps track of a list of the curves, in the curves variable. This is not needed for the application above, but convenient if one wanted to further modify the curves later. For example, one could skip the step where one gives them thickness by hand by adding:

for curve in curves:
	curve.data.bevel_depth = .025

Some more sample code #

Editing an existing curve #

Perhaps you want to adjust a curve that has already been created. If you created it in Python and assigned it to a variable, that’s easy; see above. If it was created some other way, you can get it by using bpy.context.active_object. For example, here is code that jitters the points on a curve by random vectors and then translates the whole curve by the vector (1,2,3):

import bpy, random
from mathutils import Vector
curve = bpy.context.active_object
pts = curve.data.splines[0].bezier_points
for p in pts:
	perturb = Vector((random.uniform(-.1,.1) for i in range(3)))
	p.co = p.co + perturb
curve.location = curve.location + Vector((1,2,3))

A parametric plot function #

As another example, which might be useful sometimes, here is code to plot a parametric curve.

from bpy import context, ops

def parametricPlot(f, tmin=0, tmax=1, num_pts=20, closed=False):
	"Return a parametric plot of f. f should take a float as an input and give a Vector as an output."
	if closed:
    	ops.curve.primitive_bezier_circle_add(enter_editmode=True)        
    	ops.curve.subdivide(number_cuts=num_pts/4)
	else:
    	ops.curve.primitive_bezier_curve_add(enter_editmode=True)
    	ops.curve.subdivide(number_cuts=num_pts)

    curve = context.active_object
	bez_points = curve.data.splines[0].bezier_points
	sz = len(bez_points)
	for i in range(0, sz):
    	bez_points[i].co = f(1.0*i/sz*(tmax-tmin)+tmin)
    	bez_points[i].handle_left_type = 'AUTO'
    	bez_points[i].handle_right_type = 'AUTO'

    ops.object.mode_set(mode='OBJECT')
	return curve

For example, to plot the curve (t,t2,t3) on the interval -1≤t≤1 with this:

from mathutils import Vector
def f(t):
	return Vector((t,t**2,t**3))
parametricPlot(f, -1, 1, 40, False)

A parametric plot

Further comments and resources #

  • Whether a curve is closed or not is controlled by the boolean

      data.splines[0].use_cyclic_u
    
  • Blender’s documentation for Blender Python is not great, but is here.

  • There is some more discussion of creating and working with collections in Python in the Mesh Scripting tutorial.

  • There is an excellent, but perhaps somewhat out of date, tutorial on curve scripting here. The basic framework for the code on this page comes from that tutorial.