2
$\begingroup$

I'm animating the coffee mug torus homeomorphism.

enter image description here

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:

enter image description here

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
#target torus has radius of:
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.

my file can be downloaded here:

$\endgroup$
1
  • $\begingroup$ 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. $\endgroup$ Commented Apr 29, 2022 at 14:00

1 Answer 1

3
$\begingroup$

Animate vertices with shape keys

enter image description here

1) Making mug

You can make mugs by running 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")
bpy.ops.object.shape_key_add(from_mix=False)
bpy.ops.object.shape_key_add(from_mix=False)

2) Making Torus

Add > Mesh > Torus

3) Push the mug usisg Proportional Editing.

Make it look like the Torus and be inside the Torus. enter image description here

4) Add a mesh path

Add > Mesh > circle
Used to hit a ray from a point of the path to the torus and find the target position.

enter image description here

5) Make sure apply All transforms to 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)

7) Keyframe the ShapeKeys

enter image description here

Addition) Make the inside of the mug

Like step 1 to 7

End

$\endgroup$
0

You must log in to answer this question.

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