Why can't I create a rotation animation with "bpy.ops.transform.rotate"?

I am a student studying blneder,Python.

I want to rotate one side of a Rubik’s cube with 27 cubes arranged in 3 dimensions.
The program itself works, but not with the intended result. why can’t I select one face and use “bpy.ops.transform.rotate” to create a rotation animation?

axis = ['X','Y','Z']
            
def rotation(frame,select_axis,position,angle):
    
    bpy.ops.object.select_all(action = 'SELECT')
    bpy.data.scenes['Scene'].frame_current = frame
    bpy.ops.object.select_all(action = 'DESELECT')
    
    for object in bpy.data.objects:
        if object.location[select_axis] == position:
            bpy.ops.object.editmode_toggle()

            bpy.ops.transform.rotate(
            value = angle,
            orient_axis= axis[select_axis],
            orient_type='GLOBAL', 
            orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), 
            orient_matrix_type='GLOBAL', 
            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)


    bpy.ops.object.select_all(action = 'SELECT')
    bpy.ops.anim.keyframe_insert_menu(type='Rotation')

rotation(40,0,1,1.5708)

It’s difficult to say. Your script throws an error:

Error: Python: Traceback (most recent call last):
  File "\Text", line 31, in <module>
  File "\Text", line 5, in rotation
NameError: name 'bpy' is not defined

If you want help on this particular script, then you should post the whole script or the blend file.

Maybe you need to call depsgraph.update() between each transform? Honestly, I don’t do guessing games.

1 Like

Add import bpyat the top

1 Like

My apologies. It will be all scripts.

import bpy,bmesh

for x in bpy.data.meshes:
   bpy.data.meshes.remove(x)
for y in bpy.data.materials:
   bpy.data.materials.remove(y)
for z in bpy.data.objects:
   bpy.data.objects.remove(z)

black = (0,0,0,1)
red = (1,0,0,1)
yellow = (1,1,0,1)
green = (0,1,0,1)
blue = (0,0,1,1)
white = (1,1,1,1)
orange = (0.8,0.3,0,1)
colorlist = [red,green,orange,blue,white,yellow,black]

for i in range(-1,2):
    for j in range(-1,2):
        for k in range(-1,2):
            bpy.ops.mesh.primitive_cube_add(
            size = 1,
            location = (i, j, k)
              )
            my_cube = bpy.context.object
            # ベベルを追加
            bpy.ops.object.modifier_add(type = 'BEVEL')
            bpy.ops.object.mode_set(mode = 'EDIT')
            # 選択を解除
            bpy.ops.mesh.select_all(action = 'DESELECT')
            # my_cubeのメッシュデータを取得
            bm = bmesh.from_edit_mesh(my_cube.data)
            bm.faces.ensure_lookup_table()
            
            if len(my_cube.data.materials) < 1:
                for e in range(7):
                    mat = bpy.data.materials.new('Material.%d' %e)
                    mat.diffuse_color = colorlist[e]
                    my_cube.data.materials.append(mat)
            
            
            for f in range(6):
                bm.faces[f].material_index = f    
            
            my_cube.modifiers['Bevel'].material = 6

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

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

bpy.data.scenes['Scene'].frame_start = 0
bpy.data.scenes['Scene'].frame_end = 250

bpy.ops.object.select_all(action = 'SELECT')
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')

bpy.data.scenes['Scene'].frame_current = 1
bpy.ops.anim.keyframe_insert_menu(type='Rotation')


def select(axis,position):
    for object in bpy.data.objects:
        if object.location[axis] == position:
            bpy.ops.object.select_set(True)
            
            
axis = ['X','Y','Z']
            
def rotation(frame,select_axis,position,angle):
    
    bpy.ops.object.select_all(action = 'SELECT')
    bpy.data.scenes['Scene'].frame_current = frame
    bpy.ops.object.select_all(action = 'DESELECT')
    
    for object in bpy.data.objects:
        if object.location[select_axis] == position:
            bpy.ops.object.editmode_toggle()

            bpy.ops.transform.rotate(
            value = angle,
            orient_axis= 'axis[select_axis]',
            orient_type='GLOBAL', 
            orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), 
            orient_matrix_type='GLOBAL', 
            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)


    bpy.ops.object.select_all(action = 'SELECT')
    bpy.ops.anim.keyframe_insert_menu(type='Rotation')

        
rotation(40,0,1,1.5708)

The first problem is that all objects have their origin at (0, 0, 0), so you cannot select objects based on position. You need to find the center of a cube by looking at the vertices.

The second problem is that you’re using transform operators and toggling modes. You should use low level api.

The third problem is using euler when you have multi-axis cumulative rotations. You should use quaternion instead. The downside is you can’t keyframe over 360 degrees rotation because quaternions take the shortest path, but it’s a small price to pay for avoiding axis roll issues and gimbal lock.

This is why I proposed using dynamic parenting.
Nevertheless, here’s a slightly edited version of your script.

import bpy,bmesh

for x in bpy.data.meshes:
   bpy.data.meshes.remove(x)
for y in bpy.data.materials:
   bpy.data.materials.remove(y)
for z in bpy.data.objects:
   bpy.data.objects.remove(z)

black = (0,0,0,1)
red = (1,0,0,1)
yellow = (1,1,0,1)
green = (0,1,0,1)
blue = (0,0,1,1)
white = (1,1,1,1)
orange = (0.8,0.3,0,1)
colorlist = [red,green,orange,blue,white,yellow,black]

