Animating a punctured torus

On the torus wiki page there is an animation of a punctured torus. How would one go about creating same in blender?

In case someone wants to elaborate on @MutantBob shapekey sript answer here are the Parametric equations of a torus.

class Torus():
def __init__(self, R, r):
self.R = R
self.r = r

def point(self, theta, zi):
'''parametric_equations'''
r = self.r
R = self.R
def x(theta, zi):
return (R + r * cos(zi)) * cos(theta)
def y(theta, zi):
return (R + r * cos(zi)) * sin(theta)
def z(theta, zi):
return r * sin(zi)

return x(zi, theta), y(zi, theta), z(zi, theta)


Example usage

torus = Torus(2, 1)
SEGS = 10
bm = bmesh.new()
# outer circle
matrix = []
angles = [radians(a * 360.0 / SEGS) for a in range(SEGS)]
for theta in angles:
# inner circles.
ring = [bm.verts.new(torus.point(zi, theta)) for zi in angles]
matrix.append(ring)

for i in range(SEGS):
I = (i + 1) % SEGS
for j in range(SEGS):
J = (j + 1) % SEGS
bm.faces.new([matrix[i][j],
matrix[i][J],
matrix[I][J],
matrix[I][j]])

mesh = bpy.context.object.data
bm.to_mesh(mesh)
mesh.update()


And we can use the matrix transpose to get the Final shapekey where the loops and rings are swapped.

sk = obj.shape_key_add(name="Start")
for i in range(SEGS):
for j in range(SEGS):
sk.data[matrix[i][j].index].co = matrix[j][i].co


Changing coords on end shapekey and some subsurf.

sk.data[matrix[i][j].index].co = matrix[(j+6) % SEGS][i].co.zxy


http://demonstrations.wolfram.com/TurningAPuncturedTorusInsideOut/

http://demonstrations.wolfram.com/TurningAPuncturedTorusInsideOutVariation/

http://mathworld.wolfram.com/VillarceauCircles.html

• – lemon Jul 3 '16 at 10:52
• You are very close to the result. Compared to what I did first to last frame, the point is you need an intermediate transition. This intermediate transition is here to stretch the puncture around the other parts of the mesh. As shape keys are linear, you'll need to use (at least) two SK. – lemon Jul 5 '16 at 17:51
• – lemon Jul 5 '16 at 17:57
• Yep,, my gut feel is the Villarceau circles have something to do with it. Also if the angles to create torus above were in 60 degree increments starting from 30, then would have a middle strip more like the wiki anim. The add primitive always has verts on the "equator". – batFINGER Jul 5 '16 at 20:14
• The equation of a torus I happened to already know. What I haven't fully imagined yet is the equation describing the deformation of the torus over time as it mutates from one shape to the other. Lemon's answer is a good starting point, it just is missing the circular form of the puncture perimeter at time=0.5 . – Mutant Bob Jul 5 '16 at 20:51

This question is a nice example for showing how to morph one mesh into another. Lets take a general approach that will work on multiple shapes and lets make the morphing nice (where non-linear morphing is needed).

