3
$\begingroup$

The below code will rotate a selection of objects around an arbitrary location (using the active object's location here) by the chosen angle (45 in this example). I cobbled this code together from a few places but I don't fully understand how to manipulate matrices very well yet.

If you create a few objects and press run script you'll see 45 degrees is added to the objects while transforming them around the active object. This is almost exactly what I need minus one issue. I need this code to set the rotation value to 45 (or whatever arbitrary value) when the script is run not add 45 degrees to the selection of object's matrices.

import bpy
from math import radians
from mathutils import Matrix

ob = bpy.context.object
rot_point = ob.location
rot_angle = 45
rot_axis = 'Y'

mat = (Matrix.Translation(rot_point) *
       Matrix.Rotation(radians(rot_angle), 4, rot_axis) *
       Matrix.Translation(-rot_point))


for obj in bpy.context.selected_objects:
    obj.matrix_world = mat * obj.matrix_world

EDIT: If you create a few random objects, select them and then run the script, each time you run the script 45 degrees (45 degrees is an arbitrary value, it means nothing, just a random input value for this example) will be added to the object rotation values (as displayed in the clip below). This is good, and I need the objects to move like they're moving. Ultimately the value controlled by rot_angle will be fed into from mouse coordinates in a modal though, so I don't need the code to add the rot_angle value to these object's values, I need to be able to set them. Ultimately, the code should rotate the objects around whatever chosen rot_point in the manner displayed below, however once rotated, pressing run script shouldn't do anything. As the code stands, when fed into a modal it breaks everything because the code continually adds to the values with the mouse movement instead of setting them.

enter image description here

UPDATE: The below code sort of does what I want. The problem is it starts breaking down once the object gets rotated in to many ways. If I could subtract the rotation matrix values to get the same results for the rotation angle difference (ra_dif) instead of comparing against the Euler rotation value of the object, I'm guessing it would work better. How could I do that? Or does someone see a better (less dense) way of accomplishing what I'm trying to do here?

import bpy
from math import radians,degrees
from mathutils import Matrix

ob = bpy.context.object
rot_point = bpy.context.scene.cursor_location
rot_angle = 45
rot_axis = 'Y'


ra_dif = rot_angle - round(degrees(ob.rotation_euler.y))

mat = (Matrix.Translation(rot_point) *
       Matrix.Rotation(radians(ra_dif), 4, rot_axis) *
       Matrix.Translation(-rot_point))


for obj in bpy.context.selected_objects:
    obj.matrix_world = mat * obj.matrix_world

UPDATE 2: The code posted by batFINGER works great with one issue. Seems to be some strange sliding going on when objects have been rotated by a few odd degrees. If you open up the following file, press Run Script, then press space and access "Custom Rotate" then immediately press Z to enter global rotation, you'll notice the selected objects will slide if you pull the mouse in towards the pivot point of the rotation. What could be causing this? How could it be corrected?

$\endgroup$
4
  • 2
    $\begingroup$ If you want to set the value use something like obj.matrix_world = mat (this assumes there is no scale component). Can you elaborate for instance via default scene, on what the expectation for location and rotation of cube will be when using the camera is active object? $\endgroup$
    – batFINGER
    Commented Apr 18, 2018 at 14:35
  • $\begingroup$ No, afraid not. Close though. It does set the rotation value, but it also moves all the other selected objects into the location of the active object. If you test the code with a few random objects selected, I think you'll see what I'm after here. Definitely want to keep the translation part... $\endgroup$
    – Sicut Unum
    Commented Apr 18, 2018 at 14:36
  • 1
    $\begingroup$ You can move the object to the new location via . obj.matrix_world.translation = (mat * obj.matrix_world).translation Asked for clarification above as it's a bit confusing re rotating obj 45 degrees around point and also expecting local rotation of result to be 45. $\endgroup$
    – batFINGER
    Commented Apr 18, 2018 at 14:50
  • $\begingroup$ @batFINGER I see what you mean. I don't want to rotate the object 45 degrees, I want its rotation axis value to be set to 45 degrees (45 is just a stand in for what will be mouse driven modal angle data later). It's fine to use the active object as the main reference. I just want all its selected buddies to follow along when I move it (like the GIF). My last code example successfully accomplishes this, however it starts to break down after rotating the object a bunch. I think because of my object Euler comparison. Do you know how I could accomplish something similar using the matrix data? $\endgroup$
    – Sicut Unum
    Commented Apr 18, 2018 at 19:41

2 Answers 2

3
$\begingroup$

Similarly to the answer to Convert mouse movement into rotation value (not in bge), customized to make the all selected objects rotate about the active object.

Need to subtract the pivot point of the rotation, which gives the "pivot arm" vector for the rotation. In the previous answer, where just the active object was being rotated it had been hard coded to (0, 0, 0), since that is the result of subtracting an objects global location from itself.

import bpy, math
from bpy_extras.view3d_utils import (
        location_3d_to_region_2d,
        region_2d_to_vector_3d)

from mathutils import Matrix, Vector

class CustomRotate(bpy.types.Operator):
    bl_idname = "transform.custom_rotate"
    bl_label = "Custom Rotate"
    pivot_point = Vector((0, 0))
    v2d = Vector((0, 0))
    axis = Vector((1, 0, 0))
    mw = Matrix()

    def modal(self, context, event):
        ob = context.object
        if event.type in ['LEFTMOUSE','ENTER']:
            print('finished')
            return{'FINISHED'}

        if event.type in ['ESC','RIGHTMOUSE']: 
            ob.matrix_world = self.mw
            return {'CANCELLED'}

        if event.value == 'PRESS' and event.type == self.axis:
            # two times make it local
            axis = Vector([int(c == event.type) for c in 'XYZ'])
            self.axis = ob.matrix_world * axis  - ob.matrix_world * Vector()
            return {'RUNNING_MODAL'}                
        elif event.value == 'PRESS' and event.type in "XYZ":
            # to rotate about global axes
            self.axis = event.type
            # local
            return {'RUNNING_MODAL'}

        v2d = Vector((event.mouse_region_x, event.mouse_region_y)) - self.pivot_point
        if not v2d.length:
            return {'PASS_THROUGH'}
        #Get rotation angle and update object rotation
        a = self.v2d.angle_signed(v2d)
        rot_mat = Matrix.Rotation(a, 4, self.axis)
        t = ob.matrix_world.translation.copy()
        #Print angle in degrees to help with testing...
        #print('alpha angle:',math.degrees(a))
        for o in context.selected_objects:            
            mw = o.matrix_world
            mw.translation -=  t            
            mw = rot_mat * mw 
            mw.translation  += t
            o.matrix_world = mw

        self.v2d = v2d        

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):        
        region = context.region
        rv3d = context.region_data

        ob = context.object
        self.pivot_point = location_3d_to_region_2d(region, rv3d, ob.matrix_world.translation)
        self.v2d = Vector((event.mouse_region_x, event.mouse_region_y)) - self.pivot_point
        z_axis = Vector((0, 0, -1))

        self.axis = rv3d.view_matrix.transposed().to_3x3() * z_axis
        self.mw = ob.matrix_world.copy()
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(CustomRotate)             
def unregister():
    bpy.utils.unregister_class(CustomRotate) 