for i in range(-1,2):
    for j in range(-1,2):
        for k in range(-1,2):
            bpy.ops.mesh.primitive_cube_add(
            size = 1,
            location = (i, j, k)
              )
            my_cube = bpy.context.object
            my_cube.rotation_mode = "QUATERNION"
            # ベベルを追加
            bpy.ops.object.modifier_add(type = 'BEVEL')
            bpy.ops.object.mode_set(mode = 'EDIT')
            # 選択を解除
            bpy.ops.mesh.select_all(action = 'DESELECT')
            # my_cubeのメッシュデータを取得
            bm = bmesh.from_edit_mesh(my_cube.data)
            bm.faces.ensure_lookup_table()
            
            if len(my_cube.data.materials) < 1:
                for e in range(7):
                    mat = bpy.data.materials.new('Material.%d' %e)
                    mat.diffuse_color = colorlist[e]
                    my_cube.data.materials.append(mat)
            
            
            for f in range(6):
                bm.faces[f].material_index = f    
            
            my_cube.modifiers['Bevel'].material = 6

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

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

bpy.data.scenes['Scene'].frame_start = 0
bpy.data.scenes['Scene'].frame_end = 250

bpy.ops.object.select_all(action = 'SELECT')
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')

bpy.data.scenes['Scene'].frame_current = 1
bpy.ops.anim.keyframe_insert_menu(type='Rotation')


def select(axis,position):
    for object in bpy.data.objects:
        if object.location[axis] == position:
            bpy.ops.object.select_set(True)
            
            
axis = ['X','Y','Z']


from mathutils import Vector, Quaternion
from math import isclose
def get_cube_center(obj):
    summed = Vector()
    for vert in obj.data.vertices:
        summed += vert.co
    return obj.matrix_world @ summed / 8


def rotation(frame,select_axis,position,angle):
    bpy.data.scenes['Scene'].frame_set(frame)

    for ob in bpy.data.objects:
        center = get_cube_center(ob)

        if isclose(center[select_axis], position, abs_tol=0.1):
            q_axis = [0.0, 0.0, 0.0]
            q_axis[select_axis] = 1.0

            q_delta = Quaternion(q_axis, angle)
            q = q_delta @ ob.rotation_quaternion
            ob.rotation_quaternion = q

    bpy.ops.object.select_all(action = 'SELECT')
    bpy.ops.anim.keyframe_insert_menu(type='Rotation')


rotation(20,0,-1,1.5708)
1 Like

Thanks for the reply. I have a feeling your script will help me. So I would like to study and understand the meaning of this script in turn.

def get_cube_center(obj):
    summed = Vector()
    for vert in obj.data.vertices:
        summed += vert.co
    return obj.matrix_world @ summed / 8

First, is this script manipulating the function to get the center of each cube by counting the number of vertices?

Yes. A cube has 8 vertices.
When you sum the position of all vertices and divide by 8, you get the center.
But before returning the center, it must be multiplied with the object world matrix.

1 Like

In my mind, I recognize that the world matrix is to figure out the direction of the XYZ axis of an object. What is the benefit of multiplying this before returning the center? I would also appreciate it if you could point out any errors in my perception.

Correct. The world matrix stores orientation. It also stores the location and scale of the object.

When the cube is rotated, the vertex position is unchanged, because the position is in local space.

If a vertex has a position x = 0.5, and you set cube.location.x = 999, the vertex will still be at 0.5.
Likewise, if you rotated a cube, the vertex position stays the same.

So, to get the world center of a cube, we must multiply the local center with the world matrix.
This gives a position in world space.

I see! I’m learning a lot!,And I understood.
Next, I have a few questions about this script.

 if isclose(center[select_axis], position, abs_tol=0.1):
            q_axis = [0.0, 0.0, 0.0]
            q_axis[select_axis] = 1.0

            q_delta = Quaternion(q_axis, angle)
            q = q_delta @ ob.rotation_quaternion
            ob.rotation_quaternion = q

As far as I have been able to find out about the math module, I thought it set the minimum absolute tolerance to 0.1 by comparing the position to an arbitrary xyz axis in

if isclose(center[select_axis], position, abs_tol=0.1):

. What exactly is this statement doing?

It checks if the position of the selected axis is close enough to the position you passed to the function.

Some values are impossible to represent in floating point, so there will be rounding errors.

When working with floating point comparisons, it’s common to use epsilon.
It is a value that represent the maximum error tolerance.
An epsilon of 0.1 means that 3.95 and 4.04 is close enough.
See https://en.wikipedia.org/wiki/Machine_epsilon.

result = isclose(a, b, abs_tol=0.1)

is the same as

tolerance = 0.1
result = abs(a - b) <= tolerance

abs_tol is safer to use, because rel_tol is percent-based and will fail if any argument is zero.

Sorry for the very basic question. Why can I assign any number to “select_axis” in

center = get_cube_center(ob)
center[select_axis]

to select a specific axis? (For example, assigning 0 to x-axis, 1 to y-axis, 2 to z-axis)
What does this [ ] represent?

Yes. center represents a location x, y, z and select_axis is used for selecting a single value.
If center is Vector((1, 2, 3)) and select_axis is 0, then center[select_axis] becomes 1.

Your own script did this:

    for object in bpy.data.objects:
        if object.location[select_axis] == position:

so I would assume you understood what it meant.

But in case you’re still learning, it’s called array subscription.

sequence = (3, 6, 2, 7, 5)
sequence[0]  # 3
sequence[1]  # 6

select = 4
sequence[select]  # 5

I understand everything about this script. Thank you for staying with us to the end. I learned a lot.

1 Like