1
$\begingroup$

I am a bit of a Blender novice and started getting into Blender using the Blender Python API (bpy). I am trying to animate multiple objects on a set of paths defined by mathematical equations (the paths resemble Mobius strips).

As a first experiment I am trying to move just a cone along a simple circle with its apex pointed in the direction away from the center of the circle (would be along the normal to the curve):

example animation

The animation of the location works great but I am not able to figure out how to define the rotation for the cone so it doesn't do weird flips as it rotates. I have tried defining Euler as well as Quaternion rotations without any luck.

Say I define the following keyframes (with rotations defined with respect to the Z-axis)

  1. 0 degree
  2. 90 degrees
  3. 180 degrees
  4. 270 degrees
  5. 0 degrees

I would also like the rotation to repeat without any glitches in between. Clearly the transition from 270 to 0 is causing the object to move in the wrong direction but even if I use 360 instead of 0, I am not sure how the transition from one loop of the animation to the next can be made to go smoothly.

I realize that there are possibly solutions to this using the graph editor, geometry nodes, modifiers and what not, but I was looking for the best way to handle this using bpy.

Thanks! Deep

Note: the following script limits the number of cones to 1 by the 3 lines if j == 0 and k == 10: in the script. Just remove them to see all cones.

import numpy as np
import mathutils
import math
from mathutils import Vector
import bpy
import sys

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=True)

matr = bpy.data.materials.new("Red")
matr.diffuse_color = (1,0,0,1)
matg = bpy.data.materials.new("Green")
matg.diffuse_color = (0,1,0,1)
matb = bpy.data.materials.new("Blue")
matb.diffuse_color = (0,0,1,1)

columns = 20
rows = 22

frame_cnt = 500

key_frame_cnt = rows - 2

frame_step = frame_cnt / key_frame_cnt

bpy.context.scene.frame_end = frame_cnt

theta = np.radians(np.linspace(0,360,columns+1)[:-1])
phi = np.radians(np.linspace(0,180,rows)[1:-1])

theta = theta[:,None]

r = 2

x = r * np.sin(phi) * np.cos(theta)
y = r * np.sin(phi) * np.sin(theta)
z = r * np.cos(phi)
z = np.tile(z,(columns,1))

frame_num = 0

for j in range(theta.size):
    for k in range(phi.size):
        if j == 0 and k == 10:
            bpy.ops.mesh.primitive_cone_add(vertices=4)
            cone = bpy.context.active_object
            if j % 3 == 0:
                cone.active_material = matr
            elif j % 3 == 1:
                cone.active_material = matg
            elif j % 3 == 2:
                cone.active_material = matb
            cone.scale = (0.075, 0.075, 0.03)
            cone.name = 'cone_{}_{}'.format(j,k)

for i in range(phi.size):
    for j in range(theta.size):
        for k in range(phi.size):
            if j == 0 and k == 10:
                if j + i > columns - 1:
                    m = j + i - columns + 1
                else:
                    m = j + i
                cone = bpy.data.objects['cone_{}_{}'.format(j,k)]
                cone.location = (x[m][k],y[m][k],z[m][k])

                rot_x = 0
                rot_y = phi[k]
                rot_z = theta[m]
                cone.rotation_euler = (rot_x,rot_y,rot_z)
                cone.keyframe_insert(data_path="location", frame=frame_num)
                cone.keyframe_insert(data_path="rotation_euler", frame=frame_num)
    frame_num += frame_step

i = columns

# Last key frame
for j in range(theta.size):
    for k in range(phi.size):
        if j == 0 and k == 10:
            cone = bpy.data.objects['cone_{}_{}'.format(j,k)]
            m = 0
            cone.location = (x[0][k],y[0][k],z[0][k])

            rot_x = 0
            rot_y = phi[k]
            rot_z = theta[0]
            cone.rotation_euler = (rot_x,rot_y,rot_z)

            cone.keyframe_insert(data_path="location", frame=frame_num)
            cone.keyframe_insert(data_path="rotation_euler", frame=frame_num)

