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.

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.

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).

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
rad0 = radians( 0.0 )
rad1 = radians( 1.0 )
rad45 = radians( 45.0 )
rad90 = radians( 90.0 )
rad180 = radians( 180.0 )
#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