First we will need the start and end meshes. In this example, the diameter of the hole of the start torus is the same like thickness of the end torus. Because the diameter of subdivision surfaces is different than the control cage, I chose to work with real geometry. This method shouldn't depend on vertex count anyway, so let's have some nicely detailed toruses (subdiv surfaces don't produce perfectly circular shapes).

I obtained these by spinning touching circles around 3d cursor locations:

There are 32 rings with 32 vertices each on both toruses. Now we could map the start vertices to the end vertices (by fiddling with their id, or based on vertex paint, or UV unwrap data,..) and make appropriate shape-key (by scripting), but this is not the intention, because such morphing will result in linear ugly motion, which could cause self-intersecting and other non-desired results.

We will convert edges of the meridians of the start torus into bone chains, and rotate them into the final positions as equators of the end torus. Then we will skin a mesh to this armature and render it.

As the bones will come from Skin modifier, we need to mark the root vertices and also split the toruses for chain ends first (seen as 2 hard lines in shading - because of duplicite vertices, you can split vertices with V when the ring is selected). Unwanted edges were removed, to leave only the chains:

Let's put Skin modifiers on, adjust the thickness and set the root vertices. Setting the roots location and where the chains end will shape the look of animation (roots highlighted and ends visible by shading):

Both toruses are still separate objects, so we'll get 2 armatures. Delete unwanted bones, you'll end up with 2 bone chains per ring:

Similarly we can map any corresponding topologies onto each other to morph them.

The corresponding start and end bones have to have a corresponding name inside those armatures. To fix it, here is a script to name the root bones based on shared location and direction and the chain bones based on chain distance (parent count) from roots. The bones also get a start/end prefix, because we will merge them into single armature later (to have a clean scene). Name the start armature Start, and the end one End:

import bpy

start = bpy.data.objects["Start"]
end = bpy.data.objects["End"]

i = 0
for pboneA in start.pose.bones:
for pboneB in end.pose.bones:

pboneA.name = "Start_Chain_" + str(i) + "_Bone_0"
pboneB.name = "End_Chain_" + str(i) + "_Bone_0"
i += 1
break

for object in [start, end]:
for pbone in object.pose.bones:
parents = pbone.parent_recursive
if len(parents):
pbone.name = parents[-1].name[:-1] + str(len(parents))

for pbone in start.data.bones:
pbone.use_inherit_rotation = False
pbone.use_inherit_scale = False

for pbone in end.data.bones:
pbone.use_deform = False


You can now join the armatures together Ctrl+J. The deformation on end bones was also deactivated by the script.

The next script will create a custom property on the armature to drive the blending:

import bpy

def add_driver(expression, item, property, property_index = -1):

driver.driver.type = 'SCRIPTED'
driver.driver.expression = expression
var = driver.driver.variables.get("blend")
var = driver.driver.variables.new()
var.name = "blend"
var.targets[0].id = bpy.context.active_object
var.type = 'SINGLE_PROP'
var.targets[0].data_path = 'data["blend"]'

bpy.context.active_object.data["blend"] = 0.0
bpy.context.active_object.data["_RNA_UI"] = {"blend": {"min":0.0,
"max":1.0,
"soft_min":0.0,
"soft_max":1.0}}

pbones = bpy.context.active_object.pose.bones

for pboneA in [pbone for pbone in pbones if "Start" in pbone.name]:

pboneB = pbones[pboneA.name.replace("Start","End")]

constraint = pboneA.constraints.new('COPY_ROTATION')
constraint.target = bpy.context.active_object
constraint.subtarget = pboneB.name

add_driver("(1-blend) + blend * " + str(l_B/l_A), pboneA, "scale", 1)


This is the result with skinned mesh:

Lastly we will unwrap the torus mesh into a single rectangle UV with the point where the hole will be in the middle. The intention is to animate a material mask to create the transparent hole. In the picture are marked seams and the UV map - I used UV Squares addon to have such nice regular UV map:

In the material lets use a procedural gradient texture set to Quadratic Sphere, offset into the center of UV space, with an animated threshold (the Color Ramp node is animated):

The only problem is that the texture does not warp with the hole. Good news is the procedural spherical gradient to drive the texture warping effect is already in place:

There is the displacement mask on the left, UV coordinates in the middle with progressively blended displacement from 0 to 100% influence and the warped texture on the right.

This is the final result:

And a finished .blend to peak into:

• Of the ones posted so far, this is my favorite. It still doesn't capture the geometry of the original animation (notice that the border of the hole appears to be a circle at the halfway point in the original), but it is better than the ones that look like a plus in the early stages. – Mutant Bob Jul 10 '16 at 19:59
• @MutantBob yeah I am aware of the circular opening, you are right. Unfortunately to do that it requires more complicated rigging - stretchy IK chains. I just covered basics like generating control armature, controlling the bones with code and dealing with surface stretching and it ended up longer than I wanted:). A different shape might not have this problem with the hole mid-way though. The original animation looks like it came from some math model. – Jaroslav Jerryno Novotny Jul 10 '16 at 20:13
• @Jerryno Here is a looping version of your gif. Took me a long time to get it under the 2mb limit. Hope you like it. Awesome answer! The result looks great. – David Jul 16 '16 at 19:27
• @David with what tool did you managed to do that? I tried 2 programs and several online converters and it was always >2MB looped..I'll put the looped gif into the answer, thanks! – Jaroslav Jerryno Novotny Jul 17 '16 at 9:56
• @Jerryno I tried everything till one worked :) combination of ScreenToGif, then fireworks compressed it. – David Jul 17 '16 at 15:15

Two steps answer here : initial post with more bones (so longer to achieve) and the new one (with explanations) less bones but also less precision in the texture (should be fixed / ameliorated).

