1

I was wondering if there was any way to change the orientation/rotation of an objects origin without changing the actual mesh?

I've got this bit of code, which gets the rotation value I want the object to inherit... but I have no clue how I would actually go about applying it to the object.

    import bpy, bmesh
ob = bpy.context.object

bm = bmesh.from_edit_mesh(ob.data) # Only works in Edit Mode
bm.faces.ensure_lookup_table()
bm.edges.ensure_lookup_table()

active_geo = bm.select_history.active
if active_geo is not None:
    _loc, rot, _scl = ob.matrix_world.decompose()
    rot_matrix = rot.to_matrix().to_4x4()

    direction_vec = rot_matrix @ (active_geo.verts[0].co - active_geo.verts[1].co)

    normal_quat = direction_vec.to_track_quat('Z', 'Y')
    normal_euler = normal_quat.to_euler('XYZ')

    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.empty_add(type='ARROWS', align='WORLD', rotation=normal_euler, scale=(1, 1, 1))

The empty that gets added is representative of the orientation I want the cubes origin to have while keeping the mesh the same.

Script result with 3D Cursor in the correct position

Below is an example of how I can brute force the same effect I am looking for by enabling Affect Only Origins, and using Align Rotation to Target with snapping.

https://i.stack.imgur.com/6MCsx.jpg

Any help is appreciated, thank you.

razed
  • 23
  • 4
  • You do know about object rotation, right? – TheLabCat May 21 '22 at 02:38
  • Yes, I do. My question is about changing the objects rotation without actually rotating the mesh. – razed May 21 '22 at 15:56
  • bpy.data.objects[“CubeThing”].rotation_euler – TheLabCat May 21 '22 at 16:02
  • without rotating the actual mesh, I want to change the pivots orient so the local space can be set up properly.

    https://blender.stackexchange.com/questions/230321/rotate-origin-only-to-match-other-object-rotation

    I found this example, but the use case is different and I had trouble applying it in this specific context.

    – razed May 21 '22 at 16:05
  • The object rotation does not rotate the actual mesh within the object space. If you want to change where the origin is, you either have to translate the mesh within the object space, or you have to parent the object to an empty and use that as an “origin” instead. – TheLabCat May 21 '22 at 16:07
  • Apologies, I updated my last comment to include an example. – razed May 21 '22 at 16:10
  • Does this work? (1) Apply the current transform, if any (2) Set the object's transform to the inverse of the one you want (3) Apply the current transform (4) Set the object's transform to the one you want – scurest May 21 '22 at 17:05

1 Answers1

1

It seems what you need is as simple as:

  1. Save current vertex positions in the world space (multiply local coords by ob.matrix_world).
  2. Do whatever you want to do, it seems you figured it out because you can align the empty the way you want, but also apply that to the object.
  3. Since changing the object's transformation matrix will move vertices in the world space, restore their positions to the same world space as in p. 1. - so set local coords to saved copy of world space, multiplied by ob.matrix_world.inversed().
import bpy, bmesh
from bpy import context as C

ob = C.object me = ob.data bm = bmesh.from_edit_mesh(me) mat = ob.matrix_world

an error is better than the script failing silently

co0, co1 = map(lambda x:x.co, bm.select_history.active.verts[:2]) direction = co1 - co0

_loc, rot, _scl = mat.decompose() rot_matrix = rot.to_matrix().to_4x4() direction_vec = rot_matrix @ direction normal_quat = direction_vec.to_track_quat('Z', 'Y') normal_euler = normal_quat.to_euler('XYZ')

bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.empty_add(type='ARROWS', align='WORLD', rotation=normal_euler, scale=(1, 1, 1))

save in world space

coords = [mat @ v.co for v in me.vertices]

maybe you want to position your empty

C.object.location = mat @ (co0 + co1) / 2

easiest way to apply new rotation

ob.rotation_euler = normal_euler C.view_layer.update()

restore to old world space

mat_inv = ob.matrix_world.inverted() for v, prev_co in zip(me.vertices, coords): v.co = mat_inv @ prev_co

I don't understand everything you're trying to do, so maybe the code could be improved - for example, you multiply direction by only rotation matrix - but what if the scales aren't uniform? Why not simply multiply it by mat?

V2

Less readable but faster code:

import bpy, bmesh
from bpy import context as C
from mathutils import Matrix

ob = C.object me = ob.data bm = bmesh.from_edit_mesh(me) mat = ob.matrix_world

an error is better than the script failing silently

co0, co1 = map(lambda x:mat @ x.co, bm.select_history.active.verts[:2]) normal_quat = (co1 - co0).to_track_quat('Z', 'Y') normal_euler = normal_quat.to_euler('XYZ')

bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.empty_add(type='ARROWS', align='WORLD', rotation=normal_euler, scale=(1, 1, 1))

maybe you want to position your empty

C.object.location = (co0 + co1) / 2