if __name__ == "__main__":
    register()
$\endgroup$
3
  • $\begingroup$ Thanks for your help! A few follow up questions: the rotation works, but the active object (and thus the rest of the objects) jump to grid 0,0,0 is that supposed to happen, if not could I offset this value so the selected objects don't jump to grid 0,0,0 when running the operator? Would it be possible to offset the rotation point from say the active object to an arbitrary location like the cursor, still while rotating the other objects? $\endgroup$
    – Sicut Unum
    Commented Apr 19, 2018 at 15:00
  • $\begingroup$ Yes sorry realized my mistake, amended. Will also need to keep all selected objects mw's to make cancel work correctly. $\endgroup$
    – batFINGER
    Commented Apr 19, 2018 at 15:04
  • $\begingroup$ Thanks that works for the most part. I've noticed some strange sliding that occurs if rotating objects along a global axis with the code you posted. It's rare but seems to happen when objects have been previously rotated in some ways. I've posted a blend file above and a brief explanation (at the end of the question) of how to repeat the results. How could I fix this sliding or what do you think might be causing it? $\endgroup$
    – Sicut Unum
    Commented Apr 28, 2018 at 17:04
0
$\begingroup$

taking the first script I did some modifications explaining effects and finally lightened the script to keep what is essential I show the complementary situation where we keep the original rotation and location

import bpy
from math import radians
from mathutils import Matrix

obj = bpy.context.object
#new rotation not added/same location
for obj in bpy.context.selected_objects:
    mat = Matrix.Translation(obj.location) @ Matrix.Rotation(radians(45), 4, 'Y')
    obj.matrix_world = mat

#rotation added to previous one
for obj in bpy.context.selected_objects:
    T = Matrix.Translation(obj.location)
    R = Matrix.Rotation(radians(45), 4, 'Y')
    Ti = Matrix.Translation(-obj.location) #to cancel '@ obj.matrix_world' in last line
    mat = T @ R @ Ti 
    obj.matrix_world = mat @ obj.matrix_world
    
#do we need to recalculate location? No
for obj in bpy.context.selected_objects:
    R = Matrix.Rotation(radians(45), 4, 'Y')
    obj.matrix_world = obj.matrix_world @ R #order matters
    #so we do modif from original matrix (location already in)
$\endgroup$

You must log in to answer this question.

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