The two parts are on the same principle : using bones and animating.

Not perfect, really... this is just a rig. So no math and no simple deform modifiers... only bones placement.

Here is the blend file :

I hope someone will give a more elegant solution.

The blend file below contains 7 layers. Each correspond to a step of the job, with textual indications.

• Making the torus

Use a plane divided, with two simple deform modifiers. This approach is not really needed, but this was the 'origin' of this question. Using this modifier also allows to have a flat starting surface for the UV map. The simple deform (bending) configuration is -360 then 360, with driving empties respectively rotated to (90, 0 ,0) and (90, 0, 90).

The plane is subdivided (but less than in the first answer above). This subdivisions are chosen so that we have summits along the axis. Also, the amount of subdivisions is the same in X and Y : the reversion principle (longitude becomes latitude) drives to this choice (not mandatory but easier).

• The initial (blue) and target (green) positions

The torus need to be reverted from the puncture. In this process, we have to keep in mind that longitude of the first will become latitude of the second.

• Symmetries

As we can see below, the shapes are symmetrical in X and Z. It is not in Y because of the puncture itself (symbolized by a sphere empty on the picture). Here the two shapes are shifted in Y, but this is just a choice and not mandatory.

Finally the symmetry allows us to use a mirror modifier, with X and Z. This is important because we'll later have to manipulate "many bones" (so working by quarter is better).

• Guidelines

Finally, we have this two shapes : in blue the shape that will be animated and in green its target. We'll see in the next steps that the green mesh will help to locate the vertices final positions.

• Puncture

Add the puncture. To do that, triangulate the face Ctrl T, subdivide the 3 edges that converge two the puncture center, drive the newly created vertices near this center, and remove the center itself. We notice that had an impact on the UV map (on the proportionality of the surfaces) : that will be to correct later.

• Creating the armature

We want a bone per vertex. To do it quickly, add an armature, set our mesh as parent for this armature, go to the duplication options of the mesh : choose 'verts', and 'rotation'. Now go to edit mode of the initial armature and rotate it to Y in order to have all dupli bones along the vertices normals (not mandatory, but easier to see the bones after that).

Now, go to the object menu of the mesh, then 'apply' and 'make duplicates real' (or Shift Ctrl A).

We now have all the bones separated but they are not single user (they share the same data). So (still having all the bones selected) go to 'object' again, 'make single user' then 'object and data'.

Now you can join all the (ex)dupli bones together in a single armature : Ctrl J.

• Parenting the mesh to the armature

Select the mesh and select the armature, use Ctrl P then the envelope options. The envelope will 'guaranty' here that no vertices misses a weighting (if we were using automatic weights we may miss some vertices). The only point to fix manually : the 3 puncture vertices that are to close to be weighted individually.

• Animation

We'll animate from initial position to target and initial again. I have chosen here to do it at frames 1, 120 and 240. There will be intermediate steps at frames 60 and 180.

Enter to the pose mode of the armature, select all the bones and add this as initial position in the pose library. Then insert a keyframe for the frame 1 in the timeline (I then 'location').

Now we want to go to the target position (go in frame 120 of the timeline).

To help for that, activate the snap options and set it to 'vertex'.

Doing that we can use the green target torus as target for the snapping (select the bone, move the mouse to the target location, then G and click) :

We have to do that for each bone... "just" follow the longitude vertices of the mesh and grab them progressively to the corresponding latitudes of the target. At the end, you will obtain the torus reversion final position (with inverted normals as expected) :

Add this as 'final' pose in the pose library.

But from frame 1 to 120, the bones are going straight to their final positions :

So we have to drive the puncture in the outer of the mesh.

Note the names of the 3 concerned bones so that you can identify them in frame 60. Go to frame 60. Select the top left one and drive it along Z. Select the bottom right one and drive it along X. And for the last one, drive it along X/Z. Insert the key frames for this frame 60.

The 3 bones positions at frame 60 :

Add these positions to a new pose in the library (named 'intermediate').

Last steps : Go to frame 180, get the intermediate pose in the library and insert key frames. Then go to frame 240, and do the same with the initial pose.

Here is the blend file : it contains 7 layers with explanations of each step.

Note

The first version looks better, except for the puncture. I wont have time to redo it, but I think the 7x7 vertices of the first version are needed (the second version is 5x5), especially for the textures.