make local space = world space

me.transform(mat)

a faster way to calculate new matrix - without refreshing the View Layer

t, _, s = mat.decompose() T = Matrix.Translation(t) R = normal_euler.to_matrix().to_4x4() S = Matrix.Diagonal(s.to_4d()) ob.matrix_world = T @ R @ S

now apply the inverted matrix on the mesh

me.transform(ob.matrix_world.inverted())

V3

The below solution never exits Edit Mode and applies only a single transform:

import bpy, bmesh
from bpy import context as C, data as D
from mathutils import Matrix

ob = C.object me = ob.data bm = bmesh.from_edit_mesh(me) mat = ob.matrix_world

co0, co1 = map(lambda x:mat @ x.co, bm.select_history.active.verts[:2]) normal_quat = (co1 - co0).to_track_quat('Z', 'Y') normal_euler = normal_quat.to_euler('XYZ')

t, r, s = mat.decompose() quat_diff = normal_quat.rotation_difference(r) mat_diff = quat_diff.to_matrix().to_4x4() T = Matrix.Translation(t) R = normal_euler.to_matrix().to_4x4() S = Matrix.Diagonal(s.to_4d()) ob.matrix_world = T @ R @ S

add empty without going to Object mode

https://blender.stackexchange.com/a/51291/60486

empty = D.objects.new("empty", None) C.scene.collection.objects.link(empty) empty.empty_display_size = 2 empty.empty_display_type = 'ARROWS' empty.empty_display_size = 1 empty.rotation_euler = normal_euler empty.location = (co0 + co1) / 2

def slow(): """possibly slower, but stays in edit mode""" bm.transform(mat_diff) # transform only once for f in bm.faces: f.normal_update() bmesh.update_edit_mesh(me)

def fast(): """possibly faster, but switches modes""" global bm # to write to it bm.transform(mat_diff) # transform only once bmesh.update_edit_mesh(me) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(me)

def best(): """Probably the best of both worlds""" bm.transform(mat_diff) # transform only once bmesh.ops.recalc_face_normals(bm, faces=bm.faces) bmesh.update_edit_mesh(me)

if it really is slow or fast probably depends on the amount

of geometry and for small geos doesn't matter

best()

Markus von Broady
  • 36,563
  • 3
  • 30
  • 99
  • Yeah apologies, I don't really know much about matrix math so my code example is more or less what I scrounged up from the internet. The new empty was created for visualization of the rotation I want the object to inherit, but it may have been confusing.

    This solution works for my needs so it's probably worthy of confirming as the solution, but it also seems particularly slow? Is there a more efficient solution than saving the global vertex positions? Based on this I assumed there may be a method that just involves inversion of the applied transform? Thanks again.

    – razed May 21 '22 at 19:43
  • @razed you have to modify vertex data if you want the vertices to stay in words space while moving their container. There are faster ways to modify the vertices, like the one I just added, there's also a possibility to use numpy. – Markus von Broady May 21 '22 at 21:16
  • What does container mean in this context? Is it the origin? Also, this code seems to get the wrong location for the empty, and the wrong rotation if you don't decompose the matrix before getting direction_vec. Image example here (this example is on an object with un-applied location & rotation) – razed May 21 '22 at 22:12
  • @razed see updated code. – Markus von Broady May 22 '22 at 10:37
  • I've marked it as the answer as its everything I need... But I was wondering why the switch to object mode is required? (besides for creating the empty). I tried using it without the mode switch and the me.transform() stuff seems to not work. – razed May 24 '22 at 22:50
  • 1
    @razed me.transform() modifies the mesh data, however bmesh creates a temporary copy of that data and operates on that. Once you go back to object mode, the bmesh copy is saved back to the mesh data. Think of that as a = Vector((0, 1, 2)); b = a.copy(); a *= 2; a = b - whatever you replace the third statement (a *= 2) with, doesn't matter, because a will be overridden with the temporary copy. – Markus von Broady May 25 '22 at 09:21
  • So would you say its possible to do this operation without the context switch? I tried using bmesh.update_edit_mesh(me) in various locations without the context switch code and it didn't seem to work. Maybe the wrong function to use or something. – razed May 25 '22 at 16:18
  • 1
    @razed you certainly can do that without going to Edit Mode, though if you want to select an edge in edit mode, then you can't use the transform method. Since bmesh also doesn't have foreach_set, I don't think you can make a fast transformation there (well, there are ways, but nothing trivia,l I think). And apparently, since you complained on the speed, there's too much geometry to just use a Python's list comprehension. – Markus von Broady May 25 '22 at 16:48
  • Sorry I mean while IN edit mode, this hypothetical operator is based on being in edit mode when it is used. – razed May 25 '22 at 18:23
  • 1
    @razed you can use bmesh.transform(), but then you will have to recalculate normals... – Markus von Broady May 25 '22 at 19:20