$\endgroup$
4
  • $\begingroup$ a small demo script or a demo blend file would be helpful. $\endgroup$
    – Blunder
    Mar 8 at 20:20
  • $\begingroup$ I have attached my code. Here the code can create cones that form a grid on the surface of a spherical shape. But for the purposes of this animation I am only enabling the creation of one cone which rotates in a circle. When I actually run the script in Blender the cone does a weird flip towards the end of one loop around the circle. I will appreciate any help I can get! $\endgroup$
    – Deep Shen
    Mar 9 at 1:32
  • $\begingroup$ Thanks for the update! The core problem is that the key frames are not correctly calculated. If you want to do a full 360° rotation with e.g. 5 key frames you need to have 0°, 90°, 180°, 270°, 360°. But in your case the last one is 0° again. It wraps: 0°, 90° ,180° , 270°, 0°. This makes Blender to spin it backwards from 270° to 0°. Also note that you have to add the starting rotation and if it rotates more than one full rotation you need to add multiple times of 360° (or radians = 2*pi in the script)., e.g. 200°, 290°, 380°, 470°, 560°. But you have 200°, 290°, 20° (wrapped!), 110°, 200°. $\endgroup$
    – Blunder
    Mar 9 at 14:10
  • $\begingroup$ Thank you so much for the response. This is exactly what I suspected. But I am still not clear what I can do to have it keep rotating seamlessly in an infinite loop. If the last key frame is at 360 and it goes back to the start for the next loop is there a way I can avoid the backwards spin ? $\endgroup$
    – Deep Shen
    Mar 9 at 14:27

1 Answer 1

1
$\begingroup$

There are 3 little problems here:

  1. the rotation is mixed up when the cones have completed a full rotation.

  2. a location keyframe is skipped and the first column of green cones covers the column of the red cones when the animation starts. Then the column of blue cones moves to where the column of red ones should be, and so on...

  3. the last key frame collapses the sphere and moves all the columns of cones to the same place.

The main problem is the key frames. If you want to make a "rotation on its own axis" animation with, say, 4 keyframes, then you actually need another keyframe to complete the cycle, i.e. a total of 5 keyframes. You have this in your example, but you have not continuously increased the rotation values. They are reset when the rotation exceeds one full complete turn. This causes the rotation glitch.

You can see this clearly in the Graph Editor if you select a column of cones (viewport menu Select > Select Pattern...) and filter for Rotation in the Graph Editor to see only the rotation values. Use View > Frame Selected (Numpad .) to display them properly:

screenshot of the graph editor

Blender interpolates the transformation values between the key frames. At frame 225, the selected cone has a rotation of 242°. At the next key frame, frame 250, it is 18°. The cone is therefore rotated backwards between these frames.

Correct Example:

  • key frame 1: start angle -> 30°
  • key frame 2: turn it by 90° -> 120°
  • key frame 3: turn it by 90° -> 210°
  • key frame 4: turn it by 90° -> 300°
  • key frame 5: turn it by 90° -> 390° = 360° + 30°

How to Fix the Script

For key frame 5 in the example, your script only calculates the angle of 30° when an overflow occurs. The full rotation (360°) is ignored. This is done by the index m and the lookup table theta[m]:

                if j + i > columns - 1:
                    m = j + i - columns + 1
                else:
                    m = j + i

                # (... left out a few more lines ...)

                rot_z = theta[m]

By the way, the + 1 makes the new index start at 1 after an overflow, but indices in Python start at 0. This leads to an error in the position (point 2 above).

You can simply replace the if with a modulo (%) calculation. And then add an integer division // to calculate the number of full rotations (n times 360°). For the rotation value rot_z add the value of 2 * pi (radian) = 360° for each full rotation to get the correct rotation value:

            m, num_of_full_rotations = (j + i) % columns, (j + i) // columns

            # (... left out a few more lines ...)

            rot_z = theta[m] + 2 * math.pi * num_of_full_rotations