Also, the intermediate step of the second version is too enhance a bit, concerning the other vertices around the puncture (I wont have time to do it).

Probably, it is possible to avoid the mirror modifier at some step... and to mirror the bones (X and Z ?)... I don't know.

• please add more detail to your answer. Looking at at blend file does not imply that the users are going to understand what you did and what the logic is for whatever path you took to get there. – cegaton Jul 2 '16 at 19:53
• Thought I agree with you that the answer doesn't provide the explanation on how to do it I don't think that it deserves a downvote since @lemon 's effort here is worth appreciating. – Paul Gonet Jul 2 '16 at 20:11
• @cegaton I will provide some explanatios, and probably some enhancements, too. – lemon Jul 3 '16 at 5:44
• @batFINGER, I am currently working on a new version (with less bones), but the same principle. I felt bones were more easy to manipulate than shape keys suggested by Mutant Bob. A revision of the answer soon... – lemon Jul 3 '16 at 8:16
• @batFINGER yes great ! thanks... I did not think about that – lemon Jul 3 '16 at 10:48

Python approach

• Definition of slices and rings

I use ring to indicate a cut along the small circle, and slices to indicate a cut along the large circle.

Slices have a angle for 0 inner (in blue) to 180° outer (in red) : left part of the picture below. Rings have a angle for 0 right (in blue) to 180° left counterclockwise (in red) : right part of the picture below.

• Simplest torus

If we look at the simplest torus quarter, a quarter of torus with 3 vertices in each direction, we can see :

• 180° slice moves to its "opposite", turning 180° around z (top flat cone of the picture below)
• 90° slice moves to the center, turning 90° (left cone, not flat as stretched to x)
• 0° slice translate to its "opposite" (turning 0° ; bottom flat cone)

So we have here the principle of the movement we need.

• Animation (or almost)

Here is the result : this is not a 1 torus animation, as I do not know how to do it from there (not skilled enough in Blender and Python), but we can see each step of the animation (so surely, some will know how to do that).

• Python code

Note : I know how to code, but I am very new in Python and Blender API. So, surely many things are to be enhanced here.

The code is commented, so just some precision about the key point : what is driving the vertices movements.

It is all in the function called "mySlerp".

This function is in fact a mix between a slerp and a translation. Each vertex knows its starting point (normalized) and end point (normalized also).

The most inner slices are mainly translating from start to end. And outer slices are mainly slerped from start to end.

So these two interpolated vectors are calculated and mixed depending of their slice angle value.

The movement is not really perfect : it gets bad if two many vertices, so I used few and compensated by a subsurface modifier.

Note : I did not used Vector.slerp as it has some unwanted limitations when angles are opposite.

import bmesh
import bpy

from math import *
from mathutils import Vector
from mathutils import Euler

#Makes faces
def Faces( bm, r1, r2, segs, open ):
for i in range(1, segs) :
bm.faces.new( [r1[i-1], r1[i], r2[i], r2[i-1]] )
if not open :
bm.faces.new( [r1[segs-1], r1[0], r2[0], r2[segs-1]] )

#Makes faces from a matrix of rings
def FacesFromRings( bm, mat, segs, open, puncture = False ) :
prevRing = mat[0]
for i in range(1, segs) :
ring = mat[i]
Faces( bm, prevRing, ring, segs, open )
prevRing = ring
if not open :
if puncture:
Faces( bm, prevRing, mat[0], segs-1, open )
else:
Faces( bm, prevRing, mat[0], segs, open )

#Makes faces from a matrix of slices
def FacesFromSlices( bm, mat, segs, open ) :
prevSlice = mat[0][1]
for i in range(1, segs) :
slice = mat[i][1]
Faces( bm, prevSlice, slice, segs, open )
prevSlice = slice
if not open :
Faces( bm, prevSlice, mat[0][1], segs, open )

def MakeScale( v, t, dim ):
if v == 0 :
return 1
else:
return (v - (t * (v - dim))) / v

def TFactor( t ):
return -2.0 *(abs(t - 0.5) - 0.5)

def Norm( v, start, end ):
if start == end:
return 1
else:
return (v - start) / (end - start)

class Vertex():
def __init__( self, vector, ringAngle, sliceAngle ):
#A vertex knows :
self.vector = vector #The initial point location
self.vectorNorm = sqrt(vector.dot(vector)) #vertex size from the origin
self.vectorN = Vector( vector )
self.vectorN.normalize() #Normilzed vector for the location

