# How can I transform my coffee cup into a torus as seen on wikipidea?

I'm animating the coffee mug torus homeomorphism.

I want my animation to match this gif as closely as possible. The gif above, along with the POV-ray code used to create it, can be found here:https://en.wikipedia.org/wiki/File:Mug_and_Torus_morph.gif.

I'm working with a relatively low poly mesh for now and here is what I have created:

The first part of the Wikipedia animation where the base of the cup rises is easy to make, so I'm excluding that part of the animation for this post. I just need help on the main transformation part.

To create this, I wrote a python script that sends the points of the cup to the closest point on the torus. The pseudocode of the python code is roughly as follows:

# for each vertex of the mug,
# keyframe the vertex coordinate at initial frame
# find the angle of the vertex relative to the center of the torus
# find the central point of the torus of which this angle belongs to (this new central point is a point along the major radius ring)
# solve for where the line through the vertex and central point intersects the torus (there are two points of intersection)
# set vertex coordinate to value of intersection that's closest to the vertex
# keyframe vertex coordinate at final frame


Here is the actual code:

import bpy
from math import *
from mathutils import *

mug = bpy.context.scene.objects["MorphingMug"]
verts = mug.data.vertices

#initial handle torus and target torus have a major radius of:
bigR = .5
r = .25
#center of both initial handle torus and target torus has x value of:
originX = .8

#for each vertex of the mug
for v in verts:
#keyframe vertex at framei
v.keyframe_insert(data_path = "co", frame = 1)

#get x, y, and z values of vertex
x = v.co.x
y = v.co.y
z = v.co.z

#difference vector between vertex and (center of torus = [originX, 0, 0])
d = Vector([x - originX, y, z])

#find the angle in the xz-plane of this difference vector
if v.co == Vector([originX, 0, 0]):
theta = pi
elif d.x == 0 and d.z > 0:
theta = pi/2
elif d.x == 0 and d.z < 0:
theta = -pi/2
elif d.x < 0:
theta = pi + atan(d.z/d.x)
else:
theta = atan(d.z/d.x)

#find the central point of the target torus of which this angle belongs to
c_1 = bigR * cos(theta) + originX
c_3 = bigR * sin(theta)

#line parameterization values of where the line through vertex and central point intersect target torus
t1 = -r/sqrt(c_1**2 - 2*c_1*x + c_3**2 - 2*c_3*z + x**2 + y**2 + z**2) + 1
t2 = r/sqrt(c_1**2 - 2*c_1*x + c_3**2 - 2*c_3*z + x**2 + y**2 + z**2) + 1

#first value of intersection
v1 = x + t1*(c_1 - x)
v2 = y + t1*(c_2 - y)
v3 = z + t1*(c_3 - z)

vec1 = [v1, v2, v3]

#second value of intersection
v1 = x + t2*(c_1 - x)
v2 = y + t2*(-y)
v3 = z + t2*(c_3 - z)

vec2 = [v1, v2, v3]

#difference of these intersection values and vertex
diff1 = Vector(vec1) - v.co
diff2 = Vector(vec2) - v.co

#choose the value that's closest to vertex
if diff1.magnitude > diff2.magnitude:
vec = vec2
else:
vec = vec1

#set vertex equal to closest intersection
v.co = vec

#keyframe at final position
v.keyframe_insert(data_path = "co", frame = 120)


There are clearly some issues with my animation. I don't think I'm seeing the math to this projection clearly. I do see that the top face up the cup should be sent to the uppermost point of the torus and same for the bottom face and the lowermost point. I also see the rightmost points of the non-handle part of the cup that are also at y = 0 are mapped upwards and downwards, which is incorrect, except for the point [.8, 0, 0], which I manually mapped to the correct position.

Maybe I need to pick different projective origin points based on the vertex position? Maybe I should split the mesh into vertex groups to make it more of a piecewise transformation? Maybe I need to reposition the non-handle part of the mug slightly more in the +x direction? Any help would be greatly appreciated. Thanks.

• Hello and welcome. While files, images, and external videos or links may be helpful additions they should not be the only way to obtain information about your issue. Don't make understanding your question rely on downloading a file, watching a video or visiting an external site. Use the builtin tools to upload images or gifs, along with thoroughly explaining the problem in written form so it can be indexed and searched for thus helping future visitors with similar issues. Apr 29 at 14:00

## Use Shape Keys to animate the vertices

### 1) Make mug

You can make the mug by run my script

import bpy, bmesh, math

def select_vert(verts, i):
bpy.ops.object.mode_set(mode = 'OBJECT')
verts[i].select = True
bpy.ops.object.mode_set(mode = 'EDIT')

