If you rotate around the center (i.e.: location) of the object, the process is:
- translate the object to location (0, 0, 0)
- rotate the object
- translate it back to the original location
which is what this does:
obj.matrix_world = orig_loc_mat @ rot_mat @ orig_rot_mat @ orig_scale_mat
In order to rotate around an arbitrary point the process is:
- translate the object to location (0, 0, 0)
- rotate the object
- rotate the location
- translate it back to the new location
where rotating the location is the same as rotating any object, so what you do is:
# get the rotation matrix (deg is negated - bpy goes clockwise, mathutils counter-clockwise
rot_matrix = mathutils.Matrix.Rotation(math.radians(-deg), 4, axis.upper())
# decompose the world matrix
orig_loc, orig_rot, orig_scale = obj.matrix_world.decompose()
orig_rot_matrix = orig_rot.to_matrix().to_4x4()
orig_scale_matrix = mathutils.Matrix.Scale(orig_scale[0],4,(1,0,0)) @ mathutils.Matrix.Scale(orig_scale[1],4,(0,1,0)) @ mathutils.Matrix.Scale(orig_scale[2],4,(0,0,1))
# this does the job
# 1. translate to center
# 2. rotate |
# | | 3. translate back
# | | |
new_loc = rot_matrix @ (orig_loc - center) + center)
# make a matrix out of the new location
new_loc_matrix = mathutils.Matrix.Translation(new_loc)
# all together now
obj.matrix_world = new_loc_matrix @ rot_matrix @ orig_rot_matrix @ orig_scale_matrix
I pieced this together from answers on rotating without bpy.opy and on rotation around a cursor, so I was clearly standing on the shoulders of giants. Summing things up here in hope it's useful for someone else.