4
$\begingroup$

I'm trying to write a script that animates a rubik's cube. enter image description here

I want to rotate the left side by 90°, then the front by -90°

Step 1. (rotate left side by 90°):

enter image description here

Step 2. (rotate front side by -90°):

enter image description here

I've wrote a script that rotates the corresponding cubes (the whole rubik's cube is made out of 27 little cubs) around the corresponding axis. I'm using cube.rotation_euler[axis] += angle to rotate the left cubes 90° around the Y-axis (green) and then the now front cubes -90° around the X-axis (red). Both axis are oriented so, that the pos. direction is in the right half of the image, going to the right. But my rubik's cube looks like this: enter image description here

If check the rotation in the transform tab of the most top right cube, in this image, it shows:

X: -90°
Y:  90°
Z:   0°

This makes sense to me, since this cube got rotated twice.

I rotated the cubes again manually by selecting them in the 3D viewer and using the rotate tool. After Step 1. (90° around Y), the cube had a rotation of:

X:   0°
Y:  90°
Z:   0°

Exactly what I'd expect. But after Step 2. (-90° around X) it shows that the cube has a rotation of:

X: -90°
Y:   0°
Z: -90°

Can some one explain to me what is happening there? Why does the Y and Z rotation suddenly change? I assume some coordinate transformation from relative to absolut coordinates or the other way around maybe. What do I have to use in python to mimic the behaviour of the rotation tool in the 3D view. How do I figure out what euler angle I need? It should be scalable for many rotations back to back. Or is there maybe a different rotation mode so that I don't have to use euler?

EDIT: I found a way using bpy.ops.transform.rotate(value=angle, orient_axis=axis) instead of cube.rotation_euler[axis] += angle. But I got a new problem. I want to insert a key frame after each rotation. Which works with the following code:

    global CURRENT_FRAME
    CURRENT_FRAME = int(CURRENT_FRAME + FRAMES_PER_STEP)
    bpy.context.scene.frame_set(CURRENT_FRAME)
    
    # select
    for cube in cube_list:
        cube.select_set(True)
    
    # rotate (global)
    bpy.ops.transform.rotate(value=angle, orient_axis=axis)

    # deselect
    for cube in cube_list:
        cube.select_set(False)

The keyframes end up how they are supposed to be, but the frames in between are all over the place. How can I fix that? enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

EDIT 2: I've implemented the Euler rotation like @Markus von Broady suggested. But I still have the same problem as before. The Keyframes are correct but the animation in between them is not. Here is my full code:

def execute_command(command):
    angle = np.pi/2

    # rotate one face
    if len(command) <= 2:
        if len(command) == 2: # invers
            angle *= -1

        if command[0] == 'u':
            face = 'up'
            rot = Euler((0, 0, -angle))
        elif command[0] == 'd':
            face = 'down'
            rot = Euler((0, 0, angle))
        elif command[0] == 'r':
            face = 'right'
            rot = Euler((0, -angle, 0))
        elif command[0] == 'l':
            face = 'left'
            rot = Euler((0, angle, 0))
        elif command[0] == 'f':
            face = 'front'
            rot = Euler((-angle, 0, 0))
        elif command[0] == 'b':
            face = 'back'
            rot = Euler((angle, 0, 0))
        
        cube_list = find_subcubes_in_face(face)

    # flip whole cube
    else:
        if command == 'fl_u':
            rot = Euler((0, -angle, 0))
        elif command == 'fl_d':
            rot = Euler((0, angle, 0))
        elif command == 'fl_l':
            rot = Euler((0, 0, -angle))
        elif command == 'fl_r':
            rot = Euler((0, 0, angle))

        cube_list = SUBCUBES

    global CURRENT_FRAME
    CURRENT_FRAME = int(CURRENT_FRAME + FRAMES_PER_STEP)
    bpy.context.scene.frame_set(CURRENT_FRAME)
    for cube in cube_list:
        # select
        cube.select_set(True)

        # rotate
        mat = cube.rotation_euler.to_matrix().to_4x4()
        rot_mat = rot.to_matrix().to_4x4()
        mat = rot_mat @ mat
        cube.rotation_euler = mat.to_euler()
    
        # deselect
        cube.select_set(False)
        
    # keyframe
    keyframe_all_cubes()