def create_mug():
bpy.ops.mesh.primitive_circle_add(vertices=6, fill_type='TRIFAN', calc_uvs=True, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
cup = bpy.context.object
md = cup.modifiers.new(type = "SUBSURF", name = "sub")
md.levels = 2
bpy.ops.object.modifier_apply(modifier="sub")
bpy.ops.object.mode_set(mode = 'EDIT')
for r in range(10):
bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={"use_normal_flip":False, "use_dissolve_ortho_edges":False, "mirror":False}, TRANSFORM_OT_translate={"value":(0, 0, 0.2), "orient_type":'NORMAL', "orient_matrix":((-0.290283, 0.956941, -4.25348e-09), (-0.956941, -0.290283, -1.40219e-08), (-1.46529e-08, 0, 1)), "orient_matrix_type":'NORMAL', "constraint_axis":(False, False, True), "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "gpencil_strokes":False, "cursor_transform":False, "texture_space":False, "remove_on_cancel":False, "release_confirm":False, "use_accurate":False, "use_automerge_and_split":False})
bpy.ops.mesh.select_less()

def extrude():
bpy.ops.mesh.extrude_region_shrink_fatten(MESH_OT_extrude_region={"use_normal_flip":False, "use_dissolve_ortho_edges":False, "mirror":False}, TRANSFORM_OT_shrink_fatten={"value":0.2, "use_even_offset":False, "mirror":False, "use_proportional_edit":False, "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "use_proportional_connected":False, "use_proportional_projected":False, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "release_confirm":False, "use_accurate":False})
def rotate(v):
bpy.ops.transform.rotate(value=v, orient_axis='Y', orient_type='GLOBAL', orient_matrix=((-1, 0, -0), (-0, -3.42285e-08, 1), (0, 1, -3.42285e-08)), orient_matrix_type='VIEW', mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)
def scale():
bpy.ops.transform.resize(value=(0, 0.9, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, True, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)

bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.rotate(value=0.523599, orient_axis='Z', orient_type='GLOBAL', orient_matrix=((1, -0, -0), (-0, 1, -0), (0, -0, 1)), orient_matrix_type='VIEW', mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)

v = -math.pi/8

bpy.ops.mesh.select_all(action='DESELECT')
for i in (118, 130, 141, 209, 247, 279, 293, 305, 332):
select_vert(cup.data.vertices, i)

extrude()
scale()
rotate(v)

for r in range(3):
extrude()
rotate(v)

bpy.ops.mesh.select_all(action='DESELECT')
for i in (90, 103, 106, 109, 117, 145, 158, 186, 194):
select_vert(cup.data.vertices, i)

extrude()
scale()
rotate(-v)

for r in range(3):
extrude()
rotate(-v)

for i in (397, 410, 411, 412, 413, 414, 415, 416, 417):
select_vert(cup.data.vertices, i)
bpy.ops.mesh.bridge_edge_loops()
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(-0.3, -3.42816e-08, -1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, True, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)
bpy.ops.mesh.select_all(action='DESELECT')

bpy.ops.object.mode_set(mode = 'OBJECT')
return cup

mug = create_mug()
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
md = mug.modifiers.new(type = "SUBSURF", name = "sub")
md.levels = 2
bpy.ops.object.modifier_apply(modifier="sub")


### 2) Make Torus

Make a Torus from Add > Mesh > Torus

### 3) Use Proportional Editing to push the mug

Make it similar to the Torus and inside the Torus

### 4) Add a mesh path

Add > Mesh > circle
Use for hit a ray to the Torus from the path points to find the target position

### 5) Make sure apply All transforms for all object

Object > Apply > All Transforms

### 6) Run script

rename the object to mug, path, torus and select the mug

import bpy, bmesh, math
from mathutils import Vector
import mathutils

def find_nearest_from(path, co):
l           = 99999999999999
path_verts  = path.data.vertices
for path_vert in path_verts:
new_l   = (path_vert.co - co).length_squared
if new_l < l:   l, output = new_l, path_vert

return output.co

def tran(mug, torus, path): # context.object must be mug
bpy.ops.object.mode_set(mode = 'EDIT')
me          = mug.data
bm          = bmesh.from_edit_mesh(me)
mug_verts   = bm.verts
mug_verts.ensure_lookup_table()

for mug_vert in mug_verts:
nearest     = find_nearest_from(path, mug_vert.co)
direction   = mug_vert.co - nearest

success, location, normal, face_index = torus.ray_cast(nearest, direction, distance=10)
while not success:
direction.y *= 0.99
success, location, normal, face_index = torus.ray_cast(nearest, direction, distance=10)

mug_vert.co = location

# bpy.ops.object.mode_set(mode = 'OBJECT')

objs        = bpy.data.objects
mug         = objs["mug"]
torus       = objs["torus"]
path        = objs["path"]

tran(mug, torus, path)


### Addition) Make the Top part of the mug

Similar of 1 to 7