3
$\begingroup$

The simulated transformations of a rigid body can be read out through .matrix_local, .matrix_world, and animation drivers set to "Transform Channel".

However, Euler rotations are wrapped to their lowest magnitude equivalents. E.G. 190° becomes -170°, 360° becomes , etc.

Is there a way to get the full, unwrapped rotation, relative to the start of the simulation?

$\endgroup$
4
$\begingroup$

Matrices store rotations as quaternions (to_euler is only a method converting them), so if you get your data from a matrix, you know you lost information about the number of rotations. I don't think there's a way to read the euler values.

However, because 360° range is quite big, as long as your object is rotating slower than 180° per frame (or per simulation step, if you know how to access data in simulation process), you can remember previous rotation, get current rotation, and check the difference. A big (> 180°) difference suggests wrapping occurred, so you can unwrap the value, and add the difference to a variable that stores the real rotation.

rotation = deg(rotating_object.matrix_world.to_euler()[0])
difference = rotation - prev_rotation
if abs(difference) > 180:
    difference += 360 if prev_rotation > 0 else -360
    
real_rotation += difference
prev_rotation = rotation

Full code:

"""
Type in console:
mod = D.texts['drawhandler.py'].as_module()
mod.register()
"""

import bpy
from math import degrees as deg

view_handler = None
objects = bpy.data.objects
c = objects['Cylinder']
t = objects['Text']
prev_rotation = 0
real_rotation = 0


def callback():
    global prev_rotation
    global real_rotation
    
    rotation = deg(c.matrix_world.to_euler()[0])
    difference = rotation - prev_rotation
    if abs(difference) > 180:
        # rotation += 360 if prev_rotation > 0 else -360
        # difference = rotation - prev_rotation
        # shortcut:
        difference += 360 if prev_rotation > 0 else -360
        
    real_rotation += difference
    prev_rotation = rotation
        
    t.data.body = str(int(real_rotation))



def register():
    global view_handler
    view_handler = bpy.types.SpaceView3D.draw_handler_add(callback, (), 'WINDOW', 'PRE_VIEW') 
    

def unregister():
    bpy.types.SpaceView3D.draw_handler_remove(view_handler, 'WINDOW')  # remove handler added on end of register() function


if __name__ == "__main__":
    register()

You probably want to use a driver instead of a dirty draw handler.

$\endgroup$
6
  • 1
    $\begingroup$ For quickly spinning objects: keep track not only of previous_rotation, but also calculate and save angular velocity, and use it to predict the next rotation and replace prev_rotation with the predicted rotation in the calculations. Likewise you can keep track of acceleration, or even acceleration of acceleration, but each added step will have diminished returns in accuracy. It's worth to remember that this approach will fail in abrupt changes in angular velocity due to collisions. Also, in the example in the answer, you can calculate the rotation directly from position instead. $\endgroup$ – Markus von Broady Mar 18 at 19:02
  • $\begingroup$ Cool. Thanks for the examples; they're much more straightforward and cleaner than what I was thinking. I will also try storing the rotations in a dictionary or array somewhere and multiply angular velocity by the frame change so that frame scrubbing mostly works, and maybe average them over a couple frames to ignore glitchy twitching. $\endgroup$ – Will Chen Mar 18 at 20:08
  • $\begingroup$ @WillChen just like you only simulate physics once, you could update the script to only work if frame - 1 exists as a key in a dictionary, and if so, calculate the rotation and write it to the dictionary as a value associated with frame key. $\endgroup$ – Markus von Broady Mar 18 at 20:14
  • $\begingroup$ I've found that you'll probably want to store the Euler(s) and pass them as euler_compat in .to_euler(). Otherwise, for gimbal-locked objects, the matrices can produce equivalent Eulers where the axis you want jumps by less than 180°, because the other axes have flipped around. Mind if I edit the answer to show that? $\endgroup$ – Will Chen Mar 21 at 9:11
  • $\begingroup$ Sure, go for it. $\endgroup$ – Markus von Broady Mar 21 at 9:34
3
$\begingroup$

The .to_euler() function of transformation matrices allows passing an euler_compat argument that acts as a reference rotation from which new Euler values are generated.

Thus, as long as the object in question doesn't rotate more than 180° in any one step, it should be possible to always get the valid rotation by passing the known valid Euler rotation from the last (sub)frame:

rot_euler = obj.matrix_world.to_euler()

def callback():
    global rot_euler
    
    rot_euler = obj.matrix_world.to_euler('XYZ', rot_euler)

This should automatically work for all axes and account for effects like gimbal locking.

enter image description here

(Animation from Markus von Broady's answer, since I tried it and it looked exactly the same.)

$\endgroup$
1
  • $\begingroup$ Nice! That makes it even simpler. $\endgroup$ – Markus von Broady Mar 21 at 11:08
3
$\begingroup$

Arc distance.

enter image description here

Before it rolls off the edge, can use the arc distance formula

$$d = r \theta$$

Where d is the distance travelled, r the radius of the cylinder (half y (or x) dimension) and theta the total angle in radians.

An empty is added the scene at location of cylinder at frame 1.

Or as demonstrated in callback "hack" (one of my favourites)

import bpy
from math import degrees



def callback():  
    from bpy import context
    mt = context.scene.objects.get("Empty")
    ob = context.scene.objects.get("Cylinder")
    t = context.scene.objects.get("Text")
    if not (t and mt and ob):
        return None 
    radius = ob.dimensions.y / 2
    distance = (
            mt.matrix_world.translation 
          - ob.matrix_world.translation
          ).length
          
        
    t.data.body = f"{degrees(distance / radius) :4.0f}"



def register():
    global view_handler
    view_handler = bpy.types.SpaceView3D.draw_handler_add(callback, (), 'WINDOW', 'PRE_VIEW') 
    

def unregister():
    bpy.types.SpaceView3D.draw_handler_remove(view_handler, 'WINDOW')  # remove handler added on end of register() function


if __name__ == "__main__":
    register()

This can also be set up as a driver. Added a rotating arrow and drove its rotation using a global distance empty to cylinder, with half Y dimension for radius.

enter image description here Cam tracked to barrel, text and triangle parented to camera. Triangle's rotation Z driven. Showing a big case of "Wagon-wheel effect"

Rendering.

To have the result rendered it could also be an idea to consider frame change handlers, as well as drivers as mentioned by Markus.-----

Related

Transformation constraint does not contain mapping?

Calculate sphere's rotation for rolling

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.