$\endgroup$
0

1 Answer 1

5
$\begingroup$

Let's try to deal with it once and for all...

First, let's rotate two objects:

  • the left cube, by using a rotation operator; using a gizmo as on the animation, or RY90 will give the same result,
  • the right cube, by setting $y$ component of Euler rotation to $90°$; setting it by code C.object.rotation_euler.y = 90 would give the same result.

Both cubes ended with the same rotation. However, time for the second step in the question, $-90°$ around $x$ axis:

This may be confusing

  • as soon as the left cube is being rotated, $x$ and $z$ components suddenly jump from $0$ to $-90°$ (but visually the cube rotates only around $x$ axis!),
  • $y$ component of Euler rotation changes instead of $x$,
  • the left and right cube end up with different Euler rotations and different visual state,
  • visually, the right cube rotates around $z$ axis, though clearly it's the $y$ Euler component that is being changed!

So let's explain the left cube first

The default Transformation Orientation is set to Global:

In this mode, whenever you move, rotate, scale, and specify an axis, the world's axis is used - it doesn't matter if the object is parented to something or rotated, because the world's axes never change.

Imagine a car, and a cube inside the car (parented to the car). If you want to move this cube up, you just increase its $z$ component of location: the cube moves up through the world, and it gets closer to the car's roof. However, if there have been an accident and the car got turned to its right side, in order to move the cube up, you need to move it not towards a car's roof, but towards the car's left door. So in order to move the cube up in the world's space, you need to move it left in the local space of its parent. You move the cube up, but only its $x$ location component changes. If the car is on a transport ship, and the cube is inside of the box in this car, and the world is not just the Earth, but entire Solar System inside which Earth rotates around its own axis, then it can get tricky to calculate how exactly the cube's local coordinates should change to move it up in the world's space - fortunately Blender does it for you.

But why cubes end up with different rotation?

They're not parented!

Well, technically, according to Blender nomenclature, they aren't. But for XYZ Euler rotation, you can think of the $z$ rotation being parented to $y$, and that being parented to $x$, because that's the order of rotations. See, Blender doesn't maintain a history of rotations, instead it allows you to specify the order of rotations, and then after relevant operators it calculates what values those rotations need to have, to produce the visual result intended by the user. Think of it, as each object coming with 2 empties, where the main object is parented to one empty (e.g. the arrows), and that empty is parented to yet another empty (e.g. the cube empty):

Now imagine that each of those objects: the main objects and two empties, can have only a single rotation, around a single axis. The Euler rotation order is simply saying which of the three rotates around which axis. So, going back to the cubes example, if you switch to the Local orientation, you can see how now the gizmos change, because they are bound to the object's axes which are also rotated:

$x$ and $z$ axes of the object moved! So of course rotating around world's $x$ or $z$ axis will give a different result than rotating around an equivalent local axis.

Why the sudden $90°$ changes?

This is due to a Gimbal Lock - changing either $x$ or $z$ component would rotate around the world's $z$ axis, and changing $y$ component alone would rotate around the world's $y$ axis (because $y$ axis remains aligned with the world after rotating around $y$), so neither would rotate around $x$ axis... I'm not sure what Blender does exactly, but probably something equivalent to this Python code:

Python: rotate $90°$ around world's $y$ axis:

import bpy
from bpy import context as C
from math import radians
from mathutils import Euler

ob = C.object
# In order to rotate around world's axis but moved towards the object's
# origin, either temporarily set object's origin to 0 (so move it to the
# axis), or create a matrix without the location data, which I do below,
# by only using rotation data to create the matrix:
mat = ob.rotation_euler.to_matrix().to_4x4()

# code operates on radians, so convert the 90 degrees value to radians
rot_value = radians(90)  # convert degrees to radians

y_rot = Euler((0, rot_value, 0))  # default order is XYZ

