# Non-wrapping rigid body rotations?

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 0°, etc.

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

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.

• 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. Mar 18 '21 at 19:02
• 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. Mar 18 '21 at 20:08
• @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. Mar 18 '21 at 20:14
• 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? Mar 21 '21 at 9:11
• Sure, go for it. Mar 21 '21 at 9:34

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.

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

• Nice! That makes it even simpler. Mar 21 '21 at 11:08

Arc distance.

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
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.

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