self.ringAngle = ringAngle #Position of the point along the "torus rings"
self.sliceAngle = sliceAngle #Position of the point along the "torus slices"
self.t100 = {}
self.t100N = {}

def SetT100( self, t100 ):
#Attaches data at the target point
self.t100 = t100
self.t100Norm = sqrt(t100.vector.dot(t100.vector))
self.t100N = Vector( t100.vector )
self.t100N.normalize()

class Torus():
def __init__(self, R, r, segs, turns ):
self.R = R
self.r = r
self.Segs = segs
self.Turns = turns
self.Rings = []
self.Open = turns != 360.0

def point(self, ringAngle, sliceAngle):
'''parametric_equations''' #batFINGER code for parametric torus (few renaming)
r = self.r
R = self.R
def x(ringAngle, sliceAngle):
return r * sin(sliceAngle)
def y(ringAngle, sliceAngle):
return (R + r * cos(sliceAngle)) * cos(ringAngle)
def z(ringAngle, sliceAngle):
return (R + r * cos(sliceAngle)) * sin(ringAngle)

return x(ringAngle, sliceAngle), y(ringAngle, sliceAngle),  z(ringAngle, sliceAngle)

def Initialize( self ):
#Torus initialisation

#Calculate the iteration amounts
effectiveSegs = self.Segs
if self.Open:
effectiveSegs = self.Segs - 1

angles = [radians(a * turns / effectiveSegs) for a in range(self.Segs)]
reversedAngles = [a for a in reversed(angles)]

#Makes the initial geometry
self.Rings = []
for ringAngle in angles:
ring = [Vertex( Vector( self.point(ringAngle, sliceAngle) ), ringAngle, rad180 - sliceAngle ) for sliceAngle in angles]
self.Rings.append(ring)

#Attach final geometry
for ring in self.Rings:
for vertex in ring:
vertex.SetT100( self.MakeSliceVertexAtT100( vertex ) )

def RingMatrixToBMesh( self, bm ):
#Creates the geometry for the torus matrix
self.RingMatrixToBMeshFromExt( bm, self.Rings )

def RingMatrixToBMeshFromExt( self, bm, matrix, puncture = False ):
#Creates the geometry for the given matrix
mat = []
for ring in matrix:
matPart = [bm.verts.new( [v.vector.x, v.vector.y, v.vector.z] ) for v in ring]
mat.append( matPart )

FacesFromRings( bm, mat, self.Segs, self.Open, True )

def FindVertexIndex( self, mat, ringAngle, sliceAngle ):
partIndex = 0
for part in mat:
vIndex = 0
for v in part:
if v.ringAngle == ringAngle and v.sliceAngle == sliceAngle:
return partIndex, vIndex
vIndex += 1
partIndex +=1
return -1, -1

def ToT100( self ):
#Calculation for t = 100%
result = []
for ring in self.Rings:
resultPart = [self.MakeSliceVertexAtT100( v ) for v in ring]
result.append( resultPart )

return result

def MakeSliceVertexAtT100( self, vertex ) :
#Slice circles are rotate following the slice angle
#and scaled following the ratio given by R and r

R = self.R
r = self.r

#Initial values
x = vertex.vector.x
y = vertex.vector.y
z = vertex.vector.z

scale1 = MakeScale( x, 1, r )
scale2 = MakeScale( sqrt( (y * y) + (z * z) ), 1, r )

#Scaled values
sx = x * scale1
sy = (y * scale2) - R
sz = z * scale2

angle = vertex.sliceAngle
cosAZ = cos(angle)
sinAZ = sin(angle)

#Rotated values
rx = - (sy * sinAZ)
ry = + (sy * cosAZ) #+ smallDim
rz = sz

return Vertex( Vector( [rx, ry, rz] ), 0, 0 )

def ToT( self, t, offset ):

result = []
for ring in self.Rings:
resultPart = [self.MakeSliceVertexAtT( v, t, offset ) for v in ring]
result.append( resultPart )

return result

def mySlerp( self, start, end, percent, sliceAngle, ringAngle, t ):
#'fake' slerp : a mix between simple interpolation and real slerp depending on the ringAngle
# - the lower ring angles are interpolated
# - the bigger are slerped

slerp1 = (1 - t) * start + t * end

dot = start.dot(end)
theta = acos(dot) * percent