To fix the last key frame problem (point 3 above), just add + 1 for one more loop iteration and delete the last code block. So: for i in range(phi.size + 1):. That's it.

fixed key frames
In the Graph Editor the rotation values form an ascending line. The calculated sin() and cos() values for the location are also displayed.

Final note: keep in mind that Blender interpolates the transformations between key frames. So objects move in a straight line from location A to B if interpolation is Linear. That is, a "circle" with only 4+1 key frames is actually a square. You can change the interpolation to Bezier & change the handle types, etc to smooth it but the movement might be still not perfect. The more key frames you have the smoother the calculated animation will be. It might be the best to calculate the tranformation values for every frame, similar to a "baked animation". This way no interpolation is used at all.

Here is the complete script with a UV Sphere for comparision. frame_step is rounded so we don't have key frames at frames like 214.111.

import numpy as np
import mathutils
import math
from mathutils import Vector
import bpy
import sys

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=True)

matr = bpy.data.materials.new("Red")
matr.diffuse_color = (1,0,0,1)
matg = bpy.data.materials.new("Green")
matg.diffuse_color = (0,1,0,1)
matb = bpy.data.materials.new("Blue")
matb.diffuse_color = (0,0,1,1)

columns = 22
rows = 20

frame_cnt = 500

key_frame_cnt = rows - 2

frame_step = round(frame_cnt / key_frame_cnt)

bpy.context.scene.frame_end = frame_cnt

theta = np.radians(np.linspace(0,360,columns+1)[:-1])
phi = np.radians(np.linspace(0,180,rows)[1:-1])

theta = theta[:,None]

r = 2

x = r * np.sin(phi) * np.cos(theta)
y = r * np.sin(phi) * np.sin(theta)
z = r * np.cos(phi)
z = np.tile(z,(columns,1))

frame_num = 1

for j in range(theta.size):
    for k in range(phi.size):
        bpy.ops.mesh.primitive_cone_add(vertices=4)
        cone = bpy.context.active_object
        if j % 3 == 0:
            cone.active_material = matr
        elif j % 3 == 1:
            cone.active_material = matg
        elif j % 3 == 2:
            cone.active_material = matb
        cone.scale = (0.075, 0.075, 0.03)
        cone.name = 'cone_{}_{}'.format(j,k)

for i in range(phi.size+1):
    for j in range(theta.size):
        for k in range(phi.size):
            
            # calculate the index for the rotation, and check if it overflows/wraps, that is, rotation > 360°
            # return the index for the remaining rotation and the number of full rotations, 
            # e.g. 4 (index for 90°)  and 3 for full rotations = 30°+3*360°=1110° in total for the key frame)
            m, num_of_full_rotations = (j + i) % columns, (j + i) // columns
            
            cone = bpy.data.objects['cone_{}_{}'.format(j,k)]
            cone.location = (x[m][k],y[m][k],z[m][k])
                
            rot_x = 0
            rot_y = phi[k]
            # here m can be wrapped, happens when the rotation > 360°, so we need to add 360° = 2*pi for each full rotation done yet.
            rot_z = theta[m] + 2 * math.pi * num_of_full_rotations

            cone.rotation_euler = (rot_x,rot_y,rot_z)
            cone.keyframe_insert(data_path="location", frame=frame_num)
            cone.keyframe_insert(data_path="rotation_euler", frame=frame_num)
    frame_num += frame_step


# add sphere for comparision
bpy.ops.mesh.primitive_uv_sphere_add(segments=columns, ring_count=rows-1, radius=r, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
$\endgroup$
1
  • $\begingroup$ I appreciate all the work you put into answering my question. Thanks again! $\endgroup$
    – Deep Shen
    Mar 10 at 19:32

You must log in to answer this question.

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