# create transformation matrix
y_mat = y_rot.to_matrix().to_4x4()

# apply transformation matrix
mat = y_mat @ mat

# convert the resulting matrix back to an euler value
new_euler = mat.to_euler()

# update object's euler:
ob.rotation_euler = mat.to_euler()

Python: rotate $-90°$ around world's $y$ axis:

import bpy
from bpy import context as C
from math import radians
from mathutils import Euler

ob = C.object
mat = ob.rotation_euler.to_matrix().to_4x4()
rot_value = radians(-90)  # convert degrees to radians
x_rot = Euler((rot_value, 0, 0))
x_mat = x_rot.to_matrix().to_4x4()
mat = x_mat @ mat
new_euler = mat.to_euler()
ob.rotation_euler = mat.to_euler()

Of course if you're writing a code like this, you can apply both rotations in one go, see previous revision of this answer to see the code for that.

As you can hopefully see, Blender doesn't simply increase or decrease a single component of an Euler property. Instead it works on Matrices (or so I think, I didn't investigate the source; surely it at least converts Eulers to Quaternions). This means, that there's no guarantee of continuity between values, at least as long as a single rotation in Euler can be described by multiple combinations of rotations in Quaternions. Blender does try to help with this continuation a little bit and in the to_euler() method accepts the euler_compat argument, which is another Euler, perhaps the previous state - then Blender creates an Euler as close to that other Euler, making sure that no value changes by more than $180°$ [citation needed], and allowing you to rotate beyond the $-180°..+180°$ range, even though quaternions can't describe values beyond that range (outside of Blender one could specify the range to be e.g. 0..360°, but it's always a period of 360 degrees).

What about the right cube?

The only thing left is to mention that modifying Euler components in the Numbers panel or by Python script is the same as rotating in the Local orientation. Another way to look at it, is to imagine Rubik cube rotation instructions. If you have a list of instructions, it doesn't matter in which order those instructions were written (which may be a lost information, e.g. when you save a text file, there's no undo history embedded). It only matters in which order those instructions are presented:

Is the reader of the above supposed to first rotate around $x$, then $z$, and at the end around $y$ axis? If you don't see the process of writing that, but only the end result, you'd just read the instructions from top to bottom. This is also what Blender does: every time Blender renders your viewport or the actual render, it takes an object without rotations, then applies $x$ rotation, then $y$ and finally $z$ rotation. You can change that order by changing the XYZ Euler to e.g. XZY Euler - this way you tell Blender that the order of rotation is different than going through the array of rotations (which always contains 3 elements) from the beginning to an end.

$\endgroup$
9
  • $\begingroup$ Thank you for you answer. If I understand it right this rotates it all that once. Is there a way to do it in a step by step way, so I can add keyframes in between each step. I'm familiar with matrices, if you need to go deeper in to the math. $\endgroup$
    – Akut Luna
    Sep 17, 2022 at 13:42
  • $\begingroup$ @Akut the way you do it, is you do it like I do in my script, calculate the resulting matrix and take the resulting euler from it, and keyframe that. Better yet, since you calculate that value rather than arbitrarily setting it, you may want to use quaternions instead - unless of course you want the euler-specific movement... $\endgroup$ Sep 17, 2022 at 15:31
  • $\begingroup$ How do quaternions differ from euler and how do I use them? I'm sorry I know python, but I'm very new to Blender. $\endgroup$
    – Akut Luna
    Sep 17, 2022 at 15:48
  • $\begingroup$ @Akut quaternions define an axis around which a rotation is done, so the interpolation between two quaternions often seems more "natural" or a shorter path, than between eulers. However, quaternions can store only a value within a single period, e.g. 0...360° or -180°...+180°, so for multiple rotations (e.g. 1200°) you need intermediate keyframes. $\endgroup$ Sep 17, 2022 at 15:51
  • $\begingroup$ I've managed to implement your euler code (see Edit 2), but I don't understand how to do it with quaternions. Could you give me an example of how to implement it? $\endgroup$
    – Akut Luna
    Sep 17, 2022 at 19:25

You must log in to answer this question.

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