relativeVec = (end - (dot * start))
relativeVec.normalize()

slerp2 = (cos(theta) * start) + (sin(theta) * relativeVec)

slerp = ( (rad180 - ringAngle) * slerp1 + (ringAngle) * slerp2 ) / rad180

return slerp

def MakeSliceVertexAtT( self, vertex, t, offset ) :

n1 = vertex.vectorNorm
n2 = vertex.t100Norm
fact = n1 + (t * (n2 - n1)) #Scale of the vertex at this time

#some tests about time (not used)
#        tFact = TFactor( t )
#        tFact = (tFact * tFact) * (vertex.ringAngle * vertex.sliceAngle) / 8.0
tFact = 0

slerp = self.mySlerp( vertex.vectorN, vertex.t100N, t, vertex.sliceAngle, vertex.ringAngle, t )
slerp *= (fact + tFact)

return Vertex( Vector( [slerp.x + offset[0], slerp.y + offset[1], slerp.z + offset[2]] ), 0, 0 )

bigDim = 2.0
smallDim = 1.0
SEGS = 5
turns = 180.0

torus = Torus(bigDim, smallDim, SEGS, turns)

torus.Initialize()

bm = bmesh.new()

#t100 = torus.ToT100()
#torus.RingMatrixToBMeshFromExt( bm, t100 )

torus.RingMatrixToBMesh( bm )

#for ring in torus.Rings:
#    for v in ring:
#        vect = v.vector #.t100N
#        print( str(vect.x) + ";" + str(vect.y) + ";" + str(vect.z) + ";" + str(v.sliceAngle) + ";" + str(v.ringAngle) )

t = 0.1
for t in range(1, 11):
offset = [0, (t+1)*6, 0]
ringsAtT = torus.ToT( t / 10.0, offset )
torus.RingMatrixToBMeshFromExt( bm, ringsAtT )

mesh = bpy.context.object.data
bm.to_mesh(mesh)
mesh.update()


The blend file :

• The mesh animated, thanks to Jerryno

My recommendation would be shape keys. I have source code that animates a "waving fin" at http://web.purplefrog.com/~thoth/blender/python-cookbook/shape-key-fin.html

Here's an excerpt:

import bpy
import bmesh
import math

def vert1For(u, t):
return [ 0, u, 0]

def vert2For(u, t, dTheta, z1):
theta1 = dTheta * math.sin(t+u*0.4)
return [ math.sin(theta1)*z1, u, math.cos(theta1)*z1]

def vert3For(u, t, dTheta, z2, thetaLag):
theta2 = dTheta * math.sin(t+u*0.4-thetaLag)
return [ math.sin(theta2)*z2, u, math.cos(theta2)*z2]

def makeMesh(name, nSegs, z1, z2, dTheta, thetaLag):
mesh = bpy.data.meshes.new(name)
verts = []
faces = []
for u in range(0,nSegs+1):
v4=len(verts)
verts.append( vert1For(u,0) )
verts.append( vert2For(u,0,dTheta, z1) )
verts.append( vert3For(u,0,dTheta, z2, thetaLag) )
if (u>0):
v1 = v4-3
v2 = v1+1
v3 = v1+2
v5 = v1+4
v6 = v1+5
faces.append( [ v1, v4, v5, v2] )
faces.append( [ v2, v5, v6, v3] )
mesh.from_pydata(verts, [], faces)
mesh.validate(True)
mesh.show_normal_face = True

return mesh

def addShapeKey(obj, i, nKeys, z1, z2, dTheta, thetaLag):
kn = "phase %d"%i
#    sk.value = 0
#    sk.frame = i/nKeys
#    sk.frame = i*i/(nKeys*nKeys) # crazy version
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()
sl = bm.verts.layers.shape.get(kn)

for u in range( math.floor(len(bm.verts) / 3)):
t = math.pi*2*i/nKeys
bm.verts[u*3][sl] = vert1For(u, t)
bm.verts[u*3+1][sl] = vert2For(u, t, dTheta, z1)
bm.verts[u*3+2][sl] = vert3For(u, t, dTheta, z2, thetaLag)

bm.to_mesh(obj.data)

dTheta = 0.8
thetaLag = 0.2
z1 = 2
z2 = 3
mesh = makeMesh("fin", 40, z1, z2, dTheta, thetaLag)

obj = bpy.data.objects.new("fin", mesh)